diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index 965c0c36ad..34bdf041d9 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -3,7 +3,7 @@ cd web && npm install echo 'alias start-api="cd /workspaces/dify/api && flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc -echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail"' >> ~/.bashrc +echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..dcf74f06de --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Ensure that .sh scripts use LF as line separator, even if they are checked out +# to Windows(NTFS) file-system, by a user of Docker for Window. +# These .sh scripts will be run from the Container after `docker compose up -d`. +# If they appear to be CRLF style, Dash from the Container will fail to execute +# them. + +*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ab7118f04e..8824c5dba6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,6 +14,8 @@ body: 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: "请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true diff --git a/.github/ISSUE_TEMPLATE/document_issue.yml b/.github/ISSUE_TEMPLATE/document_issue.yml index c5aeb7fd73..45ee37ca39 100644 --- a/.github/ISSUE_TEMPLATE/document_issue.yml +++ b/.github/ISSUE_TEMPLATE/document_issue.yml @@ -12,6 +12,8 @@ body: 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: "请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 3fa3b513c6..1b0eaaf4ab 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -12,6 +12,8 @@ body: 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: "请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml index 898e2cdf58..440ea92616 100644 --- a/.github/ISSUE_TEMPLATE/translation_issue.yml +++ b/.github/ISSUE_TEMPLATE/translation_issue.yml @@ -12,6 +12,8 @@ body: 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: "请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true - type: input diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index da788d8f32..059be38362 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,21 @@ +# Checklist: + +> [!IMPORTANT] +> Please review the checklist below before submitting your pull request. + +- [ ] Please open an issue before creating a PR or link to an existing issue +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods + # Description -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. +Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. Close issue syntax: `Fixes #`, see [documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more details. -Fixes # (issue) +Fixes ## Type of Change -Please delete options that are not relevant. - - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) @@ -15,18 +23,12 @@ Please delete options that are not relevant. - [ ] Improvement, including but not limited to code refactoring, performance optimization, and UI/UX improvement - [ ] Dependency upgrade -# How Has This Been Tested? +# Testing Instructions Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -- [ ] TODO +- [ ] Test A +- [ ] Test B + -# Suggested Checklist: -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] My changes generate no new warnings -- [ ] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods -- [ ] `optional` I have made corresponding changes to the documentation -- [ ] `optional` I have added tests that prove my fix is effective or that my feature works -- [ ] `optional` New and existing unit tests pass locally with my changes diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 4b75f886fd..e424171019 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -55,6 +55,14 @@ jobs: - name: Run Tool run: poetry run -C api bash dev/pytest/pytest_tools.sh + - name: Set up dotenvs + run: | + cp docker/.env.example docker/.env + cp docker/middleware.env.example docker/middleware.env + + - name: Expose Service Ports + run: sh .github/workflows/expose_service_ports.sh + - name: Set up Sandbox uses: hoverkraft-tech/compose-action@v2.0.0 with: @@ -71,13 +79,7 @@ jobs: uses: hoverkraft-tech/compose-action@v2.0.0 with: compose-file: | - docker/docker-compose.middleware.yaml - docker/docker-compose.qdrant.yaml - docker/docker-compose.milvus.yaml - docker/docker-compose.pgvecto-rs.yaml - docker/docker-compose.pgvector.yaml - docker/docker-compose.chroma.yaml - docker/docker-compose.oracle.yaml + docker/docker-compose.yaml services: | weaviate qdrant @@ -87,7 +89,5 @@ jobs: pgvecto-rs pgvector chroma - oracle - - name: Test Vector Stores run: poetry run -C api bash dev/pytest/pytest_vdb.sh diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 64e8eb291c..67d1558dbc 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -38,6 +38,11 @@ jobs: - name: Install dependencies run: poetry install -C api + - name: Prepare middleware env + run: | + cd docker + cp middleware.env.example middleware.env + - name: Set up Middlewares uses: hoverkraft-tech/compose-action@v2.0.0 with: diff --git a/.github/workflows/expose_service_ports.sh b/.github/workflows/expose_service_ports.sh new file mode 100755 index 0000000000..3418bf0c6f --- /dev/null +++ b/.github/workflows/expose_service_ports.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml +yq eval '.services.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml +yq eval '.services.chroma.ports += ["8000:8000"]' -i docker/docker-compose.yaml +yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml +yq eval '.services.pgvector.ports += ["5433:5432"]' -i docker/docker-compose.yaml +yq eval '.services["pgvecto-rs"].ports += ["5431:5432"]' -i docker/docker-compose.yaml + +echo "Ports exposed for sandbox, weaviate, qdrant, chroma, milvus, pgvector, pgvecto-rs." \ No newline at end of file diff --git a/.gitignore b/.gitignore index e71b381610..2f44cf7934 100644 --- a/.gitignore +++ b/.gitignore @@ -139,10 +139,21 @@ web/.vscode/settings.json !.idea/icon.png .ideaDataSources/ *.iml +api/.idea api/.env api/storage/* +docker-legacy/volumes/app/storage/* +docker-legacy/volumes/db/data/* +docker-legacy/volumes/redis/data/* +docker-legacy/volumes/weaviate/* +docker-legacy/volumes/qdrant/* +docker-legacy/volumes/etcd/* +docker-legacy/volumes/minio/* +docker-legacy/volumes/milvus/* +docker-legacy/volumes/chroma/* + docker/volumes/app/storage/* docker/volumes/db/data/* docker/volumes/redis/data/* @@ -153,6 +164,9 @@ docker/volumes/minio/* docker/volumes/milvus/* docker/volumes/chroma/* +docker/nginx/conf.d/default.conf +docker/middleware.env + sdks/python-client/build sdks/python-client/dist sdks/python-client/dify_client.egg-info @@ -160,3 +174,5 @@ sdks/python-client/dify_client.egg-info .vscode/* !.vscode/launch.json pyrightconfig.json + +.idea/ diff --git a/api/.vscode/launch.json b/.vscode/launch.json similarity index 58% rename from api/.vscode/launch.json rename to .vscode/launch.json index 2039d2ca94..1b1c05281b 100644 --- a/api/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,30 +1,16 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "Python: Celery", - "type": "debugpy", - "request": "launch", - "module": "celery", - "justMyCode": true, - "args": ["-A", "app.celery", "worker", "-P", "gevent", "-c", "1", "--loglevel", "info", "-Q", "dataset,generation,mail"], - "envFile": "${workspaceFolder}/.env", - "env": { - "FLASK_APP": "app.py", - "FLASK_DEBUG": "1", - "GEVENT_SUPPORT": "True" - }, - "console": "integratedTerminal", - "python": "${command:python.interpreterPath}" - }, { "name": "Python: Flask", "type": "debugpy", "request": "launch", + "python": "${workspaceFolder}/api/.venv/bin/python", + "cwd": "${workspaceFolder}/api", + "envFile": ".env", "module": "flask", + "justMyCode": true, + "jinja": true, "env": { "FLASK_APP": "app.py", "FLASK_DEBUG": "1", @@ -34,11 +20,36 @@ "run", "--host=0.0.0.0", "--port=5001", - "--debug" - ], - "jinja": true, + ] + }, + { + "name": "Python: Celery", + "type": "debugpy", + "request": "launch", + "python": "${workspaceFolder}/api/.venv/bin/python", + "cwd": "${workspaceFolder}/api", + "module": "celery", "justMyCode": true, - "python": "${command:python.interpreterPath}" - } + "envFile": ".env", + "console": "integratedTerminal", + "env": { + "FLASK_APP": "app.py", + "FLASK_DEBUG": "1", + "GEVENT_SUPPORT": "True" + }, + "args": [ + "-A", + "app.celery", + "worker", + "-P", + "gevent", + "-c", + "1", + "--loglevel", + "info", + "-Q", + "dataset,generation,mail,ops_trace,app_deletion" + ] + }, ] } \ No newline at end of file diff --git a/README.md b/README.md index f5e06ce0fb..40a6837c42 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ The easiest way to start the Dify server is to run our [docker-compose.yml](dock ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -183,7 +184,7 @@ After running, you can access the Dify dashboard in your browser at [http://loca ## Next steps -If you need to customize the configuration, please refer to the comments in our [docker-compose.yml](docker/docker-compose.yaml) file and manually set the environment configuration. After making the changes, please run `docker-compose up -d` again. You can see the full list of environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). +If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes. @@ -191,6 +192,11 @@ If you'd like to configure a highly-available setup, there are community-contrib - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Using Terraform for Deployment + +##### Azure Global +Deploy Dify to Azure with a single click using [terraform](https://www.terraform.io/). +- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) ## Contributing diff --git a/README_AR.md b/README_AR.md index 1b0127b9e6..35be2ba9b6 100644 --- a/README_AR.md +++ b/README_AR.md @@ -157,15 +157,17 @@ ```bash cd docker +cp .env.example .env docker compose up -d ``` + بعد التشغيل، يمكنك الوصول إلى لوحة تحكم Dify في متصفحك على [http://localhost/install](http://localhost/install) وبدء عملية التهيئة. > إذا كنت ترغب في المساهمة في Dify أو القيام بتطوير إضافي، فانظر إلى [دليلنا للنشر من الشفرة (code) المصدرية](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) ## الخطوات التالية -إذا كنت بحاجة إلى تخصيص التكوين، يرجى الرجوع إلى التعليقات في ملف [docker-compose.yml](docker/docker-compose.yaml) لدينا وتعيين التكوينات البيئية يدويًا. بعد إجراء التغييرات، يرجى تشغيل `docker-compose up -d` مرة أخرى. يمكنك رؤية قائمة كاملة بالمتغيرات البيئية [هنا](https://docs.dify.ai/getting-started/install-self-hosted/environments). +إذا كنت بحاجة إلى تخصيص الإعدادات، فيرجى الرجوع إلى التعليقات في ملف [.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 التي تسمح بتنفيذ Dify على Kubernetes للنظام من الإيجابيات العلوية. @@ -173,6 +175,12 @@ docker compose up -d - [رسم بياني Helm من قبل @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### استخدام Terraform للتوزيع + +##### Azure Global +استخدم [terraform](https://www.terraform.io/) لنشر Dify على Azure بنقرة واحدة. +- [Azure Terraform بواسطة @nikawang](https://github.com/nikawang/dify-azure-terraform) + ## المساهمة diff --git a/README_CN.md b/README_CN.md index 141dc152ec..8224001f1a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -179,11 +179,16 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI ```bash cd docker +cp .env.example .env docker compose up -d ``` 运行后,可以在浏览器上访问 [http://localhost/install](http://localhost/install) 进入 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 Chart 部署 使用 [Helm Chart](https://helm.sh/) 版本或者 YAML 文件,可以在 Kubernetes 上部署 Dify。 @@ -192,9 +197,11 @@ docker compose up -d - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes) -### 配置 +#### 使用 Terraform 部署 -如果您需要自定义配置,请参考我们的 [docker-compose.yml](docker/docker-compose.yaml) 文件中的注释,并手动设置环境配置。更改后,请再次运行 `docker-compose up -d`。您可以在我们的[文档](https://docs.dify.ai/getting-started/install-self-hosted/environments)中查看所有环境变量的完整列表。 +##### Azure Global +使用 [terraform](https://www.terraform.io/) 一键部署 Dify 到 Azure。 +- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) ## Star History diff --git a/README_ES.md b/README_ES.md index a26719a2f2..ed613be8d4 100644 --- a/README_ES.md +++ b/README_ES.md @@ -179,6 +179,7 @@ La forma más fácil de iniciar el servidor de Dify es ejecutar nuestro archivo ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -188,7 +189,7 @@ Después de ejecutarlo, puedes acceder al panel de control de Dify en tu navegad ## Próximos pasos -Si necesitas personalizar la configuración, consulta los comentarios en nuestro archivo [docker-compose.yml](docker/docker-compose.yaml) y configura manualmente la configuración del entorno +Si necesita personalizar la configuración, consulte los comentarios en nuestro archivo [.env.example](docker/.env.example) y actualice los valores correspondientes en su archivo `.env`. Además, es posible que deba realizar ajustes en el propio archivo `docker-compose.yaml`, como cambiar las versiones de las imágenes, las asignaciones de puertos o los montajes de volúmenes, según su entorno de implementación y requisitos específicos. Después de realizar cualquier cambio, vuelva a ejecutar `docker-compose up -d`. Puede encontrar la lista completa de variables de entorno disponibles [aquí](https://docs.dify.ai/getting-started/install-self-hosted/environments). . Después de realizar los cambios, ejecuta `docker-compose up -d` nuevamente. Puedes ver la lista completa de variables de entorno [aquí](https://docs.dify.ai/getting-started/install-self-hosted/environments). @@ -198,6 +199,12 @@ Si desea configurar una configuración de alta disponibilidad, la comunidad prop - [Gráfico Helm por @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Uso de Terraform para el despliegue + +##### Azure Global +Utiliza [terraform](https://www.terraform.io/) para desplegar Dify en Azure con un solo clic. +- [Azure Terraform por @nikawang](https://github.com/nikawang/dify-azure-terraform) + ## Contribuir diff --git a/README_FR.md b/README_FR.md index b754ffaef7..6f09773bf2 100644 --- a/README_FR.md +++ b/README_FR.md @@ -179,6 +179,7 @@ La manière la plus simple de démarrer le serveur Dify est d'exécuter notre fi ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -188,9 +189,7 @@ Après l'exécution, vous pouvez accéder au tableau de bord Dify dans votre nav ## Prochaines étapes -Si vous devez personnaliser la configuration, veuillez - - vous référer aux commentaires dans notre fichier [docker-compose.yml](docker/docker-compose.yaml) et définir manuellement la configuration de l'environnement. Après avoir apporté les modifications, veuillez exécuter à nouveau `docker-compose up -d`. Vous pouvez voir la liste complète des variables d'environnement [ici](https://docs.dify.ai/getting-started/install-self-hosted/environments). +Si vous devez personnaliser la configuration, veuillez vous référer aux commentaires dans notre fichier [.env.example](docker/.env.example) et mettre à jour les valeurs correspondantes dans votre fichier `.env`. De plus, vous devrez peut-être apporter des modifications au fichier `docker-compose.yaml` lui-même, comme changer les versions d'image, les mappages de ports ou les montages de volumes, en fonction de votre environnement de déploiement et de vos exigences spécifiques. Après avoir effectué des modifications, veuillez réexécuter `docker-compose up -d`. Vous pouvez trouver la liste complète des variables d'environnement disponibles [ici](https://docs.dify.ai/getting-started/install-self-hosted/environments). Si vous souhaitez configurer une configuration haute disponibilité, la communauté fournit des [Helm Charts](https://helm.sh/) et des fichiers YAML, à travers lesquels vous pouvez déployer Dify sur Kubernetes. @@ -198,6 +197,12 @@ Si vous souhaitez configurer une configuration haute disponibilité, la communau - [Helm Chart par @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Utilisation de Terraform pour le déploiement + +##### Azure Global +Utilisez [terraform](https://www.terraform.io/) pour déployer Dify sur Azure en un clic. +- [Azure Terraform par @nikawang](https://github.com/nikawang/dify-azure-terraform) + ## Contribuer diff --git a/README_JA.md b/README_JA.md index 2d78992eb3..55f6e173fd 100644 --- a/README_JA.md +++ b/README_JA.md @@ -178,6 +178,7 @@ Difyサーバーを起動する最も簡単な方法は、[docker-compose.yml](d ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -187,7 +188,7 @@ docker compose up -d ## 次のステップ -環境設定をカスタマイズする場合は、[docker-compose.yml](docker/docker-compose.yaml)ファイル内のコメントを参照して、環境設定を手動で設定してください。変更を加えた後は、再び `docker-compose up -d` を実行してください。環境変数の完全なリストは[こちら](https://docs.dify.ai/getting-started/install-self-hosted/environments)をご覧ください。 +設定をカスタマイズする必要がある場合は、[.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ファイルにより、DifyをKubernetesにデプロイすることができます。 @@ -195,6 +196,12 @@ docker compose up -d - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Terraformを使用したデプロイ + +##### Azure Global +[terraform](https://www.terraform.io/) を使用して、AzureにDifyをワンクリックでデプロイします。 +- [nikawangのAzure Terraform](https://github.com/nikawang/dify-azure-terraform) + ## 貢献 diff --git a/README_KL.md b/README_KL.md index 033c73fb99..7fdc0b5181 100644 --- a/README_KL.md +++ b/README_KL.md @@ -179,6 +179,7 @@ The easiest way to start the Dify server is to run our [docker-compose.yml](dock ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -188,7 +189,7 @@ After running, you can access the Dify dashboard in your browser at [http://loca ## Next steps -If you need to customize the configuration, please refer to the comments in our [docker-compose.yml](docker/docker-compose.yaml) file and manually set the environment configuration. After making the changes, please run `docker-compose up -d` again. You can see the full list of environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). +If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes. @@ -196,6 +197,13 @@ If you'd like to configure a highly-available setup, there are community-contrib - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Terraform atorlugu pilersitsineq + +##### Azure Global +Atoruk [terraform](https://www.terraform.io/) Dify-mik Azure-mut ataatsikkut ikkussuilluarlugu. +- [Azure Terraform atorlugu @nikawang](https://github.com/nikawang/dify-azure-terraform) + + ## 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 4c48faa7fb..fa1980a99f 100644 --- a/README_KR.md +++ b/README_KR.md @@ -172,6 +172,7 @@ Dify 서버를 시작하는 가장 쉬운 방법은 [docker-compose.yml](docker/ ```bash cd docker +cp .env.example .env docker compose up -d ``` @@ -181,8 +182,7 @@ docker compose up -d ## 다음 단계 -구성 커스터마이징이 필요한 경우, [docker-compose.yml](docker/docker-compose.yaml) 파일의 코멘트를 참조하여 환경 구성을 수동으로 설정하십시오. 변경 후 `docker-compose up -d` 를 다시 실행하십시오. 환경 변수의 전체 목록은 [여기](https://docs.dify.ai/getting-started/install-self-hosted/environments)에서 확인할 수 있습니다. - +구성을 사용자 정의해야 하는 경우 [.env.example](docker/.env.example) 파일의 주석을 참조하고 `.env` 파일에서 해당 값을 업데이트하십시오. 또한 특정 배포 환경 및 요구 사항에 따라 `docker-compose.yaml` 파일 자체를 조정해야 할 수도 있습니다. 예를 들어 이미지 버전, 포트 매핑 또는 볼륨 마운트를 변경합니다. 변경 한 후 `docker-compose up -d`를 다시 실행하십시오. 사용 가능한 환경 변수의 전체 목록은 [여기](https://docs.dify.ai/getting-started/install-self-hosted/environments)에서 찾을 수 있습니다. Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했다는 커뮤니티가 제공하는 [Helm Charts](https://helm.sh/)와 YAML 파일이 존재합니다. @@ -190,6 +190,12 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) +#### Terraform을 사용한 배포 + +##### Azure Global +[terraform](https://www.terraform.io/)을 사용하여 Azure에 Dify를 원클릭으로 배포하세요. +- [nikawang의 Azure Terraform](https://github.com/nikawang/dify-azure-terraform) + ## 기여 코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. diff --git a/api/.env.example b/api/.env.example index 26b8283a7b..573c8bf90c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -39,7 +39,7 @@ DB_DATABASE=dify # Storage configuration # use for store upload files, private keys... -# storage type: local, s3, azure-blob +# storage type: local, s3, azure-blob, google-storage STORAGE_TYPE=local STORAGE_LOCAL_PATH=storage S3_USE_AWS_MANAGED_IAM=false @@ -63,7 +63,7 @@ ALIYUN_OSS_REGION=your-region # Google Storage configuration GOOGLE_STORAGE_BUCKET_NAME=yout-bucket-name -GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON=your-google-service-account-json-base64-string +GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string # Tencent COS Storage configuration TENCENT_COS_BUCKET_NAME=your-bucket-name @@ -72,11 +72,18 @@ TENCENT_COS_SECRET_ID=your-secret-id TENCENT_COS_REGION=your-region TENCENT_COS_SCHEME=your-scheme +# OCI Storage configuration +OCI_ENDPOINT=your-endpoint +OCI_BUCKET_NAME=your-bucket-name +OCI_ACCESS_KEY=your-access-key +OCI_SECRET_KEY=your-secret-key +OCI_REGION=your-region + # 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, relyt, pgvecto_rs, pgvector +# Vector database configuration, support: weaviate, qdrant, milvus, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector VECTOR_STORE=weaviate # Weaviate configuration @@ -144,6 +151,13 @@ CHROMA_DATABASE=default_database CHROMA_AUTH_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider CHROMA_AUTH_CREDENTIALS=difyai123456 +# OpenSearch configuration +OPENSEARCH_HOST=127.0.0.1 +OPENSEARCH_PORT=9200 +OPENSEARCH_USER=admin +OPENSEARCH_PASSWORD=admin +OPENSEARCH_SECURE=true + # Upload configuration UPLOAD_FILE_SIZE_LIMIT=15 UPLOAD_FILE_BATCH_LIMIT=5 diff --git a/api/Dockerfile b/api/Dockerfile index 15fd9d88e0..55776f80e1 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,12 +1,11 @@ # base image -FROM python:3.10-slim-bookworm as base +FROM python:3.10-slim-bookworm AS base WORKDIR /app/api # Install Poetry ENV POETRY_VERSION=1.8.3 -RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir --upgrade poetry==${POETRY_VERSION} +RUN pip install --no-cache-dir poetry==${POETRY_VERSION} # Configure Poetry ENV POETRY_CACHE_DIR=/tmp/poetry_cache @@ -14,7 +13,7 @@ ENV POETRY_NO_INTERACTION=1 ENV POETRY_VIRTUALENVS_IN_PROJECT=true ENV POETRY_VIRTUALENVS_CREATE=true -FROM base as packages +FROM base AS packages RUN apt-get update \ && apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev @@ -23,22 +22,21 @@ RUN apt-get update \ COPY pyproject.toml poetry.lock ./ RUN poetry install --sync --no-cache --no-root - # production stage FROM base AS production -ENV FLASK_APP app.py -ENV EDITION SELF_HOSTED -ENV DEPLOY_ENV PRODUCTION -ENV CONSOLE_API_URL http://127.0.0.1:5001 -ENV CONSOLE_WEB_URL http://127.0.0.1:3000 -ENV SERVICE_API_URL http://127.0.0.1:5001 -ENV APP_WEB_URL http://127.0.0.1:3000 +ENV FLASK_APP=app.py +ENV EDITION=SELF_HOSTED +ENV DEPLOY_ENV=PRODUCTION +ENV CONSOLE_API_URL=http://127.0.0.1:5001 +ENV CONSOLE_WEB_URL=http://127.0.0.1:3000 +ENV SERVICE_API_URL=http://127.0.0.1:5001 +ENV APP_WEB_URL=http://127.0.0.1:3000 EXPOSE 5001 # set timezone -ENV TZ UTC +ENV TZ=UTC WORKDIR /app/api @@ -61,6 +59,6 @@ RUN chmod +x /entrypoint.sh ARG COMMIT_SHA -ENV COMMIT_SHA ${COMMIT_SHA} +ENV COMMIT_SHA=${COMMIT_SHA} ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] diff --git a/api/README.md b/api/README.md index 5f71dbe5f0..8d2afa5e80 100644 --- a/api/README.md +++ b/api/README.md @@ -11,7 +11,8 @@ ```bash cd ../docker - docker-compose -f docker-compose.middleware.yaml -p dify up -d + cp middleware.env.example middleware.env + docker compose -f docker-compose.middleware.yaml -p dify up -d cd ../api ``` @@ -66,7 +67,7 @@ 10. If you need to debug local async processing, please start the worker service. ```bash - poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail + poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion ``` The started celery app handles the async tasks, e.g. dataset importing and documents indexing. diff --git a/api/app.py b/api/app.py index 40a90fdfa7..f5a6d40e1a 100644 --- a/api/app.py +++ b/api/app.py @@ -1,8 +1,8 @@ import os -from configs.app_config import DifyConfig +from configs import dify_config -if not os.environ.get("DEBUG") or os.environ.get("DEBUG", "false").lower() != 'true': +if os.environ.get("DEBUG", "false").lower() != 'true': from gevent import monkey monkey.patch_all() @@ -24,7 +24,6 @@ from flask_cors import CORS from werkzeug.exceptions import Unauthorized from commands import register_commands -from config import Config # DO NOT REMOVE BELOW from events import event_handlers @@ -44,6 +43,8 @@ from extensions import ( from extensions.ext_database import db from extensions.ext_login import login_manager from libs.passport import PassportService + +# TODO: Find a way to avoid importing models here from models import account, dataset, model, source, task, tool, tools, web from services.account_service import AccountService @@ -82,8 +83,17 @@ def create_flask_app_with_configs() -> Flask: with configs loaded from .env file """ dify_app = DifyApp(__name__) - dify_app.config.from_object(Config()) - dify_app.config.from_mapping(DifyConfig().model_dump()) + dify_app.config.from_mapping(dify_config.model_dump()) + + # populate configs into system environment variables + for key, value in dify_app.config.items(): + if isinstance(value, str): + os.environ[key] = value + elif isinstance(value, int | float | bool): + os.environ[key] = str(value) + elif value is None: + os.environ[key] = '' + return dify_app @@ -232,7 +242,7 @@ def register_blueprints(app): app = create_app() celery = app.extensions["celery"] -if app.config['TESTING']: +if app.config.get('TESTING'): print("App is running in TESTING mode") diff --git a/api/commands.py b/api/commands.py index 91d7737023..cc49824b4f 100644 --- a/api/commands.py +++ b/api/commands.py @@ -8,10 +8,12 @@ import click from flask import current_app from werkzeug.exceptions import NotFound +from configs import dify_config from constants.languages import languages from core.rag.datasource.vdb.vector_factory import Vector from core.rag.datasource.vdb.vector_type import VectorType from core.rag.models.document import Document +from events.app_event import app_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client from libs.helper import email as email_validate @@ -111,7 +113,7 @@ def reset_encrypt_key_pair(): After the reset, all LLM credentials will become invalid, requiring re-entry. Only support SELF_HOSTED mode. """ - if current_app.config['EDITION'] != 'SELF_HOSTED': + if dify_config.EDITION != 'SELF_HOSTED': click.echo(click.style('Sorry, only support SELF_HOSTED mode.', fg='red')) return @@ -585,6 +587,53 @@ def upgrade_db(): click.echo('Database migration skipped') +@click.command('fix-app-site-missing', help='Fix app related site missing issue.') +def fix_app_site_missing(): + """ + Fix app related site missing issue. + """ + click.echo(click.style('Start fix app related site missing issue.', fg='green')) + + failed_app_ids = [] + while True: + sql = """select apps.id as id from apps left join sites on sites.app_id=apps.id +where sites.id is null limit 1000""" + with db.engine.begin() as conn: + rs = conn.execute(db.text(sql)) + + processed_count = 0 + for i in rs: + processed_count += 1 + app_id = str(i.id) + + if app_id in failed_app_ids: + continue + + try: + app = db.session.query(App).filter(App.id == app_id).first() + tenant = app.tenant + if tenant: + accounts = tenant.get_accounts() + if not accounts: + print("Fix app {} failed.".format(app.id)) + continue + + account = accounts[0] + print("Fix app {} related site missing issue.".format(app.id)) + app_was_created.send(app, account=account) + except Exception as e: + failed_app_ids.append(app_id) + click.echo(click.style('Fix app {} related site missing issue failed!'.format(app_id), fg='red')) + logging.exception(f'Fix app related site missing issue failed, error: {e}') + continue + + if not processed_count: + break + + + click.echo(click.style('Congratulations! Fix app related site missing issue successful!', fg='green')) + + def register_commands(app): app.cli.add_command(reset_password) app.cli.add_command(reset_email) @@ -594,3 +643,4 @@ def register_commands(app): app.cli.add_command(add_qdrant_doc_id_index) app.cli.add_command(create_tenant) app.cli.add_command(upgrade_db) + app.cli.add_command(fix_app_site_missing) diff --git a/api/config.py b/api/config.py deleted file mode 100644 index 35e8ab5e94..0000000000 --- a/api/config.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -import dotenv - -DEFAULTS = { -} - - -def get_env(key): - return os.environ.get(key, DEFAULTS.get(key)) - - -def get_bool_env(key): - value = get_env(key) - return value.lower() == 'true' if value is not None else False - - -def get_cors_allow_origins(env, default): - cors_allow_origins = [] - if get_env(env): - for origin in get_env(env).split(','): - cors_allow_origins.append(origin) - else: - cors_allow_origins = [default] - - return cors_allow_origins - - -class Config: - """Application configuration class.""" - - def __init__(self): - dotenv.load_dotenv() - - self.TESTING = False - - # cors settings - self.CONSOLE_CORS_ALLOW_ORIGINS = get_cors_allow_origins( - 'CONSOLE_CORS_ALLOW_ORIGINS', get_env('CONSOLE_WEB_URL')) - self.WEB_API_CORS_ALLOW_ORIGINS = get_cors_allow_origins( - 'WEB_API_CORS_ALLOW_ORIGINS', '*') diff --git a/api/configs/__init__.py b/api/configs/__init__.py index e69de29bb2..c0e28c34e1 100644 --- a/api/configs/__init__.py +++ b/api/configs/__init__.py @@ -0,0 +1,3 @@ +from .app_config import DifyConfig + +dify_config = DifyConfig() \ No newline at end of file diff --git a/api/configs/app_config.py b/api/configs/app_config.py index 4467b84c86..d1099a9036 100644 --- a/api/configs/app_config.py +++ b/api/configs/app_config.py @@ -1,4 +1,5 @@ -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field, computed_field +from pydantic_settings import SettingsConfigDict from configs.deploy import DeploymentConfig from configs.enterprise import EnterpriseFeatureConfig @@ -9,9 +10,6 @@ from configs.packaging import PackagingInfo class DifyConfig( - # based on pydantic-settings - BaseSettings, - # Packaging info PackagingInfo, @@ -31,12 +29,39 @@ class DifyConfig( # **Before using, please contact business@dify.ai by email to inquire about licensing matters.** EnterpriseFeatureConfig, ): + DEBUG: bool = Field(default=False, description='whether to enable debug mode.') model_config = SettingsConfigDict( # read from dotenv format config file env_file='.env', env_file_encoding='utf-8', + frozen=True, # ignore extra attributes extra='ignore', ) + + CODE_MAX_NUMBER: int = 9223372036854775807 + CODE_MIN_NUMBER: int = -9223372036854775808 + CODE_MAX_STRING_LENGTH: int = 80000 + CODE_MAX_STRING_ARRAY_LENGTH: int = 30 + CODE_MAX_OBJECT_ARRAY_LENGTH: int = 30 + CODE_MAX_NUMBER_ARRAY_LENGTH: int = 1000 + + HTTP_REQUEST_MAX_CONNECT_TIMEOUT: int = 300 + HTTP_REQUEST_MAX_READ_TIMEOUT: int = 600 + HTTP_REQUEST_MAX_WRITE_TIMEOUT: int = 600 + HTTP_REQUEST_NODE_MAX_BINARY_SIZE: int = 1024 * 1024 * 10 + + @computed_field + def HTTP_REQUEST_NODE_READABLE_MAX_BINARY_SIZE(self) -> str: + return f'{self.HTTP_REQUEST_NODE_MAX_BINARY_SIZE / 1024 / 1024:.2f}MB' + + HTTP_REQUEST_NODE_MAX_TEXT_SIZE: int = 1024 * 1024 + + @computed_field + def HTTP_REQUEST_NODE_READABLE_MAX_TEXT_SIZE(self) -> str: + return f'{self.HTTP_REQUEST_NODE_MAX_TEXT_SIZE / 1024 / 1024:.2f}MB' + + SSRF_PROXY_HTTP_URL: str | None = None + SSRF_PROXY_HTTPS_URL: str | None = None \ No newline at end of file diff --git a/api/configs/deploy/__init__.py b/api/configs/deploy/__init__.py index f7b118201f..219b315784 100644 --- a/api/configs/deploy/__init__.py +++ b/api/configs/deploy/__init__.py @@ -1,10 +1,21 @@ -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class DeploymentConfig(BaseModel): +class DeploymentConfig(BaseSettings): """ Deployment configs """ + APPLICATION_NAME: str = Field( + description='application name', + default='langgenius/dify', + ) + + TESTING: bool = Field( + description='', + default=False, + ) + EDITION: str = Field( description='deployment edition', default='SELF_HOSTED', diff --git a/api/configs/enterprise/__init__.py b/api/configs/enterprise/__init__.py index 39983036eb..b5d884e10e 100644 --- a/api/configs/enterprise/__init__.py +++ b/api/configs/enterprise/__init__.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class EnterpriseFeatureConfig(BaseModel): +class EnterpriseFeatureConfig(BaseSettings): """ Enterprise feature configs. **Before using, please contact business@dify.ai by email to inquire about licensing matters.** diff --git a/api/configs/extra/__init__.py b/api/configs/extra/__init__.py index 358c12d63a..4543b5389d 100644 --- a/api/configs/extra/__init__.py +++ b/api/configs/extra/__init__.py @@ -1,5 +1,3 @@ -from pydantic import BaseModel - from configs.extra.notion_config import NotionConfig from configs.extra.sentry_config import SentryConfig diff --git a/api/configs/extra/notion_config.py b/api/configs/extra/notion_config.py index f8df28cefd..b77e8adaae 100644 --- a/api/configs/extra/notion_config.py +++ b/api/configs/extra/notion_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class NotionConfig(BaseModel): +class NotionConfig(BaseSettings): """ Notion integration configs """ diff --git a/api/configs/extra/sentry_config.py b/api/configs/extra/sentry_config.py index 8cdb8cf45a..e6517f730a 100644 --- a/api/configs/extra/sentry_config.py +++ b/api/configs/extra/sentry_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, NonNegativeFloat +from pydantic import Field, NonNegativeFloat +from pydantic_settings import BaseSettings -class SentryConfig(BaseModel): +class SentryConfig(BaseSettings): """ Sentry configs """ diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index e25a17f3b9..bd0ef983c4 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,11 +1,12 @@ from typing import Optional -from pydantic import AliasChoices, BaseModel, Field, NonNegativeInt, PositiveInt +from pydantic import AliasChoices, Field, NonNegativeInt, PositiveInt, computed_field +from pydantic_settings import BaseSettings from configs.feature.hosted_service import HostedServiceConfig -class SecurityConfig(BaseModel): +class SecurityConfig(BaseSettings): """ Secret Key configs """ @@ -17,8 +18,12 @@ class SecurityConfig(BaseModel): default=None, ) + RESET_PASSWORD_TOKEN_EXPIRY_HOURS: PositiveInt = Field( + description='Expiry time in hours for reset token', + default=24, + ) -class AppExecutionConfig(BaseModel): +class AppExecutionConfig(BaseSettings): """ App Execution configs """ @@ -28,12 +33,12 @@ class AppExecutionConfig(BaseModel): ) -class CodeExecutionSandboxConfig(BaseModel): +class CodeExecutionSandboxConfig(BaseSettings): """ Code Execution Sandbox configs """ CODE_EXECUTION_ENDPOINT: str = Field( - description='whether to enable HTTP response compression of gzip', + description='endpoint URL of code execution servcie', default='http://sandbox:8194', ) @@ -43,36 +48,36 @@ class CodeExecutionSandboxConfig(BaseModel): ) -class EndpointConfig(BaseModel): +class EndpointConfig(BaseSettings): """ Module URL configs """ CONSOLE_API_URL: str = Field( description='The backend URL prefix of the console API.' 'used to concatenate the login authorization callback or notion integration callback.', - default='https://cloud.dify.ai', + default='', ) CONSOLE_WEB_URL: str = Field( description='The front-end URL prefix of the console web.' 'used to concatenate some front-end addresses and for CORS configuration use.', - default='https://cloud.dify.ai', + default='', ) SERVICE_API_URL: str = Field( description='Service API Url prefix.' 'used to display Service API Base Url to the front-end.', - default='https://api.dify.ai', + default='', ) APP_WEB_URL: str = Field( description='WebApp Url prefix.' 'used to display WebAPP API Base Url to the front-end.', - default='https://udify.app', + default='', ) -class FileAccessConfig(BaseModel): +class FileAccessConfig(BaseSettings): """ File Access configs """ @@ -82,7 +87,7 @@ class FileAccessConfig(BaseModel): 'Url is signed and has expiration time.', validation_alias=AliasChoices('FILES_URL', 'CONSOLE_API_URL'), alias_priority=1, - default='https://cloud.dify.ai', + default='', ) FILES_ACCESS_TIMEOUT: int = Field( @@ -91,7 +96,7 @@ class FileAccessConfig(BaseModel): ) -class FileUploadConfig(BaseModel): +class FileUploadConfig(BaseSettings): """ File Uploading configs """ @@ -116,7 +121,7 @@ class FileUploadConfig(BaseModel): ) -class HttpConfig(BaseModel): +class HttpConfig(BaseSettings): """ HTTP configs """ @@ -125,8 +130,30 @@ class HttpConfig(BaseModel): default=False, ) + inner_CONSOLE_CORS_ALLOW_ORIGINS: str = Field( + description='', + validation_alias=AliasChoices('CONSOLE_CORS_ALLOW_ORIGINS', 'CONSOLE_WEB_URL'), + default='', + ) -class InnerAPIConfig(BaseModel): + @computed_field + @property + def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]: + return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(',') + + inner_WEB_API_CORS_ALLOW_ORIGINS: str = Field( + description='', + validation_alias=AliasChoices('WEB_API_CORS_ALLOW_ORIGINS'), + default='*', + ) + + @computed_field + @property + def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]: + return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(',') + + +class InnerAPIConfig(BaseSettings): """ Inner API configs """ @@ -141,7 +168,7 @@ class InnerAPIConfig(BaseModel): ) -class LoggingConfig(BaseModel): +class LoggingConfig(BaseSettings): """ Logging configs """ @@ -173,7 +200,7 @@ class LoggingConfig(BaseModel): ) -class ModelLoadBalanceConfig(BaseModel): +class ModelLoadBalanceConfig(BaseSettings): """ Model load balance configs """ @@ -183,7 +210,7 @@ class ModelLoadBalanceConfig(BaseModel): ) -class BillingConfig(BaseModel): +class BillingConfig(BaseSettings): """ Platform Billing Configurations """ @@ -193,7 +220,7 @@ class BillingConfig(BaseModel): ) -class UpdateConfig(BaseModel): +class UpdateConfig(BaseSettings): """ Update configs """ @@ -203,7 +230,7 @@ class UpdateConfig(BaseModel): ) -class WorkflowConfig(BaseModel): +class WorkflowConfig(BaseSettings): """ Workflow feature configs """ @@ -224,7 +251,7 @@ class WorkflowConfig(BaseModel): ) -class OAuthConfig(BaseModel): +class OAuthConfig(BaseSettings): """ oauth configs """ @@ -254,7 +281,7 @@ class OAuthConfig(BaseModel): ) -class ModerationConfig(BaseModel): +class ModerationConfig(BaseSettings): """ Moderation in app configs. """ @@ -266,7 +293,7 @@ class ModerationConfig(BaseModel): ) -class ToolConfig(BaseModel): +class ToolConfig(BaseSettings): """ Tool configs """ @@ -277,7 +304,7 @@ class ToolConfig(BaseModel): ) -class MailConfig(BaseModel): +class MailConfig(BaseSettings): """ Mail Configurations """ @@ -309,7 +336,7 @@ class MailConfig(BaseModel): SMTP_PORT: Optional[int] = Field( description='smtp server port', - default=None, + default=465, ) SMTP_USERNAME: Optional[str] = Field( @@ -333,7 +360,7 @@ class MailConfig(BaseModel): ) -class RagEtlConfig(BaseModel): +class RagEtlConfig(BaseSettings): """ RAG ETL Configurations. """ @@ -359,7 +386,7 @@ class RagEtlConfig(BaseModel): ) -class DataSetConfig(BaseModel): +class DataSetConfig(BaseSettings): """ Dataset configs """ @@ -370,7 +397,7 @@ class DataSetConfig(BaseModel): ) -class WorkspaceConfig(BaseModel): +class WorkspaceConfig(BaseSettings): """ Workspace configs """ @@ -381,7 +408,7 @@ class WorkspaceConfig(BaseModel): ) -class IndexingConfig(BaseModel): +class IndexingConfig(BaseSettings): """ Indexing configs. """ @@ -392,7 +419,7 @@ class IndexingConfig(BaseModel): ) -class ImageFormatConfig(BaseModel): +class ImageFormatConfig(BaseSettings): MULTIMODAL_SEND_IMAGE_FORMAT: str = Field( description='multi model send image format, support base64, url, default is base64', default='base64', diff --git a/api/configs/feature/hosted_service/__init__.py b/api/configs/feature/hosted_service/__init__.py index b09b6fd041..209d46bb76 100644 --- a/api/configs/feature/hosted_service/__init__.py +++ b/api/configs/feature/hosted_service/__init__.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, NonNegativeInt +from pydantic import Field, NonNegativeInt +from pydantic_settings import BaseSettings -class HostedOpenAiConfig(BaseModel): +class HostedOpenAiConfig(BaseSettings): """ Hosted OpenAI service config """ @@ -68,7 +69,7 @@ class HostedOpenAiConfig(BaseModel): ) -class HostedAzureOpenAiConfig(BaseModel): +class HostedAzureOpenAiConfig(BaseSettings): """ Hosted OpenAI service config """ @@ -94,7 +95,7 @@ class HostedAzureOpenAiConfig(BaseModel): ) -class HostedAnthropicConfig(BaseModel): +class HostedAnthropicConfig(BaseSettings): """ Hosted Azure OpenAI service config """ @@ -125,7 +126,7 @@ class HostedAnthropicConfig(BaseModel): ) -class HostedMinmaxConfig(BaseModel): +class HostedMinmaxConfig(BaseSettings): """ Hosted Minmax service config """ @@ -136,7 +137,7 @@ class HostedMinmaxConfig(BaseModel): ) -class HostedSparkConfig(BaseModel): +class HostedSparkConfig(BaseSettings): """ Hosted Spark service config """ @@ -147,7 +148,7 @@ class HostedSparkConfig(BaseModel): ) -class HostedZhipuAIConfig(BaseModel): +class HostedZhipuAIConfig(BaseSettings): """ Hosted Minmax service config """ @@ -158,7 +159,7 @@ class HostedZhipuAIConfig(BaseModel): ) -class HostedModerationConfig(BaseModel): +class HostedModerationConfig(BaseSettings): """ Hosted Moderation service config """ @@ -174,7 +175,7 @@ class HostedModerationConfig(BaseModel): ) -class HostedFetchAppTemplateConfig(BaseModel): +class HostedFetchAppTemplateConfig(BaseSettings): """ Hosted Moderation service config """ diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index c454f4e603..d8a2fe683a 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -1,12 +1,14 @@ from typing import Any, Optional -from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt, computed_field +from pydantic import Field, NonNegativeInt, PositiveInt, computed_field +from pydantic_settings import BaseSettings -from configs.middleware.redis_config import RedisConfig +from configs.middleware.cache.redis_config import RedisConfig from configs.middleware.storage.aliyun_oss_storage_config import AliyunOSSStorageConfig from configs.middleware.storage.amazon_s3_storage_config import S3StorageConfig from configs.middleware.storage.azure_blob_storage_config import AzureBlobStorageConfig from configs.middleware.storage.google_cloud_storage_config import GoogleCloudStorageConfig +from configs.middleware.storage.oci_storage_config import OCIStorageConfig from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig from configs.middleware.vdb.chroma_config import ChromaConfig from configs.middleware.vdb.milvus_config import MilvusConfig @@ -21,7 +23,7 @@ from configs.middleware.vdb.tidb_vector_config import TiDBVectorConfig from configs.middleware.vdb.weaviate_config import WeaviateConfig -class StorageConfig(BaseModel): +class StorageConfig(BaseSettings): STORAGE_TYPE: str = Field( description='storage type,' ' default to `local`,' @@ -35,14 +37,14 @@ class StorageConfig(BaseModel): ) -class VectorStoreConfig(BaseModel): +class VectorStoreConfig(BaseSettings): VECTOR_STORE: Optional[str] = Field( description='vector store type', default=None, ) -class KeywordStoreConfig(BaseModel): +class KeywordStoreConfig(BaseSettings): KEYWORD_STORE: str = Field( description='keyword store type', default='jieba', @@ -80,6 +82,11 @@ class DatabaseConfig: default='', ) + DB_EXTRAS: str = Field( + description='db extras options. Example: keepalives_idle=60&keepalives=1', + default='', + ) + SQLALCHEMY_DATABASE_URI_SCHEME: str = Field( description='db uri scheme', default='postgresql', @@ -88,7 +95,12 @@ class DatabaseConfig: @computed_field @property def SQLALCHEMY_DATABASE_URI(self) -> str: - db_extras = f"?client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else "" + db_extras = ( + f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" + if self.DB_CHARSET + else self.DB_EXTRAS + ).strip("&") + db_extras = f"?{db_extras}" if db_extras else "" return (f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://" f"{self.DB_USERNAME}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}" f"{db_extras}") @@ -113,7 +125,7 @@ class DatabaseConfig: default=False, ) - SQLALCHEMY_ECHO: bool = Field( + SQLALCHEMY_ECHO: bool | str = Field( description='whether to enable SqlAlchemy echo', default=False, ) @@ -143,7 +155,7 @@ class CeleryConfig(DatabaseConfig): @computed_field @property - def CELERY_RESULT_BACKEND(self) -> str: + def CELERY_RESULT_BACKEND(self) -> str | None: return 'db+{}'.format(self.SQLALCHEMY_DATABASE_URI) \ if self.CELERY_BACKEND == 'database' else self.CELERY_BROKER_URL @@ -167,6 +179,7 @@ class MiddlewareConfig( GoogleCloudStorageConfig, TencentCloudCOSStorageConfig, S3StorageConfig, + OCIStorageConfig, # configs of vdb and vdb providers VectorStoreConfig, diff --git a/api/requirements.txt b/api/configs/middleware/cache/__init__.py similarity index 100% rename from api/requirements.txt rename to api/configs/middleware/cache/__init__.py diff --git a/api/configs/middleware/redis_config.py b/api/configs/middleware/cache/redis_config.py similarity index 84% rename from api/configs/middleware/redis_config.py rename to api/configs/middleware/cache/redis_config.py index 4cc40bbe6d..436ba5d4c0 100644 --- a/api/configs/middleware/redis_config.py +++ b/api/configs/middleware/cache/redis_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt +from pydantic import Field, NonNegativeInt, PositiveInt +from pydantic_settings import BaseSettings -class RedisConfig(BaseModel): +class RedisConfig(BaseSettings): """ Redis configs """ diff --git a/api/configs/middleware/storage/aliyun_oss_storage_config.py b/api/configs/middleware/storage/aliyun_oss_storage_config.py index 67921149d6..19e6cafb12 100644 --- a/api/configs/middleware/storage/aliyun_oss_storage_config.py +++ b/api/configs/middleware/storage/aliyun_oss_storage_config.py @@ -1,39 +1,40 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class AliyunOSSStorageConfig(BaseModel): +class AliyunOSSStorageConfig(BaseSettings): """ Aliyun storage configs """ ALIYUN_OSS_BUCKET_NAME: Optional[str] = Field( - description='Aliyun storage ', + description='Aliyun OSS bucket name', default=None, ) ALIYUN_OSS_ACCESS_KEY: Optional[str] = Field( - description='Aliyun storage access key', + description='Aliyun OSS access key', default=None, ) ALIYUN_OSS_SECRET_KEY: Optional[str] = Field( - description='Aliyun storage secret key', + description='Aliyun OSS secret key', default=None, ) ALIYUN_OSS_ENDPOINT: Optional[str] = Field( - description='Aliyun storage endpoint URL', + description='Aliyun OSS endpoint URL', default=None, ) ALIYUN_OSS_REGION: Optional[str] = Field( - description='Aliyun storage region', + description='Aliyun OSS region', default=None, ) ALIYUN_OSS_AUTH_VERSION: Optional[str] = Field( - description='Aliyun storage authentication version', + description='Aliyun OSS authentication version', default=None, ) diff --git a/api/configs/middleware/storage/amazon_s3_storage_config.py b/api/configs/middleware/storage/amazon_s3_storage_config.py index 21fe425fa8..2566fbd5da 100644 --- a/api/configs/middleware/storage/amazon_s3_storage_config.py +++ b/api/configs/middleware/storage/amazon_s3_storage_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class S3StorageConfig(BaseModel): +class S3StorageConfig(BaseSettings): """ S3 storage configs """ diff --git a/api/configs/middleware/storage/azure_blob_storage_config.py b/api/configs/middleware/storage/azure_blob_storage_config.py index a37aa496f1..26e441c89b 100644 --- a/api/configs/middleware/storage/azure_blob_storage_config.py +++ b/api/configs/middleware/storage/azure_blob_storage_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class AzureBlobStorageConfig(BaseModel): +class AzureBlobStorageConfig(BaseSettings): """ Azure Blob storage configs """ @@ -24,6 +25,6 @@ class AzureBlobStorageConfig(BaseModel): ) AZURE_BLOB_ACCOUNT_URL: Optional[str] = Field( - description='Azure Blob account url', + description='Azure Blob account URL', default=None, ) diff --git a/api/configs/middleware/storage/google_cloud_storage_config.py b/api/configs/middleware/storage/google_cloud_storage_config.py index 1f4d9f9883..e1b0e34e0c 100644 --- a/api/configs/middleware/storage/google_cloud_storage_config.py +++ b/api/configs/middleware/storage/google_cloud_storage_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class GoogleCloudStorageConfig(BaseModel): +class GoogleCloudStorageConfig(BaseSettings): """ Google Cloud storage configs """ diff --git a/api/configs/middleware/storage/oci_storage_config.py b/api/configs/middleware/storage/oci_storage_config.py new file mode 100644 index 0000000000..6c0c067469 --- /dev/null +++ b/api/configs/middleware/storage/oci_storage_config.py @@ -0,0 +1,36 @@ +from typing import Optional + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class OCIStorageConfig(BaseSettings): + """ + OCI storage configs + """ + + OCI_ENDPOINT: Optional[str] = Field( + description='OCI storage endpoint', + default=None, + ) + + OCI_REGION: Optional[str] = Field( + description='OCI storage region', + default=None, + ) + + OCI_BUCKET_NAME: Optional[str] = Field( + description='OCI storage bucket name', + default=None, + ) + + OCI_ACCESS_KEY: Optional[str] = Field( + description='OCI storage access key', + default=None, + ) + + OCI_SECRET_KEY: Optional[str] = Field( + description='OCI storage secret key', + default=None, + ) + diff --git a/api/configs/middleware/storage/tencent_cos_storage_config.py b/api/configs/middleware/storage/tencent_cos_storage_config.py index 1bcc4b7b44..1060c7b93e 100644 --- a/api/configs/middleware/storage/tencent_cos_storage_config.py +++ b/api/configs/middleware/storage/tencent_cos_storage_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class TencentCloudCOSStorageConfig(BaseModel): +class TencentCloudCOSStorageConfig(BaseSettings): """ Tencent Cloud COS storage configs """ diff --git a/api/configs/middleware/vdb/chroma_config.py b/api/configs/middleware/vdb/chroma_config.py index a764ddc796..f365879efb 100644 --- a/api/configs/middleware/vdb/chroma_config.py +++ b/api/configs/middleware/vdb/chroma_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class ChromaConfig(BaseModel): +class ChromaConfig(BaseSettings): """ Chroma configs """ diff --git a/api/configs/middleware/vdb/milvus_config.py b/api/configs/middleware/vdb/milvus_config.py index 88855db877..01502d4590 100644 --- a/api/configs/middleware/vdb/milvus_config.py +++ b/api/configs/middleware/vdb/milvus_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class MilvusConfig(BaseModel): +class MilvusConfig(BaseSettings): """ Milvus configs """ @@ -29,11 +30,11 @@ class MilvusConfig(BaseModel): ) MILVUS_SECURE: bool = Field( - description='wheter to use SSL connection for Milvus', + description='whether to use SSL connection for Milvus', default=False, ) MILVUS_DATABASE: str = Field( - description='Milvus database', + description='Milvus database, default to `default`', default='default', ) diff --git a/api/configs/middleware/vdb/opensearch_config.py b/api/configs/middleware/vdb/opensearch_config.py index 4d77e7be94..15d6f5b6a9 100644 --- a/api/configs/middleware/vdb/opensearch_config.py +++ b/api/configs/middleware/vdb/opensearch_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class OpenSearchConfig(BaseModel): +class OpenSearchConfig(BaseSettings): """ OpenSearch configs """ diff --git a/api/configs/middleware/vdb/oracle_config.py b/api/configs/middleware/vdb/oracle_config.py index e5c479a66f..888fc19492 100644 --- a/api/configs/middleware/vdb/oracle_config.py +++ b/api/configs/middleware/vdb/oracle_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class OracleConfig(BaseModel): +class OracleConfig(BaseSettings): """ ORACLE configs """ @@ -15,7 +16,7 @@ class OracleConfig(BaseModel): ORACLE_PORT: Optional[PositiveInt] = Field( description='ORACLE port', - default=None, + default=1521, ) ORACLE_USER: Optional[str] = Field( diff --git a/api/configs/middleware/vdb/pgvector_config.py b/api/configs/middleware/vdb/pgvector_config.py index c544a84031..8a677f60a3 100644 --- a/api/configs/middleware/vdb/pgvector_config.py +++ b/api/configs/middleware/vdb/pgvector_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class PGVectorConfig(BaseModel): +class PGVectorConfig(BaseSettings): """ PGVector configs """ @@ -15,7 +16,7 @@ class PGVectorConfig(BaseModel): PGVECTOR_PORT: Optional[PositiveInt] = Field( description='PGVector port', - default=None, + default=5433, ) PGVECTOR_USER: Optional[str] = Field( diff --git a/api/configs/middleware/vdb/pgvectors_config.py b/api/configs/middleware/vdb/pgvectors_config.py index 78cb4e570e..39f52f22ff 100644 --- a/api/configs/middleware/vdb/pgvectors_config.py +++ b/api/configs/middleware/vdb/pgvectors_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class PGVectoRSConfig(BaseModel): +class PGVectoRSConfig(BaseSettings): """ PGVectoRS configs """ @@ -15,7 +16,7 @@ class PGVectoRSConfig(BaseModel): PGVECTO_RS_PORT: Optional[PositiveInt] = Field( description='PGVectoRS port', - default=None, + default=5431, ) PGVECTO_RS_USER: Optional[str] = Field( diff --git a/api/configs/middleware/vdb/qdrant_config.py b/api/configs/middleware/vdb/qdrant_config.py index f0223ffa1c..c85bf9c7dc 100644 --- a/api/configs/middleware/vdb/qdrant_config.py +++ b/api/configs/middleware/vdb/qdrant_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt +from pydantic import Field, NonNegativeInt, PositiveInt +from pydantic_settings import BaseSettings -class QdrantConfig(BaseModel): +class QdrantConfig(BaseSettings): """ Qdrant configs """ diff --git a/api/configs/middleware/vdb/relyt_config.py b/api/configs/middleware/vdb/relyt_config.py index b550fa8e00..be93185f3c 100644 --- a/api/configs/middleware/vdb/relyt_config.py +++ b/api/configs/middleware/vdb/relyt_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class RelytConfig(BaseModel): +class RelytConfig(BaseSettings): """ Relyt configs """ diff --git a/api/configs/middleware/vdb/tencent_vector_config.py b/api/configs/middleware/vdb/tencent_vector_config.py index 083f10b40b..531ec84068 100644 --- a/api/configs/middleware/vdb/tencent_vector_config.py +++ b/api/configs/middleware/vdb/tencent_vector_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, NonNegativeInt, PositiveInt +from pydantic_settings import BaseSettings -class TencentVectorDBConfig(BaseModel): +class TencentVectorDBConfig(BaseSettings): """ Tencent Vector configs """ @@ -14,17 +15,17 @@ class TencentVectorDBConfig(BaseModel): ) TENCENT_VECTOR_DB_API_KEY: Optional[str] = Field( - description='Tencent Vector api key', + description='Tencent Vector API key', default=None, ) TENCENT_VECTOR_DB_TIMEOUT: PositiveInt = Field( - description='Tencent Vector timeout', + description='Tencent Vector timeout in seconds', default=30, ) TENCENT_VECTOR_DB_USERNAME: Optional[str] = Field( - description='Tencent Vector password', + description='Tencent Vector username', default=None, ) @@ -38,7 +39,12 @@ class TencentVectorDBConfig(BaseModel): default=1, ) - TENCENT_VECTOR_DB_REPLICAS: PositiveInt = Field( + TENCENT_VECTOR_DB_REPLICAS: NonNegativeInt = Field( description='Tencent Vector replicas', default=2, ) + + TENCENT_VECTOR_DB_DATABASE: Optional[str] = Field( + description='Tencent Vector Database', + default=None, + ) diff --git a/api/configs/middleware/vdb/tidb_vector_config.py b/api/configs/middleware/vdb/tidb_vector_config.py index 53f985e386..8d459691a8 100644 --- a/api/configs/middleware/vdb/tidb_vector_config.py +++ b/api/configs/middleware/vdb/tidb_vector_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class TiDBVectorConfig(BaseModel): +class TiDBVectorConfig(BaseSettings): """ TiDB Vector configs """ @@ -15,7 +16,7 @@ class TiDBVectorConfig(BaseModel): TIDB_VECTOR_PORT: Optional[PositiveInt] = Field( description='TiDB Vector port', - default=None, + default=4000, ) TIDB_VECTOR_USER: Optional[str] = Field( diff --git a/api/configs/middleware/vdb/weaviate_config.py b/api/configs/middleware/vdb/weaviate_config.py index d1c9f5b5be..b985ecea12 100644 --- a/api/configs/middleware/vdb/weaviate_config.py +++ b/api/configs/middleware/vdb/weaviate_config.py @@ -1,9 +1,10 @@ from typing import Optional -from pydantic import BaseModel, Field, PositiveInt +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings -class WeaviateConfig(BaseModel): +class WeaviateConfig(BaseSettings): """ Weaviate configs """ diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 95ccb850ed..dc812a15be 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -1,14 +1,15 @@ -from pydantic import BaseModel, Field +from pydantic import Field +from pydantic_settings import BaseSettings -class PackagingInfo(BaseModel): +class PackagingInfo(BaseSettings): """ Packaging build information """ CURRENT_VERSION: str = Field( description='Dify version', - default='0.6.11', + default='0.6.12-fix1', ) COMMIT_SHA: str = Field( diff --git a/api/constants/languages.py b/api/constants/languages.py index 14bb149e1d..efc668d4ee 100644 --- a/api/constants/languages.py +++ b/api/constants/languages.py @@ -1,7 +1,3 @@ - - -languages = ['en-US', 'zh-Hans', 'zh-Hant', 'pt-BR', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'ko-KR', 'ru-RU', 'it-IT', 'uk-UA', 'vi-VN', 'pl-PL', 'hi-IN'] - language_timezone_mapping = { 'en-US': 'America/New_York', 'zh-Hans': 'Asia/Shanghai', @@ -18,9 +14,11 @@ language_timezone_mapping = { 'vi-VN': 'Asia/Ho_Chi_Minh', 'ro-RO': 'Europe/Bucharest', 'pl-PL': 'Europe/Warsaw', - 'hi-IN': 'Asia/Kolkata' + 'hi-IN': 'Asia/Kolkata', } +languages = list(language_timezone_mapping.keys()) + def supported_language(lang): if lang in languages: diff --git a/api/constants/model_template.py b/api/constants/model_template.py index 42e182236f..cc5a370254 100644 --- a/api/constants/model_template.py +++ b/api/constants/model_template.py @@ -22,7 +22,7 @@ default_app_templates = { 'model_config': { 'model': { "provider": "openai", - "name": "gpt-4", + "name": "gpt-4o", "mode": "chat", "completion_params": {} }, @@ -51,7 +51,7 @@ default_app_templates = { 'model_config': { 'model': { "provider": "openai", - "name": "gpt-4", + "name": "gpt-4o", "mode": "chat", "completion_params": {} } @@ -77,7 +77,7 @@ default_app_templates = { 'model_config': { 'model': { "provider": "openai", - "name": "gpt-4", + "name": "gpt-4o", "mode": "chat", "completion_params": {} } diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index 29eac070a0..bef40bea7e 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -20,6 +20,7 @@ from .app import ( generator, message, model_config, + ops_trace, site, statistic, workflow, @@ -29,7 +30,7 @@ from .app import ( ) # Import auth controllers -from .auth import activate, data_source_bearer_auth, data_source_oauth, login, oauth +from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth # Import billing controllers from .billing import billing diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 082838334a..fb3205813d 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,4 +1,3 @@ -import json import uuid from flask_login import current_user @@ -9,17 +8,14 @@ from controllers.console import api from controllers.console.app.wraps import get_app_model from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check -from core.tools.tool_manager import ToolManager -from core.tools.utils.configuration import ToolParameterConfigurationManager +from core.ops.ops_trace_manager import OpsTraceManager from fields.app_fields import ( app_detail_fields, app_detail_fields_with_site, app_pagination_fields, ) from libs.login import login_required -from models.model import App, AppMode, AppModelConfig from services.app_service import AppService -from services.tag_service import TagService ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion'] @@ -194,6 +190,10 @@ class AppExportApi(Resource): @get_app_model def get(self, app_model): """Export app""" + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + app_service = AppService() return { @@ -286,6 +286,39 @@ class AppApiStatus(Resource): return app_model +class AppTraceApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, app_id): + """Get app trace""" + app_trace_config = OpsTraceManager.get_app_tracing_config( + app_id=app_id + ) + + return app_trace_config + + @setup_required + @login_required + @account_initialization_required + def post(self, app_id): + # add app trace + if not current_user.is_admin_or_owner: + raise Forbidden() + parser = reqparse.RequestParser() + parser.add_argument('enabled', type=bool, required=True, location='json') + parser.add_argument('tracing_provider', type=str, required=True, location='json') + args = parser.parse_args() + + OpsTraceManager.update_app_tracing_config( + app_id=app_id, + enabled=args['enabled'], + tracing_provider=args['tracing_provider'], + ) + + return {"result": "success"} + + api.add_resource(AppListApi, '/apps') api.add_resource(AppImportApi, '/apps/import') api.add_resource(AppApi, '/apps/') @@ -295,3 +328,4 @@ api.add_resource(AppNameApi, '/apps//name') api.add_resource(AppIconApi, '/apps//icon') api.add_resource(AppSiteStatus, '/apps//site-enable') api.add_resource(AppApiStatus, '/apps//api-enable') +api.add_resource(AppTraceApi, '/apps//trace') diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index fbe42fbd2a..f6feed1221 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -97,3 +97,21 @@ class DraftWorkflowNotSync(BaseHTTPException): error_code = 'draft_workflow_not_sync' description = "Workflow graph might have been modified, please refresh and resubmit." code = 400 + + +class TracingConfigNotExist(BaseHTTPException): + error_code = 'trace_config_not_exist' + description = "Trace config not exist." + code = 400 + + +class TracingConfigIsExist(BaseHTTPException): + error_code = 'trace_config_is_exist' + description = "Trace config is exist." + code = 400 + + +class TracingConfigCheckError(BaseHTTPException): + error_code = 'trace_config_check_error' + description = "Invalid Credentials." + code = 400 diff --git a/api/controllers/console/app/model_config.py b/api/controllers/console/app/model_config.py index 05d7958f7d..c8df879a29 100644 --- a/api/controllers/console/app/model_config.py +++ b/api/controllers/console/app/model_config.py @@ -25,6 +25,7 @@ class ModelConfigResource(Resource): @account_initialization_required @get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION]) def post(self, app_model): + """Modify app model config""" # validate config model_configuration = AppModelConfigService.validate_configuration( diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py new file mode 100644 index 0000000000..c0cf7b9e33 --- /dev/null +++ b/api/controllers/console/app/ops_trace.py @@ -0,0 +1,101 @@ +from flask_restful import Resource, reqparse + +from controllers.console import api +from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist +from controllers.console.setup import setup_required +from controllers.console.wraps import account_initialization_required +from libs.login import login_required +from services.ops_service import OpsService + + +class TraceAppConfigApi(Resource): + """ + Manage trace app configurations + """ + + @setup_required + @login_required + @account_initialization_required + def get(self, app_id): + parser = reqparse.RequestParser() + parser.add_argument('tracing_provider', type=str, required=True, location='args') + args = parser.parse_args() + + try: + trace_config = OpsService.get_tracing_app_config( + app_id=app_id, tracing_provider=args['tracing_provider'] + ) + if not trace_config: + return {"has_not_configured": True} + return trace_config + except Exception as e: + raise e + + @setup_required + @login_required + @account_initialization_required + def post(self, app_id): + """Create a new trace app configuration""" + parser = reqparse.RequestParser() + parser.add_argument('tracing_provider', type=str, required=True, location='json') + parser.add_argument('tracing_config', type=dict, required=True, location='json') + args = parser.parse_args() + + try: + result = OpsService.create_tracing_app_config( + app_id=app_id, + tracing_provider=args['tracing_provider'], + tracing_config=args['tracing_config'] + ) + if not result: + raise TracingConfigIsExist() + if result.get('error'): + raise TracingConfigCheckError() + return result + except Exception as e: + raise e + + @setup_required + @login_required + @account_initialization_required + def patch(self, app_id): + """Update an existing trace app configuration""" + parser = reqparse.RequestParser() + parser.add_argument('tracing_provider', type=str, required=True, location='json') + parser.add_argument('tracing_config', type=dict, required=True, location='json') + args = parser.parse_args() + + try: + result = OpsService.update_tracing_app_config( + app_id=app_id, + tracing_provider=args['tracing_provider'], + tracing_config=args['tracing_config'] + ) + if not result: + raise TracingConfigNotExist() + return {"result": "success"} + except Exception as e: + raise e + + @setup_required + @login_required + @account_initialization_required + def delete(self, app_id): + """Delete an existing trace app configuration""" + parser = reqparse.RequestParser() + parser.add_argument('tracing_provider', type=str, required=True, location='args') + args = parser.parse_args() + + try: + result = OpsService.delete_tracing_app_config( + app_id=app_id, + tracing_provider=args['tracing_provider'] + ) + if not result: + raise TracingConfigNotExist() + return {"result": "success"} + except Exception as e: + raise e + + +api.add_resource(TraceAppConfigApi, '/apps//trace-config') diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 2024db65b2..6aa9f0b475 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -20,6 +20,8 @@ def parse_app_site_args(): parser.add_argument('icon_background', type=str, required=False, location='json') parser.add_argument('description', type=str, required=False, location='json') parser.add_argument('default_language', type=supported_language, required=False, location='json') + parser.add_argument('chat_color_theme', type=str, required=False, location='json') + parser.add_argument('chat_color_theme_inverted', type=bool, required=False, location='json') parser.add_argument('customize_domain', type=str, required=False, location='json') parser.add_argument('copyright', type=str, required=False, location='json') parser.add_argument('privacy_policy', type=str, required=False, location='json') @@ -55,6 +57,8 @@ class AppSite(Resource): 'icon_background', 'description', 'default_language', + 'chat_color_theme', + 'chat_color_theme_inverted', 'customize_domain', 'copyright', 'privacy_policy', diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 668a722bf7..08c2d47746 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -109,6 +109,34 @@ class DraftWorkflowApi(Resource): } +class DraftWorkflowImportApi(Resource): + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @marshal_with(workflow_fields) + def post(self, app_model: App): + """ + Import draft workflow + """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + + parser = reqparse.RequestParser() + parser.add_argument('data', type=str, required=True, nullable=False, location='json') + args = parser.parse_args() + + workflow_service = WorkflowService() + workflow = workflow_service.import_draft_workflow( + app_model=app_model, + data=args['data'], + account=current_user + ) + + return workflow + + class AdvancedChatDraftWorkflowRunApi(Resource): @setup_required @login_required @@ -439,6 +467,7 @@ class ConvertToWorkflowApi(Resource): api.add_resource(DraftWorkflowApi, '/apps//workflows/draft') +api.add_resource(DraftWorkflowImportApi, '/apps//workflows/draft/import') api.add_resource(AdvancedChatDraftWorkflowRunApi, '/apps//advanced-chat/workflows/draft/run') api.add_resource(DraftWorkflowRunApi, '/apps//workflows/draft/run') api.add_resource(WorkflowTaskStopApi, '/apps//workflow-runs/tasks//stop') diff --git a/api/controllers/console/auth/data_source_oauth.py b/api/controllers/console/auth/data_source_oauth.py index 293ec1c4d3..6268347244 100644 --- a/api/controllers/console/auth/data_source_oauth.py +++ b/api/controllers/console/auth/data_source_oauth.py @@ -6,6 +6,7 @@ from flask_login import current_user from flask_restful import Resource from werkzeug.exceptions import Forbidden +from configs import dify_config from controllers.console import api from libs.login import login_required from libs.oauth_data_source import NotionOAuth @@ -16,11 +17,11 @@ from ..wraps import account_initialization_required def get_oauth_providers(): with current_app.app_context(): - notion_oauth = NotionOAuth(client_id=current_app.config.get('NOTION_CLIENT_ID'), - client_secret=current_app.config.get( - 'NOTION_CLIENT_SECRET'), - redirect_uri=current_app.config.get( - 'CONSOLE_API_URL') + '/console/api/oauth/data-source/callback/notion') + if not dify_config.NOTION_CLIENT_ID or not dify_config.NOTION_CLIENT_SECRET: + return {} + notion_oauth = NotionOAuth(client_id=dify_config.NOTION_CLIENT_ID, + client_secret=dify_config.NOTION_CLIENT_SECRET, + redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/data-source/callback/notion') OAUTH_PROVIDERS = { 'notion': notion_oauth @@ -39,8 +40,10 @@ class OAuthDataSource(Resource): print(vars(oauth_provider)) if not oauth_provider: return {'error': 'Invalid provider'}, 400 - if current_app.config.get('NOTION_INTEGRATION_TYPE') == 'internal': - internal_secret = current_app.config.get('NOTION_INTERNAL_SECRET') + if dify_config.NOTION_INTEGRATION_TYPE == 'internal': + internal_secret = dify_config.NOTION_INTERNAL_SECRET + if not internal_secret: + return {'error': 'Internal secret is not set'}, oauth_provider.save_internal_access_token(internal_secret) return { 'data': '' } else: @@ -60,13 +63,13 @@ class OAuthDataSourceCallback(Resource): if 'code' in request.args: code = request.args.get('code') - return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&code={code}') + return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&code={code}') elif 'error' in request.args: error = request.args.get('error') - return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&error={error}') + return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&error={error}') else: - return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&error=Access denied') + return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied') class OAuthDataSourceBinding(Resource): diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index c55ff8707d..53dab3298f 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -5,3 +5,28 @@ class ApiKeyAuthFailedError(BaseHTTPException): error_code = 'auth_failed' description = "{message}" code = 500 + + +class InvalidEmailError(BaseHTTPException): + error_code = 'invalid_email' + description = "The email address is not valid." + code = 400 + + +class PasswordMismatchError(BaseHTTPException): + error_code = 'password_mismatch' + description = "The passwords do not match." + code = 400 + + +class InvalidTokenError(BaseHTTPException): + error_code = 'invalid_or_expired_token' + description = "The token is invalid or has expired." + code = 400 + + +class PasswordResetRateLimitExceededError(BaseHTTPException): + error_code = 'password_reset_rate_limit_exceeded' + description = "Password reset rate limit exceeded. Try again later." + code = 429 + diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py new file mode 100644 index 0000000000..d78be770ab --- /dev/null +++ b/api/controllers/console/auth/forgot_password.py @@ -0,0 +1,107 @@ +import base64 +import logging +import secrets + +from flask_restful import Resource, reqparse + +from controllers.console import api +from controllers.console.auth.error import ( + InvalidEmailError, + InvalidTokenError, + PasswordMismatchError, + PasswordResetRateLimitExceededError, +) +from controllers.console.setup import setup_required +from extensions.ext_database import db +from libs.helper import email as email_validate +from libs.password import hash_password, valid_password +from models.account import Account +from services.account_service import AccountService +from services.errors.account import RateLimitExceededError + + +class ForgotPasswordSendEmailApi(Resource): + + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument('email', type=str, required=True, location='json') + args = parser.parse_args() + + email = args['email'] + + if not email_validate(email): + raise InvalidEmailError() + + account = Account.query.filter_by(email=email).first() + + if account: + try: + AccountService.send_reset_password_email(account=account) + except RateLimitExceededError: + logging.warning(f"Rate limit exceeded for email: {account.email}") + raise PasswordResetRateLimitExceededError() + else: + # Return success to avoid revealing email registration status + logging.warning(f"Attempt to reset password for unregistered email: {email}") + + return {"result": "success"} + + +class ForgotPasswordCheckApi(Resource): + + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument('token', type=str, required=True, nullable=False, location='json') + args = parser.parse_args() + token = args['token'] + + reset_data = AccountService.get_reset_password_data(token) + + if reset_data is None: + return {'is_valid': False, 'email': None} + return {'is_valid': True, 'email': reset_data.get('email')} + + +class ForgotPasswordResetApi(Resource): + + @setup_required + 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() + + new_password = args['new_password'] + password_confirm = args['password_confirm'] + + if str(new_password).strip() != str(password_confirm).strip(): + raise PasswordMismatchError() + + token = args['token'] + reset_data = AccountService.get_reset_password_data(token) + + if reset_data is None: + raise InvalidTokenError() + + AccountService.revoke_reset_password_token(token) + + salt = secrets.token_bytes(16) + base64_salt = base64.b64encode(salt).decode() + + password_hashed = hash_password(new_password, salt) + base64_password_hashed = base64.b64encode(password_hashed).decode() + + account = Account.query.filter_by(email=reset_data.get('email')).first() + account.password = base64_password_hashed + account.password_salt = base64_salt + db.session.commit() + + return {'result': 'success'} + + +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/console/auth/login.py b/api/controllers/console/auth/login.py index 67d6dc8e95..3a0e5ea94d 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,7 +1,7 @@ from typing import cast import flask_login -from flask import current_app, request +from flask import request from flask_restful import Resource, reqparse import services @@ -56,14 +56,14 @@ class LogoutApi(Resource): class ResetPasswordApi(Resource): @setup_required def get(self): - parser = reqparse.RequestParser() - parser.add_argument('email', type=email, required=True, location='json') - args = parser.parse_args() + # parser = reqparse.RequestParser() + # parser.add_argument('email', type=email, required=True, location='json') + # args = parser.parse_args() # import mailchimp_transactional as MailchimpTransactional # from mailchimp_transactional.api_client import ApiClientError - account = {'email': args['email']} + # account = {'email': args['email']} # account = AccountService.get_by_email(args['email']) # if account is None: # raise ValueError('Email not found') @@ -71,22 +71,22 @@ class ResetPasswordApi(Resource): # AccountService.update_password(account, new_password) # todo: Send email - MAILCHIMP_API_KEY = current_app.config['MAILCHIMP_TRANSACTIONAL_API_KEY'] + # MAILCHIMP_API_KEY = current_app.config['MAILCHIMP_TRANSACTIONAL_API_KEY'] # mailchimp = MailchimpTransactional(MAILCHIMP_API_KEY) - message = { - 'from_email': 'noreply@example.com', - 'to': [{'email': account.email}], - 'subject': 'Reset your Dify password', - 'html': """ -

Dear User,

-

The Dify team has generated a new password for you, details as follows:

-

{new_password}

-

Please change your password to log in as soon as possible.

-

Regards,

-

The Dify Team

- """ - } + # message = { + # 'from_email': 'noreply@example.com', + # 'to': [{'email': account['email']}], + # 'subject': 'Reset your Dify password', + # 'html': """ + #

Dear User,

+ #

The Dify team has generated a new password for you, details as follows:

+ #

{new_password}

+ #

Please change your password to log in as soon as possible.

+ #

Regards,

+ #

The Dify Team

+ # """ + # } # response = mailchimp.messages.send({ # 'message': message, diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 2e4a627e06..4a651bfe7b 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -6,6 +6,7 @@ import requests from flask import current_app, redirect, request from flask_restful import Resource +from configs import dify_config from constants.languages import languages from extensions.ext_database import db from libs.helper import get_remote_ip @@ -18,22 +19,24 @@ from .. import api def get_oauth_providers(): with current_app.app_context(): - github_oauth = GitHubOAuth(client_id=current_app.config.get('GITHUB_CLIENT_ID'), - client_secret=current_app.config.get( - 'GITHUB_CLIENT_SECRET'), - redirect_uri=current_app.config.get( - 'CONSOLE_API_URL') + '/console/api/oauth/authorize/github') + if not dify_config.GITHUB_CLIENT_ID or not dify_config.GITHUB_CLIENT_SECRET: + github_oauth = None + else: + github_oauth = GitHubOAuth( + client_id=dify_config.GITHUB_CLIENT_ID, + client_secret=dify_config.GITHUB_CLIENT_SECRET, + redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/authorize/github', + ) + if not dify_config.GOOGLE_CLIENT_ID or not dify_config.GOOGLE_CLIENT_SECRET: + google_oauth = None + else: + google_oauth = GoogleOAuth( + client_id=dify_config.GOOGLE_CLIENT_ID, + client_secret=dify_config.GOOGLE_CLIENT_SECRET, + redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/authorize/google', + ) - google_oauth = GoogleOAuth(client_id=current_app.config.get('GOOGLE_CLIENT_ID'), - client_secret=current_app.config.get( - 'GOOGLE_CLIENT_SECRET'), - redirect_uri=current_app.config.get( - 'CONSOLE_API_URL') + '/console/api/oauth/authorize/google') - - OAUTH_PROVIDERS = { - 'github': github_oauth, - 'google': google_oauth - } + OAUTH_PROVIDERS = {'github': github_oauth, 'google': google_oauth} return OAUTH_PROVIDERS @@ -63,8 +66,7 @@ class OAuthCallback(Resource): token = oauth_provider.get_access_token(code) user_info = oauth_provider.get_user_info(token) except requests.exceptions.HTTPError as e: - logging.exception( - f"An error occurred during the OAuth process with {provider}: {e.response.text}") + logging.exception(f'An error occurred during the OAuth process with {provider}: {e.response.text}') return {'error': 'OAuth process failed'}, 400 account = _generate_account(provider, user_info) @@ -81,7 +83,7 @@ class OAuthCallback(Resource): token = AccountService.login(account, ip_address=get_remote_ip(request)) - return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?console_token={token}') + return redirect(f'{dify_config.CONSOLE_WEB_URL}?console_token={token}') def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]: @@ -101,11 +103,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): # Create account account_name = user_info.name if user_info.name else 'Dify' account = RegisterService.register( - email=user_info.email, - name=account_name, - password=None, - open_id=user_info.id, - provider=provider + email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider ) # Set interface language diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 619ab4f7e2..fdd61b0a0c 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -8,7 +8,7 @@ import services from controllers.console import api from controllers.console.apikey import api_key_fields, api_key_list from controllers.console.app.error import ProviderNotInitializeError -from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError +from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError @@ -226,6 +226,15 @@ class DatasetApi(Resource): except services.errors.dataset.DatasetInUseError: raise DatasetInUseError() +class DatasetUseCheckApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, dataset_id): + dataset_id_str = str(dataset_id) + + dataset_is_using = DatasetService.dataset_use_check(dataset_id_str) + return {'is_using': dataset_is_using}, 200 class DatasetQueryApi(Resource): @@ -346,6 +355,8 @@ class DatasetIndexingEstimateApi(Resource): "in the Settings -> Model Provider.") except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) + except Exception as e: + raise IndexingEstimateError(str(e)) return response, 200 @@ -560,6 +571,7 @@ class DatasetErrorDocs(Resource): api.add_resource(DatasetListApi, '/datasets') api.add_resource(DatasetApi, '/datasets/') +api.add_resource(DatasetUseCheckApi, '/datasets//use-check') api.add_resource(DatasetQueryApi, '/datasets//queries') api.add_resource(DatasetErrorDocs, '/datasets//error-docs') api.add_resource(DatasetIndexingEstimateApi, '/datasets/indexing-estimate') diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 976b7df629..b3a253c167 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -20,6 +20,7 @@ from controllers.console.datasets.error import ( ArchivedDocumentImmutableError, DocumentAlreadyFinishedError, DocumentIndexingError, + IndexingEstimateError, InvalidActionError, InvalidMetadataError, ) @@ -388,6 +389,8 @@ class DocumentIndexingEstimateApi(DocumentResource): "in the Settings -> Model Provider.") except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) + except Exception as e: + raise IndexingEstimateError(str(e)) return response @@ -493,6 +496,8 @@ class DocumentBatchIndexingEstimateApi(DocumentResource): "in the Settings -> Model Provider.") except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) + except Exception as e: + raise IndexingEstimateError(str(e)) return response diff --git a/api/controllers/console/datasets/error.py b/api/controllers/console/datasets/error.py index 71476764aa..9270b610c2 100644 --- a/api/controllers/console/datasets/error.py +++ b/api/controllers/console/datasets/error.py @@ -83,3 +83,9 @@ class DatasetInUseError(BaseHTTPException): error_code = 'dataset_in_use' description = "The dataset is being used by some apps. Please remove the dataset from the apps before deleting it." code = 409 + + +class IndexingEstimateError(BaseHTTPException): + error_code = 'indexing_estimate_error' + description = "Knowledge indexing estimate failed: {message}" + code = 500 diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py index a8fdde2791..def50212a1 100644 --- a/api/controllers/console/setup.py +++ b/api/controllers/console/setup.py @@ -3,11 +3,10 @@ from functools import wraps from flask import current_app, request from flask_restful import Resource, reqparse -from extensions.ext_database import db from libs.helper import email, get_remote_ip, str_len from libs.password import valid_password from models.model import DifySetup -from services.account_service import AccountService, RegisterService, TenantService +from services.account_service import RegisterService, TenantService from . import api from .error import AlreadySetupError, NotInitValidateError, NotSetupError @@ -51,28 +50,17 @@ class SetupApi(Resource): required=True, location='json') args = parser.parse_args() - # Register - account = RegisterService.register( + # setup + RegisterService.setup( email=args['email'], name=args['name'], - password=args['password'] + password=args['password'], + ip_address=get_remote_ip(request) ) - TenantService.create_owner_tenant_if_not_exist(account) - - setup() - AccountService.update_last_login(account, ip_address=get_remote_ip(request)) - return {'result': 'success'}, 201 -def setup(): - dify_setup = DifySetup( - version=current_app.config['CURRENT_VERSION'] - ) - db.session.add(dify_setup) - - def setup_required(view): @wraps(view) def decorated(*args, **kwargs): diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 198409bba7..0b5c84c2a3 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -245,6 +245,8 @@ class AccountIntegrateApi(Resource): return {'data': integrate_data} + + # Register API resources api.add_resource(AccountInitApi, '/account/init') api.add_resource(AccountProfileApi, '/account/profile') diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py index c5c70d810a..c307959b20 100644 --- a/api/controllers/web/site.py +++ b/api/controllers/web/site.py @@ -26,6 +26,8 @@ class AppSiteApi(WebApiResource): site_fields = { 'title': fields.String, + 'chat_color_theme': fields.String, + 'chat_color_theme_inverted': fields.Boolean, 'icon': fields.String, 'icon_background': fields.String, 'description': fields.String, diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index d228a3ac29..0ff9389793 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -32,7 +32,6 @@ from core.model_runtime.entities.model_entities import ModelFeature from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.utils.encoders import jsonable_encoder from core.tools.entities.tool_entities import ( - ToolInvokeMessage, ToolParameter, ToolRuntimeVariablePool, ) @@ -141,24 +140,6 @@ class BaseAgentRunner(AppRunner): app_generate_entity.app_config.prompt_template.simple_prompt_template = '' return app_generate_entity - - def _convert_tool_response_to_str(self, tool_response: list[ToolInvokeMessage]) -> str: - """ - Handle tool response - """ - result = '' - for response in tool_response: - if response.type == ToolInvokeMessage.MessageType.TEXT: - result += response.message - elif response.type == ToolInvokeMessage.MessageType.LINK: - result += f"result link: {response.message}. please tell user to check it." - elif response.type == ToolInvokeMessage.MessageType.IMAGE_LINK or \ - response.type == ToolInvokeMessage.MessageType.IMAGE: - result += "image has been created and sent to user already, you do not need to create it, just tell the user to check it now." - else: - result += f"tool response: {response.message}." - - return result def _convert_tool_to_prompt_message_tool(self, tool: AgentToolEntity) -> tuple[PromptMessageTool, Tool]: """ diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py index 982477138b..9bd8f37d85 100644 --- a/api/core/agent/cot_agent_runner.py +++ b/api/core/agent/cot_agent_runner.py @@ -1,7 +1,7 @@ import json from abc import ABC, abstractmethod from collections.abc import Generator -from typing import Union +from typing import Optional, Union from core.agent.base_agent_runner import BaseAgentRunner from core.agent.entities import AgentScratchpadUnit @@ -15,6 +15,7 @@ from core.model_runtime.entities.message_entities import ( ToolPromptMessage, UserPromptMessage, ) +from core.ops.ops_trace_manager import TraceQueueManager from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform from core.tools.entities.tool_entities import ToolInvokeMeta from core.tools.tool.tool import Tool @@ -42,6 +43,8 @@ class CotAgentRunner(BaseAgentRunner, ABC): self._repack_app_generate_entity(app_generate_entity) self._init_react_state(query) + trace_manager = app_generate_entity.trace_manager + # check model mode if 'Observation' not in app_generate_entity.model_conf.stop: if app_generate_entity.model_conf.provider not in self._ignore_observation_providers: @@ -211,7 +214,8 @@ class CotAgentRunner(BaseAgentRunner, ABC): tool_invoke_response, tool_invoke_meta = self._handle_invoke_action( action=scratchpad.action, tool_instances=tool_instances, - message_file_ids=message_file_ids + message_file_ids=message_file_ids, + trace_manager=trace_manager, ) scratchpad.observation = tool_invoke_response scratchpad.agent_response = tool_invoke_response @@ -237,8 +241,7 @@ class CotAgentRunner(BaseAgentRunner, ABC): # update prompt tool message for prompt_tool in self._prompt_messages_tools: - self.update_prompt_message_tool( - tool_instances[prompt_tool.name], prompt_tool) + self.update_prompt_message_tool(tool_instances[prompt_tool.name], prompt_tool) iteration_step += 1 @@ -275,14 +278,15 @@ class CotAgentRunner(BaseAgentRunner, ABC): message=AssistantPromptMessage( content=final_answer ), - usage=llm_usage['usage'] if llm_usage['usage'] else LLMUsage.empty_usage( - ), + usage=llm_usage['usage'] if llm_usage['usage'] else LLMUsage.empty_usage(), system_fingerprint='' )), PublishFrom.APPLICATION_MANAGER) def _handle_invoke_action(self, action: AgentScratchpadUnit.Action, tool_instances: dict[str, Tool], - message_file_ids: list[str]) -> tuple[str, ToolInvokeMeta]: + message_file_ids: list[str], + trace_manager: Optional[TraceQueueManager] = None + ) -> tuple[str, ToolInvokeMeta]: """ handle invoke action :param action: action @@ -312,21 +316,22 @@ class CotAgentRunner(BaseAgentRunner, ABC): tenant_id=self.tenant_id, message=self.message, invoke_from=self.application_generate_entity.invoke_from, - agent_tool_callback=self.agent_callback + agent_tool_callback=self.agent_callback, + trace_manager=trace_manager, ) # publish files - for message_file, save_as in message_files: + for message_file_id, save_as in message_files: if save_as: self.variables_pool.set_file( - tool_name=tool_call_name, value=message_file.id, name=save_as) + tool_name=tool_call_name, value=message_file_id, name=save_as) # publish message file self.queue_manager.publish(QueueMessageFileEvent( - message_file_id=message_file.id + message_file_id=message_file_id ), PublishFrom.APPLICATION_MANAGER) # add message file ids - message_file_ids.append(message_file.id) + message_file_ids.append(message_file_id) return tool_invoke_response, tool_invoke_meta diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index d7b063eb92..bec76e7a24 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -50,6 +50,9 @@ class FunctionCallAgentRunner(BaseAgentRunner): } final_answer = '' + # get tracing instance + trace_manager = app_generate_entity.trace_manager + def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage): if not final_llm_usage_dict['usage']: final_llm_usage_dict['usage'] = usage @@ -243,18 +246,19 @@ class FunctionCallAgentRunner(BaseAgentRunner): message=self.message, invoke_from=self.application_generate_entity.invoke_from, agent_tool_callback=self.agent_callback, + trace_manager=trace_manager, ) # publish files - for message_file, save_as in message_files: + for message_file_id, save_as in message_files: if save_as: - self.variables_pool.set_file(tool_name=tool_call_name, value=message_file.id, name=save_as) + self.variables_pool.set_file(tool_name=tool_call_name, value=message_file_id, name=save_as) # publish message file self.queue_manager.publish(QueueMessageFileEvent( - message_file_id=message_file.id + message_file_id=message_file_id ), PublishFrom.APPLICATION_MANAGER) # add message file ids - message_file_ids.append(message_file.id) + message_file_ids.append(message_file_id) tool_response = { "tool_call_id": tool_call_id, diff --git a/api/core/app/app_config/easy_ui_based_app/agent/manager.py b/api/core/app/app_config/easy_ui_based_app/agent/manager.py index f271aeed0c..dc65d4439b 100644 --- a/api/core/app/app_config/easy_ui_based_app/agent/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/agent/manager.py @@ -40,7 +40,7 @@ class AgentConfigManager: 'provider_type': tool['provider_type'], 'provider_id': tool['provider_id'], 'tool_name': tool['tool_name'], - 'tool_parameters': tool['tool_parameters'] if 'tool_parameters' in tool else {} + 'tool_parameters': tool.get('tool_parameters', {}) } agent_tools.append(AgentToolEntity(**agent_tool_properties)) diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index d6b6d89416..9b7012c3fb 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -114,6 +114,10 @@ class VariableEntity(BaseModel): default: Optional[str] = None hint: Optional[str] = None + @property + def name(self) -> str: + return self.variable + class ExternalDataVariableEntity(BaseModel): """ @@ -183,6 +187,14 @@ class TextToSpeechEntity(BaseModel): language: Optional[str] = None +class TracingConfigEntity(BaseModel): + """ + Tracing Config Entity. + """ + enabled: bool + tracing_provider: str + + class FileExtraConfig(BaseModel): """ File Upload Entity. @@ -199,7 +211,7 @@ class AppAdditionalFeatures(BaseModel): more_like_this: bool = False speech_to_text: bool = False text_to_speech: Optional[TextToSpeechEntity] = None - + trace_config: Optional[TracingConfigEntity] = None class AppConfig(BaseModel): """ diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 3b1ee3578d..84723cb5c7 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -20,6 +20,7 @@ from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse from core.file.message_file_parser import MessageFileParser from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from models.account import Account from models.model import App, Conversation, EndUser, Message @@ -29,13 +30,14 @@ logger = logging.getLogger(__name__) class AdvancedChatAppGenerator(MessageBasedAppGenerator): - def generate(self, app_model: App, - workflow: Workflow, - user: Union[Account, EndUser], - args: dict, - invoke_from: InvokeFrom, - stream: bool = True) \ - -> Union[dict, Generator[dict, None, None]]: + def generate( + self, app_model: App, + workflow: Workflow, + user: Union[Account, EndUser], + args: dict, + invoke_from: InvokeFrom, + stream: bool = True, + ) -> Union[dict, Generator[dict, None, None]]: """ Generate App response. @@ -57,7 +59,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): inputs = args['inputs'] extras = { - "auto_generate_conversation_name": args['auto_generate_name'] if 'auto_generate_name' in args else False + "auto_generate_conversation_name": args.get('auto_generate_name', False) } # get conversation @@ -84,6 +86,13 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow=workflow ) + # get tracing instance + trace_manager = TraceQueueManager(app_id=app_model.id) + + if invoke_from == InvokeFrom.DEBUGGER: + # always enable retriever resource in debugger mode + app_config.additional_features.show_retrieve_source = True + # init application generate entity application_generate_entity = AdvancedChatAppGenerateEntity( task_id=str(uuid.uuid4()), @@ -95,7 +104,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): user_id=user.id, stream=stream, invoke_from=invoke_from, - extras=extras + extras=extras, + trace_manager=trace_manager ) return self._generate( diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index a4ee47cc1f..8a47f45774 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -70,7 +70,8 @@ class AdvancedChatAppRunner(AppRunner): app_record=app_record, app_generate_entity=application_generate_entity, inputs=inputs, - query=query + query=query, + message_id=message.id ): return @@ -156,11 +157,14 @@ class AdvancedChatAppRunner(AppRunner): # return workflow return workflow - def handle_input_moderation(self, queue_manager: AppQueueManager, - app_record: App, - app_generate_entity: AdvancedChatAppGenerateEntity, - inputs: dict, - query: str) -> bool: + def handle_input_moderation( + self, queue_manager: AppQueueManager, + app_record: App, + app_generate_entity: AdvancedChatAppGenerateEntity, + inputs: dict, + query: str, + message_id: str + ) -> bool: """ Handle input moderation :param queue_manager: application queue manager @@ -168,6 +172,7 @@ class AdvancedChatAppRunner(AppRunner): :param app_generate_entity: application generate entity :param inputs: inputs :param query: query + :param message_id: message id :return: """ try: @@ -178,6 +183,7 @@ class AdvancedChatAppRunner(AppRunner): app_generate_entity=app_generate_entity, inputs=inputs, query=query, + message_id=message_id, ) except ModerationException as e: self._stream_output( 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 7c70afc2ae..5ca0fe2191 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -42,6 +42,7 @@ from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.file.file_obj import FileVar from core.model_runtime.entities.llm_entities import LLMUsage from core.model_runtime.utils.encoders import jsonable_encoder +from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.entities.node_entities import NodeType, SystemVariable from core.workflow.nodes.answer.answer_node import AnswerNode from core.workflow.nodes.answer.entities import TextGenerateRouteChunk, VarGenerateRouteChunk @@ -69,13 +70,15 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc _workflow_system_variables: dict[SystemVariable, Any] _iteration_nested_relations: dict[str, list[str]] - def __init__(self, application_generate_entity: AdvancedChatAppGenerateEntity, - workflow: Workflow, - queue_manager: AppQueueManager, - conversation: Conversation, - message: Message, - user: Union[Account, EndUser], - stream: bool) -> None: + def __init__( + self, application_generate_entity: AdvancedChatAppGenerateEntity, + workflow: Workflow, + queue_manager: AppQueueManager, + conversation: Conversation, + message: Message, + user: Union[Account, EndUser], + stream: bool + ) -> None: """ Initialize AdvancedChatAppGenerateTaskPipeline. :param application_generate_entity: application generate entity @@ -126,14 +129,16 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc self._application_generate_entity.query ) - generator = self._process_stream_response() + generator = self._process_stream_response( + trace_manager=self._application_generate_entity.trace_manager + ) if self._stream: return self._to_stream_response(generator) else: return self._to_blocking_response(generator) def _to_blocking_response(self, generator: Generator[StreamResponse, None, None]) \ - -> ChatbotAppBlockingResponse: + -> ChatbotAppBlockingResponse: """ Process blocking response. :return: @@ -164,7 +169,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc raise Exception('Queue listening stopped unexpectedly.') def _to_stream_response(self, generator: Generator[StreamResponse, None, None]) \ - -> Generator[ChatbotAppStreamResponse, None, None]: + -> Generator[ChatbotAppStreamResponse, None, None]: """ To stream response. :return: @@ -177,7 +182,9 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc stream_response=stream_response ) - def _process_stream_response(self) -> Generator[StreamResponse, None, None]: + def _process_stream_response( + self, trace_manager: Optional[TraceQueueManager] = None + ) -> Generator[StreamResponse, None, None]: """ Process stream response. :return: @@ -249,7 +256,9 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc yield self._handle_iteration_to_stream_response(self._application_generate_entity.task_id, event) self._handle_iteration_operation(event) elif isinstance(event, QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent): - workflow_run = self._handle_workflow_finished(event) + workflow_run = self._handle_workflow_finished( + event, conversation_id=self._conversation.id, trace_manager=trace_manager + ) if workflow_run: yield self._workflow_finish_to_stream_response( task_id=self._application_generate_entity.task_id, @@ -292,7 +301,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc continue if not self._is_stream_out_support( - event=event + event=event ): continue @@ -361,7 +370,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc id=self._message.id, **extras ) - + def _get_stream_generate_routes(self) -> dict[str, ChatflowStreamGenerateRoute]: """ Get stream generate routes. @@ -391,9 +400,9 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc ) return stream_generate_routes - + def _get_answer_start_at_node_ids(self, graph: dict, target_node_id: str) \ - -> list[str]: + -> list[str]: """ Get answer start at node id. :param graph: graph @@ -414,14 +423,14 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc target_node = next((node for node in nodes if node.get('id') == target_node_id), None) if not target_node: return [] - + node_iteration_id = target_node.get('data', {}).get('iteration_id') # get iteration start node id for node in nodes: if node.get('id') == node_iteration_id: if node.get('data', {}).get('start_node_id') == target_node_id: return [target_node_id] - + return [] start_node_ids = [] @@ -457,7 +466,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc start_node_ids.extend(sub_start_node_ids) return start_node_ids - + def _get_iteration_nested_relations(self, graph: dict) -> dict[str, list[str]]: """ Get iteration nested relations. @@ -466,18 +475,18 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc """ nodes = graph.get('nodes') - iteration_ids = [node.get('id') for node in nodes + iteration_ids = [node.get('id') for node in nodes if node.get('data', {}).get('type') in [ NodeType.ITERATION.value, NodeType.LOOP.value, - ]] + ]] return { iteration_id: [ node.get('id') for node in nodes if node.get('data', {}).get('iteration_id') == iteration_id ] for iteration_id in iteration_ids } - + def _generate_stream_outputs_when_node_started(self) -> Generator: """ Generate stream outputs. @@ -485,8 +494,8 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc """ if self._task_state.current_stream_generate_state: route_chunks = self._task_state.current_stream_generate_state.generate_route[ - self._task_state.current_stream_generate_state.current_route_position: - ] + self._task_state.current_stream_generate_state.current_route_position: + ] for route_chunk in route_chunks: if route_chunk.type == 'text': @@ -506,7 +515,8 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc # all route chunks are generated if self._task_state.current_stream_generate_state.current_route_position == len( - self._task_state.current_stream_generate_state.generate_route): + self._task_state.current_stream_generate_state.generate_route + ): self._task_state.current_stream_generate_state = None def _generate_stream_outputs_when_node_finished(self) -> Optional[Generator]: @@ -519,7 +529,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc route_chunks = self._task_state.current_stream_generate_state.generate_route[ self._task_state.current_stream_generate_state.current_route_position:] - + for route_chunk in route_chunks: if route_chunk.type == 'text': route_chunk = cast(TextGenerateRouteChunk, route_chunk) @@ -551,7 +561,8 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc value = iteration_state.current_index elif value_selector[1] == 'item': value = iterator_selector[iteration_state.current_index] if iteration_state.current_index < len( - iterator_selector) else None + iterator_selector + ) else None else: # check chunk node id is before current node id or equal to current node id if route_chunk_node_id not in self._task_state.ran_node_execution_infos: @@ -562,14 +573,15 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc # get route chunk node execution info route_chunk_node_execution_info = self._task_state.ran_node_execution_infos[route_chunk_node_id] if (route_chunk_node_execution_info.node_type == NodeType.LLM - and latest_node_execution_info.node_type == NodeType.LLM): + and latest_node_execution_info.node_type == NodeType.LLM): # only LLM support chunk stream output self._task_state.current_stream_generate_state.current_route_position += 1 continue # get route chunk node execution route_chunk_node_execution = db.session.query(WorkflowNodeExecution).filter( - WorkflowNodeExecution.id == route_chunk_node_execution_info.workflow_node_execution_id).first() + WorkflowNodeExecution.id == route_chunk_node_execution_info.workflow_node_execution_id + ).first() outputs = route_chunk_node_execution.outputs_dict @@ -631,7 +643,8 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc # all route chunks are generated if self._task_state.current_stream_generate_state.current_route_position == len( - self._task_state.current_stream_generate_state.generate_route): + self._task_state.current_stream_generate_state.generate_route + ): self._task_state.current_stream_generate_state = None def _is_stream_out_support(self, event: QueueTextChunkEvent) -> bool: diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 407fb931ec..df6a35918b 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -19,6 +19,7 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, InvokeFrom from core.file.message_file_parser import MessageFileParser from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from models.account import Account from models.model import App, EndUser @@ -56,7 +57,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): inputs = args['inputs'] extras = { - "auto_generate_conversation_name": args['auto_generate_name'] if 'auto_generate_name' in args else True + "auto_generate_conversation_name": args.get('auto_generate_name', True) } # get conversation @@ -82,6 +83,11 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): config=args.get('model_config') ) + # always enable retriever resource in debugger mode + override_model_config_dict["retriever_resource"] = { + "enabled": True + } + # parse files files = args['files'] if args.get('files') else [] message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) @@ -103,6 +109,9 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): override_config_dict=override_model_config_dict ) + # get tracing instance + trace_manager = TraceQueueManager(app_model.id) + # init application generate entity application_generate_entity = AgentChatAppGenerateEntity( task_id=str(uuid.uuid4()), @@ -116,7 +125,8 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): stream=stream, invoke_from=invoke_from, extras=extras, - call_depth=0 + call_depth=0, + trace_manager=trace_manager ) # init generate records @@ -153,7 +163,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, user=user, - stream=stream + stream=stream, ) return AgentChatAppGenerateResponseConverter.convert( @@ -161,11 +171,13 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): invoke_from=invoke_from ) - def _generate_worker(self, flask_app: Flask, - application_generate_entity: AgentChatAppGenerateEntity, - queue_manager: AppQueueManager, - conversation_id: str, - message_id: str) -> None: + def _generate_worker( + self, flask_app: Flask, + application_generate_entity: AgentChatAppGenerateEntity, + queue_manager: AppQueueManager, + conversation_id: str, + message_id: str, + ) -> None: """ Generate worker in a new thread. :param flask_app: Flask app @@ -187,7 +199,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): application_generate_entity=application_generate_entity, queue_manager=queue_manager, conversation=conversation, - message=message + message=message, ) except GenerateTaskStoppedException: pass diff --git a/api/core/app/apps/agent_chat/app_runner.py b/api/core/app/apps/agent_chat/app_runner.py index d6367300de..d1bbf679c5 100644 --- a/api/core/app/apps/agent_chat/app_runner.py +++ b/api/core/app/apps/agent_chat/app_runner.py @@ -28,10 +28,13 @@ class AgentChatAppRunner(AppRunner): """ Agent Application Runner """ - def run(self, application_generate_entity: AgentChatAppGenerateEntity, - queue_manager: AppQueueManager, - conversation: Conversation, - message: Message) -> None: + + def run( + self, application_generate_entity: AgentChatAppGenerateEntity, + queue_manager: AppQueueManager, + conversation: Conversation, + message: Message, + ) -> None: """ Run assistant application :param application_generate_entity: application generate entity @@ -100,6 +103,7 @@ class AgentChatAppRunner(AppRunner): app_generate_entity=application_generate_entity, inputs=inputs, query=query, + message_id=message.id ) except ModerationException as e: self.direct_output( @@ -199,7 +203,7 @@ class AgentChatAppRunner(AppRunner): llm_model = cast(LargeLanguageModel, model_instance.model_type_instance) model_schema = llm_model.get_model_schema(model_instance.model, model_instance.credentials) - if set([ModelFeature.MULTI_TOOL_CALL, ModelFeature.TOOL_CALL]).intersection(model_schema.features or []): + if {ModelFeature.MULTI_TOOL_CALL, ModelFeature.TOOL_CALL}.intersection(model_schema.features or []): agent_entity.strategy = AgentEntity.Strategy.FUNCTION_CALLING conversation = db.session.query(Conversation).filter(Conversation.id == conversation.id).first() @@ -219,7 +223,7 @@ class AgentChatAppRunner(AppRunner): runner_cls = FunctionCallAgentRunner else: raise ValueError(f"Invalid agent strategy: {agent_entity.strategy}") - + runner = runner_cls( tenant_id=app_config.tenant_id, application_generate_entity=application_generate_entity, diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 20ae6ff676..6f48aa2363 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -1,52 +1,56 @@ +from collections.abc import Mapping +from typing import Any, Optional + from core.app.app_config.entities import AppConfig, VariableEntity class BaseAppGenerator: - def _get_cleaned_inputs(self, user_inputs: dict, app_config: AppConfig): - if user_inputs is None: - user_inputs = {} - - filtered_inputs = {} - + def _get_cleaned_inputs(self, user_inputs: Optional[Mapping[str, Any]], app_config: AppConfig) -> Mapping[str, Any]: + user_inputs = user_inputs or {} # Filter input variables from form configuration, handle required fields, default values, and option values variables = app_config.variables - for variable_config in variables: - variable = variable_config.variable - - if (variable not in user_inputs - or user_inputs[variable] is None - or (isinstance(user_inputs[variable], str) and user_inputs[variable] == '')): - if variable_config.required: - raise ValueError(f"{variable} is required in input form") - else: - filtered_inputs[variable] = variable_config.default if variable_config.default is not None else "" - continue - - value = user_inputs[variable] - - if value is not None: - if variable_config.type != VariableEntity.Type.NUMBER and not isinstance(value, str): - raise ValueError(f"{variable} in input form must be a string") - elif variable_config.type == VariableEntity.Type.NUMBER and isinstance(value, str): - if '.' in value: - value = float(value) - else: - value = int(value) - - if variable_config.type == VariableEntity.Type.SELECT: - options = variable_config.options if variable_config.options is not None else [] - if value not in options: - raise ValueError(f"{variable} in input form must be one of the following: {options}") - elif variable_config.type in [VariableEntity.Type.TEXT_INPUT, VariableEntity.Type.PARAGRAPH]: - if variable_config.max_length is not None: - max_length = variable_config.max_length - if len(value) > max_length: - raise ValueError(f'{variable} in input form must be less than {max_length} characters') - - if value and isinstance(value, str): - filtered_inputs[variable] = value.replace('\x00', '') - else: - filtered_inputs[variable] = value if value is not None else None - + filtered_inputs = {var.name: self._validate_input(inputs=user_inputs, var=var) for var in variables} + filtered_inputs = {k: self._sanitize_value(v) for k, v in filtered_inputs.items()} return filtered_inputs + def _validate_input(self, *, inputs: Mapping[str, Any], var: VariableEntity): + user_input_value = inputs.get(var.name) + if var.required and not user_input_value: + raise ValueError(f'{var.name} is required in input form') + if not var.required and not user_input_value: + # TODO: should we return None here if the default value is None? + return var.default or '' + if ( + var.type + in ( + VariableEntity.Type.TEXT_INPUT, + VariableEntity.Type.SELECT, + VariableEntity.Type.PARAGRAPH, + ) + and user_input_value + and not isinstance(user_input_value, str) + ): + raise ValueError(f"(type '{var.type}') {var.name} in input form must be a string") + if var.type == VariableEntity.Type.NUMBER and isinstance(user_input_value, str): + # may raise ValueError if user_input_value is not a valid number + try: + if '.' in user_input_value: + return float(user_input_value) + else: + return int(user_input_value) + except ValueError: + raise ValueError(f"{var.name} in input form must be a valid number") + if var.type == VariableEntity.Type.SELECT: + options = var.options or [] + if user_input_value not in options: + raise ValueError(f'{var.name} in input form must be one of the following: {options}') + elif var.type in (VariableEntity.Type.TEXT_INPUT, VariableEntity.Type.PARAGRAPH): + if var.max_length and user_input_value and len(user_input_value) > var.max_length: + raise ValueError(f'{var.name} in input form must be less than {var.max_length} characters') + + return user_input_value + + def _sanitize_value(self, value: Any) -> Any: + if isinstance(value, str): + return value.replace('\x00', '') + return value diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index 53f457cb11..58c7d04b83 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -338,11 +338,14 @@ class AppRunner: ), PublishFrom.APPLICATION_MANAGER ) - def moderation_for_inputs(self, app_id: str, - tenant_id: str, - app_generate_entity: AppGenerateEntity, - inputs: dict, - query: str) -> tuple[bool, dict, str]: + def moderation_for_inputs( + self, app_id: str, + tenant_id: str, + app_generate_entity: AppGenerateEntity, + inputs: dict, + query: str, + message_id: str, + ) -> tuple[bool, dict, str]: """ Process sensitive_word_avoidance. :param app_id: app id @@ -350,6 +353,7 @@ class AppRunner: :param app_generate_entity: app generate entity :param inputs: inputs :param query: query + :param message_id: message id :return: """ moderation_feature = InputModeration() @@ -358,7 +362,9 @@ class AppRunner: tenant_id=tenant_id, app_config=app_generate_entity.app_config, inputs=inputs, - query=query if query else '' + query=query if query else '', + message_id=message_id, + trace_manager=app_generate_entity.trace_manager ) def check_hosting_moderation(self, application_generate_entity: EasyUIBasedAppGenerateEntity, diff --git a/api/core/app/apps/chat/app_config_manager.py b/api/core/app/apps/chat/app_config_manager.py index 925062a66a..a286c349b2 100644 --- a/api/core/app/apps/chat/app_config_manager.py +++ b/api/core/app/apps/chat/app_config_manager.py @@ -50,6 +50,9 @@ class ChatAppConfigManager(BaseAppConfigManager): app_model_config_dict = app_model_config.to_dict() config_dict = app_model_config_dict.copy() else: + if not override_config_dict: + raise Exception('override_config_dict is required when config_from is ARGS') + config_dict = override_config_dict app_mode = AppMode.value_of(app_model.mode) diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index 505ada09db..5b896e2845 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -19,6 +19,7 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa from core.app.entities.app_invoke_entities import ChatAppGenerateEntity, InvokeFrom from core.file.message_file_parser import MessageFileParser from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from models.account import Account from models.model import App, EndUser @@ -27,12 +28,13 @@ logger = logging.getLogger(__name__) class ChatAppGenerator(MessageBasedAppGenerator): - def generate(self, app_model: App, - user: Union[Account, EndUser], - args: Any, - invoke_from: InvokeFrom, - stream: bool = True) \ - -> Union[dict, Generator[dict, None, None]]: + def generate( + self, app_model: App, + user: Union[Account, EndUser], + args: Any, + invoke_from: InvokeFrom, + stream: bool = True, + ) -> Union[dict, Generator[dict, None, None]]: """ Generate App response. @@ -53,7 +55,7 @@ class ChatAppGenerator(MessageBasedAppGenerator): inputs = args['inputs'] extras = { - "auto_generate_conversation_name": args['auto_generate_name'] if 'auto_generate_name' in args else True + "auto_generate_conversation_name": args.get('auto_generate_name', True) } # get conversation @@ -79,6 +81,11 @@ class ChatAppGenerator(MessageBasedAppGenerator): config=args.get('model_config') ) + # always enable retriever resource in debugger mode + override_model_config_dict["retriever_resource"] = { + "enabled": True + } + # parse files files = args['files'] if args.get('files') else [] message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) @@ -100,6 +107,9 @@ class ChatAppGenerator(MessageBasedAppGenerator): override_config_dict=override_model_config_dict ) + # get tracing instance + trace_manager = TraceQueueManager(app_model.id) + # init application generate entity application_generate_entity = ChatAppGenerateEntity( task_id=str(uuid.uuid4()), @@ -112,7 +122,8 @@ class ChatAppGenerator(MessageBasedAppGenerator): user_id=user.id, stream=stream, invoke_from=invoke_from, - extras=extras + extras=extras, + trace_manager=trace_manager ) # init generate records @@ -149,7 +160,7 @@ class ChatAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, user=user, - stream=stream + stream=stream, ) return ChatAppGenerateResponseConverter.convert( diff --git a/api/core/app/apps/chat/app_runner.py b/api/core/app/apps/chat/app_runner.py index 7d243d0726..89a498eb36 100644 --- a/api/core/app/apps/chat/app_runner.py +++ b/api/core/app/apps/chat/app_runner.py @@ -96,6 +96,7 @@ class ChatAppRunner(AppRunner): app_generate_entity=application_generate_entity, inputs=inputs, query=query, + message_id=message.id ) except ModerationException as e: self.direct_output( @@ -154,7 +155,7 @@ class ChatAppRunner(AppRunner): application_generate_entity.invoke_from ) - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = DatasetRetrieval(application_generate_entity) context = dataset_retrieval.retrieve( app_id=app_record.id, user_id=application_generate_entity.user_id, @@ -165,7 +166,8 @@ class ChatAppRunner(AppRunner): invoke_from=application_generate_entity.invoke_from, show_retrieve_source=app_config.additional_features.show_retrieve_source, hit_callback=hit_callback, - memory=memory + memory=memory, + message_id=message.id, ) # reorganize all inputs and template to prompt messages diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 52d907b535..c4e1caf65a 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -19,6 +19,7 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa from core.app.entities.app_invoke_entities import CompletionAppGenerateEntity, InvokeFrom from core.file.message_file_parser import MessageFileParser from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from models.account import Account from models.model import App, EndUser, Message @@ -94,6 +95,9 @@ class CompletionAppGenerator(MessageBasedAppGenerator): override_config_dict=override_model_config_dict ) + # get tracing instance + trace_manager = TraceQueueManager(app_model.id) + # init application generate entity application_generate_entity = CompletionAppGenerateEntity( task_id=str(uuid.uuid4()), @@ -105,7 +109,8 @@ class CompletionAppGenerator(MessageBasedAppGenerator): user_id=user.id, stream=stream, invoke_from=invoke_from, - extras=extras + extras=extras, + trace_manager=trace_manager ) # init generate records @@ -141,7 +146,7 @@ class CompletionAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, user=user, - stream=stream + stream=stream, ) return CompletionAppGenerateResponseConverter.convert( @@ -158,7 +163,6 @@ class CompletionAppGenerator(MessageBasedAppGenerator): :param flask_app: Flask app :param application_generate_entity: application generate entity :param queue_manager: queue manager - :param conversation_id: conversation ID :param message_id: message ID :return: """ @@ -300,7 +304,7 @@ class CompletionAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, user=user, - stream=stream + stream=stream, ) return CompletionAppGenerateResponseConverter.convert( diff --git a/api/core/app/apps/completion/app_runner.py b/api/core/app/apps/completion/app_runner.py index a3a9945bc0..f0e5f9ae17 100644 --- a/api/core/app/apps/completion/app_runner.py +++ b/api/core/app/apps/completion/app_runner.py @@ -77,6 +77,7 @@ class CompletionAppRunner(AppRunner): app_generate_entity=application_generate_entity, inputs=inputs, query=query, + message_id=message.id ) except ModerationException as e: self.direct_output( @@ -114,7 +115,7 @@ class CompletionAppRunner(AppRunner): if dataset_config and dataset_config.retrieve_config.query_variable: query = inputs.get(dataset_config.retrieve_config.query_variable, "") - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = DatasetRetrieval(application_generate_entity) context = dataset_retrieval.retrieve( app_id=app_record.id, user_id=application_generate_entity.user_id, @@ -124,7 +125,8 @@ class CompletionAppRunner(AppRunner): query=query, invoke_from=application_generate_entity.invoke_from, show_retrieve_source=app_config.additional_features.show_retrieve_source, - hit_callback=hit_callback + hit_callback=hit_callback, + message_id=message.id ) # reorganize all inputs and template to prompt messages diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py index 6acf5da8df..c5cd686402 100644 --- a/api/core/app/apps/message_based_app_generator.py +++ b/api/core/app/apps/message_based_app_generator.py @@ -35,22 +35,23 @@ logger = logging.getLogger(__name__) class MessageBasedAppGenerator(BaseAppGenerator): - def _handle_response(self, application_generate_entity: Union[ - ChatAppGenerateEntity, - CompletionAppGenerateEntity, - AgentChatAppGenerateEntity, - AdvancedChatAppGenerateEntity - ], - queue_manager: AppQueueManager, - conversation: Conversation, - message: Message, - user: Union[Account, EndUser], - stream: bool = False) \ - -> Union[ - ChatbotAppBlockingResponse, - CompletionAppBlockingResponse, - Generator[Union[ChatbotAppStreamResponse, CompletionAppStreamResponse], None, None] - ]: + def _handle_response( + self, application_generate_entity: Union[ + ChatAppGenerateEntity, + CompletionAppGenerateEntity, + AgentChatAppGenerateEntity, + AdvancedChatAppGenerateEntity + ], + queue_manager: AppQueueManager, + conversation: Conversation, + message: Message, + user: Union[Account, EndUser], + stream: bool = False, + ) -> Union[ + ChatbotAppBlockingResponse, + CompletionAppBlockingResponse, + Generator[Union[ChatbotAppStreamResponse, CompletionAppStreamResponse], None, None] + ]: """ Handle response. :param application_generate_entity: application generate entity diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index c4324978d8..0f547ca164 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -20,6 +20,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse from core.file.message_file_parser import MessageFileParser from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from models.account import Account from models.model import App, EndUser @@ -29,14 +30,15 @@ logger = logging.getLogger(__name__) class WorkflowAppGenerator(BaseAppGenerator): - def generate(self, app_model: App, - workflow: Workflow, - user: Union[Account, EndUser], - args: dict, - invoke_from: InvokeFrom, - stream: bool = True, - call_depth: int = 0) \ - -> Union[dict, Generator[dict, None, None]]: + def generate( + self, app_model: App, + workflow: Workflow, + user: Union[Account, EndUser], + args: dict, + invoke_from: InvokeFrom, + stream: bool = True, + call_depth: int = 0, + ) -> Union[dict, Generator[dict, None, None]]: """ Generate App response. @@ -46,6 +48,7 @@ class WorkflowAppGenerator(BaseAppGenerator): :param args: request args :param invoke_from: invoke from source :param stream: is stream + :param call_depth: call depth """ inputs = args['inputs'] @@ -68,6 +71,9 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow=workflow ) + # get tracing instance + trace_manager = TraceQueueManager(app_model.id) + # init application generate entity application_generate_entity = WorkflowAppGenerateEntity( task_id=str(uuid.uuid4()), @@ -77,7 +83,8 @@ class WorkflowAppGenerator(BaseAppGenerator): user_id=user.id, stream=stream, invoke_from=invoke_from, - call_depth=call_depth + call_depth=call_depth, + trace_manager=trace_manager ) return self._generate( @@ -87,17 +94,16 @@ class WorkflowAppGenerator(BaseAppGenerator): application_generate_entity=application_generate_entity, invoke_from=invoke_from, stream=stream, - call_depth=call_depth ) - def _generate(self, app_model: App, - workflow: Workflow, - user: Union[Account, EndUser], - application_generate_entity: WorkflowAppGenerateEntity, - invoke_from: InvokeFrom, - stream: bool = True, - call_depth: int = 0) \ - -> Union[dict, Generator[dict, None, None]]: + def _generate( + self, app_model: App, + workflow: Workflow, + user: Union[Account, EndUser], + application_generate_entity: WorkflowAppGenerateEntity, + invoke_from: InvokeFrom, + stream: bool = True, + ) -> Union[dict, Generator[dict, None, None]]: """ Generate App response. @@ -131,7 +137,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow=workflow, queue_manager=queue_manager, user=user, - stream=stream + stream=stream, ) return WorkflowAppGenerateResponseConverter.convert( @@ -158,10 +164,10 @@ class WorkflowAppGenerator(BaseAppGenerator): """ if not node_id: raise ValueError('node_id is required') - + if args.get('inputs') is None: raise ValueError('inputs is required') - + extras = { "auto_generate_conversation_name": False } diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 8d961e0993..f4bd396f46 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -1,6 +1,6 @@ import logging from collections.abc import Generator -from typing import Any, Union +from typing import Any, Optional, Union from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import ( @@ -36,6 +36,7 @@ from core.app.entities.task_entities import ( ) from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage +from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.entities.node_entities import NodeType, SystemVariable from core.workflow.nodes.end.end_node import EndNode from extensions.ext_database import db @@ -104,7 +105,9 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa db.session.refresh(self._user) db.session.close() - generator = self._process_stream_response() + generator = self._process_stream_response( + trace_manager=self._application_generate_entity.trace_manager + ) if self._stream: return self._to_stream_response(generator) else: @@ -158,7 +161,10 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa stream_response=stream_response ) - def _process_stream_response(self) -> Generator[StreamResponse, None, None]: + def _process_stream_response( + self, + trace_manager: Optional[TraceQueueManager] = None + ) -> Generator[StreamResponse, None, None]: """ Process stream response. :return: @@ -215,7 +221,9 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa yield self._handle_iteration_to_stream_response(self._application_generate_entity.task_id, event) self._handle_iteration_operation(event) elif isinstance(event, QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent): - workflow_run = self._handle_workflow_finished(event) + workflow_run = self._handle_workflow_finished( + event, trace_manager=trace_manager + ) # save workflow app log self._save_workflow_app_log(workflow_run) diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index f27a110870..1d2ad4a373 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -7,6 +7,7 @@ from core.app.app_config.entities import AppConfig, EasyUIBasedAppConfig, Workfl from core.entities.provider_configuration import ProviderModelBundle from core.file.file_obj import FileVar from core.model_runtime.entities.model_entities import AIModelEntity +from core.ops.ops_trace_manager import TraceQueueManager class InvokeFrom(Enum): @@ -89,6 +90,12 @@ class AppGenerateEntity(BaseModel): # extra parameters, like: auto_generate_conversation_name extras: dict[str, Any] = {} + # tracing instance + trace_manager: Optional[TraceQueueManager] = None + + class Config: + arbitrary_types_allowed = True + class EasyUIBasedAppGenerateEntity(AppGenerateEntity): """ 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 ccb684d84b..7d16d015bf 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 @@ -44,6 +44,7 @@ from core.model_runtime.entities.message_entities import ( ) from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.utils.encoders import jsonable_encoder +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName from core.prompt.utils.prompt_message_util import PromptMessageUtil from core.prompt.utils.prompt_template_parser import PromptTemplateParser from events.message_event import message_was_created @@ -100,7 +101,9 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan self._conversation_name_generate_thread = None - def process(self) -> Union[ + def process( + self, + ) -> Union[ ChatbotAppBlockingResponse, CompletionAppBlockingResponse, Generator[Union[ChatbotAppStreamResponse, CompletionAppStreamResponse], None, None] @@ -120,7 +123,9 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan self._application_generate_entity.query ) - generator = self._process_stream_response() + generator = self._process_stream_response( + trace_manager=self._application_generate_entity.trace_manager + ) if self._stream: return self._to_stream_response(generator) else: @@ -197,7 +202,9 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan stream_response=stream_response ) - def _process_stream_response(self) -> Generator[StreamResponse, None, None]: + def _process_stream_response( + self, trace_manager: Optional[TraceQueueManager] = None + ) -> Generator[StreamResponse, None, None]: """ Process stream response. :return: @@ -224,7 +231,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan yield self._message_replace_to_stream_response(answer=output_moderation_answer) # Save message - self._save_message() + self._save_message(trace_manager) yield self._message_end_to_stream_response() elif isinstance(event, QueueRetrieverResourcesEvent): @@ -269,7 +276,9 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan if self._conversation_name_generate_thread: self._conversation_name_generate_thread.join() - def _save_message(self) -> None: + def _save_message( + self, trace_manager: Optional[TraceQueueManager] = None + ) -> None: """ Save message. :return: @@ -300,6 +309,15 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan db.session.commit() + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.MESSAGE_TRACE, + conversation_id=self._conversation.id, + message_id=self._message.id + ) + ) + message_was_created.send( self._message, application_generate_entity=self._application_generate_entity, diff --git a/api/core/app/task_pipeline/message_cycle_manage.py b/api/core/app/task_pipeline/message_cycle_manage.py index 271f8329a4..76c50809cf 100644 --- a/api/core/app/task_pipeline/message_cycle_manage.py +++ b/api/core/app/task_pipeline/message_cycle_manage.py @@ -167,8 +167,11 @@ class MessageCycleManage: extension = '.bin' else: extension = '.bin' - # add sign url - url = ToolFileManager.sign_file(tool_file_id=tool_file_id, extension=extension) + # add sign url to local file + if message_file.url.startswith('http'): + url = message_file.url + else: + url = ToolFileManager.sign_file(tool_file_id=tool_file_id, extension=extension) return MessageFileStreamResponse( task_id=self._application_generate_entity.task_id, diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 978a318279..513fc692ff 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -22,6 +22,7 @@ from core.app.entities.task_entities import ( from core.app.task_pipeline.workflow_iteration_cycle_manage import WorkflowIterationCycleManage from core.file.file_obj import FileVar from core.model_runtime.utils.encoders import jsonable_encoder +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName from core.tools.tool_manager import ToolManager from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeType from core.workflow.nodes.tool.entities import ToolNodeData @@ -94,11 +95,15 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): return workflow_run - def _workflow_run_success(self, workflow_run: WorkflowRun, - start_at: float, - total_tokens: int, - total_steps: int, - outputs: Optional[str] = None) -> WorkflowRun: + def _workflow_run_success( + self, workflow_run: WorkflowRun, + start_at: float, + total_tokens: int, + total_steps: int, + outputs: Optional[str] = None, + conversation_id: Optional[str] = None, + trace_manager: Optional[TraceQueueManager] = None + ) -> WorkflowRun: """ Workflow run success :param workflow_run: workflow run @@ -106,6 +111,7 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): :param total_tokens: total tokens :param total_steps: total steps :param outputs: outputs + :param conversation_id: conversation id :return: """ workflow_run.status = WorkflowRunStatus.SUCCEEDED.value @@ -119,14 +125,27 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): db.session.refresh(workflow_run) db.session.close() + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.WORKFLOW_TRACE, + workflow_run=workflow_run, + conversation_id=conversation_id, + ) + ) + return workflow_run - def _workflow_run_failed(self, workflow_run: WorkflowRun, - start_at: float, - total_tokens: int, - total_steps: int, - status: WorkflowRunStatus, - error: str) -> WorkflowRun: + def _workflow_run_failed( + self, workflow_run: WorkflowRun, + start_at: float, + total_tokens: int, + total_steps: int, + status: WorkflowRunStatus, + error: str, + conversation_id: Optional[str] = None, + trace_manager: Optional[TraceQueueManager] = None + ) -> WorkflowRun: """ Workflow run failed :param workflow_run: workflow run @@ -148,6 +167,15 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): db.session.refresh(workflow_run) db.session.close() + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.WORKFLOW_TRACE, + workflow_run=workflow_run, + conversation_id=conversation_id, + ) + ) + return workflow_run def _init_node_execution_from_workflow_run(self, workflow_run: WorkflowRun, @@ -180,7 +208,8 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): title=node_title, status=WorkflowNodeExecutionStatus.RUNNING.value, created_by_role=workflow_run.created_by_role, - created_by=workflow_run.created_by + created_by=workflow_run.created_by, + created_at=datetime.now(timezone.utc).replace(tzinfo=None) ) db.session.add(workflow_node_execution) @@ -440,9 +469,9 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): current_node_execution = self._task_state.ran_node_execution_infos[event.node_id] workflow_node_execution = db.session.query(WorkflowNodeExecution).filter( WorkflowNodeExecution.id == current_node_execution.workflow_node_execution_id).first() - + execution_metadata = event.execution_metadata if isinstance(event, QueueNodeSucceededEvent) else None - + if self._iteration_state and self._iteration_state.current_iterations: if not execution_metadata: execution_metadata = {} @@ -470,7 +499,7 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): if execution_metadata and execution_metadata.get(NodeRunMetadataKey.TOTAL_TOKENS): self._task_state.total_tokens += ( int(execution_metadata.get(NodeRunMetadataKey.TOTAL_TOKENS))) - + if self._iteration_state: for iteration_node_id in self._iteration_state.current_iterations: data = self._iteration_state.current_iterations[iteration_node_id] @@ -496,13 +525,18 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): return workflow_node_execution - def _handle_workflow_finished(self, event: QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent) \ - -> Optional[WorkflowRun]: + def _handle_workflow_finished( + self, event: QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent, + conversation_id: Optional[str] = None, + trace_manager: Optional[TraceQueueManager] = None + ) -> Optional[WorkflowRun]: workflow_run = db.session.query(WorkflowRun).filter( WorkflowRun.id == self._task_state.workflow_run_id).first() if not workflow_run: return None + if conversation_id is None: + conversation_id = self._application_generate_entity.inputs.get('sys.conversation_id') if isinstance(event, QueueStopEvent): workflow_run = self._workflow_run_failed( workflow_run=workflow_run, @@ -510,7 +544,9 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): total_tokens=self._task_state.total_tokens, total_steps=self._task_state.total_steps, status=WorkflowRunStatus.STOPPED, - error='Workflow stopped.' + error='Workflow stopped.', + conversation_id=conversation_id, + trace_manager=trace_manager ) latest_node_execution_info = self._task_state.latest_node_execution_info @@ -531,7 +567,9 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): total_tokens=self._task_state.total_tokens, total_steps=self._task_state.total_steps, status=WorkflowRunStatus.FAILED, - error=event.error + error=event.error, + conversation_id=conversation_id, + trace_manager=trace_manager ) else: if self._task_state.latest_node_execution_info: @@ -546,7 +584,9 @@ class WorkflowCycleManage(WorkflowIterationCycleManage): start_at=self._task_state.start_at, total_tokens=self._task_state.total_tokens, total_steps=self._task_state.total_steps, - outputs=outputs + outputs=outputs, + conversation_id=conversation_id, + trace_manager=trace_manager ) self._task_state.workflow_run_id = workflow_run.id diff --git a/api/core/app/task_pipeline/workflow_iteration_cycle_manage.py b/api/core/app/task_pipeline/workflow_iteration_cycle_manage.py index 69af81d026..aff1870714 100644 --- a/api/core/app/task_pipeline/workflow_iteration_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_iteration_cycle_manage.py @@ -1,6 +1,7 @@ import json import time from collections.abc import Generator +from datetime import datetime, timezone from typing import Optional, Union from core.app.entities.queue_entities import ( @@ -131,7 +132,8 @@ class WorkflowIterationCycleManage(WorkflowCycleStateManager): 'started_run_index': node_run_index + 1, 'current_index': 0, 'steps_boundary': [], - }) + }), + created_at=datetime.now(timezone.utc).replace(tzinfo=None) ) db.session.add(workflow_node_execution) diff --git a/api/core/callback_handler/agent_tool_callback_handler.py b/api/core/callback_handler/agent_tool_callback_handler.py index ac5076cd01..f973b7e1ce 100644 --- a/api/core/callback_handler/agent_tool_callback_handler.py +++ b/api/core/callback_handler/agent_tool_callback_handler.py @@ -3,6 +3,8 @@ from typing import Any, Optional, TextIO, Union from pydantic import BaseModel +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName + _TEXT_COLOR_MAPPING = { "blue": "36;1", "yellow": "33;1", @@ -51,6 +53,9 @@ class DifyAgentCallbackHandler(BaseModel): tool_name: str, tool_inputs: dict[str, Any], tool_outputs: str, + message_id: Optional[str] = None, + timer: Optional[Any] = None, + trace_manager: Optional[TraceQueueManager] = None ) -> None: """If not the final action, print out observation.""" print_text("\n[on_tool_end]\n", color=self.color) @@ -59,6 +64,18 @@ class DifyAgentCallbackHandler(BaseModel): print_text("Outputs: " + str(tool_outputs)[:1000] + "\n", color=self.color) print_text("\n") + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.TOOL_TRACE, + message_id=message_id, + tool_name=tool_name, + tool_inputs=tool_inputs, + tool_outputs=tool_outputs, + timer=timer, + ) + ) + def on_tool_error( self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any ) -> None: diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 564dfd8973..f3cf54a58e 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -66,8 +66,8 @@ class ProviderConfiguration(BaseModel): original_provider_configurate_methods[self.provider.provider].append(configurate_method) if original_provider_configurate_methods[self.provider.provider] == [ConfigurateMethod.CUSTOMIZABLE_MODEL]: - if (any([len(quota_configuration.restrict_models) > 0 - for quota_configuration in self.system_configuration.quota_configurations]) + if (any(len(quota_configuration.restrict_models) > 0 + for quota_configuration in self.system_configuration.quota_configurations) and ConfigurateMethod.PREDEFINED_MODEL not in self.provider.configurate_methods): self.provider.configurate_methods.append(ConfigurateMethod.PREDEFINED_MODEL) diff --git a/api/core/extension/api_based_extension_requestor.py b/api/core/extension/api_based_extension_requestor.py index 40e60687b2..4db7a99973 100644 --- a/api/core/extension/api_based_extension_requestor.py +++ b/api/core/extension/api_based_extension_requestor.py @@ -1,7 +1,6 @@ -import os - import requests +from configs import dify_config from models.api_based_extension import APIBasedExtensionPoint @@ -31,10 +30,10 @@ class APIBasedExtensionRequestor: try: # proxy support for security proxies = None - if os.environ.get("SSRF_PROXY_HTTP_URL") and os.environ.get("SSRF_PROXY_HTTPS_URL"): + if dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL: proxies = { - 'http': os.environ.get("SSRF_PROXY_HTTP_URL"), - 'https': os.environ.get("SSRF_PROXY_HTTPS_URL"), + 'http': dify_config.SSRF_PROXY_HTTP_URL, + 'https': dify_config.SSRF_PROXY_HTTPS_URL, } response = requests.request( diff --git a/api/core/file/file_obj.py b/api/core/file/file_obj.py index 2e31b4bac1..268ef5df86 100644 --- a/api/core/file/file_obj.py +++ b/api/core/file/file_obj.py @@ -65,6 +65,7 @@ class FileVar(BaseModel): 'type': self.type.value, 'transfer_method': self.transfer_method.value, 'url': self.preview_url, + 'remote_url': self.url, 'related_id': self.related_id, 'filename': self.filename, 'extension': self.extension, diff --git a/api/core/file/message_file_parser.py b/api/core/file/message_file_parser.py index 52498eb871..842b539ad1 100644 --- a/api/core/file/message_file_parser.py +++ b/api/core/file/message_file_parser.py @@ -186,7 +186,7 @@ class MessageFileParser: } response = requests.head(url, headers=headers, allow_redirects=True) - if response.status_code == 200: + if response.status_code in {200, 304}: return True, "" else: return False, "URL does not exist." diff --git a/api/core/helper/code_executor/code_executor.py b/api/core/helper/code_executor/code_executor.py index c1874ed641..f094f7d79b 100644 --- a/api/core/helper/code_executor/code_executor.py +++ b/api/core/helper/code_executor/code_executor.py @@ -8,7 +8,7 @@ from httpx import get, post from pydantic import BaseModel from yarl import URL -from config import get_env +from configs import dify_config from core.helper.code_executor.entities import CodeDependency from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer @@ -18,8 +18,8 @@ from core.helper.code_executor.template_transformer import TemplateTransformer logger = logging.getLogger(__name__) # Code Executor -CODE_EXECUTION_ENDPOINT = get_env('CODE_EXECUTION_ENDPOINT') -CODE_EXECUTION_API_KEY = get_env('CODE_EXECUTION_API_KEY') +CODE_EXECUTION_ENDPOINT = dify_config.CODE_EXECUTION_ENDPOINT +CODE_EXECUTION_API_KEY = dify_config.CODE_EXECUTION_API_KEY CODE_EXECUTION_TIMEOUT= (10, 60) diff --git a/api/core/helper/ssrf_proxy.py b/api/core/helper/ssrf_proxy.py index 276c8a34e7..019b27f28a 100644 --- a/api/core/helper/ssrf_proxy.py +++ b/api/core/helper/ssrf_proxy.py @@ -1,65 +1,48 @@ """ Proxy requests to avoid SSRF """ - import os -from httpx import get as _get -from httpx import head as _head -from httpx import options as _options -from httpx import patch as _patch -from httpx import post as _post -from httpx import put as _put -from requests import delete as _delete +import httpx +SSRF_PROXY_ALL_URL = os.getenv('SSRF_PROXY_ALL_URL', '') SSRF_PROXY_HTTP_URL = os.getenv('SSRF_PROXY_HTTP_URL', '') SSRF_PROXY_HTTPS_URL = os.getenv('SSRF_PROXY_HTTPS_URL', '') -requests_proxies = { - 'http': SSRF_PROXY_HTTP_URL, - 'https': SSRF_PROXY_HTTPS_URL -} if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None - -httpx_proxies = { +proxies = { 'http://': SSRF_PROXY_HTTP_URL, 'https://': SSRF_PROXY_HTTPS_URL } if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None -def get(url, *args, **kwargs): - return _get(url=url, *args, proxies=httpx_proxies, **kwargs) -def post(url, *args, **kwargs): - return _post(url=url, *args, proxies=httpx_proxies, **kwargs) +def make_request(method, url, **kwargs): + if SSRF_PROXY_ALL_URL: + return httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs) + elif proxies: + return httpx.request(method=method, url=url, proxies=proxies, **kwargs) + else: + return httpx.request(method=method, url=url, **kwargs) -def put(url, *args, **kwargs): - return _put(url=url, *args, proxies=httpx_proxies, **kwargs) -def patch(url, *args, **kwargs): - return _patch(url=url, *args, proxies=httpx_proxies, **kwargs) +def get(url, **kwargs): + return make_request('GET', url, **kwargs) -def delete(url, *args, **kwargs): - if 'follow_redirects' in kwargs: - if kwargs['follow_redirects']: - kwargs['allow_redirects'] = kwargs['follow_redirects'] - kwargs.pop('follow_redirects') - if 'timeout' in kwargs: - timeout = kwargs['timeout'] - if timeout is None: - kwargs.pop('timeout') - elif isinstance(timeout, tuple): - # check length of tuple - if len(timeout) == 2: - kwargs['timeout'] = timeout - elif len(timeout) == 1: - kwargs['timeout'] = timeout[0] - elif len(timeout) > 2: - kwargs['timeout'] = (timeout[0], timeout[1]) - else: - kwargs['timeout'] = (timeout, timeout) - return _delete(url=url, *args, proxies=requests_proxies, **kwargs) -def head(url, *args, **kwargs): - return _head(url=url, *args, proxies=httpx_proxies, **kwargs) +def post(url, **kwargs): + return make_request('POST', url, **kwargs) -def options(url, *args, **kwargs): - return _options(url=url, *args, proxies=httpx_proxies, **kwargs) + +def put(url, **kwargs): + return make_request('PUT', url, **kwargs) + + +def patch(url, **kwargs): + return make_request('PATCH', url, **kwargs) + + +def delete(url, **kwargs): + return make_request('DELETE', url, **kwargs) + + +def head(url, **kwargs): + return make_request('HEAD', url, **kwargs) diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index af4bed13ef..826edff608 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -397,7 +397,7 @@ class IndexingRunner: document_id=dataset_document.id, after_indexing_status="splitting", extra_update_params={ - DatasetDocument.word_count: sum([len(text_doc.page_content) for text_doc in text_docs]), + DatasetDocument.word_count: sum(len(text_doc.page_content) for text_doc in text_docs), DatasetDocument.parsing_completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) } ) diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index 14de8649c6..70d3befbbd 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -1,5 +1,7 @@ import json import logging +import re +from typing import Optional from core.llm_generator.output_parser.errors import OutputParserException from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser @@ -9,12 +11,16 @@ from core.model_manager import ModelManager from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage from core.model_runtime.entities.model_entities import ModelType from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName +from core.ops.utils import measure_time from core.prompt.utils.prompt_template_parser import PromptTemplateParser class LLMGenerator: @classmethod - def generate_conversation_name(cls, tenant_id: str, query): + def generate_conversation_name( + cls, tenant_id: str, query, conversation_id: Optional[str] = None, app_id: Optional[str] = None + ): prompt = CONVERSATION_TITLE_PROMPT if len(query) > 2000: @@ -29,25 +35,39 @@ class LLMGenerator: tenant_id=tenant_id, model_type=ModelType.LLM, ) - prompts = [UserPromptMessage(content=prompt)] - response = model_instance.invoke_llm( - prompt_messages=prompts, - model_parameters={ - "max_tokens": 100, - "temperature": 1 - }, - stream=False - ) - answer = response.message.content - result_dict = json.loads(answer) + with measure_time() as timer: + response = model_instance.invoke_llm( + prompt_messages=prompts, + model_parameters={ + "max_tokens": 100, + "temperature": 1 + }, + stream=False + ) + answer = response.message.content + cleaned_answer = re.sub(r'^.*(\{.*\}).*$', r'\1', answer, flags=re.DOTALL) + result_dict = json.loads(cleaned_answer) answer = result_dict['Your Output'] name = answer.strip() if len(name) > 75: name = name[:75] + '...' + # get tracing instance + trace_manager = TraceQueueManager(app_id=app_id) + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.GENERATE_NAME_TRACE, + conversation_id=conversation_id, + generate_conversation_name=name, + inputs=prompt, + timer=timer, + tenant_id=tenant_id, + ) + ) + return name @classmethod diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index 4f4f5045f5..21f1965e93 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -12,7 +12,8 @@ from core.model_runtime.entities.message_entities import ( UserPromptMessage, ) from extensions.ext_database import db -from models.model import AppMode, Conversation, Message +from models.model import AppMode, Conversation, Message, MessageFile +from models.workflow import WorkflowRun class TokenBufferMemory: @@ -30,33 +31,46 @@ class TokenBufferMemory: app_record = self.conversation.app # fetch limited messages, and return reversed - query = db.session.query(Message).filter( + query = db.session.query( + Message.id, + Message.query, + Message.answer, + Message.created_at, + Message.workflow_run_id + ).filter( Message.conversation_id == self.conversation.id, Message.answer != '' ).order_by(Message.created_at.desc()) if message_limit and message_limit > 0: - messages = query.limit(message_limit).all() + message_limit = message_limit if message_limit <= 500 else 500 else: - messages = query.all() + message_limit = 500 + + messages = query.limit(message_limit).all() messages = list(reversed(messages)) message_file_parser = MessageFileParser( tenant_id=app_record.tenant_id, app_id=app_record.id ) - prompt_messages = [] for message in messages: - files = message.message_files + 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.value, AppMode.WORKFLOW.value]: - file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict()) + file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config) else: - file_extra_config = FileUploadConfigManager.convert( - message.workflow_run.workflow.features_dict, - is_vision=False - ) + if message.workflow_run_id: + workflow_run = (db.session.query(WorkflowRun) + .filter(WorkflowRun.id == message.workflow_run_id).first()) + + if workflow_run: + file_extra_config = FileUploadConfigManager.convert( + workflow_run.workflow.features_dict, + is_vision=False + ) if file_extra_config: file_objs = message_file_parser.transform_message_files( @@ -136,4 +150,4 @@ class TokenBufferMemory: message = f"{role}: {m.content}" string_messages.append(message) - return "\n".join(string_messages) \ No newline at end of file + return "\n".join(string_messages) diff --git a/api/core/model_runtime/model_providers/azure_openai/llm/llm.py b/api/core/model_runtime/model_providers/azure_openai/llm/llm.py index eb6d985f23..25bc94cde6 100644 --- a/api/core/model_runtime/model_providers/azure_openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/azure_openai/llm/llm.py @@ -1,14 +1,13 @@ import copy import logging -from collections.abc import Generator +from collections.abc import Generator, Sequence from typing import Optional, Union, cast import tiktoken from openai import AzureOpenAI, Stream from openai.types import Completion from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessageToolCall -from openai.types.chat.chat_completion_chunk import ChoiceDeltaFunctionCall, ChoiceDeltaToolCall -from openai.types.chat.chat_completion_message import FunctionCall +from openai.types.chat.chat_completion_chunk import ChoiceDeltaToolCall from core.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMResultChunk, LLMResultChunkDelta from core.model_runtime.entities.message_entities import ( @@ -16,6 +15,7 @@ from core.model_runtime.entities.message_entities import ( ImagePromptMessageContent, PromptMessage, PromptMessageContentType, + PromptMessageFunction, PromptMessageTool, SystemPromptMessage, TextPromptMessageContent, @@ -26,7 +26,8 @@ from core.model_runtime.entities.model_entities import AIModelEntity, ModelPrope from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.model_providers.azure_openai._common import _CommonAzureOpenAI -from core.model_runtime.model_providers.azure_openai._constant import LLM_BASE_MODELS, AzureBaseModel +from core.model_runtime.model_providers.azure_openai._constant import LLM_BASE_MODELS +from core.model_runtime.utils import helper logger = logging.getLogger(__name__) @@ -39,9 +40,12 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): stream: bool = True, user: Optional[str] = None) \ -> Union[LLMResult, Generator]: - ai_model_entity = self._get_ai_model_entity(credentials.get('base_model_name'), model) + base_model_name = credentials.get('base_model_name') + if not base_model_name: + raise ValueError('Base Model Name is required') + ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model) - if ai_model_entity.entity.model_properties.get(ModelPropertyKey.MODE) == LLMMode.CHAT.value: + if ai_model_entity and ai_model_entity.entity.model_properties.get(ModelPropertyKey.MODE) == LLMMode.CHAT.value: # chat model return self._chat_generate( model=model, @@ -65,18 +69,29 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): user=user ) - def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - tools: Optional[list[PromptMessageTool]] = None) -> int: - - model_mode = self._get_ai_model_entity(credentials.get('base_model_name'), model).entity.model_properties.get( - ModelPropertyKey.MODE) + def get_num_tokens( + self, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + tools: Optional[list[PromptMessageTool]] = None + ) -> int: + base_model_name = credentials.get('base_model_name') + if not base_model_name: + raise ValueError('Base Model Name is required') + model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model) + if not model_entity: + raise ValueError(f'Base Model Name {base_model_name} is invalid') + model_mode = model_entity.entity.model_properties.get(ModelPropertyKey.MODE) if model_mode == LLMMode.CHAT.value: # chat model return self._num_tokens_from_messages(credentials, prompt_messages, tools) else: # text completion model, do not support tool calling - return self._num_tokens_from_string(credentials, prompt_messages[0].content) + content = prompt_messages[0].content + assert isinstance(content, str) + return self._num_tokens_from_string(credentials,content) def validate_credentials(self, model: str, credentials: dict) -> None: if 'openai_api_base' not in credentials: @@ -88,7 +103,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): if 'base_model_name' not in credentials: raise CredentialsValidateFailedError('Base Model Name is required') - ai_model_entity = self._get_ai_model_entity(credentials.get('base_model_name'), model) + base_model_name = credentials.get('base_model_name') + if not base_model_name: + raise CredentialsValidateFailedError('Base Model Name is required') + ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model) if not ai_model_entity: raise CredentialsValidateFailedError(f'Base Model Name {credentials["base_model_name"]} is invalid') @@ -118,7 +136,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): raise CredentialsValidateFailedError(str(ex)) def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: - ai_model_entity = self._get_ai_model_entity(credentials.get('base_model_name'), model) + base_model_name = credentials.get('base_model_name') + if not base_model_name: + raise ValueError('Base Model Name is required') + ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model) return ai_model_entity.entity if ai_model_entity else None def _generate(self, model: str, credentials: dict, @@ -149,8 +170,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): return self._handle_generate_response(model, credentials, response, prompt_messages) - def _handle_generate_response(self, model: str, credentials: dict, response: Completion, - prompt_messages: list[PromptMessage]) -> LLMResult: + def _handle_generate_response( + self, model: str, credentials: dict, response: Completion, + prompt_messages: list[PromptMessage] + ): assistant_text = response.choices[0].text # transform assistant message to prompt message @@ -165,7 +188,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): completion_tokens = response.usage.completion_tokens else: # calculate num tokens - prompt_tokens = self._num_tokens_from_string(credentials, prompt_messages[0].content) + content = prompt_messages[0].content + assert isinstance(content, str) + prompt_tokens = self._num_tokens_from_string(credentials, content) completion_tokens = self._num_tokens_from_string(credentials, assistant_text) # transform usage @@ -182,8 +207,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): return result - def _handle_generate_stream_response(self, model: str, credentials: dict, response: Stream[Completion], - prompt_messages: list[PromptMessage]) -> Generator: + def _handle_generate_stream_response( + self, model: str, credentials: dict, response: Stream[Completion], + prompt_messages: list[PromptMessage] + ) -> Generator: full_text = '' for chunk in response: if len(chunk.choices) == 0: @@ -210,7 +237,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): completion_tokens = chunk.usage.completion_tokens else: # calculate num tokens - prompt_tokens = self._num_tokens_from_string(credentials, prompt_messages[0].content) + content = prompt_messages[0].content + assert isinstance(content, str) + prompt_tokens = self._num_tokens_from_string(credentials, content) completion_tokens = self._num_tokens_from_string(credentials, full_text) # transform usage @@ -257,12 +286,12 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): extra_model_kwargs = {} if tools: - # extra_model_kwargs['tools'] = [helper.dump_model(PromptMessageFunction(function=tool)) for tool in tools] - extra_model_kwargs['functions'] = [{ - "name": tool.name, - "description": tool.description, - "parameters": tool.parameters - } for tool in tools] + extra_model_kwargs['tools'] = [helper.dump_model(PromptMessageFunction(function=tool)) for tool in tools] + # extra_model_kwargs['functions'] = [{ + # "name": tool.name, + # "description": tool.description, + # "parameters": tool.parameters + # } for tool in tools] if stop: extra_model_kwargs['stop'] = stop @@ -271,8 +300,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): extra_model_kwargs['user'] = user # chat model + messages = [self._convert_prompt_message_to_dict(m) for m in prompt_messages] response = client.chat.completions.create( - messages=[self._convert_prompt_message_to_dict(m) for m in prompt_messages], + messages=messages, model=model, stream=stream, **model_parameters, @@ -284,18 +314,17 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): return self._handle_chat_generate_response(model, credentials, response, prompt_messages, tools) - def _handle_chat_generate_response(self, model: str, credentials: dict, response: ChatCompletion, - prompt_messages: list[PromptMessage], - tools: Optional[list[PromptMessageTool]] = None) -> LLMResult: - + def _handle_chat_generate_response( + self, model: str, credentials: dict, response: ChatCompletion, + prompt_messages: list[PromptMessage], + tools: Optional[list[PromptMessageTool]] = None + ): assistant_message = response.choices[0].message - # assistant_message_tool_calls = assistant_message.tool_calls - assistant_message_function_call = assistant_message.function_call + assistant_message_tool_calls = assistant_message.tool_calls # extract tool calls from response - # tool_calls = self._extract_response_tool_calls(assistant_message_tool_calls) - function_call = self._extract_response_function_call(assistant_message_function_call) - tool_calls = [function_call] if function_call else [] + tool_calls = [] + self._update_tool_calls(tool_calls=tool_calls, tool_calls_response=assistant_message_tool_calls) # transform assistant message to prompt message assistant_prompt_message = AssistantPromptMessage( @@ -317,7 +346,7 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens) # transform response - response = LLMResult( + result = LLMResult( model=response.model or model, prompt_messages=prompt_messages, message=assistant_prompt_message, @@ -325,58 +354,34 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): system_fingerprint=response.system_fingerprint, ) - return response + return result - def _handle_chat_generate_stream_response(self, model: str, credentials: dict, - response: Stream[ChatCompletionChunk], - prompt_messages: list[PromptMessage], - tools: Optional[list[PromptMessageTool]] = None) -> Generator: + def _handle_chat_generate_stream_response( + self, + model: str, + credentials: dict, + response: Stream[ChatCompletionChunk], + prompt_messages: list[PromptMessage], + tools: Optional[list[PromptMessageTool]] = None + ): index = 0 full_assistant_content = '' - delta_assistant_message_function_call_storage: ChoiceDeltaFunctionCall = None real_model = model system_fingerprint = None completion = '' + tool_calls = [] for chunk in response: if len(chunk.choices) == 0: continue delta = chunk.choices[0] + # extract tool calls from response + self._update_tool_calls(tool_calls=tool_calls, tool_calls_response=delta.delta.tool_calls) + # Handling exceptions when content filters' streaming mode is set to asynchronous modified filter - if delta.delta is None or ( - delta.finish_reason is None - and (delta.delta.content is None or delta.delta.content == '') - and delta.delta.function_call is None - ): + if delta.finish_reason is None and not delta.delta.content: continue - - # assistant_message_tool_calls = delta.delta.tool_calls - assistant_message_function_call = delta.delta.function_call - - # extract tool calls from response - if delta_assistant_message_function_call_storage is not None: - # handle process of stream function call - if assistant_message_function_call: - # message has not ended ever - delta_assistant_message_function_call_storage.arguments += assistant_message_function_call.arguments - continue - else: - # message has ended - assistant_message_function_call = delta_assistant_message_function_call_storage - delta_assistant_message_function_call_storage = None - else: - if assistant_message_function_call: - # start of stream function call - delta_assistant_message_function_call_storage = assistant_message_function_call - if delta_assistant_message_function_call_storage.arguments is None: - delta_assistant_message_function_call_storage.arguments = '' - continue - - # extract tool calls from response - # tool_calls = self._extract_response_tool_calls(assistant_message_tool_calls) - function_call = self._extract_response_function_call(assistant_message_function_call) - tool_calls = [function_call] if function_call else [] # transform assistant message to prompt message assistant_prompt_message = AssistantPromptMessage( @@ -426,54 +431,56 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): ) @staticmethod - def _extract_response_tool_calls(response_tool_calls: list[ChatCompletionMessageToolCall | ChoiceDeltaToolCall]) \ - -> list[AssistantPromptMessage.ToolCall]: + def _update_tool_calls(tool_calls: list[AssistantPromptMessage.ToolCall], tool_calls_response: Optional[Sequence[ChatCompletionMessageToolCall | ChoiceDeltaToolCall]]) -> None: + if tool_calls_response: + for response_tool_call in tool_calls_response: + if isinstance(response_tool_call, ChatCompletionMessageToolCall): + function = AssistantPromptMessage.ToolCall.ToolCallFunction( + name=response_tool_call.function.name, + arguments=response_tool_call.function.arguments + ) - tool_calls = [] - if response_tool_calls: - for response_tool_call in response_tool_calls: - function = AssistantPromptMessage.ToolCall.ToolCallFunction( - name=response_tool_call.function.name, - arguments=response_tool_call.function.arguments - ) + tool_call = AssistantPromptMessage.ToolCall( + id=response_tool_call.id, + type=response_tool_call.type, + function=function + ) + tool_calls.append(tool_call) + elif isinstance(response_tool_call, ChoiceDeltaToolCall): + index = response_tool_call.index + if index < len(tool_calls): + tool_calls[index].id = response_tool_call.id or tool_calls[index].id + tool_calls[index].type = response_tool_call.type or tool_calls[index].type + if response_tool_call.function: + tool_calls[index].function.name = response_tool_call.function.name or tool_calls[index].function.name + tool_calls[index].function.arguments += response_tool_call.function.arguments or '' + else: + assert response_tool_call.id is not None + assert response_tool_call.type is not None + assert response_tool_call.function is not None + assert response_tool_call.function.name is not None + assert response_tool_call.function.arguments is not None - tool_call = AssistantPromptMessage.ToolCall( - id=response_tool_call.id, - type=response_tool_call.type, - function=function - ) - tool_calls.append(tool_call) - - return tool_calls + function = AssistantPromptMessage.ToolCall.ToolCallFunction( + name=response_tool_call.function.name, + arguments=response_tool_call.function.arguments + ) + tool_call = AssistantPromptMessage.ToolCall( + id=response_tool_call.id, + type=response_tool_call.type, + function=function + ) + tool_calls.append(tool_call) @staticmethod - def _extract_response_function_call(response_function_call: FunctionCall | ChoiceDeltaFunctionCall) \ - -> AssistantPromptMessage.ToolCall: - - tool_call = None - if response_function_call: - function = AssistantPromptMessage.ToolCall.ToolCallFunction( - name=response_function_call.name, - arguments=response_function_call.arguments - ) - - tool_call = AssistantPromptMessage.ToolCall( - id=response_function_call.name, - type="function", - function=function - ) - - return tool_call - - @staticmethod - def _convert_prompt_message_to_dict(message: PromptMessage) -> dict: - + def _convert_prompt_message_to_dict(message: PromptMessage): if isinstance(message, UserPromptMessage): message = cast(UserPromptMessage, message) if isinstance(message.content, str): message_dict = {"role": "user", "content": message.content} else: sub_messages = [] + assert message.content is not None for message_content in message.content: if message_content.type == PromptMessageContentType.TEXT: message_content = cast(TextPromptMessageContent, message_content) @@ -492,33 +499,22 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): } } sub_messages.append(sub_message_dict) - message_dict = {"role": "user", "content": sub_messages} elif isinstance(message, AssistantPromptMessage): message = cast(AssistantPromptMessage, message) message_dict = {"role": "assistant", "content": message.content} if message.tool_calls: - # message_dict["tool_calls"] = [helper.dump_model(tool_call) for tool_call in - # message.tool_calls] - function_call = message.tool_calls[0] - message_dict["function_call"] = { - "name": function_call.function.name, - "arguments": function_call.function.arguments, - } + message_dict["tool_calls"] = [helper.dump_model(tool_call) for tool_call in message.tool_calls] elif isinstance(message, SystemPromptMessage): message = cast(SystemPromptMessage, message) message_dict = {"role": "system", "content": message.content} elif isinstance(message, ToolPromptMessage): message = cast(ToolPromptMessage, message) - # message_dict = { - # "role": "tool", - # "content": message.content, - # "tool_call_id": message.tool_call_id - # } message_dict = { - "role": "function", + "role": "tool", + "name": message.name, "content": message.content, - "name": message.tool_call_id + "tool_call_id": message.tool_call_id } else: raise ValueError(f"Got unknown type {message}") @@ -542,8 +538,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): return num_tokens - def _num_tokens_from_messages(self, credentials: dict, messages: list[PromptMessage], - tools: Optional[list[PromptMessageTool]] = None) -> int: + def _num_tokens_from_messages( + self, credentials: dict, messages: list[PromptMessage], + tools: Optional[list[PromptMessageTool]] = None + ) -> int: """Calculate num tokens for gpt-3.5-turbo and gpt-4 with tiktoken package. Official documentation: https://github.com/openai/openai-cookbook/blob/ @@ -591,6 +589,7 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): if key == "tool_calls": for tool_call in value: + assert isinstance(tool_call, dict) for t_key, t_value in tool_call.items(): num_tokens += len(encoding.encode(t_key)) if t_key == "function": @@ -631,12 +630,12 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): num_tokens += len(encoding.encode('parameters')) if 'title' in parameters: num_tokens += len(encoding.encode('title')) - num_tokens += len(encoding.encode(parameters.get("title"))) + num_tokens += len(encoding.encode(parameters['title'])) num_tokens += len(encoding.encode('type')) - num_tokens += len(encoding.encode(parameters.get("type"))) + num_tokens += len(encoding.encode(parameters['type'])) if 'properties' in parameters: num_tokens += len(encoding.encode('properties')) - for key, value in parameters.get('properties').items(): + for key, value in parameters['properties'].items(): num_tokens += len(encoding.encode(key)) for field_key, field_value in value.items(): num_tokens += len(encoding.encode(field_key)) @@ -656,7 +655,7 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): return num_tokens @staticmethod - def _get_ai_model_entity(base_model_name: str, model: str) -> AzureBaseModel: + def _get_ai_model_entity(base_model_name: str, model: str): for ai_model_entity in LLM_BASE_MODELS: if ai_model_entity.base_model_name == base_model_name: ai_model_entity_copy = copy.deepcopy(ai_model_entity) @@ -664,5 +663,3 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel): ai_model_entity_copy.entity.label.en_US = model ai_model_entity_copy.entity.label.zh_Hans = model return ai_model_entity_copy - - return None diff --git a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py index 585b061afe..dcd154cff0 100644 --- a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py +++ b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py @@ -83,7 +83,7 @@ class AzureOpenAIText2SpeechModel(_CommonAzureOpenAI, TTSModel): max_workers = self._get_model_workers_limit(model, credentials) try: sentences = list(self._split_text_into_sentences(text=content_text, limit=word_limit)) - audio_bytes_list = list() + audio_bytes_list = [] # Create a thread pool and map the function to the list of sentences with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-haiku-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-haiku-v1.yaml index 181b192769..53657c08a9 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-haiku-v1.yaml +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-haiku-v1.yaml @@ -5,6 +5,8 @@ model_type: llm features: - agent-thought - vision + - tool-call + - stream-tool-call model_properties: mode: chat context_size: 200000 diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-opus-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-opus-v1.yaml index f858afe417..d083d31e30 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-opus-v1.yaml +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-opus-v1.yaml @@ -5,6 +5,8 @@ model_type: llm features: - agent-thought - vision + - tool-call + - stream-tool-call model_properties: mode: chat context_size: 200000 diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.5.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.5.yaml index 2ae7b8ffaa..5302231086 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.5.yaml +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.5.yaml @@ -5,6 +5,8 @@ model_type: llm features: - agent-thought - vision + - tool-call + - stream-tool-call model_properties: mode: chat context_size: 200000 diff --git a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.yaml b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.yaml index b782faddba..6995d2bf56 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.yaml +++ b/api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.yaml @@ -5,6 +5,8 @@ model_type: llm features: - agent-thought - vision + - tool-call + - stream-tool-call model_properties: mode: chat context_size: 200000 diff --git a/api/core/model_runtime/model_providers/bedrock/llm/llm.py b/api/core/model_runtime/model_providers/bedrock/llm/llm.py index dad5120d83..efb8c395fa 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/llm.py +++ b/api/core/model_runtime/model_providers/bedrock/llm/llm.py @@ -1,22 +1,14 @@ +# standard import import base64 import json import logging import mimetypes -import time from collections.abc import Generator from typing import Optional, Union, cast +# 3rd import import boto3 import requests -from anthropic import AnthropicBedrock, Stream -from anthropic.types import ( - ContentBlockDeltaEvent, - Message, - MessageDeltaEvent, - MessageStartEvent, - MessageStopEvent, - MessageStreamEvent, -) from botocore.config import Config from botocore.exceptions import ( ClientError, @@ -27,7 +19,8 @@ from botocore.exceptions import ( ) from cohere import ChatMessage -from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +# local import +from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, ImagePromptMessageContent, @@ -36,9 +29,9 @@ from core.model_runtime.entities.message_entities import ( PromptMessageTool, SystemPromptMessage, TextPromptMessageContent, + ToolPromptMessage, UserPromptMessage, ) -from core.model_runtime.entities.model_entities import PriceType from core.model_runtime.errors.invoke import ( InvokeAuthorizationError, InvokeBadRequestError, @@ -73,10 +66,10 @@ class BedrockLargeLanguageModel(LargeLanguageModel): :param user: unique user id :return: full response or stream response chunk generator result """ - - # invoke anthropic models via anthropic official SDK + # TODO: consolidate different invocation methods for models based on base model capabilities + # invoke anthropic models via boto3 client if "anthropic" in model: - return self._generate_anthropic(model, credentials, prompt_messages, model_parameters, stop, stream, user) + return self._generate_anthropic(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools) # invoke Cohere models via boto3 client if "cohere.command-r" in model: return self._generate_cohere_chat(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools) @@ -159,7 +152,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel): def _generate_anthropic(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, - stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None) -> Union[LLMResult, Generator]: + stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]: """ Invoke Anthropic large language model @@ -171,48 +164,35 @@ class BedrockLargeLanguageModel(LargeLanguageModel): :param stream: is stream response :return: full response or stream response chunk generator result """ - # use Anthropic official SDK references - # - https://docs.anthropic.com/claude/reference/claude-on-amazon-bedrock - # - https://github.com/anthropics/anthropic-sdk-python - client = AnthropicBedrock( - aws_access_key=credentials.get("aws_access_key_id", None), - aws_secret_key=credentials.get("aws_secret_access_key", None), - aws_region=credentials["aws_region"], - ) + bedrock_client = boto3.client(service_name='bedrock-runtime', + aws_access_key_id=credentials.get("aws_access_key_id"), + aws_secret_access_key=credentials.get("aws_secret_access_key"), + region_name=credentials["aws_region"]) - extra_model_kwargs = {} - if stop: - extra_model_kwargs['stop_sequences'] = stop + system, prompt_message_dicts = self._convert_converse_prompt_messages(prompt_messages) + inference_config, additional_model_fields = self._convert_converse_api_model_parameters(model_parameters, stop) - # Notice: If you request the current version of the SDK to the bedrock server, - # you will get the following error message and you need to wait for the service or SDK to be updated. - # Response: Error code: 400 - # {'message': 'Malformed input request: #: subject must not be valid against schema - # {"required":["messages"]}#: extraneous key [metadata] is not permitted, please reformat your input and try again.'} - # TODO: Open in the future when the interface is properly supported - # if user: - # ref: https://github.com/anthropics/anthropic-sdk-python/blob/e84645b07ca5267066700a104b4d8d6a8da1383d/src/anthropic/resources/messages.py#L465 - # extra_model_kwargs['metadata'] = message_create_params.Metadata(user_id=user) + parameters = { + 'modelId': model, + 'messages': prompt_message_dicts, + 'inferenceConfig': inference_config, + 'additionalModelRequestFields': additional_model_fields, + } - system, prompt_message_dicts = self._convert_claude_prompt_messages(prompt_messages) + if system and len(system) > 0: + parameters['system'] = system - if system: - extra_model_kwargs['system'] = system - - response = client.messages.create( - model=model, - messages=prompt_message_dicts, - stream=stream, - **model_parameters, - **extra_model_kwargs - ) + if tools: + parameters['toolConfig'] = self._convert_converse_tool_config(tools=tools) if stream: - return self._handle_claude_stream_response(model, credentials, response, prompt_messages) + response = bedrock_client.converse_stream(**parameters) + return self._handle_converse_stream_response(model, credentials, response, prompt_messages) + else: + response = bedrock_client.converse(**parameters) + return self._handle_converse_response(model, credentials, response, prompt_messages) - return self._handle_claude_response(model, credentials, response, prompt_messages) - - def _handle_claude_response(self, model: str, credentials: dict, response: Message, + def _handle_converse_response(self, model: str, credentials: dict, response: dict, prompt_messages: list[PromptMessage]) -> LLMResult: """ Handle llm chat response @@ -223,17 +203,16 @@ class BedrockLargeLanguageModel(LargeLanguageModel): :param prompt_messages: prompt messages :return: full response chunk generator result """ - # transform assistant message to prompt message assistant_prompt_message = AssistantPromptMessage( - content=response.content[0].text + content=response['output']['message']['content'][0]['text'] ) # calculate num tokens - if response.usage: + if response['usage']: # transform usage - prompt_tokens = response.usage.input_tokens - completion_tokens = response.usage.output_tokens + prompt_tokens = response['usage']['inputTokens'] + completion_tokens = response['usage']['outputTokens'] else: # calculate num tokens prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages) @@ -242,17 +221,15 @@ class BedrockLargeLanguageModel(LargeLanguageModel): # transform usage usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens) - # transform response - response = LLMResult( - model=response.model, + result = LLMResult( + model=model, prompt_messages=prompt_messages, message=assistant_prompt_message, - usage=usage + usage=usage, ) + return result - return response - - def _handle_claude_stream_response(self, model: str, credentials: dict, response: Stream[MessageStreamEvent], + def _handle_converse_stream_response(self, model: str, credentials: dict, response: dict, prompt_messages: list[PromptMessage], ) -> Generator: """ Handle llm chat stream response @@ -271,129 +248,143 @@ class BedrockLargeLanguageModel(LargeLanguageModel): output_tokens = 0 finish_reason = None index = 0 + tool_calls: list[AssistantPromptMessage.ToolCall] = [] + tool_use = {} - for chunk in response: - if isinstance(chunk, MessageStartEvent): - return_model = chunk.message.model - input_tokens = chunk.message.usage.input_tokens - elif isinstance(chunk, MessageDeltaEvent): - output_tokens = chunk.usage.output_tokens - finish_reason = chunk.delta.stop_reason - elif isinstance(chunk, MessageStopEvent): + for chunk in response['stream']: + if 'messageStart' in chunk: + return_model = model + elif 'messageStop' in chunk: + finish_reason = chunk['messageStop']['stopReason'] + elif 'contentBlockStart' in chunk: + tool = chunk['contentBlockStart']['start']['toolUse'] + tool_use['toolUseId'] = tool['toolUseId'] + tool_use['name'] = tool['name'] + elif 'metadata' in chunk: + input_tokens = chunk['metadata']['usage']['inputTokens'] + output_tokens = chunk['metadata']['usage']['outputTokens'] usage = self._calc_response_usage(model, credentials, input_tokens, output_tokens) yield LLMResultChunk( model=return_model, prompt_messages=prompt_messages, delta=LLMResultChunkDelta( - index=index + 1, + index=index, message=AssistantPromptMessage( - content='' + content='', + tool_calls=tool_calls ), finish_reason=finish_reason, usage=usage ) ) - elif isinstance(chunk, ContentBlockDeltaEvent): - chunk_text = chunk.delta.text if chunk.delta.text else '' - full_assistant_content += chunk_text - assistant_prompt_message = AssistantPromptMessage( - content=chunk_text if chunk_text else '', - ) - index = chunk.index - yield LLMResultChunk( - model=model, - prompt_messages=prompt_messages, - delta=LLMResultChunkDelta( - index=index, - message=assistant_prompt_message, + elif 'contentBlockDelta' in chunk: + delta = chunk['contentBlockDelta']['delta'] + if 'text' in delta: + chunk_text = delta['text'] if delta['text'] else '' + full_assistant_content += chunk_text + assistant_prompt_message = AssistantPromptMessage( + content=chunk_text if chunk_text else '', ) - ) + index = chunk['contentBlockDelta']['contentBlockIndex'] + yield LLMResultChunk( + model=model, + prompt_messages=prompt_messages, + delta=LLMResultChunkDelta( + index=index+1, + message=assistant_prompt_message, + ) + ) + elif 'toolUse' in delta: + if 'input' not in tool_use: + tool_use['input'] = '' + tool_use['input'] += delta['toolUse']['input'] + elif 'contentBlockStop' in chunk: + if 'input' in tool_use: + tool_call = AssistantPromptMessage.ToolCall( + id=tool_use['toolUseId'], + type='function', + function=AssistantPromptMessage.ToolCall.ToolCallFunction( + name=tool_use['name'], + arguments=tool_use['input'] + ) + ) + tool_calls.append(tool_call) + tool_use = {} + except Exception as ex: raise InvokeError(str(ex)) + + def _convert_converse_api_model_parameters(self, model_parameters: dict, stop: Optional[list[str]] = None) -> tuple[dict, dict]: + inference_config = {} + additional_model_fields = {} + if 'max_tokens' in model_parameters: + inference_config['maxTokens'] = model_parameters['max_tokens'] - def _calc_claude_response_usage(self, model: str, credentials: dict, prompt_tokens: int, completion_tokens: int) -> LLMUsage: - """ - Calculate response usage + if 'temperature' in model_parameters: + inference_config['temperature'] = model_parameters['temperature'] + + if 'top_p' in model_parameters: + inference_config['topP'] = model_parameters['temperature'] - :param model: model name - :param credentials: model credentials - :param prompt_tokens: prompt tokens - :param completion_tokens: completion tokens - :return: usage - """ - # get prompt price info - prompt_price_info = self.get_price( - model=model, - credentials=credentials, - price_type=PriceType.INPUT, - tokens=prompt_tokens, - ) + if stop: + inference_config['stopSequences'] = stop + + if 'top_k' in model_parameters: + additional_model_fields['top_k'] = model_parameters['top_k'] + + return inference_config, additional_model_fields - # get completion price info - completion_price_info = self.get_price( - model=model, - credentials=credentials, - price_type=PriceType.OUTPUT, - tokens=completion_tokens - ) - - # transform usage - usage = LLMUsage( - prompt_tokens=prompt_tokens, - prompt_unit_price=prompt_price_info.unit_price, - prompt_price_unit=prompt_price_info.unit, - prompt_price=prompt_price_info.total_amount, - completion_tokens=completion_tokens, - completion_unit_price=completion_price_info.unit_price, - completion_price_unit=completion_price_info.unit, - completion_price=completion_price_info.total_amount, - total_tokens=prompt_tokens + completion_tokens, - total_price=prompt_price_info.total_amount + completion_price_info.total_amount, - currency=prompt_price_info.currency, - latency=time.perf_counter() - self.started_at - ) - - return usage - - def _convert_claude_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]: + def _convert_converse_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]: """ Convert prompt messages to dict list and system """ - system = "" - first_loop = True + system = [] for message in prompt_messages: if isinstance(message, SystemPromptMessage): message.content=message.content.strip() - if first_loop: - system=message.content - first_loop=False - else: - system+="\n" - system+=message.content + system.append({"text": message.content}) prompt_message_dicts = [] for message in prompt_messages: if not isinstance(message, SystemPromptMessage): - prompt_message_dicts.append(self._convert_claude_prompt_message_to_dict(message)) + prompt_message_dicts.append(self._convert_prompt_message_to_dict(message)) return system, prompt_message_dicts - def _convert_claude_prompt_message_to_dict(self, message: PromptMessage) -> dict: + def _convert_converse_tool_config(self, tools: Optional[list[PromptMessageTool]] = None) -> dict: + tool_config = {} + configs = [] + if tools: + for tool in tools: + configs.append( + { + "toolSpec": { + "name": tool.name, + "description": tool.description, + "inputSchema": { + "json": tool.parameters + } + } + } + ) + tool_config["tools"] = configs + return tool_config + + def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict: """ Convert PromptMessage to dict """ if isinstance(message, UserPromptMessage): message = cast(UserPromptMessage, message) if isinstance(message.content, str): - message_dict = {"role": "user", "content": message.content} + message_dict = {"role": "user", "content": [{'text': message.content}]} else: sub_messages = [] for message_content in message.content: if message_content.type == PromptMessageContentType.TEXT: message_content = cast(TextPromptMessageContent, message_content) sub_message_dict = { - "type": "text", "text": message_content.data } sub_messages.append(sub_message_dict) @@ -404,24 +395,24 @@ class BedrockLargeLanguageModel(LargeLanguageModel): try: image_content = requests.get(message_content.data).content mime_type, _ = mimetypes.guess_type(message_content.data) - base64_data = base64.b64encode(image_content).decode('utf-8') except Exception as ex: raise ValueError(f"Failed to fetch image data from url {message_content.data}, {ex}") else: data_split = message_content.data.split(";base64,") mime_type = data_split[0].replace("data:", "") base64_data = data_split[1] + image_content = base64.b64decode(base64_data) if mime_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]: raise ValueError(f"Unsupported image type {mime_type}, " f"only support image/jpeg, image/png, image/gif, and image/webp") sub_message_dict = { - "type": "image", - "source": { - "type": "base64", - "media_type": mime_type, - "data": base64_data + "image": { + "format": mime_type.replace('image/', ''), + "source": { + "bytes": image_content + } } } sub_messages.append(sub_message_dict) @@ -429,10 +420,32 @@ class BedrockLargeLanguageModel(LargeLanguageModel): message_dict = {"role": "user", "content": sub_messages} elif isinstance(message, AssistantPromptMessage): message = cast(AssistantPromptMessage, message) - message_dict = {"role": "assistant", "content": message.content} + if message.tool_calls: + message_dict = { + "role": "assistant", "content":[{ + "toolUse": { + "toolUseId": message.tool_calls[0].id, + "name": message.tool_calls[0].function.name, + "input": json.loads(message.tool_calls[0].function.arguments) + } + }] + } + else: + message_dict = {"role": "assistant", "content": [{'text': message.content}]} elif isinstance(message, SystemPromptMessage): message = cast(SystemPromptMessage, message) - message_dict = {"role": "system", "content": message.content} + message_dict = [{'text': message.content}] + elif isinstance(message, ToolPromptMessage): + message = cast(ToolPromptMessage, message) + message_dict = { + "role": "user", + "content": [{ + "toolResult": { + "toolUseId": message.tool_call_id, + "content": [{"json": {"text": message.content}}] + } + }] + } else: raise ValueError(f"Got unknown type {message}") @@ -451,11 +464,13 @@ class BedrockLargeLanguageModel(LargeLanguageModel): """ prefix = model.split('.')[0] model_name = model.split('.')[1] + if isinstance(prompt_messages, str): prompt = prompt_messages else: prompt = self._convert_messages_to_prompt(prompt_messages, prefix, model_name) + return self._get_num_tokens_by_gpt2(prompt) def validate_credentials(self, model: str, credentials: dict) -> None: @@ -539,11 +554,16 @@ class BedrockLargeLanguageModel(LargeLanguageModel): content = message.content if isinstance(message, UserPromptMessage): - message_text = f"{human_prompt_prefix} {content} {human_prompt_postfix}" + body = content + if (isinstance(content, list)): + body = "".join([c.data for c in content if c.type == PromptMessageContentType.TEXT]) + message_text = f"{human_prompt_prefix} {body} {human_prompt_postfix}" elif isinstance(message, AssistantPromptMessage): message_text = f"{ai_prompt} {content}" elif isinstance(message, SystemPromptMessage): message_text = content + elif isinstance(message, ToolPromptMessage): + message_text = f"{human_prompt_prefix} {message.content}" else: raise ValueError(f"Got unknown type {message}") @@ -576,7 +596,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel): """ Create payload for bedrock api call depending on model provider """ - payload = dict() + payload = {} model_prefix = model.split('.')[0] model_name = model.split('.')[1] @@ -648,8 +668,8 @@ class BedrockLargeLanguageModel(LargeLanguageModel): runtime_client = boto3.client( service_name='bedrock-runtime', config=client_config, - aws_access_key_id=credentials.get("aws_access_key_id", None), - aws_secret_access_key=credentials.get("aws_secret_access_key", None) + aws_access_key_id=credentials.get("aws_access_key_id"), + aws_secret_access_key=credentials.get("aws_secret_access_key") ) model_prefix = model.split('.')[0] diff --git a/api/core/model_runtime/model_providers/bedrock/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/bedrock/text_embedding/text_embedding.py index 35b1a8f389..993416cdc8 100644 --- a/api/core/model_runtime/model_providers/bedrock/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/bedrock/text_embedding/text_embedding.py @@ -49,8 +49,8 @@ class BedrockTextEmbeddingModel(TextEmbeddingModel): bedrock_runtime = boto3.client( service_name='bedrock-runtime', config=client_config, - aws_access_key_id=credentials.get("aws_access_key_id", None), - aws_secret_access_key=credentials.get("aws_secret_access_key", None) + aws_access_key_id=credentials.get("aws_access_key_id"), + aws_secret_access_key=credentials.get("aws_secret_access_key") ) embeddings = [] @@ -148,7 +148,7 @@ class BedrockTextEmbeddingModel(TextEmbeddingModel): """ Create payload for bedrock api call depending on model provider """ - payload = dict() + payload = {} if model_prefix == "amazon": payload['inputText'] = texts diff --git a/api/core/model_runtime/model_providers/cohere/llm/llm.py b/api/core/model_runtime/model_providers/cohere/llm/llm.py index f9fae5e8ca..89b04c0279 100644 --- a/api/core/model_runtime/model_providers/cohere/llm/llm.py +++ b/api/core/model_runtime/model_providers/cohere/llm/llm.py @@ -696,12 +696,10 @@ class CohereLargeLanguageModel(LargeLanguageModel): en_US=model ), model_type=ModelType.LLM, - features=[feature for feature in base_model_schema_features], + features=list(base_model_schema_features), fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ - key: property for key, property in base_model_schema_model_properties.items() - }, - parameter_rules=[rule for rule in base_model_schema_parameters_rules], + model_properties=dict(base_model_schema_model_properties.items()), + parameter_rules=list(base_model_schema_parameters_rules), pricing=base_model_schema.pricing ) diff --git a/api/core/model_runtime/model_providers/google/llm/llm.py b/api/core/model_runtime/model_providers/google/llm/llm.py index c934c54634..ebcd0af35b 100644 --- a/api/core/model_runtime/model_providers/google/llm/llm.py +++ b/api/core/model_runtime/model_providers/google/llm/llm.py @@ -277,10 +277,7 @@ class GoogleLargeLanguageModel(LargeLanguageModel): type='function', function=AssistantPromptMessage.ToolCall.ToolCallFunction( name=part.function_call.name, - arguments=json.dumps({ - key: value - for key, value in part.function_call.args.items() - }) + arguments=json.dumps(dict(part.function_call.args.items())) ) ) ] diff --git a/api/core/model_runtime/model_providers/jina/rerank/_position.yaml b/api/core/model_runtime/model_providers/jina/rerank/_position.yaml new file mode 100644 index 0000000000..c9ddaad758 --- /dev/null +++ b/api/core/model_runtime/model_providers/jina/rerank/_position.yaml @@ -0,0 +1,5 @@ +- jina-reranker-v2-base-multilingual +- jina-reranker-v1-base-en +- jina-reranker-v1-turbo-en +- jina-colbert-v1-en +- jina-reranker-v1-tiny-en diff --git a/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-tiny-en.yaml b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-tiny-en.yaml new file mode 100644 index 0000000000..b81711195b --- /dev/null +++ b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-tiny-en.yaml @@ -0,0 +1,4 @@ +model: jina-reranker-v1-tiny-en +model_type: rerank +model_properties: + context_size: 8192 diff --git a/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-turbo-en.yaml b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-turbo-en.yaml new file mode 100644 index 0000000000..d05f4bb4a2 --- /dev/null +++ b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v1-turbo-en.yaml @@ -0,0 +1,4 @@ +model: jina-reranker-v1-turbo-en +model_type: rerank +model_properties: + context_size: 8192 diff --git a/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v2-base-multilingual.yaml b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v2-base-multilingual.yaml new file mode 100644 index 0000000000..acf576719c --- /dev/null +++ b/api/core/model_runtime/model_providers/jina/rerank/jina-reranker-v2-base-multilingual.yaml @@ -0,0 +1,4 @@ +model: jina-reranker-v2-base-multilingual +model_type: rerank +model_properties: + context_size: 8192 diff --git a/api/core/model_runtime/model_providers/moonshot/llm/llm.py b/api/core/model_runtime/model_providers/moonshot/llm/llm.py index 3e146559c8..17cf65dc3a 100644 --- a/api/core/model_runtime/model_providers/moonshot/llm/llm.py +++ b/api/core/model_runtime/model_providers/moonshot/llm/llm.py @@ -88,12 +88,12 @@ class MoonshotLargeLanguageModel(OAIAPICompatLargeLanguageModel): def _add_function_call(self, model: str, credentials: dict) -> None: model_schema = self.get_model_schema(model, credentials) - if model_schema and set([ + if model_schema and { ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL - ]).intersection(model_schema.features or []): + }.intersection(model_schema.features or []): credentials['function_calling_type'] = 'tool_call' - def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict: + def _convert_prompt_message_to_dict(self, message: PromptMessage, credentials: Optional[dict] = None) -> dict: """ Convert PromptMessage to dict for OpenAI API format """ diff --git a/api/core/model_runtime/model_providers/nvidia/llm/llm.py b/api/core/model_runtime/model_providers/nvidia/llm/llm.py index 047bbeda63..11252b9211 100644 --- a/api/core/model_runtime/model_providers/nvidia/llm/llm.py +++ b/api/core/model_runtime/model_providers/nvidia/llm/llm.py @@ -100,10 +100,10 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel): if api_key: headers["Authorization"] = f"Bearer {api_key}" - endpoint_url = credentials['endpoint_url'] if 'endpoint_url' in credentials else None + endpoint_url = credentials.get('endpoint_url') if endpoint_url and not endpoint_url.endswith('/'): endpoint_url += '/' - server_url = credentials['server_url'] if 'server_url' in credentials else None + server_url = credentials.get('server_url') # prepare the payload for a simple ping to the model data = { @@ -182,10 +182,10 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel): if stream: headers['Accept'] = 'text/event-stream' - endpoint_url = credentials['endpoint_url'] if 'endpoint_url' in credentials else None + endpoint_url = credentials.get('endpoint_url') if endpoint_url and not endpoint_url.endswith('/'): endpoint_url += '/' - server_url = credentials['server_url'] if 'server_url' in credentials else None + server_url = credentials.get('server_url') data = { "model": model, @@ -200,7 +200,7 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel): endpoint_url = str(URL(endpoint_url) / 'chat' / 'completions') elif 'server_url' in credentials: endpoint_url = server_url - data['messages'] = [self._convert_prompt_message_to_dict(m) for m in prompt_messages] + data['messages'] = [self._convert_prompt_message_to_dict(m, credentials) for m in prompt_messages] elif completion_type is LLMMode.COMPLETION: data['prompt'] = 'ping' if 'endpoint_url' in credentials: diff --git a/api/core/model_runtime/model_providers/openai/llm/llm.py b/api/core/model_runtime/model_providers/openai/llm/llm.py index 69afabadb3..aae2729bdf 100644 --- a/api/core/model_runtime/model_providers/openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai/llm/llm.py @@ -1073,12 +1073,10 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): en_US=model ), model_type=ModelType.LLM, - features=[feature for feature in base_model_schema_features], + features=list(base_model_schema_features), fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ - key: property for key, property in base_model_schema_model_properties.items() - }, - parameter_rules=[rule for rule in base_model_schema_parameters_rules], + model_properties=dict(base_model_schema_model_properties.items()), + parameter_rules=list(base_model_schema_parameters_rules), pricing=base_model_schema.pricing ) diff --git a/api/core/model_runtime/model_providers/openai/tts/tts.py b/api/core/model_runtime/model_providers/openai/tts/tts.py index f5e2ec4b7c..f83c57078a 100644 --- a/api/core/model_runtime/model_providers/openai/tts/tts.py +++ b/api/core/model_runtime/model_providers/openai/tts/tts.py @@ -80,7 +80,7 @@ class OpenAIText2SpeechModel(_CommonOpenAI, TTSModel): max_workers = self._get_model_workers_limit(model, credentials) try: sentences = list(self._split_text_into_sentences(text=content_text, limit=word_limit)) - audio_bytes_list = list() + audio_bytes_list = [] # Create a thread pool and map the function to the list of sentences with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py index f8726c853a..b76f460737 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py @@ -88,7 +88,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): :param tools: tools for tool calling :return: """ - return self._num_tokens_from_messages(model, prompt_messages, tools) + return self._num_tokens_from_messages(model, prompt_messages, tools, credentials) def validate_credentials(self, model: str, credentials: dict) -> None: """ @@ -305,7 +305,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): if completion_type is LLMMode.CHAT: endpoint_url = urljoin(endpoint_url, 'chat/completions') - data['messages'] = [self._convert_prompt_message_to_dict(m) for m in prompt_messages] + data['messages'] = [self._convert_prompt_message_to_dict(m, credentials) for m in prompt_messages] elif completion_type is LLMMode.COMPLETION: endpoint_url = urljoin(endpoint_url, 'completions') data['prompt'] = prompt_messages[0].content @@ -582,7 +582,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): return result - def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict: + def _convert_prompt_message_to_dict(self, message: PromptMessage, credentials: Optional[dict] = None) -> dict: """ Convert PromptMessage to dict for OpenAI API format """ @@ -636,7 +636,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): # "tool_call_id": message.tool_call_id # } message_dict = { - "role": "function", + "role": "tool" if credentials and credentials.get('function_calling_type', 'no_call') == 'tool_call' else "function", "content": message.content, "name": message.tool_call_id } @@ -675,7 +675,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): return num_tokens def _num_tokens_from_messages(self, model: str, messages: list[PromptMessage], - tools: Optional[list[PromptMessageTool]] = None) -> int: + tools: Optional[list[PromptMessageTool]] = None, credentials: dict = None) -> int: """ Approximate num tokens with GPT2 tokenizer. """ @@ -684,7 +684,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel): tokens_per_name = 1 num_tokens = 0 - messages_dict = [self._convert_prompt_message_to_dict(m) for m in messages] + messages_dict = [self._convert_prompt_message_to_dict(m, credentials) for m in messages] for message in messages_dict: num_tokens += tokens_per_message for key, value in message.items(): diff --git a/api/core/model_runtime/model_providers/openrouter/llm/_position.yaml b/api/core/model_runtime/model_providers/openrouter/llm/_position.yaml new file mode 100644 index 0000000000..51131249e5 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/_position.yaml @@ -0,0 +1,21 @@ +- openai/gpt-4o +- openai/gpt-4 +- openai/gpt-4-32k +- openai/gpt-3.5-turbo +- anthropic/claude-3.5-sonnet +- anthropic/claude-3-haiku +- anthropic/claude-3-opus +- anthropic/claude-3-sonnet +- google/gemini-pro-1.5 +- google/gemini-flash-1.5 +- google/gemini-pro +- cohere/command-r-plus +- cohere/command-r +- meta-llama/llama-3-70b-instruct +- meta-llama/llama-3-8b-instruct +- mistralai/mixtral-8x22b-instruct +- mistralai/mixtral-8x7b-instruct +- mistralai/mistral-7b-instruct +- qwen/qwen-2-72b-instruct +- deepseek/deepseek-chat +- deepseek/deepseek-coder diff --git a/api/core/model_runtime/model_providers/openrouter/llm/claude-3-5-sonnet.yaml b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-5-sonnet.yaml new file mode 100644 index 0000000000..40558854e2 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-5-sonnet.yaml @@ -0,0 +1,39 @@ +model: anthropic/claude-3.5-sonnet +label: + en_US: claude-3.5-sonnet +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 4096 + min: 1 + max: 4096 + - name: response_format + use_template: response_format +pricing: + input: "3.00" + output: "15.00" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/claude-3-haiku.yaml b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-haiku.yaml new file mode 100644 index 0000000000..ce17d4123e --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-haiku.yaml @@ -0,0 +1,39 @@ +model: anthropic/claude-3-haiku +label: + en_US: claude-3-haiku +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 4096 + min: 1 + max: 4096 + - name: response_format + use_template: response_format +pricing: + input: "0.25" + output: "1.25" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/claude-3-opus.yaml b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-opus.yaml new file mode 100644 index 0000000000..68a92219eb --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-opus.yaml @@ -0,0 +1,39 @@ +model: anthropic/claude-3-opus +label: + en_US: claude-3-opus +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 4096 + min: 1 + max: 4096 + - name: response_format + use_template: response_format +pricing: + input: "15.00" + output: "75.00" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/claude-3-sonnet.yaml b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-sonnet.yaml new file mode 100644 index 0000000000..ede88459ca --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/claude-3-sonnet.yaml @@ -0,0 +1,39 @@ +model: anthropic/claude-3-sonnet +label: + en_US: claude-3-sonnet +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 200000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 4096 + min: 1 + max: 4096 + - name: response_format + use_template: response_format +pricing: + input: "3.00" + output: "15.00" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/command-r-plus.yaml b/api/core/model_runtime/model_providers/openrouter/llm/command-r-plus.yaml new file mode 100644 index 0000000000..a23eb269d1 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/command-r-plus.yaml @@ -0,0 +1,45 @@ +model: cohere/command-r-plus +label: + en_US: command-r-plus +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 128000 +parameter_rules: + - name: temperature + use_template: temperature + max: 5.0 + - name: top_p + use_template: top_p + default: 0.75 + min: 0.01 + max: 0.99 + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + default: 0 + min: 0 + max: 500 + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 1024 + max: 4096 +pricing: + input: "3" + output: "15" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/command-r.yaml b/api/core/model_runtime/model_providers/openrouter/llm/command-r.yaml new file mode 100644 index 0000000000..7165bf29b0 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/command-r.yaml @@ -0,0 +1,45 @@ +model: cohere/command-r +label: + en_US: command-r +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 128000 +parameter_rules: + - name: temperature + use_template: temperature + max: 5.0 + - name: top_p + use_template: top_p + default: 0.75 + min: 0.01 + max: 0.99 + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + default: 0 + min: 0 + max: 500 + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 1024 + max: 4096 +pricing: + input: "0.5" + output: "1.5" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/deepseek-chat.yaml b/api/core/model_runtime/model_providers/openrouter/llm/deepseek-chat.yaml new file mode 100644 index 0000000000..7a1dea6950 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/deepseek-chat.yaml @@ -0,0 +1,50 @@ +model: deepseek/deepseek-chat +label: + en_US: deepseek-chat +model_type: llm +features: + - agent-thought +model_properties: + mode: chat + context_size: 32000 +parameter_rules: + - name: temperature + use_template: temperature + type: float + default: 1 + min: 0.0 + max: 2.0 + help: + zh_Hans: 控制生成结果的多样性和随机性。数值越小,越严谨;数值越大,越发散。 + en_US: Control the diversity and randomness of generated results. The smaller the value, the more rigorous it is; the larger the value, the more divergent it is. + - name: max_tokens + use_template: max_tokens + type: int + default: 4096 + min: 1 + max: 4096 + help: + zh_Hans: 指定生成结果长度的上限。如果生成结果截断,可以调大该参数。 + en_US: Specifies the upper limit on the length of generated results. If the generated results are truncated, you can increase this parameter. + - name: top_p + use_template: top_p + type: float + default: 1 + min: 0.01 + max: 1.00 + help: + zh_Hans: 控制生成结果的随机性。数值越小,随机性越弱;数值越大,随机性越强。一般而言,top_p 和 temperature 两个参数选择一个进行调整即可。 + en_US: Control the randomness of generated results. The smaller the value, the weaker the randomness; the larger the value, the stronger the randomness. Generally speaking, you can adjust one of the two parameters top_p and temperature. + - name: frequency_penalty + use_template: frequency_penalty + default: 0 + min: -2.0 + max: 2.0 + help: + zh_Hans: 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性。 + en_US: A number between -2.0 and 2.0. If the value is positive, new tokens are penalized based on their frequency of occurrence in existing text, reducing the likelihood that the model will repeat the same content. +pricing: + input: "0.14" + output: "0.28" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/deepseek-coder.yaml b/api/core/model_runtime/model_providers/openrouter/llm/deepseek-coder.yaml new file mode 100644 index 0000000000..c05f4769b8 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/deepseek-coder.yaml @@ -0,0 +1,30 @@ +model: deepseek/deepseek-coder +label: + en_US: deepseek-coder +model_type: llm +features: + - agent-thought +model_properties: + mode: chat + context_size: 32000 +parameter_rules: + - name: temperature + use_template: temperature + min: 0 + max: 1 + default: 0.5 + - name: top_p + use_template: top_p + min: 0 + max: 1 + default: 1 + - name: max_tokens + use_template: max_tokens + min: 1 + max: 4096 + default: 1024 +pricing: + input: "0.14" + output: "0.28" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-flash.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-flash.yaml new file mode 100644 index 0000000000..0b2f329b28 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-flash.yaml @@ -0,0 +1,39 @@ +model: google/gemini-flash-1.5 +label: + en_US: gemini-flash-1.5 +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 1048576 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 8192 + min: 1 + max: 8192 + - name: response_format + use_template: response_format +pricing: + input: "0.25" + output: "0.75" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-pro.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-pro.yaml new file mode 100644 index 0000000000..679ce9bdcd --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gemini-1.5-pro.yaml @@ -0,0 +1,39 @@ +model: google/gemini-pro-1.5 +label: + en_US: gemini-pro-1.5 +model_type: llm +features: + - agent-thought + - vision + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 1048576 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 8192 + min: 1 + max: 8192 + - name: response_format + use_template: response_format +pricing: + input: "2.5" + output: "7.5" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gemini-pro.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gemini-pro.yaml new file mode 100644 index 0000000000..9f5d96c5b8 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gemini-pro.yaml @@ -0,0 +1,38 @@ +model: google/gemini-pro +label: + en_US: gemini-pro +model_type: llm +features: + - agent-thought + - tool-call + - stream-tool-call +model_properties: + mode: chat + context_size: 30720 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + help: + zh_Hans: 仅从每个后续标记的前 K 个选项中采样。 + en_US: Only sample from the top K options for each subsequent token. + required: false + - name: max_tokens + use_template: max_tokens + required: true + default: 2048 + min: 1 + max: 2048 + - name: response_format + use_template: response_format +pricing: + input: "0.125" + output: "0.375" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gpt-3.5-turbo.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gpt-3.5-turbo.yaml new file mode 100644 index 0000000000..1737c50bb1 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gpt-3.5-turbo.yaml @@ -0,0 +1,42 @@ +model: openai/gpt-3.5-turbo +label: + en_US: gpt-3.5-turbo +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 16385 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 4096 + - name: response_format + label: + zh_Hans: 回复格式 + en_US: response_format + type: string + help: + zh_Hans: 指定模型必须输出的格式 + en_US: specifying the format that the model must output + required: false + options: + - text + - json_object +pricing: + input: "0.5" + output: "1.5" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gpt-4-32k.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4-32k.yaml new file mode 100644 index 0000000000..2d55cf8565 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4-32k.yaml @@ -0,0 +1,57 @@ +model: openai/gpt-4-32k +label: + en_US: gpt-4-32k +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 32768 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 32768 + - name: seed + label: + zh_Hans: 种子 + en_US: Seed + type: int + help: + zh_Hans: + 如果指定,模型将尽最大努力进行确定性采样,使得重复的具有相同种子和参数的请求应该返回相同的结果。不能保证确定性,您应该参考 system_fingerprint + 响应参数来监视变化。 + en_US: + If specified, model will make a best effort to sample deterministically, + such that repeated requests with the same seed and parameters should return + the same result. Determinism is not guaranteed, and you should refer to the + system_fingerprint response parameter to monitor changes in the backend. + required: false + - name: response_format + label: + zh_Hans: 回复格式 + en_US: response_format + type: string + help: + zh_Hans: 指定模型必须输出的格式 + en_US: specifying the format that the model must output + required: false + options: + - text + - json_object +pricing: + input: "60" + output: "120" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gpt-4.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4.yaml new file mode 100644 index 0000000000..12015f6f64 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4.yaml @@ -0,0 +1,57 @@ +model: openai/gpt-4 +label: + en_US: gpt-4 +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 8192 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 8192 + - name: seed + label: + zh_Hans: 种子 + en_US: Seed + type: int + help: + zh_Hans: + 如果指定,模型将尽最大努力进行确定性采样,使得重复的具有相同种子和参数的请求应该返回相同的结果。不能保证确定性,您应该参考 system_fingerprint + 响应参数来监视变化。 + en_US: + If specified, model will make a best effort to sample deterministically, + such that repeated requests with the same seed and parameters should return + the same result. Determinism is not guaranteed, and you should refer to the + system_fingerprint response parameter to monitor changes in the backend. + required: false + - name: response_format + label: + zh_Hans: 回复格式 + en_US: response_format + type: string + help: + zh_Hans: 指定模型必须输出的格式 + en_US: specifying the format that the model must output + required: false + options: + - text + - json_object +pricing: + input: "30" + output: "60" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o.yaml new file mode 100644 index 0000000000..6945402c72 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o.yaml @@ -0,0 +1,43 @@ +model: openai/gpt-4o +label: + en_US: gpt-4o +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call + - vision +model_properties: + mode: chat + context_size: 128000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 4096 + - name: response_format + label: + zh_Hans: 回复格式 + en_US: response_format + type: string + help: + zh_Hans: 指定模型必须输出的格式 + en_US: specifying the format that the model must output + required: false + options: + - text + - json_object +pricing: + input: "5.00" + output: "15.00" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/llama-3-70b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/llama-3-70b-instruct.yaml new file mode 100644 index 0000000000..b91c39e729 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/llama-3-70b-instruct.yaml @@ -0,0 +1,23 @@ +model: meta-llama/llama-3-70b-instruct +label: + en_US: llama-3-70b-instruct +model_type: llm +model_properties: + mode: completion + context_size: 8192 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: max_tokens + use_template: max_tokens + required: true + default: 512 + min: 1 + max: 2048 +pricing: + input: "0.59" + output: "0.79" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/llama-3-8b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/llama-3-8b-instruct.yaml new file mode 100644 index 0000000000..84b2c7fac2 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/llama-3-8b-instruct.yaml @@ -0,0 +1,23 @@ +model: meta-llama/llama-3-8b-instruct +label: + en_US: llama-3-8b-instruct +model_type: llm +model_properties: + mode: completion + context_size: 8192 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: max_tokens + use_template: max_tokens + required: true + default: 512 + min: 1 + max: 2048 +pricing: + input: "0.07" + output: "0.07" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/llm.py b/api/core/model_runtime/model_providers/openrouter/llm/llm.py index bb62fc7bb2..e78ac4caf1 100644 --- a/api/core/model_runtime/model_providers/openrouter/llm/llm.py +++ b/api/core/model_runtime/model_providers/openrouter/llm/llm.py @@ -9,38 +9,40 @@ from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAI class OpenRouterLargeLanguageModel(OAIAPICompatLargeLanguageModel): - def _update_endpoint_url(self, credentials: dict): + def _update_credential(self, model: str, credentials: dict): credentials['endpoint_url'] = "https://openrouter.ai/api/v1" - return credentials + credentials['mode'] = self.get_model_mode(model).value + credentials['function_calling_type'] = 'tool_call' + return def _invoke(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None) \ -> Union[LLMResult, Generator]: - cred_with_endpoint = self._update_endpoint_url(credentials=credentials) + self._update_credential(model, credentials) - return super()._invoke(model, cred_with_endpoint, prompt_messages, model_parameters, tools, stop, stream, user) + return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) def validate_credentials(self, model: str, credentials: dict) -> None: - cred_with_endpoint = self._update_endpoint_url(credentials=credentials) + self._update_credential(model, credentials) - return super().validate_credentials(model, cred_with_endpoint) + return super().validate_credentials(model, credentials) def _generate(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None) -> Union[LLMResult, Generator]: - cred_with_endpoint = self._update_endpoint_url(credentials=credentials) + self._update_credential(model, credentials) - return super()._generate(model, cred_with_endpoint, prompt_messages, model_parameters, tools, stop, stream, user) + return super()._generate(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity: - cred_with_endpoint = self._update_endpoint_url(credentials=credentials) + self._update_credential(model, credentials) - return super().get_customizable_model_schema(model, cred_with_endpoint) + return super().get_customizable_model_schema(model, credentials) def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], tools: Optional[list[PromptMessageTool]] = None) -> int: - cred_with_endpoint = self._update_endpoint_url(credentials=credentials) + self._update_credential(model, credentials) - return super().get_num_tokens(model, cred_with_endpoint, prompt_messages, tools) + return super().get_num_tokens(model, credentials, prompt_messages, tools) diff --git a/api/core/model_runtime/model_providers/openrouter/llm/mistral-7b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/mistral-7b-instruct.yaml new file mode 100644 index 0000000000..012dfc55ce --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/mistral-7b-instruct.yaml @@ -0,0 +1,30 @@ +model: mistralai/mistral-7b-instruct +label: + en_US: mistral-7b-instruct +model_type: llm +features: + - agent-thought +model_properties: + mode: completion + context_size: 8000 +parameter_rules: + - name: temperature + use_template: temperature + default: 0.7 + min: 0 + max: 1 + - name: top_p + use_template: top_p + default: 1 + min: 0 + max: 1 + - name: max_tokens + use_template: max_tokens + default: 1024 + min: 1 + max: 2048 +pricing: + input: "0.07" + output: "0.07" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x22b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x22b-instruct.yaml new file mode 100644 index 0000000000..f4eb4e45d9 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x22b-instruct.yaml @@ -0,0 +1,30 @@ +model: mistralai/mixtral-8x22b-instruct +label: + en_US: mixtral-8x22b-instruct +model_type: llm +features: + - agent-thought +model_properties: + mode: completion + context_size: 64000 +parameter_rules: + - name: temperature + use_template: temperature + default: 0.7 + min: 0 + max: 1 + - name: top_p + use_template: top_p + default: 1 + min: 0 + max: 1 + - name: max_tokens + use_template: max_tokens + default: 1024 + min: 1 + max: 8000 +pricing: + input: "0.65" + output: "0.65" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x7b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x7b-instruct.yaml new file mode 100644 index 0000000000..7871e1f7a0 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/mixtral-8x7b-instruct.yaml @@ -0,0 +1,31 @@ +model: mistralai/mixtral-8x7b-instruct +label: + zh_Hans: mixtral-8x7b-instruct + en_US: mixtral-8x7b-instruct +model_type: llm +features: + - agent-thought +model_properties: + mode: completion + context_size: 32000 +parameter_rules: + - name: temperature + use_template: temperature + default: 0.7 + min: 0 + max: 1 + - name: top_p + use_template: top_p + default: 1 + min: 0 + max: 1 + - name: max_tokens + use_template: max_tokens + default: 1024 + min: 1 + max: 8000 +pricing: + input: "0.24" + output: "0.24" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/llm/qwen2-72b-instruct.yaml b/api/core/model_runtime/model_providers/openrouter/llm/qwen2-72b-instruct.yaml new file mode 100644 index 0000000000..7b75fcb0c9 --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/qwen2-72b-instruct.yaml @@ -0,0 +1,30 @@ +model: qwen/qwen-2-72b-instruct +label: + en_US: qwen-2-72b-instruct +model_type: llm +features: + - agent-thought +model_properties: + mode: completion + context_size: 32768 +parameter_rules: + - name: temperature + use_template: temperature + - name: max_tokens + use_template: max_tokens + type: int + default: 512 + min: 1 + max: 4096 + help: + zh_Hans: 指定生成结果长度的上限。如果生成结果截断,可以调大该参数。 + en_US: Specifies the upper limit on the length of generated results. If the generated results are truncated, you can increase this parameter. + - name: top_p + use_template: top_p + - name: frequency_penalty + use_template: frequency_penalty +pricing: + input: "0.59" + output: "0.79" + unit: "0.000001" + currency: USD diff --git a/api/core/model_runtime/model_providers/openrouter/openrouter.py b/api/core/model_runtime/model_providers/openrouter/openrouter.py index 81313fd29a..613f71deb1 100644 --- a/api/core/model_runtime/model_providers/openrouter/openrouter.py +++ b/api/core/model_runtime/model_providers/openrouter/openrouter.py @@ -1,5 +1,7 @@ import logging +from core.model_runtime.entities.model_entities import ModelType +from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.model_providers.__base.model_provider import ModelProvider logger = logging.getLogger(__name__) @@ -8,4 +10,15 @@ logger = logging.getLogger(__name__) class OpenRouterProvider(ModelProvider): def validate_provider_credentials(self, credentials: dict) -> None: - pass + try: + model_instance = self.get_model_instance(ModelType.LLM) + + model_instance.validate_credentials( + model='openai/gpt-3.5-turbo', + credentials=credentials + ) + except CredentialsValidateFailedError as ex: + raise ex + except Exception as ex: + logger.exception(f'{self.get_provider_schema().provider} credentials validate failed') + raise ex \ No newline at end of file diff --git a/api/core/model_runtime/model_providers/openrouter/openrouter.yaml b/api/core/model_runtime/model_providers/openrouter/openrouter.yaml index df7d762a6f..f7536609ec 100644 --- a/api/core/model_runtime/model_providers/openrouter/openrouter.yaml +++ b/api/core/model_runtime/model_providers/openrouter/openrouter.yaml @@ -1,6 +1,6 @@ provider: openrouter label: - en_US: openrouter.ai + en_US: OpenRouter icon_small: en_US: openrouter_square.svg icon_large: @@ -15,6 +15,7 @@ help: supported_model_types: - llm configurate_methods: + - predefined-model - customizable-model model_credential_schema: model: @@ -82,13 +83,23 @@ model_credential_schema: en_US: Vision Support type: radio required: false - default: 'no_support' + default: "no_support" options: - - value: 'support' + - value: "support" label: - en_US: 'Yes' + en_US: "Yes" zh_Hans: 是 - - value: 'no_support' + - value: "no_support" label: - en_US: 'No' + en_US: "No" zh_Hans: 否 +provider_credential_schema: + credential_form_schemas: + - variable: api_key + required: true + label: + en_US: API Key + type: secret-input + placeholder: + zh_Hans: 在此输入您的 API Key + en_US: Enter your API Key diff --git a/api/core/model_runtime/model_providers/replicate/llm/llm.py b/api/core/model_runtime/model_providers/replicate/llm/llm.py index f4198dbfa7..31b81a829e 100644 --- a/api/core/model_runtime/model_providers/replicate/llm/llm.py +++ b/api/core/model_runtime/model_providers/replicate/llm/llm.py @@ -275,14 +275,13 @@ class ReplicateLargeLanguageModel(_CommonReplicate, LargeLanguageModel): @classmethod def _get_parameter_type(cls, param_type: str) -> str: - if param_type == 'integer': - return 'int' - elif param_type == 'number': - return 'float' - elif param_type == 'boolean': - return 'boolean' - elif param_type == 'string': - return 'string' + type_mapping = { + 'integer': 'int', + 'number': 'float', + 'boolean': 'boolean', + 'string': 'string' + } + return type_mapping.get(param_type) def _convert_messages_to_prompt(self, messages: list[PromptMessage]) -> str: messages = messages.copy() # don't mutate the original list diff --git a/api/core/model_runtime/model_providers/spark/llm/_client.py b/api/core/model_runtime/model_providers/spark/llm/_client.py index 4c8790141d..10da265701 100644 --- a/api/core/model_runtime/model_providers/spark/llm/_client.py +++ b/api/core/model_runtime/model_providers/spark/llm/_client.py @@ -38,6 +38,10 @@ class SparkLLMClient: 'spark-3.5': { 'version': 'v3.5', 'chat_domain': 'generalv3.5' + }, + 'spark-4': { + 'version': 'v4.0', + 'chat_domain': '4.0Ultra' } } diff --git a/api/core/model_runtime/model_providers/spark/llm/_position.yaml b/api/core/model_runtime/model_providers/spark/llm/_position.yaml index 64c2db77ce..e49ee97db7 100644 --- a/api/core/model_runtime/model_providers/spark/llm/_position.yaml +++ b/api/core/model_runtime/model_providers/spark/llm/_position.yaml @@ -1,3 +1,4 @@ +- spark-4 - spark-3.5 - spark-3 - spark-1.5 diff --git a/api/core/model_runtime/model_providers/spark/llm/spark-1.5.yaml b/api/core/model_runtime/model_providers/spark/llm/spark-1.5.yaml index effbe45e27..41b8765fe6 100644 --- a/api/core/model_runtime/model_providers/spark/llm/spark-1.5.yaml +++ b/api/core/model_runtime/model_providers/spark/llm/spark-1.5.yaml @@ -28,6 +28,6 @@ parameter_rules: min: 1 max: 6 help: - zh_Hans: 从 k 个候选中随机选择⼀个(⾮等概率)。 + zh_Hans: 从 k 个候选中随机选择一个(非等概率)。 en_US: Randomly select one from k candidates (non-equal probability). required: false diff --git a/api/core/model_runtime/model_providers/spark/llm/spark-2.yaml b/api/core/model_runtime/model_providers/spark/llm/spark-2.yaml index 2afd1fc538..2db6805a2e 100644 --- a/api/core/model_runtime/model_providers/spark/llm/spark-2.yaml +++ b/api/core/model_runtime/model_providers/spark/llm/spark-2.yaml @@ -29,6 +29,6 @@ parameter_rules: min: 1 max: 6 help: - zh_Hans: 从 k 个候选中随机选择⼀个(⾮等概率)。 + zh_Hans: 从 k 个候选中随机选择一个(非等概率)。 en_US: Randomly select one from k candidates (non-equal probability). required: false diff --git a/api/core/model_runtime/model_providers/spark/llm/spark-3.5.yaml b/api/core/model_runtime/model_providers/spark/llm/spark-3.5.yaml index 650eff5d98..6d24932ea8 100644 --- a/api/core/model_runtime/model_providers/spark/llm/spark-3.5.yaml +++ b/api/core/model_runtime/model_providers/spark/llm/spark-3.5.yaml @@ -28,6 +28,6 @@ parameter_rules: min: 1 max: 6 help: - zh_Hans: 从 k 个候选中随机选择⼀个(⾮等概率)。 + zh_Hans: 从 k 个候选中随机选择一个(非等概率)。 en_US: Randomly select one from k candidates (non-equal probability). required: false diff --git a/api/core/model_runtime/model_providers/spark/llm/spark-3.yaml b/api/core/model_runtime/model_providers/spark/llm/spark-3.yaml index dc0f66f670..2ef9e10f45 100644 --- a/api/core/model_runtime/model_providers/spark/llm/spark-3.yaml +++ b/api/core/model_runtime/model_providers/spark/llm/spark-3.yaml @@ -28,6 +28,6 @@ parameter_rules: min: 1 max: 6 help: - zh_Hans: 从 k 个候选中随机选择⼀个(⾮等概率)。 + zh_Hans: 从 k 个候选中随机选择一个(非等概率)。 en_US: Randomly select one from k candidates (non-equal probability). required: false diff --git a/api/core/model_runtime/model_providers/spark/llm/spark-4.yaml b/api/core/model_runtime/model_providers/spark/llm/spark-4.yaml new file mode 100644 index 0000000000..4b0bf27029 --- /dev/null +++ b/api/core/model_runtime/model_providers/spark/llm/spark-4.yaml @@ -0,0 +1,33 @@ +model: spark-4 +label: + en_US: Spark V4.0 +model_type: llm +model_properties: + mode: chat +parameter_rules: + - name: temperature + use_template: temperature + default: 0.5 + help: + zh_Hans: 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。 + en_US: Kernel sampling threshold. Used to determine the randomness of the results. The higher the value, the stronger the randomness, that is, the higher the possibility of getting different answers to the same question. + - name: max_tokens + use_template: max_tokens + default: 4096 + min: 1 + max: 8192 + help: + zh_Hans: 模型回答的tokens的最大长度。 + en_US: 模型回答的tokens的最大长度。 + - name: top_k + label: + zh_Hans: 取样数量 + en_US: Top k + type: int + default: 4 + min: 1 + max: 6 + help: + zh_Hans: 从 k 个候选中随机选择一个(非等概率)。 + en_US: Randomly select one from k candidates (non-equal probability). + required: false diff --git a/api/core/model_runtime/model_providers/tongyi/_common.py b/api/core/model_runtime/model_providers/tongyi/_common.py index dfc0102666..fab18b41fd 100644 --- a/api/core/model_runtime/model_providers/tongyi/_common.py +++ b/api/core/model_runtime/model_providers/tongyi/_common.py @@ -1,4 +1,20 @@ -from core.model_runtime.errors.invoke import InvokeError +from dashscope.common.error import ( + AuthenticationError, + InvalidParameter, + RequestFailure, + ServiceUnavailableError, + UnsupportedHTTPMethod, + UnsupportedModel, +) + +from core.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) class _CommonTongyi: @@ -20,4 +36,20 @@ class _CommonTongyi: :return: Invoke error mapping """ - pass + return { + InvokeConnectionError: [ + RequestFailure, + ], + InvokeServerUnavailableError: [ + ServiceUnavailableError, + ], + InvokeRateLimitError: [], + InvokeAuthorizationError: [ + AuthenticationError, + ], + InvokeBadRequestError: [ + InvalidParameter, + UnsupportedModel, + UnsupportedHTTPMethod, + ] + } diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-long.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-long.yaml new file mode 100644 index 0000000000..b2cf3dd486 --- /dev/null +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-long.yaml @@ -0,0 +1,81 @@ +model: qwen-long +label: + en_US: qwen-long +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 10000000 +parameter_rules: + - name: temperature + use_template: temperature + type: float + default: 0.3 + min: 0.0 + max: 2.0 + help: + zh_Hans: 用于控制随机性和多样性的程度。具体来说,temperature值控制了生成文本时对每个候选词的概率分布进行平滑的程度。较高的temperature值会降低概率分布的峰值,使得更多的低概率词被选择,生成结果更加多样化;而较低的temperature值则会增强概率分布的峰值,使得高概率词更容易被选择,生成结果更加确定。 + en_US: Used to control the degree of randomness and diversity. Specifically, the temperature value controls the degree to which the probability distribution of each candidate word is smoothed when generating text. A higher temperature value will reduce the peak value of the probability distribution, allowing more low-probability words to be selected, and the generated results will be more diverse; while a lower temperature value will enhance the peak value of the probability distribution, making it easier for high-probability words to be selected. , the generated results are more certain. + - name: max_tokens + use_template: max_tokens + type: int + default: 2000 + min: 1 + max: 2000 + help: + zh_Hans: 用于指定模型在生成内容时token的最大数量,它定义了生成的上限,但不保证每次都会生成到这个数量。 + en_US: It is used to specify the maximum number of tokens when the model generates content. It defines the upper limit of generation, but does not guarantee that this number will be generated every time. + - name: top_p + use_template: top_p + type: float + default: 0.8 + min: 0.1 + max: 0.9 + help: + zh_Hans: 生成过程中核采样方法概率阈值,例如,取值为0.8时,仅保留概率加起来大于等于0.8的最可能token的最小集合作为候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的确定性越高。 + en_US: The probability threshold of the kernel sampling method during the generation process. For example, when the value is 0.8, only the smallest set of the most likely tokens with a sum of probabilities greater than or equal to 0.8 is retained as the candidate set. The value range is (0,1.0). The larger the value, the higher the randomness generated; the lower the value, the higher the certainty generated. + - name: top_k + type: int + min: 0 + max: 99 + label: + zh_Hans: 取样数量 + en_US: Top k + help: + zh_Hans: 生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。取值越大,生成的随机性越高;取值越小,生成的确定性越高。 + en_US: The size of the sample candidate set when generated. For example, when the value is 50, only the 50 highest-scoring tokens in a single generation form a randomly sampled candidate set. The larger the value, the higher the randomness generated; the smaller the value, the higher the certainty generated. + - name: seed + required: false + type: int + default: 1234 + label: + zh_Hans: 随机种子 + en_US: Random seed + help: + zh_Hans: 生成时使用的随机数种子,用户控制模型生成内容的随机性。支持无符号64位整数,默认值为 1234。在使用seed时,模型将尽可能生成相同或相似的结果,但目前不保证每次生成的结果完全相同。 + en_US: The random number seed used when generating, the user controls the randomness of the content generated by the model. Supports unsigned 64-bit integers, default value is 1234. When using seed, the model will try its best to generate the same or similar results, but there is currently no guarantee that the results will be exactly the same every time. + - name: repetition_penalty + required: false + type: float + default: 1.1 + label: + en_US: Repetition penalty + help: + zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。 + en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment. + - name: enable_search + type: boolean + default: false + help: + zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。 + en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic. + - name: response_format + use_template: response_format +pricing: + input: '0.0005' + output: '0.002' + unit: '0.001' + currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0403.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0403.yaml index 865c0c8138..935a16ebcb 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0403.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0403.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.12' + input: '0.04' output: '0.12' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0428.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0428.yaml new file mode 100644 index 0000000000..c39799a71f --- /dev/null +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-0428.yaml @@ -0,0 +1,81 @@ +model: qwen-max-0428 +label: + en_US: qwen-max-0428 +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call +model_properties: + mode: chat + context_size: 8192 +parameter_rules: + - name: temperature + use_template: temperature + type: float + default: 0.3 + min: 0.0 + max: 2.0 + help: + zh_Hans: 用于控制随机性和多样性的程度。具体来说,temperature值控制了生成文本时对每个候选词的概率分布进行平滑的程度。较高的temperature值会降低概率分布的峰值,使得更多的低概率词被选择,生成结果更加多样化;而较低的temperature值则会增强概率分布的峰值,使得高概率词更容易被选择,生成结果更加确定。 + en_US: Used to control the degree of randomness and diversity. Specifically, the temperature value controls the degree to which the probability distribution of each candidate word is smoothed when generating text. A higher temperature value will reduce the peak value of the probability distribution, allowing more low-probability words to be selected, and the generated results will be more diverse; while a lower temperature value will enhance the peak value of the probability distribution, making it easier for high-probability words to be selected. , the generated results are more certain. + - name: max_tokens + use_template: max_tokens + type: int + default: 2000 + min: 1 + max: 2000 + help: + zh_Hans: 用于指定模型在生成内容时token的最大数量,它定义了生成的上限,但不保证每次都会生成到这个数量。 + en_US: It is used to specify the maximum number of tokens when the model generates content. It defines the upper limit of generation, but does not guarantee that this number will be generated every time. + - name: top_p + use_template: top_p + type: float + default: 0.8 + min: 0.1 + max: 0.9 + help: + zh_Hans: 生成过程中核采样方法概率阈值,例如,取值为0.8时,仅保留概率加起来大于等于0.8的最可能token的最小集合作为候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的确定性越高。 + en_US: The probability threshold of the kernel sampling method during the generation process. For example, when the value is 0.8, only the smallest set of the most likely tokens with a sum of probabilities greater than or equal to 0.8 is retained as the candidate set. The value range is (0,1.0). The larger the value, the higher the randomness generated; the lower the value, the higher the certainty generated. + - name: top_k + type: int + min: 0 + max: 99 + label: + zh_Hans: 取样数量 + en_US: Top k + help: + zh_Hans: 生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。取值越大,生成的随机性越高;取值越小,生成的确定性越高。 + en_US: The size of the sample candidate set when generated. For example, when the value is 50, only the 50 highest-scoring tokens in a single generation form a randomly sampled candidate set. The larger the value, the higher the randomness generated; the smaller the value, the higher the certainty generated. + - name: seed + required: false + type: int + default: 1234 + label: + zh_Hans: 随机种子 + en_US: Random seed + help: + zh_Hans: 生成时使用的随机数种子,用户控制模型生成内容的随机性。支持无符号64位整数,默认值为 1234。在使用seed时,模型将尽可能生成相同或相似的结果,但目前不保证每次生成的结果完全相同。 + en_US: The random number seed used when generating, the user controls the randomness of the content generated by the model. Supports unsigned 64-bit integers, default value is 1234. When using seed, the model will try its best to generate the same or similar results, but there is currently no guarantee that the results will be exactly the same every time. + - name: repetition_penalty + required: false + type: float + default: 1.1 + label: + en_US: Repetition penalty + help: + zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。 + en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment. + - name: enable_search + type: boolean + default: false + help: + zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。 + en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic. + - name: response_format + use_template: response_format +pricing: + input: '0.04' + output: '0.12' + unit: '0.001' + currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-1201.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-1201.yaml index 533d99aa55..0368a4a01e 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-1201.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-1201.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.12' + input: '0.04' output: '0.12' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml index dbe3ece396..1c705670ca 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.12' + input: '0.04' output: '0.12' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml index 9a0f1afc03..64094effbb 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.12' + input: '0.04' output: '0.12' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus-chat.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus-chat.yaml index 5681f5c7b0..bc848072ed 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus-chat.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus-chat.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.02' - output: '0.02' + input: '0.004' + output: '0.012' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml index 71dabb55f0..4be78627f0 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml @@ -73,7 +73,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.02' - output: '0.02' + input: '0.004' + output: '0.012' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo-chat.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo-chat.yaml index dc8208fac6..f1950577ec 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo-chat.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo-chat.yaml @@ -75,7 +75,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.008' - output: '0.008' + input: '0.002' + output: '0.006' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml index 140dc68af8..d4c03100ec 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml @@ -73,7 +73,7 @@ parameter_rules: - name: response_format use_template: response_format pricing: - input: '0.008' - output: '0.008' + input: '0.002' + output: '0.006' unit: '0.001' currency: RMB diff --git a/api/core/model_runtime/model_providers/tongyi/tts/tts.py b/api/core/model_runtime/model_providers/tongyi/tts/tts.py index b00f7c7c93..7ef053479b 100644 --- a/api/core/model_runtime/model_providers/tongyi/tts/tts.py +++ b/api/core/model_runtime/model_providers/tongyi/tts/tts.py @@ -80,7 +80,7 @@ class TongyiText2SpeechModel(_CommonTongyi, TTSModel): max_workers = self._get_model_workers_limit(model, credentials) try: sentences = list(self._split_text_into_sentences(text=content_text, limit=word_limit)) - audio_bytes_list = list() + audio_bytes_list = [] # Create a thread pool and map the function to the list of sentences with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: diff --git a/api/core/model_runtime/model_providers/vertex_ai/llm/llm.py b/api/core/model_runtime/model_providers/vertex_ai/llm/llm.py index 804c3535fb..8901549110 100644 --- a/api/core/model_runtime/model_providers/vertex_ai/llm/llm.py +++ b/api/core/model_runtime/model_providers/vertex_ai/llm/llm.py @@ -579,10 +579,7 @@ class VertexAiLargeLanguageModel(LargeLanguageModel): type='function', function=AssistantPromptMessage.ToolCall.ToolCallFunction( name=part.function_call.name, - arguments=json.dumps({ - key: value - for key, value in part.function_call.args.items() - }) + arguments=json.dumps(dict(part.function_call.args.items())) ) ) ] diff --git a/api/core/model_runtime/model_providers/volcengine_maas/llm/models.py b/api/core/model_runtime/model_providers/volcengine_maas/llm/models.py index 3a793cd6a8..3e5938f3b4 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/llm/models.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/llm/models.py @@ -111,5 +111,71 @@ ModelConfigs = { 'mode': 'chat', }, 'features': [], + }, + 'Moonshot-v1-8k': { + 'req_params': { + 'max_prompt_tokens': 8192, + 'max_new_tokens': 4096, + }, + 'model_properties': { + 'context_size': 8192, + 'mode': 'chat', + }, + 'features': [], + }, + 'Moonshot-v1-32k': { + 'req_params': { + 'max_prompt_tokens': 32768, + 'max_new_tokens': 16384, + }, + 'model_properties': { + 'context_size': 32768, + 'mode': 'chat', + }, + 'features': [], + }, + 'Moonshot-v1-128k': { + 'req_params': { + 'max_prompt_tokens': 131072, + 'max_new_tokens': 65536, + }, + 'model_properties': { + 'context_size': 131072, + 'mode': 'chat', + }, + 'features': [], + }, + 'GLM3-130B': { + 'req_params': { + 'max_prompt_tokens': 8192, + 'max_new_tokens': 4096, + }, + 'model_properties': { + 'context_size': 8192, + 'mode': 'chat', + }, + 'features': [], + }, + 'GLM3-130B-Fin': { + 'req_params': { + 'max_prompt_tokens': 8192, + 'max_new_tokens': 4096, + }, + 'model_properties': { + 'context_size': 8192, + 'mode': 'chat', + }, + 'features': [], + }, + 'Mistral-7B': { + 'req_params': { + 'max_prompt_tokens': 8192, + 'max_new_tokens': 2048, + }, + 'model_properties': { + 'context_size': 8192, + 'mode': 'chat', + }, + 'features': [], } } diff --git a/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/auth.py b/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/auth.py index 48110f16d7..053432a089 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/auth.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/auth.py @@ -102,7 +102,7 @@ class Signer: body_hash = Util.sha256(request.body) request.headers['X-Content-Sha256'] = body_hash - signed_headers = dict() + signed_headers = {} for key in request.headers: if key in ['Content-Type', 'Content-Md5', 'Host'] or key.startswith('X-'): signed_headers[key.lower()] = request.headers[key] diff --git a/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/service.py b/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/service.py index 03734ec54f..7271ae63fd 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/service.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/volc_sdk/base/service.py @@ -150,7 +150,7 @@ class Request: self.headers = OrderedDict() self.query = OrderedDict() self.body = '' - self.form = dict() + self.form = {} self.connection_timeout = 0 self.socket_timeout = 0 diff --git a/api/core/model_runtime/model_providers/volcengine_maas/volcengine_maas.yaml b/api/core/model_runtime/model_providers/volcengine_maas/volcengine_maas.yaml index 4d468969b7..a00c1b7994 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/volcengine_maas.yaml +++ b/api/core/model_runtime/model_providers/volcengine_maas/volcengine_maas.yaml @@ -120,12 +120,6 @@ model_credential_schema: show_on: - variable: __model_type value: llm - - label: - en_US: Skylark2-pro-4k - value: Skylark2-pro-4k - show_on: - - variable: __model_type - value: llm - label: en_US: Llama3-8B value: Llama3-8B @@ -138,6 +132,42 @@ model_credential_schema: show_on: - variable: __model_type value: llm + - label: + en_US: Moonshot-v1-8k + value: Moonshot-v1-8k + show_on: + - variable: __model_type + value: llm + - label: + en_US: Moonshot-v1-32k + value: Moonshot-v1-32k + show_on: + - variable: __model_type + value: llm + - label: + en_US: Moonshot-v1-128k + value: Moonshot-v1-128k + show_on: + - variable: __model_type + value: llm + - label: + en_US: GLM3-130B + value: GLM3-130B + show_on: + - variable: __model_type + value: llm + - label: + en_US: GLM3-130B-Fin + value: GLM3-130B-Fin + show_on: + - variable: __model_type + value: llm + - label: + en_US: Mistral-7B + value: Mistral-7B + show_on: + - variable: __model_type + value: llm - label: en_US: Doubao-embedding value: Doubao-embedding @@ -181,7 +211,7 @@ model_credential_schema: zh_Hans: 模型上下文长度 en_US: Model Context Size type: text-input - default: '4096' + default: "4096" placeholder: zh_Hans: 输入您的模型上下文长度 en_US: Enter your Model Context Size @@ -195,7 +225,7 @@ model_credential_schema: label: zh_Hans: 最大 token 上限 en_US: Upper Bound for Max Tokens - default: '4096' + default: "4096" type: text-input placeholder: zh_Hans: 输入您的模型最大 token 上限 diff --git a/api/core/model_runtime/model_providers/xinference/llm/llm.py b/api/core/model_runtime/model_providers/xinference/llm/llm.py index cc3ce17975..0ef63f8e23 100644 --- a/api/core/model_runtime/model_providers/xinference/llm/llm.py +++ b/api/core/model_runtime/model_providers/xinference/llm/llm.py @@ -39,6 +39,7 @@ from core.model_runtime.entities.message_entities import ( ) from core.model_runtime.entities.model_entities import ( AIModelEntity, + DefaultParameterName, FetchFrom, ModelFeature, ModelPropertyKey, @@ -67,7 +68,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): def _invoke(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, tools: list[PromptMessageTool] | None = None, stop: list[str] | None = None, stream: bool = True, user: str | None = None) \ - -> LLMResult | Generator: + -> LLMResult | Generator: """ invoke LLM @@ -113,7 +114,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): elif 'generate' in extra_param.model_ability: credentials['completion_type'] = 'completion' else: - raise ValueError(f'xinference model ability {extra_param.model_ability} is not supported, check if you have the right model type') + raise ValueError( + f'xinference model ability {extra_param.model_ability} is not supported, check if you have the right model type') if extra_param.support_function_call: credentials['support_function_call'] = True @@ -147,7 +149,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): return self._get_num_tokens_by_gpt2(text) if is_completion_model: - return sum([tokens(str(message.content)) for message in messages]) + return sum(tokens(str(message.content)) for message in messages) tokens_per_message = 3 tokens_per_name = 1 @@ -206,6 +208,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): :param tools: tools for tool calling :return: number of tokens """ + def tokens(text: str): return self._get_num_tokens_by_gpt2(text) @@ -339,6 +342,45 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): zh_Hans='最大生成长度', en_US='Max Tokens' ) + ), + ParameterRule( + name=DefaultParameterName.PRESENCE_PENALTY, + use_template=DefaultParameterName.PRESENCE_PENALTY, + type=ParameterType.FLOAT, + label=I18nObject( + en_US='Presence Penalty', + zh_Hans='存在惩罚', + ), + required=False, + help=I18nObject( + en_US='Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they ' + 'appear in the text so far, increasing the model\'s likelihood to talk about new topics.', + zh_Hans='介于 -2.0 和 2.0 之间的数字。正值会根据新词是否已出现在文本中对其进行惩罚,从而增加模型谈论新话题的可能性。' + ), + default=0.0, + min=-2.0, + max=2.0, + precision=2 + ), + ParameterRule( + name=DefaultParameterName.FREQUENCY_PENALTY, + use_template=DefaultParameterName.FREQUENCY_PENALTY, + type=ParameterType.FLOAT, + label=I18nObject( + en_US='Frequency Penalty', + zh_Hans='频率惩罚', + ), + required=False, + help=I18nObject( + en_US='Number between -2.0 and 2.0. Positive values penalize new tokens based on their ' + 'existing frequency in the text so far, decreasing the model\'s likelihood to repeat the ' + 'same line verbatim.', + zh_Hans='介于 -2.0 和 2.0 之间的数字。正值会根据新词在文本中的现有频率对其进行惩罚,从而降低模型逐字重复相同内容的可能性。' + ), + default=0.0, + min=-2.0, + max=2.0, + precision=2 ) ] @@ -364,7 +406,6 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): else: raise ValueError(f'xinference model ability {extra_args.model_ability} is not supported') - features = [] support_function_call = credentials.get('support_function_call', False) @@ -395,9 +436,9 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): return entity def _generate(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - model_parameters: dict, extra_model_kwargs: XinferenceModelExtraParameter, - tools: list[PromptMessageTool] | None = None, - stop: list[str] | None = None, stream: bool = True, user: str | None = None) \ + model_parameters: dict, extra_model_kwargs: XinferenceModelExtraParameter, + tools: list[PromptMessageTool] | None = None, + stop: list[str] | None = None, stream: bool = True, user: str | None = None) \ -> LLMResult | Generator: """ generate text from LLM @@ -429,6 +470,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): 'temperature': model_parameters.get('temperature', 1.0), 'top_p': model_parameters.get('top_p', 0.7), 'max_tokens': model_parameters.get('max_tokens', 512), + 'presence_penalty': model_parameters.get('presence_penalty', 0.0), + 'frequency_penalty': model_parameters.get('frequency_penalty', 0.0), } if stop: @@ -453,10 +496,12 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): if stream: if tools and len(tools) > 0: raise InvokeBadRequestError('xinference tool calls does not support stream mode') - return self._handle_chat_stream_response(model=model, credentials=credentials, prompt_messages=prompt_messages, - tools=tools, resp=resp) - return self._handle_chat_generate_response(model=model, credentials=credentials, prompt_messages=prompt_messages, - tools=tools, resp=resp) + return self._handle_chat_stream_response(model=model, credentials=credentials, + prompt_messages=prompt_messages, + tools=tools, resp=resp) + return self._handle_chat_generate_response(model=model, credentials=credentials, + prompt_messages=prompt_messages, + tools=tools, resp=resp) elif isinstance(xinference_model, RESTfulGenerateModelHandle): resp = client.completions.create( model=credentials['model_uid'], @@ -466,10 +511,12 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): **generate_config, ) if stream: - return self._handle_completion_stream_response(model=model, credentials=credentials, prompt_messages=prompt_messages, - tools=tools, resp=resp) - return self._handle_completion_generate_response(model=model, credentials=credentials, prompt_messages=prompt_messages, - tools=tools, resp=resp) + return self._handle_completion_stream_response(model=model, credentials=credentials, + prompt_messages=prompt_messages, + tools=tools, resp=resp) + return self._handle_completion_generate_response(model=model, credentials=credentials, + prompt_messages=prompt_messages, + tools=tools, resp=resp) else: raise NotImplementedError(f'xinference model handle type {type(xinference_model)} is not supported') @@ -523,8 +570,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): return tool_call def _handle_chat_generate_response(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool], - resp: ChatCompletion) -> LLMResult: + tools: list[PromptMessageTool], + resp: ChatCompletion) -> LLMResult: """ handle normal chat generate response """ @@ -549,7 +596,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): prompt_tokens = self._num_tokens_from_messages(messages=prompt_messages, tools=tools) completion_tokens = self._num_tokens_from_messages(messages=[assistant_prompt_message], tools=tools) - usage = self._calc_response_usage(model=model, credentials=credentials, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens) + usage = self._calc_response_usage(model=model, credentials=credentials, prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens) response = LLMResult( model=model, @@ -560,10 +608,10 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): ) return response - + def _handle_chat_stream_response(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool], - resp: Iterator[ChatCompletionChunk]) -> Generator: + tools: list[PromptMessageTool], + resp: Iterator[ChatCompletionChunk]) -> Generator: """ handle stream chat generate response """ @@ -634,8 +682,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): full_response += delta.delta.content def _handle_completion_generate_response(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool], - resp: Completion) -> LLMResult: + tools: list[PromptMessageTool], + resp: Completion) -> LLMResult: """ handle normal completion generate response """ @@ -671,8 +719,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): return response def _handle_completion_stream_response(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool], - resp: Iterator[Completion]) -> Generator: + tools: list[PromptMessageTool], + resp: Iterator[Completion]) -> Generator: """ handle stream completion generate response """ @@ -764,4 +812,4 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): InvokeBadRequestError: [ ValueError ] - } \ No newline at end of file + } diff --git a/api/core/model_runtime/model_providers/zhipuai/_common.py b/api/core/model_runtime/model_providers/zhipuai/_common.py index 2574234abf..3412d8100f 100644 --- a/api/core/model_runtime/model_providers/zhipuai/_common.py +++ b/api/core/model_runtime/model_providers/zhipuai/_common.py @@ -18,7 +18,7 @@ class _CommonZhipuaiAI: """ credentials_kwargs = { "api_key": credentials['api_key'] if 'api_key' in credentials else - credentials['zhipuai_api_key'] if 'zhipuai_api_key' in credentials else None, + credentials.get("zhipuai_api_key"), } return credentials_kwargs diff --git a/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/_client.py b/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/_client.py index 29b1746351..6588d1dd68 100644 --- a/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/_client.py +++ b/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/_client.py @@ -29,10 +29,8 @@ class ZhipuAI(HttpClient): http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None ) -> None: - # if api_key is None: - # api_key = os.environ.get("ZHIPUAI_API_KEY") if api_key is None: - raise ZhipuAIError("未提供api_key,请通过参数或环境变量提供") + raise ZhipuAIError("No api_key provided, please provide it through parameters or environment variables") self.api_key = api_key if base_url is None: diff --git a/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/core/_http_client.py b/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/core/_http_client.py index e13d2b0233..924d009123 100644 --- a/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/core/_http_client.py +++ b/api/core/model_runtime/model_providers/zhipuai/zhipuai_sdk/core/_http_client.py @@ -7,6 +7,8 @@ from typing import Any, Union, cast import httpx import pydantic from httpx import URL, Timeout +from tenacity import retry +from tenacity.stop import stop_after_attempt from . import _errors from ._base_type import NOT_GIVEN, Body, Data, Headers, NotGiven, Query, RequestFiles, ResponseT @@ -221,6 +223,7 @@ class HttpClient: def __exit__(self, exc_type, exc_val, exc_tb): self.close() + @retry(stop=stop_after_attempt(ZHIPUAI_DEFAULT_MAX_RETRIES)) def request( self, *, diff --git a/api/core/model_runtime/utils/_compat.py b/api/core/model_runtime/utils/_compat.py deleted file mode 100644 index 5c34152751..0000000000 --- a/api/core/model_runtime/utils/_compat.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Any, Literal - -from pydantic import BaseModel -from pydantic.version import VERSION as PYDANTIC_VERSION - -PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") - -if PYDANTIC_V2: - from pydantic_core import Url as Url - - def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any - ) -> Any: - return model.model_dump(mode=mode, **kwargs) -else: - from pydantic import AnyUrl as Url # noqa: F401 - - def _model_dump( - model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any - ) -> Any: - return model.dict(**kwargs) diff --git a/api/core/model_runtime/utils/encoders.py b/api/core/model_runtime/utils/encoders.py index e41d49216c..5078f00bfa 100644 --- a/api/core/model_runtime/utils/encoders.py +++ b/api/core/model_runtime/utils/encoders.py @@ -8,16 +8,20 @@ from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6 from pathlib import Path, PurePath from re import Pattern from types import GeneratorType -from typing import Any, Optional, Union +from typing import Any, Literal, Optional, Union from uuid import UUID from pydantic import BaseModel from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from pydantic_core import Url from pydantic_extra_types.color import Color -from ._compat import PYDANTIC_V2, Url, _model_dump +def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any +) -> Any: + return model.model_dump(mode=mode, **kwargs) # Taken from Pydantic v1 as is def isoformat(o: Union[datetime.date, datetime.time]) -> str: @@ -109,12 +113,6 @@ def jsonable_encoder( if isinstance(obj, encoder_type): return encoder_instance(obj) if isinstance(obj, BaseModel): - # TODO: remove when deprecating Pydantic v1 - encoders: dict[Any, Any] = {} - if not PYDANTIC_V2: - encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] - if custom_encoder: - encoders.update(custom_encoder) obj_dict = _model_dump( obj, mode="json", @@ -131,8 +129,6 @@ def jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - # TODO: remove when deprecating Pydantic v1 - custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): diff --git a/api/core/moderation/input_moderation.py b/api/core/moderation/input_moderation.py index 8fbc0c2d50..c5dd88fb24 100644 --- a/api/core/moderation/input_moderation.py +++ b/api/core/moderation/input_moderation.py @@ -1,18 +1,25 @@ import logging +from typing import Optional from core.app.app_config.entities import AppConfig from core.moderation.base import ModerationAction, ModerationException from core.moderation.factory import ModerationFactory +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName +from core.ops.utils import measure_time logger = logging.getLogger(__name__) class InputModeration: - def check(self, app_id: str, - tenant_id: str, - app_config: AppConfig, - inputs: dict, - query: str) -> tuple[bool, dict, str]: + def check( + self, app_id: str, + tenant_id: str, + app_config: AppConfig, + inputs: dict, + query: str, + message_id: str, + trace_manager: Optional[TraceQueueManager] = None + ) -> tuple[bool, dict, str]: """ Process sensitive_word_avoidance. :param app_id: app id @@ -20,6 +27,8 @@ class InputModeration: :param app_config: app config :param inputs: inputs :param query: query + :param message_id: message id + :param trace_manager: trace manager :return: """ if not app_config.sensitive_word_avoidance: @@ -35,7 +44,19 @@ class InputModeration: config=sensitive_word_avoidance_config.config ) - moderation_result = moderation_factory.moderation_for_inputs(inputs, query) + with measure_time() as timer: + moderation_result = moderation_factory.moderation_for_inputs(inputs, query) + + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.MODERATION_TRACE, + message_id=message_id, + moderation_result=moderation_result, + inputs=inputs, + timer=timer + ) + ) if not moderation_result.flagged: return False, inputs, query diff --git a/api/core/ops/__init__.py b/api/core/ops/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/base_trace_instance.py b/api/core/ops/base_trace_instance.py new file mode 100644 index 0000000000..c7af8e2963 --- /dev/null +++ b/api/core/ops/base_trace_instance.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + +from core.ops.entities.config_entity import BaseTracingConfig +from core.ops.entities.trace_entity import BaseTraceInfo + + +class BaseTraceInstance(ABC): + """ + Base trace instance for ops trace services + """ + + @abstractmethod + def __init__(self, trace_config: BaseTracingConfig): + """ + Abstract initializer for the trace instance. + Distribute trace tasks by matching entities + """ + self.trace_config = trace_config + + @abstractmethod + def trace(self, trace_info: BaseTraceInfo): + """ + Abstract method to trace activities. + Subclasses must implement specific tracing logic for activities. + """ + ... \ No newline at end of file diff --git a/api/core/ops/entities/__init__.py b/api/core/ops/entities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/entities/config_entity.py b/api/core/ops/entities/config_entity.py new file mode 100644 index 0000000000..221e6239ab --- /dev/null +++ b/api/core/ops/entities/config_entity.py @@ -0,0 +1,51 @@ +from enum import Enum + +from pydantic import BaseModel, ValidationInfo, field_validator + + +class TracingProviderEnum(Enum): + LANGFUSE = 'langfuse' + LANGSMITH = 'langsmith' + + +class BaseTracingConfig(BaseModel): + """ + Base model class for tracing + """ + ... + + +class LangfuseConfig(BaseTracingConfig): + """ + Model class for Langfuse tracing config. + """ + public_key: str + secret_key: str + host: str = 'https://api.langfuse.com' + + @field_validator("host") + 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 + + +class LangSmithConfig(BaseTracingConfig): + """ + Model class for Langsmith tracing config. + """ + api_key: str + project: str + endpoint: str = 'https://api.smith.langchain.com' + + @field_validator("endpoint") + 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 diff --git a/api/core/ops/entities/trace_entity.py b/api/core/ops/entities/trace_entity.py new file mode 100644 index 0000000000..db7e0806ee --- /dev/null +++ b/api/core/ops/entities/trace_entity.py @@ -0,0 +1,108 @@ +from datetime import datetime +from typing import Any, Optional, Union + +from pydantic import BaseModel, ConfigDict, field_validator + + +class BaseTraceInfo(BaseModel): + message_id: Optional[str] = None + message_data: Optional[Any] = None + inputs: Optional[Union[str, dict[str, Any], list]] = None + outputs: Optional[Union[str, dict[str, Any], list]] = None + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + metadata: dict[str, Any] + + @field_validator("inputs", "outputs") + def ensure_type(cls, v): + if v is None: + return None + if isinstance(v, str | dict | list): + return v + else: + return "" + +class WorkflowTraceInfo(BaseTraceInfo): + workflow_data: Any + conversation_id: Optional[str] = None + workflow_app_log_id: Optional[str] = None + workflow_id: str + tenant_id: str + workflow_run_id: str + workflow_run_elapsed_time: Union[int, float] + workflow_run_status: str + workflow_run_inputs: dict[str, Any] + workflow_run_outputs: dict[str, Any] + workflow_run_version: str + error: Optional[str] = None + total_tokens: int + file_list: list[str] + query: str + metadata: dict[str, Any] + + +class MessageTraceInfo(BaseTraceInfo): + conversation_model: str + message_tokens: int + answer_tokens: int + total_tokens: int + error: Optional[str] = None + file_list: Optional[Union[str, dict[str, Any], list]] = None + message_file_data: Optional[Any] = None + conversation_mode: str + + +class ModerationTraceInfo(BaseTraceInfo): + flagged: bool + action: str + preset_response: str + query: str + + +class SuggestedQuestionTraceInfo(BaseTraceInfo): + total_tokens: int + status: Optional[str] = None + error: Optional[str] = None + from_account_id: Optional[str] = None + agent_based: Optional[bool] = None + from_source: Optional[str] = None + model_provider: Optional[str] = None + model_id: Optional[str] = None + suggested_question: list[str] + level: str + status_message: Optional[str] = None + workflow_run_id: Optional[str] = None + + model_config = ConfigDict(protected_namespaces=()) + + +class DatasetRetrievalTraceInfo(BaseTraceInfo): + documents: Any + + +class ToolTraceInfo(BaseTraceInfo): + tool_name: str + tool_inputs: dict[str, Any] + tool_outputs: str + metadata: dict[str, Any] + message_file_data: Any + error: Optional[str] = None + tool_config: dict[str, Any] + time_cost: Union[int, float] + tool_parameters: dict[str, Any] + file_url: Union[str, None, list] + + +class GenerateNameTraceInfo(BaseTraceInfo): + conversation_id: Optional[str] = None + tenant_id: str + +trace_info_info_map = { + 'WorkflowTraceInfo': WorkflowTraceInfo, + 'MessageTraceInfo': MessageTraceInfo, + 'ModerationTraceInfo': ModerationTraceInfo, + 'SuggestedQuestionTraceInfo': SuggestedQuestionTraceInfo, + 'DatasetRetrievalTraceInfo': DatasetRetrievalTraceInfo, + 'ToolTraceInfo': ToolTraceInfo, + 'GenerateNameTraceInfo': GenerateNameTraceInfo, +} \ No newline at end of file diff --git a/api/core/ops/langfuse_trace/__init__.py b/api/core/ops/langfuse_trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/langfuse_trace/entities/__init__.py b/api/core/ops/langfuse_trace/entities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py b/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py new file mode 100644 index 0000000000..b90c05f4cb --- /dev/null +++ b/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py @@ -0,0 +1,280 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic_core.core_schema import ValidationInfo + +from core.ops.utils import replace_text_with_content + + +def validate_input_output(v, field_name): + """ + Validate input output + :param v: + :param field_name: + :return: + """ + if v == {} or v is None: + return v + if isinstance(v, str): + return [ + { + "role": "assistant" if field_name == "output" else "user", + "content": v, + } + ] + elif isinstance(v, list): + if len(v) > 0 and isinstance(v[0], dict): + v = replace_text_with_content(data=v) + return v + else: + return [ + { + "role": "assistant" if field_name == "output" else "user", + "content": str(v), + } + ] + + return v + + +class LevelEnum(str, Enum): + DEBUG = "DEBUG" + WARNING = "WARNING" + ERROR = "ERROR" + DEFAULT = "DEFAULT" + + +class LangfuseTrace(BaseModel): + """ + Langfuse trace model + """ + id: Optional[str] = Field( + default=None, + description="The id of the trace can be set, defaults to a random id. Used to link traces to external systems " + "or when creating a distributed trace. Traces are upserted on id.", + ) + name: Optional[str] = Field( + default=None, + description="Identifier of the trace. Useful for sorting/filtering in the UI.", + ) + input: Optional[Union[str, dict[str, Any], list, None]] = Field( + default=None, description="The input of the trace. Can be any JSON object." + ) + output: Optional[Union[str, dict[str, Any], list, None]] = Field( + default=None, description="The output of the trace. Can be any JSON object." + ) + metadata: Optional[dict[str, Any]] = Field( + default=None, + description="Additional metadata of the trace. Can be any JSON object. Metadata is merged when being updated " + "via the API.", + ) + user_id: Optional[str] = Field( + default=None, + description="The id of the user that triggered the execution. Used to provide user-level analytics.", + ) + session_id: Optional[str] = Field( + default=None, + description="Used to group multiple traces into a session in Langfuse. Use your own session/thread identifier.", + ) + version: Optional[str] = Field( + default=None, + description="The version of the trace type. Used to understand how changes to the trace type affect metrics. " + "Useful in debugging.", + ) + release: Optional[str] = Field( + default=None, + description="The release identifier of the current deployment. Used to understand how changes of different " + "deployments affect metrics. Useful in debugging.", + ) + tags: Optional[list[str]] = Field( + default=None, + description="Tags are used to categorize or label traces. Traces can be filtered by tags in the UI and GET " + "API. Tags can also be changed in the UI. Tags are merged and never deleted via the API.", + ) + public: Optional[bool] = Field( + default=None, + description="You can make a trace public to share it via a public link. This allows others to view the trace " + "without needing to log in or be members of your Langfuse project.", + ) + + @field_validator("input", "output") + def ensure_dict(cls, v, info: ValidationInfo): + field_name = info.field_name + return validate_input_output(v, field_name) + + +class LangfuseSpan(BaseModel): + """ + Langfuse span model + """ + id: Optional[str] = Field( + default=None, + description="The id of the span can be set, otherwise a random id is generated. Spans are upserted on id.", + ) + session_id: Optional[str] = Field( + default=None, + description="Used to group multiple spans into a session in Langfuse. Use your own session/thread identifier.", + ) + trace_id: Optional[str] = Field( + default=None, + description="The id of the trace the span belongs to. Used to link spans to traces.", + ) + user_id: Optional[str] = Field( + default=None, + description="The id of the user that triggered the execution. Used to provide user-level analytics.", + ) + start_time: Optional[datetime | str] = Field( + default_factory=datetime.now, + description="The time at which the span started, defaults to the current time.", + ) + end_time: Optional[datetime | str] = Field( + default=None, + description="The time at which the span ended. Automatically set by span.end().", + ) + name: Optional[str] = Field( + default=None, + description="Identifier of the span. Useful for sorting/filtering in the UI.", + ) + metadata: Optional[dict[str, Any]] = Field( + default=None, + description="Additional metadata of the span. Can be any JSON object. Metadata is merged when being updated " + "via the API.", + ) + level: Optional[str] = Field( + default=None, + description="The level of the span. Can be DEBUG, DEFAULT, WARNING or ERROR. Used for sorting/filtering of " + "traces with elevated error levels and for highlighting in the UI.", + ) + status_message: Optional[str] = Field( + default=None, + description="The status message of the span. Additional field for context of the event. E.g. the error " + "message of an error event.", + ) + input: Optional[Union[str, dict[str, Any], list, None]] = Field( + default=None, description="The input of the span. Can be any JSON object." + ) + output: Optional[Union[str, dict[str, Any], list, None]] = Field( + default=None, description="The output of the span. Can be any JSON object." + ) + version: Optional[str] = Field( + default=None, + description="The version of the span type. Used to understand how changes to the span type affect metrics. " + "Useful in debugging.", + ) + parent_observation_id: Optional[str] = Field( + default=None, + description="The id of the observation the span belongs to. Used to link spans to observations.", + ) + + @field_validator("input", "output") + def ensure_dict(cls, v, info: ValidationInfo): + field_name = info.field_name + return validate_input_output(v, field_name) + + +class UnitEnum(str, Enum): + CHARACTERS = "CHARACTERS" + TOKENS = "TOKENS" + SECONDS = "SECONDS" + MILLISECONDS = "MILLISECONDS" + IMAGES = "IMAGES" + + +class GenerationUsage(BaseModel): + promptTokens: Optional[int] = None + completionTokens: Optional[int] = None + totalTokens: Optional[int] = None + input: Optional[int] = None + output: Optional[int] = None + total: Optional[int] = None + unit: Optional[UnitEnum] = None + inputCost: Optional[float] = None + outputCost: Optional[float] = None + totalCost: Optional[float] = None + + @field_validator("input", "output") + def ensure_dict(cls, v, info: ValidationInfo): + field_name = info.field_name + return validate_input_output(v, field_name) + + +class LangfuseGeneration(BaseModel): + id: Optional[str] = Field( + default=None, + description="The id of the generation can be set, defaults to random id.", + ) + trace_id: Optional[str] = Field( + default=None, + description="The id of the trace the generation belongs to. Used to link generations to traces.", + ) + parent_observation_id: Optional[str] = Field( + default=None, + description="The id of the observation the generation belongs to. Used to link generations to observations.", + ) + name: Optional[str] = Field( + default=None, + description="Identifier of the generation. Useful for sorting/filtering in the UI.", + ) + start_time: Optional[datetime | str] = Field( + default_factory=datetime.now, + description="The time at which the generation started, defaults to the current time.", + ) + completion_start_time: Optional[datetime | str] = Field( + default=None, + description="The time at which the completion started (streaming). Set it to get latency analytics broken " + "down into time until completion started and completion duration.", + ) + end_time: Optional[datetime | str] = Field( + default=None, + description="The time at which the generation ended. Automatically set by generation.end().", + ) + model: Optional[str] = Field( + default=None, description="The name of the model used for the generation." + ) + model_parameters: Optional[dict[str, Any]] = Field( + default=None, + description="The parameters of the model used for the generation; can be any key-value pairs.", + ) + input: Optional[Any] = Field( + default=None, + description="The prompt used for the generation. Can be any string or JSON object.", + ) + output: Optional[Any] = Field( + default=None, + description="The completion generated by the model. Can be any string or JSON object.", + ) + usage: Optional[GenerationUsage] = Field( + default=None, + description="The usage object supports the OpenAi structure with tokens and a more generic version with " + "detailed costs and units.", + ) + metadata: Optional[dict[str, Any]] = Field( + default=None, + description="Additional metadata of the generation. Can be any JSON object. Metadata is merged when being " + "updated via the API.", + ) + level: Optional[LevelEnum] = Field( + default=None, + description="The level of the generation. Can be DEBUG, DEFAULT, WARNING or ERROR. Used for sorting/filtering " + "of traces with elevated error levels and for highlighting in the UI.", + ) + status_message: Optional[str] = Field( + default=None, + description="The status message of the generation. Additional field for context of the event. E.g. the error " + "message of an error event.", + ) + version: Optional[str] = Field( + default=None, + description="The version of the generation type. Used to understand how changes to the span type affect " + "metrics. Useful in debugging.", + ) + + model_config = ConfigDict(protected_namespaces=()) + + @field_validator("input", "output") + def ensure_dict(cls, v, info: ValidationInfo): + field_name = info.field_name + return validate_input_output(v, field_name) + diff --git a/api/core/ops/langfuse_trace/langfuse_trace.py b/api/core/ops/langfuse_trace/langfuse_trace.py new file mode 100644 index 0000000000..cb86396420 --- /dev/null +++ b/api/core/ops/langfuse_trace/langfuse_trace.py @@ -0,0 +1,435 @@ +import json +import logging +import os +from datetime import datetime, timedelta +from typing import Optional + +from langfuse import Langfuse + +from core.ops.base_trace_instance import BaseTraceInstance +from core.ops.entities.config_entity import LangfuseConfig +from core.ops.entities.trace_entity import ( + BaseTraceInfo, + DatasetRetrievalTraceInfo, + GenerateNameTraceInfo, + MessageTraceInfo, + ModerationTraceInfo, + SuggestedQuestionTraceInfo, + ToolTraceInfo, + WorkflowTraceInfo, +) +from core.ops.langfuse_trace.entities.langfuse_trace_entity import ( + GenerationUsage, + LangfuseGeneration, + LangfuseSpan, + LangfuseTrace, + LevelEnum, + UnitEnum, +) +from core.ops.utils import filter_none_values +from extensions.ext_database import db +from models.model import EndUser +from models.workflow import WorkflowNodeExecution + +logger = logging.getLogger(__name__) + + +class LangFuseDataTrace(BaseTraceInstance): + def __init__( + self, + langfuse_config: LangfuseConfig, + ): + super().__init__(langfuse_config) + self.langfuse_client = Langfuse( + public_key=langfuse_config.public_key, + secret_key=langfuse_config.secret_key, + host=langfuse_config.host, + ) + self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001") + + 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): + 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) + + def workflow_trace(self, trace_info: WorkflowTraceInfo): + trace_id = trace_info.workflow_app_log_id if trace_info.workflow_app_log_id else trace_info.workflow_run_id + if trace_info.message_id: + trace_id = trace_info.message_id + name = f"message_{trace_info.message_id}" + trace_data = LangfuseTrace( + id=trace_info.message_id, + user_id=trace_info.tenant_id, + name=name, + input=trace_info.workflow_run_inputs, + output=trace_info.workflow_run_outputs, + metadata=trace_info.metadata, + session_id=trace_info.conversation_id, + tags=["message", "workflow"], + ) + self.add_trace(langfuse_trace_data=trace_data) + workflow_span_data = LangfuseSpan( + id=trace_info.workflow_app_log_id if trace_info.workflow_app_log_id else trace_info.workflow_run_id, + name=f"workflow_{trace_info.workflow_app_log_id}" if trace_info.workflow_app_log_id else f"workflow_{trace_info.workflow_run_id}", + input=trace_info.workflow_run_inputs, + output=trace_info.workflow_run_outputs, + trace_id=trace_id, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + metadata=trace_info.metadata, + level=LevelEnum.DEFAULT if trace_info.error == "" else LevelEnum.ERROR, + status_message=trace_info.error if trace_info.error else "", + ) + self.add_span(langfuse_span_data=workflow_span_data) + else: + trace_data = LangfuseTrace( + id=trace_id, + user_id=trace_info.tenant_id, + name=f"workflow_{trace_info.workflow_app_log_id}" if trace_info.workflow_app_log_id else f"workflow_{trace_info.workflow_run_id}", + input=trace_info.workflow_run_inputs, + output=trace_info.workflow_run_outputs, + metadata=trace_info.metadata, + session_id=trace_info.conversation_id, + tags=["workflow"], + ) + self.add_trace(langfuse_trace_data=trace_data) + + # through workflow_run_id get all_nodes_execution + workflow_nodes_executions = ( + db.session.query( + WorkflowNodeExecution.id, + WorkflowNodeExecution.tenant_id, + WorkflowNodeExecution.app_id, + WorkflowNodeExecution.title, + WorkflowNodeExecution.node_type, + WorkflowNodeExecution.status, + WorkflowNodeExecution.inputs, + WorkflowNodeExecution.outputs, + WorkflowNodeExecution.created_at, + WorkflowNodeExecution.elapsed_time, + WorkflowNodeExecution.process_data, + WorkflowNodeExecution.execution_metadata, + ) + .filter(WorkflowNodeExecution.workflow_run_id == trace_info.workflow_run_id) + .all() + ) + + for node_execution in workflow_nodes_executions: + node_execution_id = node_execution.id + tenant_id = node_execution.tenant_id + app_id = node_execution.app_id + node_name = node_execution.title + node_type = node_execution.node_type + status = node_execution.status + if node_type == "llm": + inputs = json.loads(node_execution.process_data).get( + "prompts", {} + ) if node_execution.process_data else {} + else: + inputs = json.loads(node_execution.inputs) if node_execution.inputs else {} + outputs = ( + json.loads(node_execution.outputs) if node_execution.outputs else {} + ) + created_at = node_execution.created_at if node_execution.created_at else datetime.now() + elapsed_time = node_execution.elapsed_time + finished_at = created_at + timedelta(seconds=elapsed_time) + + metadata = json.loads(node_execution.execution_metadata) if node_execution.execution_metadata else {} + metadata.update( + { + "workflow_run_id": trace_info.workflow_run_id, + "node_execution_id": node_execution_id, + "tenant_id": tenant_id, + "app_id": app_id, + "node_name": node_name, + "node_type": node_type, + "status": status, + } + ) + + # add span + if trace_info.message_id: + span_data = LangfuseSpan( + id=node_execution_id, + name=f"{node_name}_{node_execution_id}", + 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 if trace_info.error else "", + parent_observation_id=trace_info.workflow_app_log_id if trace_info.workflow_app_log_id else trace_info.workflow_run_id, + ) + else: + span_data = LangfuseSpan( + id=node_execution_id, + name=f"{node_name}_{node_execution_id}", + 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 if trace_info.error else "", + ) + + self.add_span(langfuse_span_data=span_data) + + process_data = json.loads(node_execution.process_data) if node_execution.process_data else {} + if process_data and process_data.get("model_mode") == "chat": + total_token = metadata.get("total_tokens", 0) + # add generation + generation_usage = GenerationUsage( + totalTokens=total_token, + ) + + node_generation_data = LangfuseGeneration( + name=f"generation_{node_execution_id}", + trace_id=trace_id, + parent_observation_id=node_execution_id, + start_time=created_at, + end_time=finished_at, + input=inputs, + output=outputs, + metadata=metadata, + level=LevelEnum.DEFAULT if status == 'succeeded' else LevelEnum.ERROR, + status_message=trace_info.error if trace_info.error else "", + usage=generation_usage, + ) + + self.add_generation(langfuse_generation_data=node_generation_data) + + def message_trace( + self, trace_info: MessageTraceInfo, **kwargs + ): + # get message file data + file_list = trace_info.file_list + metadata = trace_info.metadata + message_data = trace_info.message_data + message_id = message_data.id + + user_id = message_data.from_account_id + if message_data.from_end_user_id: + end_user_data: 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 + metadata["user_id"] = user_id + + trace_data = LangfuseTrace( + id=message_id, + user_id=user_id, + name=f"message_{message_id}", + input={ + "message": trace_info.inputs, + "files": file_list, + "message_tokens": trace_info.message_tokens, + "answer_tokens": trace_info.answer_tokens, + "total_tokens": trace_info.total_tokens, + "error": trace_info.error, + "provider_response_latency": message_data.provider_response_latency, + "created_at": trace_info.start_time, + }, + output=trace_info.outputs, + metadata=metadata, + session_id=message_data.conversation_id, + tags=["message", str(trace_info.conversation_mode)], + version=None, + release=None, + public=None, + ) + self.add_trace(langfuse_trace_data=trace_data) + + # start add span + generation_usage = GenerationUsage( + totalTokens=trace_info.total_tokens, + input=trace_info.message_tokens, + output=trace_info.answer_tokens, + total=trace_info.total_tokens, + unit=UnitEnum.TOKENS, + totalCost=message_data.total_price, + ) + + langfuse_generation_data = LangfuseGeneration( + name=f"generation_{message_id}", + trace_id=message_id, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + model=message_data.model_id, + input=trace_info.inputs, + output=message_data.answer, + metadata=metadata, + level=LevelEnum.DEFAULT if message_data.status != 'error' else LevelEnum.ERROR, + status_message=message_data.error if message_data.error else "", + usage=generation_usage, + ) + + self.add_generation(langfuse_generation_data) + + def moderation_trace(self, trace_info: ModerationTraceInfo): + span_data = LangfuseSpan( + name="moderation", + input=trace_info.inputs, + output={ + "action": trace_info.action, + "flagged": trace_info.flagged, + "preset_response": trace_info.preset_response, + "inputs": trace_info.inputs, + }, + trace_id=trace_info.message_id, + start_time=trace_info.start_time or trace_info.message_data.created_at, + end_time=trace_info.end_time or trace_info.message_data.created_at, + metadata=trace_info.metadata, + ) + + self.add_span(langfuse_span_data=span_data) + + def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo): + message_data = trace_info.message_data + generation_usage = GenerationUsage( + totalTokens=len(str(trace_info.suggested_question)), + input=len(trace_info.inputs), + output=len(trace_info.suggested_question), + total=len(trace_info.suggested_question), + unit=UnitEnum.CHARACTERS, + ) + + generation_data = LangfuseGeneration( + name="suggested_question", + input=trace_info.inputs, + output=str(trace_info.suggested_question), + trace_id=trace_info.message_id, + 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, + status_message=message_data.error if message_data.error else "", + usage=generation_usage, + ) + + self.add_generation(langfuse_generation_data=generation_data) + + def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo): + dataset_retrieval_span_data = LangfuseSpan( + name="dataset_retrieval", + input=trace_info.inputs, + output={"documents": trace_info.documents}, + trace_id=trace_info.message_id, + 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=trace_info.metadata, + ) + + self.add_span(langfuse_span_data=dataset_retrieval_span_data) + + def tool_trace(self, trace_info: ToolTraceInfo): + tool_span_data = LangfuseSpan( + name=trace_info.tool_name, + input=trace_info.tool_inputs, + output=trace_info.tool_outputs, + trace_id=trace_info.message_id, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + metadata=trace_info.metadata, + level=LevelEnum.DEFAULT if trace_info.error == "" or trace_info.error is None else LevelEnum.ERROR, + status_message=trace_info.error, + ) + + self.add_span(langfuse_span_data=tool_span_data) + + def generate_name_trace(self, trace_info: GenerateNameTraceInfo): + name_generation_trace_data = LangfuseTrace( + name="generate_name", + input=trace_info.inputs, + output=trace_info.outputs, + user_id=trace_info.tenant_id, + metadata=trace_info.metadata, + session_id=trace_info.conversation_id, + ) + + self.add_trace(langfuse_trace_data=name_generation_trace_data) + + name_generation_span_data = LangfuseSpan( + name="generate_name", + input=trace_info.inputs, + output=trace_info.outputs, + trace_id=trace_info.conversation_id, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + metadata=trace_info.metadata, + ) + self.add_span(langfuse_span_data=name_generation_span_data) + + def add_trace(self, langfuse_trace_data: Optional[LangfuseTrace] = None): + format_trace_data = ( + filter_none_values(langfuse_trace_data.model_dump()) if langfuse_trace_data else {} + ) + try: + self.langfuse_client.trace(**format_trace_data) + logger.debug("LangFuse Trace created successfully") + except Exception as e: + raise ValueError(f"LangFuse Failed to create trace: {str(e)}") + + def add_span(self, langfuse_span_data: Optional[LangfuseSpan] = None): + format_span_data = ( + filter_none_values(langfuse_span_data.model_dump()) if langfuse_span_data else {} + ) + try: + self.langfuse_client.span(**format_span_data) + logger.debug("LangFuse Span created successfully") + except Exception as e: + raise ValueError(f"LangFuse Failed to create span: {str(e)}") + + def update_span(self, span, langfuse_span_data: Optional[LangfuseSpan] = None): + format_span_data = ( + filter_none_values(langfuse_span_data.model_dump()) if langfuse_span_data else {} + ) + + span.end(**format_span_data) + + def add_generation( + self, langfuse_generation_data: Optional[LangfuseGeneration] = None + ): + format_generation_data = ( + filter_none_values(langfuse_generation_data.model_dump()) + if langfuse_generation_data + else {} + ) + try: + self.langfuse_client.generation(**format_generation_data) + logger.debug("LangFuse Generation created successfully") + except Exception as e: + raise ValueError(f"LangFuse Failed to create generation: {str(e)}") + + def update_generation( + self, generation, langfuse_generation_data: Optional[LangfuseGeneration] = None + ): + format_generation_data = ( + filter_none_values(langfuse_generation_data.model_dump()) + if langfuse_generation_data + else {} + ) + + generation.end(**format_generation_data) + + def api_check(self): + try: + return self.langfuse_client.auth_check() + except Exception as e: + logger.debug(f"LangFuse API check failed: {str(e)}") + raise ValueError(f"LangFuse API check failed: {str(e)}") diff --git a/api/core/ops/langsmith_trace/__init__.py b/api/core/ops/langsmith_trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/langsmith_trace/entities/__init__.py b/api/core/ops/langsmith_trace/entities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py b/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py new file mode 100644 index 0000000000..f3fc46d99a --- /dev/null +++ b/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py @@ -0,0 +1,167 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Optional, Union + +from pydantic import BaseModel, Field, field_validator +from pydantic_core.core_schema import ValidationInfo + +from core.ops.utils import replace_text_with_content + + +class LangSmithRunType(str, Enum): + tool = "tool" + chain = "chain" + llm = "llm" + retriever = "retriever" + embedding = "embedding" + prompt = "prompt" + parser = "parser" + + +class LangSmithTokenUsage(BaseModel): + input_tokens: Optional[int] = None + output_tokens: Optional[int] = None + total_tokens: Optional[int] = None + + +class LangSmithMultiModel(BaseModel): + file_list: Optional[list[str]] = Field(None, description="List of files") + + +class LangSmithRunModel(LangSmithTokenUsage, LangSmithMultiModel): + name: Optional[str] = Field(..., description="Name of the run") + inputs: Optional[Union[str, dict[str, Any], list, None]] = Field(None, description="Inputs of the run") + outputs: Optional[Union[str, dict[str, Any], list, None]] = Field(None, description="Outputs of the run") + run_type: LangSmithRunType = Field(..., description="Type of the run") + start_time: Optional[datetime | str] = Field(None, description="Start time of the run") + end_time: Optional[datetime | str] = Field(None, description="End time of the run") + extra: Optional[dict[str, Any]] = Field( + None, description="Extra information of the run" + ) + error: Optional[str] = Field(None, description="Error message of the run") + serialized: Optional[dict[str, Any]] = Field( + None, description="Serialized data of the run" + ) + parent_run_id: Optional[str] = Field(None, description="Parent run ID") + events: Optional[list[dict[str, Any]]] = Field( + None, description="Events associated with the run" + ) + tags: Optional[list[str]] = Field(None, description="Tags associated with the run") + trace_id: Optional[str] = Field( + None, description="Trace ID associated with the run" + ) + dotted_order: Optional[str] = Field(None, description="Dotted order of the run") + id: Optional[str] = Field(None, description="ID of the run") + session_id: Optional[str] = Field( + None, description="Session ID associated with the run" + ) + session_name: Optional[str] = Field( + None, description="Session name associated with the run" + ) + reference_example_id: Optional[str] = Field( + None, description="Reference example ID associated with the run" + ) + input_attachments: Optional[dict[str, Any]] = Field( + None, description="Input attachments of the run" + ) + output_attachments: Optional[dict[str, Any]] = Field( + None, description="Output attachments of the run" + ) + + @field_validator("inputs", "outputs") + def ensure_dict(cls, v, info: ValidationInfo): + field_name = info.field_name + values = info.data + if v == {} or v is None: + return v + usage_metadata = { + "input_tokens": values.get('input_tokens', 0), + "output_tokens": values.get('output_tokens', 0), + "total_tokens": values.get('total_tokens', 0), + } + file_list = values.get("file_list", []) + if isinstance(v, str): + if field_name == "inputs": + return { + "messages": { + "role": "user", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + elif field_name == "outputs": + return { + "choices": { + "role": "ai", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + elif isinstance(v, list): + data = {} + if len(v) > 0 and isinstance(v[0], dict): + # rename text to content + v = replace_text_with_content(data=v) + if field_name == "inputs": + data = { + "messages": v, + } + elif field_name == "outputs": + data = { + "choices": { + "role": "ai", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + return data + else: + return { + "choices": { + "role": "ai" if field_name == "outputs" else "user", + "content": str(v), + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + if isinstance(v, dict): + v["usage_metadata"] = usage_metadata + v["file_list"] = file_list + return v + return v + + @field_validator("start_time", "end_time") + def format_time(cls, v, info: ValidationInfo): + if not isinstance(v, datetime): + raise ValueError(f"{info.field_name} must be a datetime object") + else: + return v.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +class LangSmithRunUpdateModel(BaseModel): + run_id: str = Field(..., description="ID of the run") + trace_id: Optional[str] = Field( + None, description="Trace ID associated with the run" + ) + dotted_order: Optional[str] = Field(None, description="Dotted order of the run") + parent_run_id: Optional[str] = Field(None, description="Parent run ID") + end_time: Optional[datetime | str] = Field(None, description="End time of the run") + error: Optional[str] = Field(None, description="Error message of the run") + inputs: Optional[dict[str, Any]] = Field(None, description="Inputs of the run") + outputs: Optional[dict[str, Any]] = Field(None, description="Outputs of the run") + events: Optional[list[dict[str, Any]]] = Field( + None, description="Events associated with the run" + ) + tags: Optional[list[str]] = Field(None, description="Tags associated with the run") + extra: Optional[dict[str, Any]] = Field( + None, description="Extra information of the run" + ) + input_attachments: Optional[dict[str, Any]] = Field( + None, description="Input attachments of the run" + ) + output_attachments: Optional[dict[str, Any]] = Field( + None, description="Output attachments of the run" + ) diff --git a/api/core/ops/langsmith_trace/langsmith_trace.py b/api/core/ops/langsmith_trace/langsmith_trace.py new file mode 100644 index 0000000000..0ce91db335 --- /dev/null +++ b/api/core/ops/langsmith_trace/langsmith_trace.py @@ -0,0 +1,371 @@ +import json +import logging +import os +from datetime import datetime, timedelta + +from langsmith import Client + +from core.ops.base_trace_instance import BaseTraceInstance +from core.ops.entities.config_entity import LangSmithConfig +from core.ops.entities.trace_entity import ( + BaseTraceInfo, + DatasetRetrievalTraceInfo, + GenerateNameTraceInfo, + MessageTraceInfo, + ModerationTraceInfo, + SuggestedQuestionTraceInfo, + ToolTraceInfo, + WorkflowTraceInfo, +) +from core.ops.langsmith_trace.entities.langsmith_trace_entity import ( + LangSmithRunModel, + LangSmithRunType, + LangSmithRunUpdateModel, +) +from core.ops.utils import filter_none_values +from extensions.ext_database import db +from models.model import EndUser, MessageFile +from models.workflow import WorkflowNodeExecution + +logger = logging.getLogger(__name__) + + +class LangSmithDataTrace(BaseTraceInstance): + def __init__( + self, + langsmith_config: LangSmithConfig, + ): + super().__init__(langsmith_config) + self.langsmith_key = langsmith_config.api_key + self.project_name = langsmith_config.project + self.project_id = None + self.langsmith_client = Client( + api_key=langsmith_config.api_key, api_url=langsmith_config.endpoint + ) + self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001") + + 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): + 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) + + def workflow_trace(self, trace_info: WorkflowTraceInfo): + if trace_info.message_id: + message_run = LangSmithRunModel( + id=trace_info.message_id, + name=f"message_{trace_info.message_id}", + inputs=trace_info.workflow_run_inputs, + outputs=trace_info.workflow_run_outputs, + run_type=LangSmithRunType.chain, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + extra={ + "metadata": trace_info.metadata, + }, + tags=["message"], + error=trace_info.error + ) + self.add_run(message_run) + + langsmith_run = LangSmithRunModel( + file_list=trace_info.file_list, + total_tokens=trace_info.total_tokens, + id=trace_info.workflow_app_log_id if trace_info.workflow_app_log_id else trace_info.workflow_run_id, + name=f"workflow_{trace_info.workflow_app_log_id}" if trace_info.workflow_app_log_id else f"workflow_{trace_info.workflow_run_id}", + inputs=trace_info.workflow_run_inputs, + run_type=LangSmithRunType.tool, + start_time=trace_info.workflow_data.created_at, + end_time=trace_info.workflow_data.finished_at, + outputs=trace_info.workflow_run_outputs, + extra={ + "metadata": trace_info.metadata, + }, + error=trace_info.error, + tags=["workflow"], + parent_run_id=trace_info.message_id if trace_info.message_id else None, + ) + + self.add_run(langsmith_run) + + # through workflow_run_id get all_nodes_execution + workflow_nodes_executions = ( + db.session.query( + WorkflowNodeExecution.id, + WorkflowNodeExecution.tenant_id, + WorkflowNodeExecution.app_id, + WorkflowNodeExecution.title, + WorkflowNodeExecution.node_type, + WorkflowNodeExecution.status, + WorkflowNodeExecution.inputs, + WorkflowNodeExecution.outputs, + WorkflowNodeExecution.created_at, + WorkflowNodeExecution.elapsed_time, + WorkflowNodeExecution.process_data, + WorkflowNodeExecution.execution_metadata, + ) + .filter(WorkflowNodeExecution.workflow_run_id == trace_info.workflow_run_id) + .all() + ) + + for node_execution in workflow_nodes_executions: + node_execution_id = node_execution.id + tenant_id = node_execution.tenant_id + app_id = node_execution.app_id + node_name = node_execution.title + node_type = node_execution.node_type + status = node_execution.status + if node_type == "llm": + inputs = json.loads(node_execution.process_data).get( + "prompts", {} + ) if node_execution.process_data else {} + else: + inputs = json.loads(node_execution.inputs) if node_execution.inputs else {} + outputs = ( + json.loads(node_execution.outputs) if node_execution.outputs else {} + ) + created_at = node_execution.created_at if node_execution.created_at else datetime.now() + elapsed_time = node_execution.elapsed_time + finished_at = created_at + timedelta(seconds=elapsed_time) + + execution_metadata = ( + json.loads(node_execution.execution_metadata) + if node_execution.execution_metadata + else {} + ) + node_total_tokens = execution_metadata.get("total_tokens", 0) + + metadata = json.loads(node_execution.execution_metadata) if node_execution.execution_metadata else {} + metadata.update( + { + "workflow_run_id": trace_info.workflow_run_id, + "node_execution_id": node_execution_id, + "tenant_id": tenant_id, + "app_id": app_id, + "app_name": node_name, + "node_type": node_type, + "status": status, + } + ) + + process_data = json.loads(node_execution.process_data) if node_execution.process_data else {} + if process_data and process_data.get("model_mode") == "chat": + run_type = LangSmithRunType.llm + elif node_type == "knowledge-retrieval": + run_type = LangSmithRunType.retriever + else: + run_type = LangSmithRunType.tool + + langsmith_run = LangSmithRunModel( + total_tokens=node_total_tokens, + name=f"{node_name}_{node_execution_id}", + inputs=inputs, + run_type=run_type, + start_time=created_at, + end_time=finished_at, + outputs=outputs, + file_list=trace_info.file_list, + extra={ + "metadata": metadata, + }, + parent_run_id=trace_info.workflow_app_log_id if trace_info.workflow_app_log_id else trace_info.workflow_run_id, + tags=["node_execution"], + ) + + self.add_run(langsmith_run) + + def message_trace(self, trace_info: MessageTraceInfo): + # get message file data + file_list = trace_info.file_list + message_file_data: MessageFile = trace_info.message_file_data + file_url = f"{self.file_base_url}/{message_file_data.url}" if message_file_data else "" + file_list.append(file_url) + metadata = trace_info.metadata + message_data = trace_info.message_data + message_id = message_data.id + + user_id = message_data.from_account_id + metadata["user_id"] = user_id + + if message_data.from_end_user_id: + end_user_data: EndUser = db.session.query(EndUser).filter( + EndUser.id == message_data.from_end_user_id + ).first() + if end_user_data is not None: + end_user_id = end_user_data.session_id + metadata["end_user_id"] = end_user_id + + message_run = LangSmithRunModel( + input_tokens=trace_info.message_tokens, + output_tokens=trace_info.answer_tokens, + total_tokens=trace_info.total_tokens, + id=message_id, + name=f"message_{message_id}", + inputs=trace_info.inputs, + run_type=LangSmithRunType.chain, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + outputs=message_data.answer, + extra={ + "metadata": metadata, + }, + tags=["message", str(trace_info.conversation_mode)], + error=trace_info.error, + file_list=file_list, + ) + self.add_run(message_run) + + # create llm run parented to message run + llm_run = LangSmithRunModel( + input_tokens=trace_info.message_tokens, + output_tokens=trace_info.answer_tokens, + total_tokens=trace_info.total_tokens, + name=f"llm_{message_id}", + inputs=trace_info.inputs, + run_type=LangSmithRunType.llm, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + outputs=message_data.answer, + extra={ + "metadata": metadata, + }, + parent_run_id=message_id, + tags=["llm", str(trace_info.conversation_mode)], + error=trace_info.error, + file_list=file_list, + ) + self.add_run(llm_run) + + def moderation_trace(self, trace_info: ModerationTraceInfo): + langsmith_run = LangSmithRunModel( + name="moderation", + inputs=trace_info.inputs, + outputs={ + "action": trace_info.action, + "flagged": trace_info.flagged, + "preset_response": trace_info.preset_response, + "inputs": trace_info.inputs, + }, + run_type=LangSmithRunType.tool, + extra={ + "metadata": trace_info.metadata, + }, + tags=["moderation"], + parent_run_id=trace_info.message_id, + 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, + ) + + self.add_run(langsmith_run) + + def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo): + message_data = trace_info.message_data + suggested_question_run = LangSmithRunModel( + name="suggested_question", + inputs=trace_info.inputs, + outputs=trace_info.suggested_question, + run_type=LangSmithRunType.tool, + extra={ + "metadata": trace_info.metadata, + }, + tags=["suggested_question"], + parent_run_id=trace_info.message_id, + start_time=trace_info.start_time or message_data.created_at, + end_time=trace_info.end_time or message_data.updated_at, + ) + + self.add_run(suggested_question_run) + + def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo): + dataset_retrieval_run = LangSmithRunModel( + name="dataset_retrieval", + inputs=trace_info.inputs, + outputs={"documents": trace_info.documents}, + run_type=LangSmithRunType.retriever, + extra={ + "metadata": trace_info.metadata, + }, + tags=["dataset_retrieval"], + parent_run_id=trace_info.message_id, + 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, + ) + + self.add_run(dataset_retrieval_run) + + def tool_trace(self, trace_info: ToolTraceInfo): + tool_run = LangSmithRunModel( + name=trace_info.tool_name, + inputs=trace_info.tool_inputs, + outputs=trace_info.tool_outputs, + run_type=LangSmithRunType.tool, + extra={ + "metadata": trace_info.metadata, + }, + tags=["tool", trace_info.tool_name], + parent_run_id=trace_info.message_id, + start_time=trace_info.start_time, + end_time=trace_info.end_time, + file_list=[trace_info.file_url], + ) + + self.add_run(tool_run) + + def generate_name_trace(self, trace_info: GenerateNameTraceInfo): + name_run = LangSmithRunModel( + name="generate_name", + inputs=trace_info.inputs, + outputs=trace_info.outputs, + run_type=LangSmithRunType.tool, + extra={ + "metadata": trace_info.metadata, + }, + tags=["generate_name"], + start_time=trace_info.start_time or datetime.now(), + end_time=trace_info.end_time or datetime.now(), + ) + + self.add_run(name_run) + + def add_run(self, run_data: LangSmithRunModel): + data = run_data.model_dump() + if self.project_id: + data["session_id"] = self.project_id + elif self.project_name: + data["session_name"] = self.project_name + + data = filter_none_values(data) + try: + self.langsmith_client.create_run(**data) + logger.debug("LangSmith Run created successfully.") + except Exception as e: + raise ValueError(f"LangSmith Failed to create run: {str(e)}") + + def update_run(self, update_run_data: LangSmithRunUpdateModel): + data = update_run_data.model_dump() + data = filter_none_values(data) + try: + self.langsmith_client.update_run(**data) + logger.debug("LangSmith Run updated successfully.") + except Exception as e: + raise ValueError(f"LangSmith Failed to update run: {str(e)}") + + def api_check(self): + try: + random_project_name = f"test_project_{datetime.now().strftime('%Y%m%d%H%M%S')}" + self.langsmith_client.create_project(project_name=random_project_name) + self.langsmith_client.delete_project(project_name=random_project_name) + return True + except Exception as e: + logger.debug(f"LangSmith API check failed: {str(e)}") + raise ValueError(f"LangSmith API check failed: {str(e)}") diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py new file mode 100644 index 0000000000..ff15aa999b --- /dev/null +++ b/api/core/ops/ops_trace_manager.py @@ -0,0 +1,735 @@ +import json +import logging +import os +import queue +import threading +import time +from datetime import timedelta +from enum import Enum +from typing import Any, Optional, Union +from uuid import UUID + +from flask import current_app + +from core.helper.encrypter import decrypt_token, encrypt_token, obfuscated_token +from core.ops.entities.config_entity import ( + LangfuseConfig, + LangSmithConfig, + TracingProviderEnum, +) +from core.ops.entities.trace_entity import ( + DatasetRetrievalTraceInfo, + GenerateNameTraceInfo, + MessageTraceInfo, + ModerationTraceInfo, + SuggestedQuestionTraceInfo, + ToolTraceInfo, + WorkflowTraceInfo, +) +from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace +from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace +from core.ops.utils import get_message_data +from extensions.ext_database import db +from models.model import App, AppModelConfig, Conversation, Message, MessageAgentThought, MessageFile, TraceAppConfig +from models.workflow import WorkflowAppLog, WorkflowRun +from tasks.ops_trace_task import process_trace_tasks + +provider_config_map = { + TracingProviderEnum.LANGFUSE.value: { + 'config_class': LangfuseConfig, + 'secret_keys': ['public_key', 'secret_key'], + 'other_keys': ['host'], + 'trace_instance': LangFuseDataTrace + }, + TracingProviderEnum.LANGSMITH.value: { + 'config_class': LangSmithConfig, + 'secret_keys': ['api_key'], + 'other_keys': ['project', 'endpoint'], + 'trace_instance': LangSmithDataTrace + } +} + + +class OpsTraceManager: + @classmethod + def encrypt_tracing_config( + cls, tenant_id: str, tracing_provider: str, tracing_config: dict, current_trace_config=None + ): + """ + Encrypt tracing config. + :param tenant_id: tenant id + :param tracing_provider: tracing provider + :param tracing_config: tracing config dictionary to be encrypted + :param current_trace_config: current tracing configuration for keeping existing values + :return: encrypted tracing configuration + """ + # Get the configuration class and the keys that require encryption + config_class, secret_keys, other_keys = provider_config_map[tracing_provider]['config_class'], \ + provider_config_map[tracing_provider]['secret_keys'], provider_config_map[tracing_provider]['other_keys'] + + new_config = {} + # Encrypt necessary keys + for key in secret_keys: + if key in tracing_config: + if '*' in tracing_config[key]: + # If the key contains '*', retain the original value from the current config + new_config[key] = current_trace_config.get(key, tracing_config[key]) + else: + # Otherwise, encrypt the key + new_config[key] = encrypt_token(tenant_id, tracing_config[key]) + + for key in other_keys: + new_config[key] = tracing_config.get(key, "") + + # Create a new instance of the config class with the new configuration + encrypted_config = config_class(**new_config) + return encrypted_config.model_dump() + + @classmethod + def decrypt_tracing_config(cls, tenant_id: str, tracing_provider: str, tracing_config: dict): + """ + Decrypt tracing config + :param tenant_id: tenant id + :param tracing_provider: tracing provider + :param tracing_config: tracing config + :return: + """ + config_class, secret_keys, other_keys = provider_config_map[tracing_provider]['config_class'], \ + provider_config_map[tracing_provider]['secret_keys'], provider_config_map[tracing_provider]['other_keys'] + new_config = {} + for key in secret_keys: + if key in tracing_config: + new_config[key] = decrypt_token(tenant_id, tracing_config[key]) + + for key in other_keys: + new_config[key] = tracing_config.get(key, "") + + return config_class(**new_config).model_dump() + + @classmethod + def obfuscated_decrypt_token(cls, tracing_provider: str, decrypt_tracing_config: dict): + """ + Decrypt tracing config + :param tracing_provider: tracing provider + :param decrypt_tracing_config: tracing config + :return: + """ + config_class, secret_keys, other_keys = provider_config_map[tracing_provider]['config_class'], \ + provider_config_map[tracing_provider]['secret_keys'], provider_config_map[tracing_provider]['other_keys'] + new_config = {} + for key in secret_keys: + if key in decrypt_tracing_config: + new_config[key] = obfuscated_token(decrypt_tracing_config[key]) + + for key in other_keys: + new_config[key] = decrypt_tracing_config.get(key, "") + + return config_class(**new_config).model_dump() + + @classmethod + def get_decrypted_tracing_config(cls, app_id: str, tracing_provider: str): + """ + Get decrypted tracing config + :param app_id: app id + :param tracing_provider: tracing provider + :return: + """ + trace_config_data: TraceAppConfig = db.session.query(TraceAppConfig).filter( + TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider + ).first() + + if not trace_config_data: + return None + + # decrypt_token + tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id + decrypt_tracing_config = cls.decrypt_tracing_config( + tenant_id, tracing_provider, trace_config_data.tracing_config + ) + + return decrypt_tracing_config + + @classmethod + def get_ops_trace_instance( + cls, + app_id: Optional[Union[UUID, str]] = None, + message_id: Optional[str] = None, + conversation_id: Optional[str] = None + ): + """ + Get ops trace through model config + :param app_id: app_id + :param message_id: message_id + :param conversation_id: conversation_id + :return: + """ + if conversation_id is not None: + conversation_data: Conversation = db.session.query(Conversation).filter( + Conversation.id == conversation_id + ).first() + if conversation_data: + app_id = conversation_data.app_id + + if message_id is not None: + record: Message = db.session.query(Message).filter(Message.id == message_id).first() + app_id = record.app_id + + if isinstance(app_id, UUID): + app_id = str(app_id) + + if app_id is None: + return None + + app: App = db.session.query(App).filter( + App.id == app_id + ).first() + app_ops_trace_config = json.loads(app.tracing) if app.tracing else None + + if app_ops_trace_config is not None: + tracing_provider = app_ops_trace_config.get('tracing_provider') + else: + return None + + # decrypt_token + decrypt_trace_config = cls.get_decrypted_tracing_config(app_id, tracing_provider) + if app_ops_trace_config.get('enabled'): + trace_instance, config_class = provider_config_map[tracing_provider]['trace_instance'], \ + provider_config_map[tracing_provider]['config_class'] + tracing_instance = trace_instance(config_class(**decrypt_trace_config)) + return tracing_instance + + return None + + @classmethod + def get_app_config_through_message_id(cls, message_id: str): + app_model_config = None + message_data = db.session.query(Message).filter(Message.id == message_id).first() + conversation_id = message_data.conversation_id + conversation_data = db.session.query(Conversation).filter(Conversation.id == conversation_id).first() + + if conversation_data.app_model_config_id: + app_model_config = db.session.query(AppModelConfig).filter( + AppModelConfig.id == conversation_data.app_model_config_id + ).first() + elif conversation_data.app_model_config_id is None and conversation_data.override_model_configs: + app_model_config = conversation_data.override_model_configs + + return app_model_config + + @classmethod + def update_app_tracing_config(cls, app_id: str, enabled: bool, tracing_provider: str): + """ + Update app tracing config + :param app_id: app id + :param enabled: enabled + :param tracing_provider: tracing provider + :return: + """ + # auth check + if tracing_provider not in provider_config_map.keys() and tracing_provider is not None: + raise ValueError(f"Invalid tracing provider: {tracing_provider}") + + app_config: App = db.session.query(App).filter(App.id == app_id).first() + app_config.tracing = json.dumps( + { + "enabled": enabled, + "tracing_provider": tracing_provider, + } + ) + db.session.commit() + + @classmethod + def get_app_tracing_config(cls, app_id: str): + """ + Get app tracing config + :param app_id: app id + :return: + """ + app: App = db.session.query(App).filter(App.id == app_id).first() + if not app.tracing: + return { + "enabled": False, + "tracing_provider": None + } + app_trace_config = json.loads(app.tracing) + return app_trace_config + + @staticmethod + def check_trace_config_is_effective(tracing_config: dict, tracing_provider: str): + """ + Check trace config is effective + :param tracing_config: tracing config + :param tracing_provider: tracing provider + :return: + """ + config_type, trace_instance = provider_config_map[tracing_provider]['config_class'], \ + provider_config_map[tracing_provider]['trace_instance'] + tracing_config = config_type(**tracing_config) + return trace_instance(tracing_config).api_check() + + +class TraceTaskName(str, Enum): + CONVERSATION_TRACE = 'conversation_trace' + WORKFLOW_TRACE = 'workflow_trace' + MESSAGE_TRACE = 'message_trace' + MODERATION_TRACE = 'moderation_trace' + SUGGESTED_QUESTION_TRACE = 'suggested_question_trace' + DATASET_RETRIEVAL_TRACE = 'dataset_retrieval_trace' + TOOL_TRACE = 'tool_trace' + GENERATE_NAME_TRACE = 'generate_name_trace' + + +class TraceTask: + def __init__( + self, + trace_type: Any, + message_id: Optional[str] = None, + workflow_run: Optional[WorkflowRun] = None, + conversation_id: Optional[str] = None, + timer: Optional[Any] = None, + **kwargs + ): + self.trace_type = trace_type + self.message_id = message_id + self.workflow_run = workflow_run + self.conversation_id = conversation_id + self.timer = timer + self.kwargs = kwargs + self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001") + + def execute(self): + method_name, trace_info = self.preprocess() + return trace_info + + def preprocess(self): + if self.trace_type == TraceTaskName.CONVERSATION_TRACE: + return TraceTaskName.CONVERSATION_TRACE, self.conversation_trace(**self.kwargs) + if self.trace_type == TraceTaskName.WORKFLOW_TRACE: + return TraceTaskName.WORKFLOW_TRACE, self.workflow_trace(self.workflow_run, self.conversation_id) + elif self.trace_type == TraceTaskName.MESSAGE_TRACE: + return TraceTaskName.MESSAGE_TRACE, self.message_trace(self.message_id) + elif self.trace_type == TraceTaskName.MODERATION_TRACE: + return TraceTaskName.MODERATION_TRACE, self.moderation_trace(self.message_id, self.timer, **self.kwargs) + elif self.trace_type == TraceTaskName.SUGGESTED_QUESTION_TRACE: + return TraceTaskName.SUGGESTED_QUESTION_TRACE, self.suggested_question_trace( + self.message_id, self.timer, **self.kwargs + ) + elif self.trace_type == TraceTaskName.DATASET_RETRIEVAL_TRACE: + return TraceTaskName.DATASET_RETRIEVAL_TRACE, self.dataset_retrieval_trace( + self.message_id, self.timer, **self.kwargs + ) + elif self.trace_type == TraceTaskName.TOOL_TRACE: + return TraceTaskName.TOOL_TRACE, self.tool_trace(self.message_id, self.timer, **self.kwargs) + elif self.trace_type == TraceTaskName.GENERATE_NAME_TRACE: + return TraceTaskName.GENERATE_NAME_TRACE, self.generate_name_trace( + self.conversation_id, self.timer, **self.kwargs + ) + else: + return '', {} + + # process methods for different trace types + def conversation_trace(self, **kwargs): + return kwargs + + def workflow_trace(self, workflow_run: WorkflowRun, conversation_id): + workflow_id = workflow_run.workflow_id + tenant_id = workflow_run.tenant_id + workflow_run_id = workflow_run.id + workflow_run_elapsed_time = workflow_run.elapsed_time + workflow_run_status = workflow_run.status + workflow_run_inputs = ( + json.loads(workflow_run.inputs) if workflow_run.inputs else {} + ) + workflow_run_outputs = ( + json.loads(workflow_run.outputs) if workflow_run.outputs else {} + ) + workflow_run_version = workflow_run.version + error = workflow_run.error if workflow_run.error else "" + + total_tokens = workflow_run.total_tokens + + file_list = workflow_run_inputs.get("sys.file") if workflow_run_inputs.get("sys.file") else [] + query = workflow_run_inputs.get("query") or workflow_run_inputs.get("sys.query") or "" + + # get workflow_app_log_id + workflow_app_log_data = db.session.query(WorkflowAppLog).filter_by( + tenant_id=tenant_id, + app_id=workflow_run.app_id, + workflow_run_id=workflow_run.id + ).first() + workflow_app_log_id = str(workflow_app_log_data.id) if workflow_app_log_data else None + # get message_id + message_data = db.session.query(Message.id).filter_by( + conversation_id=conversation_id, + workflow_run_id=workflow_run_id + ).first() + message_id = str(message_data.id) if message_data else None + + metadata = { + "workflow_id": workflow_id, + "conversation_id": conversation_id, + "workflow_run_id": workflow_run_id, + "tenant_id": tenant_id, + "elapsed_time": workflow_run_elapsed_time, + "status": workflow_run_status, + "version": workflow_run_version, + "total_tokens": total_tokens, + "file_list": file_list, + "triggered_form": workflow_run.triggered_from, + } + + workflow_trace_info = WorkflowTraceInfo( + workflow_data=workflow_run.to_dict(), + conversation_id=conversation_id, + workflow_id=workflow_id, + tenant_id=tenant_id, + workflow_run_id=workflow_run_id, + workflow_run_elapsed_time=workflow_run_elapsed_time, + workflow_run_status=workflow_run_status, + workflow_run_inputs=workflow_run_inputs, + workflow_run_outputs=workflow_run_outputs, + workflow_run_version=workflow_run_version, + error=error, + total_tokens=total_tokens, + file_list=file_list, + query=query, + metadata=metadata, + workflow_app_log_id=workflow_app_log_id, + message_id=message_id, + start_time=workflow_run.created_at, + end_time=workflow_run.finished_at, + ) + + return workflow_trace_info + + def message_trace(self, message_id): + message_data = get_message_data(message_id) + if not message_data: + return {} + conversation_mode = db.session.query(Conversation.mode).filter_by(id=message_data.conversation_id).first() + conversation_mode = conversation_mode[0] + created_at = message_data.created_at + inputs = message_data.message + + # get message file data + message_file_data = db.session.query(MessageFile).filter_by(message_id=message_id).first() + file_list = [] + if message_file_data and message_file_data.url is not None: + file_url = f"{self.file_base_url}/{message_file_data.url}" if message_file_data else "" + file_list.append(file_url) + + metadata = { + "conversation_id": message_data.conversation_id, + "ls_provider": message_data.model_provider, + "ls_model_name": message_data.model_id, + "status": message_data.status, + "from_end_user_id": message_data.from_account_id, + "from_account_id": message_data.from_account_id, + "agent_based": message_data.agent_based, + "workflow_run_id": message_data.workflow_run_id, + "from_source": message_data.from_source, + "message_id": message_id, + } + + message_tokens = message_data.message_tokens + + message_trace_info = MessageTraceInfo( + message_id=message_id, + message_data=message_data.to_dict(), + conversation_model=conversation_mode, + message_tokens=message_tokens, + answer_tokens=message_data.answer_tokens, + total_tokens=message_tokens + message_data.answer_tokens, + error=message_data.error if message_data.error else "", + inputs=inputs, + outputs=message_data.answer, + file_list=file_list, + start_time=created_at, + end_time=created_at + timedelta(seconds=message_data.provider_response_latency), + metadata=metadata, + message_file_data=message_file_data, + conversation_mode=conversation_mode, + ) + + return message_trace_info + + def moderation_trace(self, message_id, timer, **kwargs): + moderation_result = kwargs.get("moderation_result") + inputs = kwargs.get("inputs") + message_data = get_message_data(message_id) + if not message_data: + return {} + metadata = { + "message_id": message_id, + "action": moderation_result.action, + "preset_response": moderation_result.preset_response, + "query": moderation_result.query, + } + + # get workflow_app_log_id + workflow_app_log_id = None + if message_data.workflow_run_id: + workflow_app_log_data = db.session.query(WorkflowAppLog).filter_by( + workflow_run_id=message_data.workflow_run_id + ).first() + workflow_app_log_id = str(workflow_app_log_data.id) if workflow_app_log_data else None + + moderation_trace_info = ModerationTraceInfo( + message_id=workflow_app_log_id if workflow_app_log_id else message_id, + inputs=inputs, + message_data=message_data.to_dict(), + flagged=moderation_result.flagged, + action=moderation_result.action, + preset_response=moderation_result.preset_response, + query=moderation_result.query, + start_time=timer.get("start"), + end_time=timer.get("end"), + metadata=metadata, + ) + + return moderation_trace_info + + def suggested_question_trace(self, message_id, timer, **kwargs): + suggested_question = kwargs.get("suggested_question") + message_data = get_message_data(message_id) + if not message_data: + return {} + metadata = { + "message_id": message_id, + "ls_provider": message_data.model_provider, + "ls_model_name": message_data.model_id, + "status": message_data.status, + "from_end_user_id": message_data.from_account_id, + "from_account_id": message_data.from_account_id, + "agent_based": message_data.agent_based, + "workflow_run_id": message_data.workflow_run_id, + "from_source": message_data.from_source, + } + + # get workflow_app_log_id + workflow_app_log_id = None + if message_data.workflow_run_id: + workflow_app_log_data = db.session.query(WorkflowAppLog).filter_by( + workflow_run_id=message_data.workflow_run_id + ).first() + workflow_app_log_id = str(workflow_app_log_data.id) if workflow_app_log_data else None + + suggested_question_trace_info = SuggestedQuestionTraceInfo( + message_id=workflow_app_log_id if workflow_app_log_id else message_id, + message_data=message_data.to_dict(), + inputs=message_data.message, + outputs=message_data.answer, + start_time=timer.get("start"), + end_time=timer.get("end"), + metadata=metadata, + total_tokens=message_data.message_tokens + message_data.answer_tokens, + status=message_data.status, + error=message_data.error, + from_account_id=message_data.from_account_id, + agent_based=message_data.agent_based, + from_source=message_data.from_source, + model_provider=message_data.model_provider, + model_id=message_data.model_id, + suggested_question=suggested_question, + level=message_data.status, + status_message=message_data.error, + ) + + return suggested_question_trace_info + + def dataset_retrieval_trace(self, message_id, timer, **kwargs): + documents = kwargs.get("documents") + message_data = get_message_data(message_id) + if not message_data: + return {} + + metadata = { + "message_id": message_id, + "ls_provider": message_data.model_provider, + "ls_model_name": message_data.model_id, + "status": message_data.status, + "from_end_user_id": message_data.from_account_id, + "from_account_id": message_data.from_account_id, + "agent_based": message_data.agent_based, + "workflow_run_id": message_data.workflow_run_id, + "from_source": message_data.from_source, + } + + dataset_retrieval_trace_info = DatasetRetrievalTraceInfo( + message_id=message_id, + inputs=message_data.query if message_data.query else message_data.inputs, + documents=[doc.model_dump() for doc in documents], + start_time=timer.get("start"), + end_time=timer.get("end"), + metadata=metadata, + message_data=message_data.to_dict(), + ) + + return dataset_retrieval_trace_info + + def tool_trace(self, message_id, timer, **kwargs): + tool_name = kwargs.get('tool_name') + tool_inputs = kwargs.get('tool_inputs') + tool_outputs = kwargs.get('tool_outputs') + message_data = get_message_data(message_id) + if not message_data: + return {} + tool_config = {} + time_cost = 0 + error = None + tool_parameters = {} + created_time = message_data.created_at + end_time = message_data.updated_at + agent_thoughts: list[MessageAgentThought] = message_data.agent_thoughts + for agent_thought in agent_thoughts: + if tool_name in agent_thought.tools: + created_time = agent_thought.created_at + tool_meta_data = agent_thought.tool_meta.get(tool_name, {}) + tool_config = tool_meta_data.get('tool_config', {}) + time_cost = tool_meta_data.get('time_cost', 0) + end_time = created_time + timedelta(seconds=time_cost) + error = tool_meta_data.get('error', "") + tool_parameters = tool_meta_data.get('tool_parameters', {}) + metadata = { + "message_id": message_id, + "tool_name": tool_name, + "tool_inputs": tool_inputs, + "tool_outputs": tool_outputs, + "tool_config": tool_config, + "time_cost": time_cost, + "error": error, + "tool_parameters": tool_parameters, + } + + file_url = "" + message_file_data = db.session.query(MessageFile).filter_by(message_id=message_id).first() + if message_file_data: + message_file_id = message_file_data.id if message_file_data else None + type = message_file_data.type + created_by_role = message_file_data.created_by_role + created_user_id = message_file_data.created_by + file_url = f"{self.file_base_url}/{message_file_data.url}" + + metadata.update( + { + "message_file_id": message_file_id, + "created_by_role": created_by_role, + "created_user_id": created_user_id, + "type": type, + } + ) + + tool_trace_info = ToolTraceInfo( + message_id=message_id, + message_data=message_data.to_dict(), + tool_name=tool_name, + start_time=timer.get("start") if timer else created_time, + end_time=timer.get("end") if timer else end_time, + tool_inputs=tool_inputs, + tool_outputs=tool_outputs, + metadata=metadata, + message_file_data=message_file_data, + error=error, + inputs=message_data.message, + outputs=message_data.answer, + tool_config=tool_config, + time_cost=time_cost, + tool_parameters=tool_parameters, + file_url=file_url, + ) + + return tool_trace_info + + def generate_name_trace(self, conversation_id, timer, **kwargs): + generate_conversation_name = kwargs.get("generate_conversation_name") + inputs = kwargs.get("inputs") + tenant_id = kwargs.get("tenant_id") + start_time = timer.get("start") + end_time = timer.get("end") + + metadata = { + "conversation_id": conversation_id, + "tenant_id": tenant_id, + } + + generate_name_trace_info = GenerateNameTraceInfo( + conversation_id=conversation_id, + inputs=inputs, + outputs=generate_conversation_name, + start_time=start_time, + end_time=end_time, + metadata=metadata, + tenant_id=tenant_id, + ) + + return generate_name_trace_info + + +trace_manager_timer = None +trace_manager_queue = queue.Queue() +trace_manager_interval = int(os.getenv("TRACE_QUEUE_MANAGER_INTERVAL", 5)) +trace_manager_batch_size = int(os.getenv("TRACE_QUEUE_MANAGER_BATCH_SIZE", 100)) + + +class TraceQueueManager: + def __init__(self, app_id=None, conversation_id=None, message_id=None): + global trace_manager_timer + + self.app_id = app_id + self.conversation_id = conversation_id + self.message_id = message_id + self.trace_instance = OpsTraceManager.get_ops_trace_instance(app_id, conversation_id, message_id) + self.flask_app = current_app._get_current_object() + if trace_manager_timer is None: + self.start_timer() + + def add_trace_task(self, trace_task: TraceTask): + global trace_manager_timer + global trace_manager_queue + try: + if self.trace_instance: + trace_manager_queue.put(trace_task) + except Exception as e: + logging.debug(f"Error adding trace task: {e}") + finally: + self.start_timer() + + def collect_tasks(self): + global trace_manager_queue + tasks = [] + while len(tasks) < trace_manager_batch_size and not trace_manager_queue.empty(): + task = trace_manager_queue.get_nowait() + tasks.append(task) + trace_manager_queue.task_done() + return tasks + + def run(self): + try: + tasks = self.collect_tasks() + if tasks: + self.send_to_celery(tasks) + except Exception as e: + logging.debug(f"Error processing trace tasks: {e}") + + def start_timer(self): + global trace_manager_timer + if trace_manager_timer is None or not trace_manager_timer.is_alive(): + trace_manager_timer = threading.Timer( + trace_manager_interval, self.run + ) + trace_manager_timer.name = f"trace_manager_timer_{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}" + trace_manager_timer.daemon = False + trace_manager_timer.start() + + def send_to_celery(self, tasks: list[TraceTask]): + with self.flask_app.app_context(): + for task in tasks: + trace_info = task.execute() + task_data = { + "app_id": self.app_id, + "conversation_id": self.conversation_id, + "message_id": self.message_id, + "trace_info_type": type(trace_info).__name__, + "trace_info": trace_info.model_dump() if trace_info else {}, + } + process_trace_tasks.delay(task_data) diff --git a/api/core/ops/utils.py b/api/core/ops/utils.py new file mode 100644 index 0000000000..2b12db0f48 --- /dev/null +++ b/api/core/ops/utils.py @@ -0,0 +1,43 @@ +from contextlib import contextmanager +from datetime import datetime + +from extensions.ext_database import db +from models.model import Message + + +def filter_none_values(data: dict): + for key, value in data.items(): + if value is None: + continue + if isinstance(value, datetime): + data[key] = value.isoformat() + return {key: value for key, value in data.items() if value is not None} + + +def get_message_data(message_id): + return db.session.query(Message).filter(Message.id == message_id).first() + + +@contextmanager +def measure_time(): + timing_info = {'start': datetime.now(), 'end': None} + try: + yield timing_info + finally: + timing_info['end'] = datetime.now() + print(f"Execution time: {timing_info['end'] - timing_info['start']}") + + +def replace_text_with_content(data): + if isinstance(data, dict): + new_data = {} + for key, value in data.items(): + if key == 'text': + new_data['content'] = value + else: + new_data[key] = replace_text_with_content(value) + return new_data + elif isinstance(data, list): + return [replace_text_with_content(item) for item in data] + else: + return data diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py index 9b0c96b8bf..452b270348 100644 --- a/api/core/prompt/simple_prompt_transform.py +++ b/api/core/prompt/simple_prompt_transform.py @@ -148,7 +148,7 @@ class SimplePromptTransform(PromptTransform): special_variable_keys.append('#histories#') if query_in_prompt: - prompt += prompt_rules['query_prompt'] if 'query_prompt' in prompt_rules else '{{#query#}}' + prompt += prompt_rules.get('query_prompt', '{{#query#}}') special_variable_keys.append('#query#') return { @@ -234,8 +234,8 @@ class SimplePromptTransform(PromptTransform): ) ), max_token_limit=rest_tokens, - human_prefix=prompt_rules['human_prefix'] if 'human_prefix' in prompt_rules else 'Human', - ai_prefix=prompt_rules['assistant_prefix'] if 'assistant_prefix' in prompt_rules else 'Assistant' + human_prefix=prompt_rules.get('human_prefix', 'Human'), + ai_prefix=prompt_rules.get('assistant_prefix', 'Assistant') ) # get prompt diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index c9447a79df..c0b3746e18 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -417,7 +417,7 @@ class ProviderManager: model_load_balancing_enabled = cache_result == 'True' if not model_load_balancing_enabled: - return dict() + return {} provider_load_balancing_configs = db.session.query(LoadBalancingModelConfig) \ .filter( @@ -451,7 +451,7 @@ class ProviderManager: if not provider_records: provider_records = [] - provider_quota_to_provider_record_dict = dict() + provider_quota_to_provider_record_dict = {} for provider_record in provider_records: if provider_record.provider_type != ProviderType.SYSTEM.value: continue @@ -661,7 +661,7 @@ class ProviderManager: provider_hosting_configuration = hosting_configuration.provider_map.get(provider_entity.provider) # Convert provider_records to dict - quota_type_to_provider_records_dict = dict() + quota_type_to_provider_records_dict = {} for provider_record in provider_records: if provider_record.provider_type != ProviderType.SYSTEM.value: continue diff --git a/api/core/rag/datasource/keyword/jieba/jieba.py b/api/core/rag/datasource/keyword/jieba/jieba.py index 0f4cbccff7..7f7c46e2dd 100644 --- a/api/core/rag/datasource/keyword/jieba/jieba.py +++ b/api/core/rag/datasource/keyword/jieba/jieba.py @@ -70,22 +70,6 @@ class Jieba(BaseKeyword): self._save_dataset_keyword_table(keyword_table) - def delete_by_document_id(self, document_id: str): - lock_name = 'keyword_indexing_lock_{}'.format(self.dataset.id) - with redis_client.lock(lock_name, timeout=600): - # get segment ids by document_id - segments = db.session.query(DocumentSegment).filter( - DocumentSegment.dataset_id == self.dataset.id, - DocumentSegment.document_id == document_id - ).all() - - ids = [segment.index_node_id for segment in segments] - - keyword_table = self._get_dataset_keyword_table() - keyword_table = self._delete_ids_from_keyword_table(keyword_table, ids) - - self._save_dataset_keyword_table(keyword_table) - def search( self, query: str, **kwargs: Any @@ -104,6 +88,7 @@ class Jieba(BaseKeyword): ).first() if segment: + documents.append(Document( page_content=segment.content, metadata={ @@ -212,7 +197,7 @@ class Jieba(BaseKeyword): chunk_indices_count[node_id] += 1 sorted_chunk_indices = sorted( - list(chunk_indices_count.keys()), + chunk_indices_count.keys(), key=lambda x: chunk_indices_count[x], reverse=True, ) diff --git a/api/core/rag/datasource/keyword/keyword_base.py b/api/core/rag/datasource/keyword/keyword_base.py index 84a5800855..02838cb1bd 100644 --- a/api/core/rag/datasource/keyword/keyword_base.py +++ b/api/core/rag/datasource/keyword/keyword_base.py @@ -28,10 +28,6 @@ class BaseKeyword(ABC): def delete_by_ids(self, ids: list[str]) -> None: raise NotImplementedError - @abstractmethod - def delete_by_document_id(self, document_id: str) -> None: - raise NotImplementedError - def delete(self) -> None: raise NotImplementedError diff --git a/api/core/rag/datasource/keyword/keyword_factory.py b/api/core/rag/datasource/keyword/keyword_factory.py index f5e2bf0f83..beb3322aa6 100644 --- a/api/core/rag/datasource/keyword/keyword_factory.py +++ b/api/core/rag/datasource/keyword/keyword_factory.py @@ -39,9 +39,6 @@ class Keyword: def delete_by_ids(self, ids: list[str]) -> None: self._keyword_processor.delete_by_ids(ids) - def delete_by_document_id(self, document_id: str) -> None: - self._keyword_processor.delete_by_document_id(document_id) - def delete(self) -> None: self._keyword_processor.delete() diff --git a/api/core/rag/datasource/vdb/milvus/milvus_vector.py b/api/core/rag/datasource/vdb/milvus/milvus_vector.py index 665a697e1a..02b715d768 100644 --- a/api/core/rag/datasource/vdb/milvus/milvus_vector.py +++ b/api/core/rag/datasource/vdb/milvus/milvus_vector.py @@ -100,12 +100,6 @@ class MilvusVector(BaseVector): raise e return pks - def delete_by_document_id(self, document_id: str): - - ids = self.get_ids_by_metadata_field('document_id', document_id) - if ids: - self._client.delete(collection_name=self._collection_name, pks=ids) - def get_ids_by_metadata_field(self, key: str, value: str): result = self._client.query(collection_name=self._collection_name, filter=f'metadata["{key}"] == "{value}"', diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py index 52f8b41bae..744ff2d517 100644 --- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py +++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py @@ -87,11 +87,6 @@ class OpenSearchVector(BaseVector): helpers.bulk(self._client, actions) - def delete_by_document_id(self, document_id: str): - ids = self.get_ids_by_metadata_field('document_id', document_id) - if ids: - self.delete_by_ids(ids) - def get_ids_by_metadata_field(self, key: str, value: str): query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}": value}}} response = self._client.search(index=self._collection_name.lower(), body=query) diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py index c087ed0cd8..5f7723508c 100644 --- a/api/core/rag/datasource/vdb/oracle/oraclevector.py +++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py @@ -156,13 +156,6 @@ class OracleVector(BaseVector): # idss.append(record[0]) # return idss - #def delete_by_document_id(self, document_id: str): - # ids = self.get_ids_by_metadata_field('doc_id', document_id) - # if len(ids)>0: - # with self._get_cursor() as cur: - # cur.execute(f"delete FROM {self.table_name} d WHERE d.meta.doc_id in '%s'" % ("','".join(ids),)) - - def delete_by_ids(self, ids: list[str]) -> None: with self._get_cursor() as cur: cur.execute(f"DELETE FROM {self.table_name} WHERE id IN %s" % (tuple(ids),)) diff --git a/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py b/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py index 61cac4f3a3..63c8edfbc3 100644 --- a/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py +++ b/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py @@ -130,14 +130,6 @@ class PGVectoRS(BaseVector): return pks - def delete_by_document_id(self, document_id: str): - ids = self.get_ids_by_metadata_field('document_id', document_id) - if ids: - with Session(self._client) as session: - select_statement = sql_text(f"DELETE FROM {self._collection_name} WHERE id = ANY(:ids)") - session.execute(select_statement, {'ids': ids}) - session.commit() - def get_ids_by_metadata_field(self, key: str, value: str): result = None with Session(self._client) as session: diff --git a/api/core/rag/datasource/vdb/relyt/relyt_vector.py b/api/core/rag/datasource/vdb/relyt/relyt_vector.py index d2b32324a1..4fe1df717a 100644 --- a/api/core/rag/datasource/vdb/relyt/relyt_vector.py +++ b/api/core/rag/datasource/vdb/relyt/relyt_vector.py @@ -151,11 +151,6 @@ class RelytVector(BaseVector): return ids - def delete_by_document_id(self, document_id: str): - ids = self.get_ids_by_metadata_field('document_id', document_id) - if ids: - self.delete_by_uuids(ids) - def get_ids_by_metadata_field(self, key: str, value: str): result = None with Session(self.client) as session: diff --git a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py index 3a9a56f93a..5922db1176 100644 --- a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py +++ b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py @@ -26,6 +26,7 @@ class TiDBVectorConfig(BaseModel): user: str password: str database: str + program_name: str @model_validator(mode='before') def validate_config(cls, values: dict) -> dict: @@ -39,6 +40,8 @@ class TiDBVectorConfig(BaseModel): raise ValueError("config TIDB_VECTOR_PASSWORD is required") if not values['database']: raise ValueError("config TIDB_VECTOR_DATABASE is required") + if not values['program_name']: + raise ValueError("config APPLICATION_NAME is required") return values @@ -65,7 +68,7 @@ class TiDBVector(BaseVector): super().__init__(collection_name) self._client_config = config self._url = (f"mysql+pymysql://{config.user}:{config.password}@{config.host}:{config.port}/{config.database}?" - f"ssl_verify_cert=true&ssl_verify_identity=true") + f"ssl_verify_cert=true&ssl_verify_identity=true&program_name={config.program_name}") self._distance_func = distance_func.lower() self._engine = create_engine(self._url) self._orm_base = declarative_base() @@ -158,11 +161,6 @@ class TiDBVector(BaseVector): print("Delete operation failed:", str(e)) return False - def delete_by_document_id(self, document_id: str): - ids = self.get_ids_by_metadata_field('document_id', document_id) - if ids: - self._delete_by_ids(ids) - def get_ids_by_metadata_field(self, key: str, value: str): with Session(self._engine) as session: select_statement = sql_text( @@ -245,5 +243,6 @@ class TiDBVectorFactory(AbstractVectorFactory): user=config.get('TIDB_VECTOR_USER'), password=config.get('TIDB_VECTOR_PASSWORD'), database=config.get('TIDB_VECTOR_DATABASE'), + program_name=config.get('APPLICATION_NAME'), ), ) \ No newline at end of file diff --git a/api/core/rag/datasource/vdb/vector_base.py b/api/core/rag/datasource/vdb/vector_base.py index 9b414e4e12..dbd8b6284b 100644 --- a/api/core/rag/datasource/vdb/vector_base.py +++ b/api/core/rag/datasource/vdb/vector_base.py @@ -31,9 +31,6 @@ class BaseVector(ABC): def delete_by_ids(self, ids: list[str]) -> None: raise NotImplementedError - def delete_by_document_id(self, document_id: str): - raise NotImplementedError - def get_ids_by_metadata_field(self, key: str, value: str): raise NotImplementedError diff --git a/api/core/rag/extractor/excel_extractor.py b/api/core/rag/extractor/excel_extractor.py index 931297c95e..2b16275dc8 100644 --- a/api/core/rag/extractor/excel_extractor.py +++ b/api/core/rag/extractor/excel_extractor.py @@ -1,4 +1,5 @@ """Abstract interface for document loader implementations.""" +import os from typing import Optional import pandas as pd @@ -29,8 +30,15 @@ class ExcelExtractor(BaseExtractor): def extract(self) -> list[Document]: """ Load from Excel file in xls or xlsx format using Pandas.""" documents = [] + # Determine the file extension + file_extension = os.path.splitext(self._file_path)[-1].lower() # Read each worksheet of an Excel file using Pandas - excel_file = pd.ExcelFile(self._file_path) + if file_extension == '.xlsx': + excel_file = pd.ExcelFile(self._file_path, engine='openpyxl') + elif file_extension == '.xls': + excel_file = pd.ExcelFile(self._file_path, engine='xlrd') + else: + raise ValueError(f"Unsupported file extension: {file_extension}") for sheet_name in excel_file.sheet_names: df: pd.DataFrame = excel_file.parse(sheet_name=sheet_name) diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index 4ec0b4fc38..7c6101010e 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -140,11 +140,10 @@ class NotionExtractor(BaseExtractor): def _get_notion_block_data(self, page_id: str) -> list[str]: result_lines_arr = [] - cur_block_id = page_id + start_cursor = None + block_url = BLOCK_CHILD_URL_TMPL.format(block_id=page_id) while True: - block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id) - query_dict: dict[str, Any] = {} - + query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor} res = requests.request( "GET", block_url, @@ -153,7 +152,7 @@ class NotionExtractor(BaseExtractor): "Content-Type": "application/json", "Notion-Version": "2022-06-28", }, - json=query_dict + params=query_dict ) data = res.json() for result in data["results"]: @@ -191,16 +190,16 @@ class NotionExtractor(BaseExtractor): if data["next_cursor"] is None: break else: - cur_block_id = data["next_cursor"] + start_cursor = data["next_cursor"] return result_lines_arr def _read_block(self, block_id: str, num_tabs: int = 0) -> str: """Read a block.""" result_lines_arr = [] - cur_block_id = block_id + start_cursor = None + block_url = BLOCK_CHILD_URL_TMPL.format(block_id=block_id) while True: - block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id) - query_dict: dict[str, Any] = {} + query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor} res = requests.request( "GET", @@ -210,7 +209,7 @@ class NotionExtractor(BaseExtractor): "Content-Type": "application/json", "Notion-Version": "2022-06-28", }, - json=query_dict + params=query_dict ) data = res.json() if 'results' not in data or data["results"] is None: @@ -249,7 +248,7 @@ class NotionExtractor(BaseExtractor): if data["next_cursor"] is None: break else: - cur_block_id = data["next_cursor"] + start_cursor = data["next_cursor"] result_lines = "\n".join(result_lines_arr) return result_lines @@ -258,10 +257,10 @@ class NotionExtractor(BaseExtractor): """Read table rows.""" done = False result_lines_arr = [] - cur_block_id = block_id + start_cursor = None + block_url = BLOCK_CHILD_URL_TMPL.format(block_id=block_id) while not done: - block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id) - query_dict: dict[str, Any] = {} + query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor} res = requests.request( "GET", @@ -271,7 +270,7 @@ class NotionExtractor(BaseExtractor): "Content-Type": "application/json", "Notion-Version": "2022-06-28", }, - json=query_dict + params=query_dict ) data = res.json() # get table headers text @@ -300,7 +299,7 @@ class NotionExtractor(BaseExtractor): done = True break else: - cur_block_id = data["next_cursor"] + start_cursor = data["next_cursor"] result_lines = "\n".join(result_lines_arr) return result_lines diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 3f50427141..ea2a194a68 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -12,6 +12,8 @@ from core.model_manager import ModelInstance, ModelManager from core.model_runtime.entities.message_entities import PromptMessageTool from core.model_runtime.entities.model_entities import ModelFeature, ModelType from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName +from core.ops.utils import measure_time from core.rag.datasource.retrieval_service import RetrievalService from core.rag.models.document import Document from core.rag.rerank.rerank import RerankRunner @@ -38,14 +40,20 @@ default_retrieval_model = { class DatasetRetrieval: - def retrieve(self, app_id: str, user_id: str, tenant_id: str, - model_config: ModelConfigWithCredentialsEntity, - config: DatasetEntity, - query: str, - invoke_from: InvokeFrom, - show_retrieve_source: bool, - hit_callback: DatasetIndexToolCallbackHandler, - memory: Optional[TokenBufferMemory] = None) -> Optional[str]: + def __init__(self, application_generate_entity=None): + self.application_generate_entity = application_generate_entity + + def retrieve( + self, app_id: str, user_id: str, tenant_id: str, + model_config: ModelConfigWithCredentialsEntity, + config: DatasetEntity, + query: str, + invoke_from: InvokeFrom, + show_retrieve_source: bool, + hit_callback: DatasetIndexToolCallbackHandler, + message_id: str, + memory: Optional[TokenBufferMemory] = None, + ) -> Optional[str]: """ Retrieve dataset. :param app_id: app_id @@ -57,6 +65,7 @@ class DatasetRetrieval: :param invoke_from: invoke from :param show_retrieve_source: show retrieve source :param hit_callback: hit callback + :param message_id: message id :param memory: memory :return: """ @@ -113,15 +122,20 @@ class DatasetRetrieval: all_documents = [] user_from = 'account' if invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] else 'end_user' if retrieve_config.retrieve_strategy == DatasetRetrieveConfigEntity.RetrieveStrategy.SINGLE: - all_documents = self.single_retrieve(app_id, tenant_id, user_id, user_from, available_datasets, query, - model_instance, - model_config, planning_strategy) + all_documents = self.single_retrieve( + app_id, tenant_id, user_id, user_from, available_datasets, query, + model_instance, + model_config, planning_strategy, message_id + ) elif retrieve_config.retrieve_strategy == DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE: - all_documents = self.multiple_retrieve(app_id, tenant_id, user_id, user_from, - available_datasets, query, retrieve_config.top_k, - retrieve_config.score_threshold, - retrieve_config.reranking_model.get('reranking_provider_name'), - retrieve_config.reranking_model.get('reranking_model_name')) + all_documents = self.multiple_retrieve( + app_id, tenant_id, user_id, user_from, + available_datasets, query, retrieve_config.top_k, + retrieve_config.score_threshold, + retrieve_config.reranking_model.get('reranking_provider_name'), + retrieve_config.reranking_model.get('reranking_model_name'), + message_id, + ) document_score_list = {} for item in all_documents: @@ -189,16 +203,18 @@ class DatasetRetrieval: return str("\n".join(document_context_list)) return '' - def single_retrieve(self, app_id: str, - tenant_id: str, - user_id: str, - user_from: str, - available_datasets: list, - query: str, - model_instance: ModelInstance, - model_config: ModelConfigWithCredentialsEntity, - planning_strategy: PlanningStrategy, - ): + def single_retrieve( + self, app_id: str, + tenant_id: str, + user_id: str, + user_from: str, + available_datasets: list, + query: str, + model_instance: ModelInstance, + model_config: ModelConfigWithCredentialsEntity, + planning_strategy: PlanningStrategy, + message_id: Optional[str] = None, + ): tools = [] for dataset in available_datasets: description = dataset.description @@ -251,27 +267,35 @@ class DatasetRetrieval: if score_threshold_enabled: score_threshold = retrieval_model_config.get("score_threshold") - results = RetrievalService.retrieve(retrival_method=retrival_method, dataset_id=dataset.id, - query=query, - top_k=top_k, score_threshold=score_threshold, - reranking_model=reranking_model) + with measure_time() as timer: + results = RetrievalService.retrieve( + retrival_method=retrival_method, dataset_id=dataset.id, + query=query, + top_k=top_k, score_threshold=score_threshold, + reranking_model=reranking_model + ) self._on_query(query, [dataset_id], app_id, user_from, user_id) + if results: - self._on_retrival_end(results) + self._on_retrival_end(results, message_id, timer) + return results return [] - def multiple_retrieve(self, - app_id: str, - tenant_id: str, - user_id: str, - user_from: str, - available_datasets: list, - query: str, - top_k: int, - score_threshold: float, - reranking_provider_name: str, - reranking_model_name: str): + def multiple_retrieve( + self, + app_id: str, + tenant_id: str, + user_id: str, + user_from: str, + available_datasets: list, + query: str, + top_k: int, + score_threshold: float, + reranking_provider_name: str, + reranking_model_name: str, + message_id: Optional[str] = None, + ): threads = [] all_documents = [] dataset_ids = [dataset.id for dataset in available_datasets] @@ -297,15 +321,23 @@ class DatasetRetrieval: ) rerank_runner = RerankRunner(rerank_model_instance) - all_documents = rerank_runner.run(query, all_documents, - score_threshold, - top_k) + + with measure_time() as timer: + all_documents = rerank_runner.run( + query, all_documents, + score_threshold, + top_k + ) self._on_query(query, dataset_ids, app_id, user_from, user_id) + if all_documents: - self._on_retrival_end(all_documents) + self._on_retrival_end(all_documents, message_id, timer) + return all_documents - def _on_retrival_end(self, documents: list[Document]) -> None: + def _on_retrival_end( + self, documents: list[Document], message_id: Optional[str] = None, timer: Optional[dict] = None + ) -> None: """Handle retrival end.""" for document in documents: query = db.session.query(DocumentSegment).filter( @@ -324,6 +356,18 @@ class DatasetRetrieval: db.session.commit() + # get tracing instance + trace_manager: TraceQueueManager = self.application_generate_entity.trace_manager if self.application_generate_entity else None + if trace_manager: + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.DATASET_RETRIEVAL_TRACE, + message_id=message_id, + documents=documents, + timer=timer + ) + ) + def _on_query(self, query: str, dataset_ids: list[str], app_id: str, user_from: str, user_id: str) -> None: """ Handle query. 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 5de2a66e2d..92f24277c1 100644 --- a/api/core/rag/retrieval/router/multi_dataset_react_route.py +++ b/api/core/rag/retrieval/router/multi_dataset_react_route.py @@ -201,7 +201,7 @@ class ReactMultiDatasetRouter: tool_strings.append( f"{tool.name}: {tool.description}, args: {{'query': {{'title': 'Query', 'description': 'Query for the dataset to be used to retrieve the dataset.', 'type': 'string'}}}}") formatted_tools = "\n".join(tool_strings) - unique_tool_names = set(tool.name for tool in tools) + unique_tool_names = {tool.name for tool in tools} tool_names = ", ".join('"' + name + '"' for name in unique_tool_names) format_instructions = format_instructions.format(tool_names=tool_names) template = "\n\n".join([prefix, formatted_tools, format_instructions, suffix]) diff --git a/api/core/tools/docs/en_US/tool_scale_out.md b/api/core/tools/docs/en_US/tool_scale_out.md index a6aa3b669d..f75c91cad6 100644 --- a/api/core/tools/docs/en_US/tool_scale_out.md +++ b/api/core/tools/docs/en_US/tool_scale_out.md @@ -5,6 +5,7 @@ Here, we will use GoogleSearch as an example to demonstrate how to quickly integ ## 1. Prepare the Tool Provider yaml ### Introduction + This yaml declares a new tool provider, and includes information like the provider's name, icon, author, and other details that are fetched by the frontend for display. ### Example @@ -28,9 +29,11 @@ identity: # Basic information of the tool provider - search ``` - - The `identity` field is mandatory, it contains the basic information of the tool provider, including author, name, label, description, icon, etc. - - The icon needs to be placed in the `_assets` folder of the current module, you can refer to [here](../../provider/builtin/google/_assets/icon.svg). - - The `tags` field is optional, it is used to classify the provider, and the frontend can filter the provider according to the tag, for all tags, they have been listed below: + +- The `identity` field is mandatory, it contains the basic information of the tool provider, including author, name, label, description, icon, etc. + - The icon needs to be placed in the `_assets` folder of the current module, you can refer to [here](../../provider/builtin/google/_assets/icon.svg). + - The `tags` field is optional, it is used to classify the provider, and the frontend can filter the provider according to the tag, for all tags, they have been listed below: + ```python class ToolLabelEnum(Enum): SEARCH = 'search' @@ -56,6 +59,7 @@ identity: # Basic information of the tool provider Google, as a third-party tool, uses the API provided by SerpApi, which requires an API Key to use. This means that this tool needs a credential to use. For tools like `wikipedia`, there is no need to fill in the credential field, you can refer to [here](../../provider/builtin/wikipedia/wikipedia.yaml). After configuring the credential field, the effect is as follows: + ```yaml identity: author: Dify @@ -87,6 +91,7 @@ credentials_for_provider: # Credential field - `type`: Credential field type, currently can be either `secret-input`, `text-input`, or `select` , corresponding to password input box, text input box, and drop-down box, respectively. If set to `secret-input`, it will mask the input content on the frontend, and the backend will encrypt the input content. ## 3. Prepare Tool yaml + A provider can have multiple tools, each tool needs a yaml file to describe, this file contains the basic information, parameters, output, etc. of the tool. Still taking GoogleSearch as an example, we need to create a `tools` module under the `google` module, and create `tools/google_search.yaml`, the content is as follows. @@ -140,21 +145,22 @@ parameters: # Parameter list - The `identity` field is mandatory, it contains the basic information of the tool, including name, author, label, description, etc. - `parameters` Parameter list - - `name` Parameter name, unique, no duplication with other parameters - - `type` Parameter type, currently supports `string`, `number`, `boolean`, `select`, `secret-input` four types, corresponding to string, number, boolean, drop-down box, and encrypted input box, respectively. For sensitive information, we recommend using `secret-input` type - - `required` Required or not - - In `llm` mode, if the parameter is required, the Agent is required to infer this parameter - - In `form` mode, if the parameter is required, the user is required to fill in this parameter on the frontend before the conversation starts - - `options` Parameter options - - In `llm` mode, Dify will pass all options to LLM, LLM can infer based on these options - - In `form` mode, when `type` is `select`, the frontend will display these options - - `default` Default value - - `label` Parameter label, for frontend display - - `human_description` Introduction for frontend display, supports multiple languages - - `llm_description` Introduction passed to LLM, in order to make LLM better understand this parameter, we suggest to write as detailed information about this parameter as possible here, so that LLM can understand this parameter - - `form` Form type, currently supports `llm`, `form` two types, corresponding to Agent self-inference and frontend filling + - `name` Parameter name, unique, no duplication with other parameters + - `type` Parameter type, currently supports `string`, `number`, `boolean`, `select`, `secret-input` four types, corresponding to string, number, boolean, drop-down box, and encrypted input box, respectively. For sensitive information, we recommend using `secret-input` type + - `required` Required or not + - In `llm` mode, if the parameter is required, the Agent is required to infer this parameter + - In `form` mode, if the parameter is required, the user is required to fill in this parameter on the frontend before the conversation starts + - `options` Parameter options + - In `llm` mode, Dify will pass all options to LLM, LLM can infer based on these options + - In `form` mode, when `type` is `select`, the frontend will display these options + - `default` Default value + - `label` Parameter label, for frontend display + - `human_description` Introduction for frontend display, supports multiple languages + - `llm_description` Introduction passed to LLM, in order to make LLM better understand this parameter, we suggest to write as detailed information about this parameter as possible here, so that LLM can understand this parameter + - `form` Form type, currently supports `llm`, `form` two types, corresponding to Agent self-inference and frontend filling ## 4. Add Tool Logic + After completing the tool configuration, we can start writing the tool code that defines how it is invoked. Create `google_search.py` under the `google/tools` module, the content is as follows. @@ -176,7 +182,7 @@ class GoogleSearchTool(BuiltinTool): query = tool_parameters['query'] result_type = tool_parameters['result_type'] api_key = self.runtime.credentials['serpapi_api_key'] - # TODO: search with serpapi + # Search with serpapi result = SerpAPI(api_key).run(query, result_type=result_type) if result_type == 'text': @@ -185,12 +191,15 @@ class GoogleSearchTool(BuiltinTool): ``` ### Parameters + The overall logic of the tool is in the `_invoke` method, this method accepts two parameters: `user_id` and `tool_parameters`, which represent the user ID and tool parameters respectively ### Return Data + When the tool returns, you can choose to return one message or multiple messages, here we return one message, using `create_text_message` and `create_link_message` can create a text message or a link message. ## 5. Add Provider Code + Finally, we need to create a provider class under the provider module to implement the provider's credential verification logic. If the credential verification fails, it will throw a `ToolProviderCredentialValidationError` exception. Create `google.py` under the `google` module, the content is as follows. @@ -227,8 +236,9 @@ class GoogleProvider(BuiltinToolProviderController): ``` ## Completion + After the above steps are completed, we can see this tool on the frontend, and it can be used in the Agent. Of course, because google_search needs a credential, before using it, you also need to input your credentials on the frontend. -![Alt text](../zh_Hans/images/index/image-2.png) \ No newline at end of file +![Alt text](../zh_Hans/images/index/image-2.png) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 3115ca7622..d00e89d5cd 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -95,6 +95,7 @@ class ToolInvokeMessage(BaseModel): IMAGE = "image" LINK = "link" BLOB = "blob" + JSON = "json" IMAGE_LINK = "image_link" FILE_VAR = "file_var" @@ -102,7 +103,7 @@ class ToolInvokeMessage(BaseModel): """ plain text, image url or link url """ - message: Union[str, bytes] = None + message: Union[str, bytes, dict] = None meta: dict[str, Any] = None save_as: str = '' diff --git a/api/core/tools/provider/_position.yaml b/api/core/tools/provider/_position.yaml index 74940e819f..fa13629ef7 100644 --- a/api/core/tools/provider/_position.yaml +++ b/api/core/tools/provider/_position.yaml @@ -7,6 +7,7 @@ - azuredalle - stability - wikipedia +- nominatim - yahoo - arxiv - pubmed diff --git a/api/core/tools/provider/builtin/bing/tools/bing_web_search.py b/api/core/tools/provider/builtin/bing/tools/bing_web_search.py index 761aecde94..f85a5ed472 100644 --- a/api/core/tools/provider/builtin/bing/tools/bing_web_search.py +++ b/api/core/tools/provider/builtin/bing/tools/bing_web_search.py @@ -105,15 +105,15 @@ class BingSearchTool(BuiltinTool): def validate_credentials(self, credentials: dict[str, Any], tool_parameters: dict[str, Any]) -> None: - key = credentials.get('subscription_key', None) + key = credentials.get('subscription_key') if not key: raise Exception('subscription_key is required') - server_url = credentials.get('server_url', None) + server_url = credentials.get('server_url') if not server_url: server_url = self.url - query = tool_parameters.get('query', None) + query = tool_parameters.get('query') if not query: raise Exception('query is required') @@ -170,7 +170,7 @@ class BingSearchTool(BuiltinTool): if not server_url: server_url = self.url - query = tool_parameters.get('query', None) + query = tool_parameters.get('query') if not query: raise Exception('query is required') diff --git a/api/core/tools/provider/builtin/chart/tools/bar.py b/api/core/tools/provider/builtin/chart/tools/bar.py index 7da2651099..749ec761c6 100644 --- a/api/core/tools/provider/builtin/chart/tools/bar.py +++ b/api/core/tools/provider/builtin/chart/tools/bar.py @@ -16,12 +16,12 @@ class BarChartTool(BuiltinTool): data = data.split(';') # if all data is int, convert to int - if all([i.isdigit() for i in data]): + if all(i.isdigit() for i in data): data = [int(i) for i in data] else: data = [float(i) for i in data] - axis = tool_parameters.get('x_axis', None) or None + axis = tool_parameters.get('x_axis') or None if axis: axis = axis.split(';') if len(axis) != len(data): diff --git a/api/core/tools/provider/builtin/chart/tools/line.py b/api/core/tools/provider/builtin/chart/tools/line.py index 9bc36be857..608bd6623c 100644 --- a/api/core/tools/provider/builtin/chart/tools/line.py +++ b/api/core/tools/provider/builtin/chart/tools/line.py @@ -17,14 +17,14 @@ class LinearChartTool(BuiltinTool): return self.create_text_message('Please input data') data = data.split(';') - axis = tool_parameters.get('x_axis', None) or None + axis = tool_parameters.get('x_axis') or None if axis: axis = axis.split(';') if len(axis) != len(data): axis = None # if all data is int, convert to int - if all([i.isdigit() for i in data]): + if all(i.isdigit() for i in data): data = [int(i) for i in data] else: data = [float(i) for i in data] diff --git a/api/core/tools/provider/builtin/chart/tools/pie.py b/api/core/tools/provider/builtin/chart/tools/pie.py index cd5e9b5329..4c551229e9 100644 --- a/api/core/tools/provider/builtin/chart/tools/pie.py +++ b/api/core/tools/provider/builtin/chart/tools/pie.py @@ -16,10 +16,10 @@ class PieChartTool(BuiltinTool): if not data: return self.create_text_message('Please input data') data = data.split(';') - categories = tool_parameters.get('categories', None) or None + categories = tool_parameters.get('categories') or None # if all data is int, convert to int - if all([i.isdigit() for i in data]): + if all(i.isdigit() for i in data): data = [int(i) for i in data] else: data = [float(i) for i in data] diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml index 1ca16e660f..1913eed1d1 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml @@ -31,6 +31,12 @@ parameters: - value: claude-3-haiku label: en_US: Claude 3 + - value: llama-3-70b + label: + en_US: Llama 3 + - value: mixtral-8x7b + label: + en_US: Mixtral default: gpt-3.5 label: en_US: Choose Model diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py index 660611e8a6..ed873cdcf6 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py @@ -19,7 +19,11 @@ class DuckDuckGoImageSearchTool(BuiltinTool): "max_results": tool_parameters.get('max_results'), } response = DDGS().images(**query_dict) - results = [] + result = [] for res in response: - results.append(self.create_image_message(image=res.get("image"))) - return results + msg = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.IMAGE_LINK, + message=res.get('image'), + save_as='', + meta=res) + result.append(msg) + return result diff --git a/api/core/tools/provider/builtin/firecrawl/firecrawl.yaml b/api/core/tools/provider/builtin/firecrawl/firecrawl.yaml index 311283dcb5..613a0e4679 100644 --- a/api/core/tools/provider/builtin/firecrawl/firecrawl.yaml +++ b/api/core/tools/provider/builtin/firecrawl/firecrawl.yaml @@ -6,7 +6,7 @@ identity: zh_CN: Firecrawl description: en_US: Firecrawl API integration for web crawling and scraping. - zh_CN: Firecrawl API 集成,用于网页爬取和数据抓取。 + zh_Hans: Firecrawl API 集成,用于网页爬取和数据抓取。 icon: icon.svg tags: - search @@ -17,11 +17,22 @@ credentials_for_provider: required: true label: en_US: Firecrawl API Key - zh_CN: Firecrawl API 密钥 + zh_Hans: Firecrawl API 密钥 placeholder: en_US: Please input your Firecrawl API key - zh_CN: 请输入您的 Firecrawl API 密钥 + zh_Hans: 请输入您的 Firecrawl API 密钥,如果是自托管版本,可以随意填写密钥 help: - en_US: Get your Firecrawl API key from your Firecrawl account settings. - zh_CN: 从您的 Firecrawl 账户设置中获取 Firecrawl API 密钥。 + en_US: Get your Firecrawl API key from your Firecrawl account settings.If you are using a self-hosted version, you may enter any key at your convenience. + zh_Hans: 从您的 Firecrawl 账户设置中获取 Firecrawl API 密钥。如果是自托管版本,可以随意填写密钥。 url: https://www.firecrawl.dev/account + base_url: + type: text-input + required: false + label: + en_US: Firecrawl server's Base URL + zh_Hans: Firecrawl服务器的API URL + pt_BR: Firecrawl server's Base URL + placeholder: + en_US: https://www.firecrawl.dev + zh_HansL: https://www.firecrawl.dev + pt_BR: https://www.firecrawl.dev diff --git a/api/core/tools/provider/builtin/firecrawl/firecrawl_appx.py b/api/core/tools/provider/builtin/firecrawl/firecrawl_appx.py new file mode 100644 index 0000000000..23cb659652 --- /dev/null +++ b/api/core/tools/provider/builtin/firecrawl/firecrawl_appx.py @@ -0,0 +1,93 @@ +import time +from collections.abc import Mapping +from typing import Any + +import requests +from requests.exceptions import HTTPError + + +class FirecrawlApp: + def __init__(self, api_key: str | None = None, base_url: str | None = None): + self.api_key = api_key + self.base_url = base_url or 'https://api.firecrawl.dev' + if not self.api_key: + raise ValueError("API key is required") + + def _prepare_headers(self, idempotency_key: str | None = None): + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}' + } + if idempotency_key: + headers['Idempotency-Key'] = idempotency_key + return headers + + def _request( + self, + method: str, + url: str, + data: Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + retries: int = 3, + backoff_factor: float = 0.3, + ) -> Mapping[str, Any] | None: + for i in range(retries): + try: + response = requests.request(method, url, json=data, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + if i < retries - 1: + time.sleep(backoff_factor * (2 ** i)) + else: + raise + return None + + def scrape_url(self, url: str, **kwargs): + endpoint = f'{self.base_url}/v0/scrape' + headers = self._prepare_headers() + data = {'url': url, **kwargs} + response = self._request('POST', endpoint, data, headers) + if response is None: + raise HTTPError("Failed to scrape URL after multiple retries") + return response + + def search(self, query: str, **kwargs): + endpoint = f'{self.base_url}/v0/search' + headers = self._prepare_headers() + data = {'query': query, **kwargs} + response = self._request('POST', endpoint, data, headers) + if response is None: + raise HTTPError("Failed to perform search after multiple retries") + return response + + def crawl_url( + self, url: str, wait: bool = False, poll_interval: int = 5, idempotency_key: str | None = None, **kwargs + ): + endpoint = f'{self.base_url}/v0/crawl' + headers = self._prepare_headers(idempotency_key) + data = {'url': url, **kwargs} + response = self._request('POST', endpoint, data, headers) + if response is None: + raise HTTPError("Failed to initiate crawl after multiple retries") + job_id: str = response['jobId'] + if wait: + return self._monitor_job_status(job_id=job_id, poll_interval=poll_interval) + return job_id + + def check_crawl_status(self, job_id: str): + endpoint = f'{self.base_url}/v0/crawl/status/{job_id}' + headers = self._prepare_headers() + response = self._request('GET', endpoint, headers=headers) + if response is None: + raise HTTPError(f"Failed to check status for job {job_id} after multiple retries") + return response + + def _monitor_job_status(self, job_id: str, poll_interval: int): + while True: + status = self.check_crawl_status(job_id) + if status['status'] == 'completed': + return status + elif status['status'] == 'failed': + raise HTTPError(f'Job {job_id} failed: {status["error"]}') + time.sleep(poll_interval) diff --git a/api/core/tools/provider/builtin/firecrawl/tools/crawl.py b/api/core/tools/provider/builtin/firecrawl/tools/crawl.py index 1eaa5d8013..b000c1c6ce 100644 --- a/api/core/tools/provider/builtin/firecrawl/tools/crawl.py +++ b/api/core/tools/provider/builtin/firecrawl/tools/crawl.py @@ -1,15 +1,14 @@ +import json from typing import Any, Union -from firecrawl import FirecrawlApp - from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp from core.tools.tool.builtin_tool import BuiltinTool class CrawlTool(BuiltinTool): def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: - # initialize the app object with the api key - app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key']) + app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url']) options = { 'crawlerOptions': { @@ -22,29 +21,16 @@ class CrawlTool(BuiltinTool): } } - # crawl the url crawl_result = app.crawl_url( url=tool_parameters['url'], params=options, - wait_until_done=True, + wait=True ) - - # reformat crawl result - crawl_output = "**Crawl Result**\n\n" - try: - for result in crawl_result: - crawl_output += f"**- Title:** {result.get('metadata', {}).get('title', '')}\n" - crawl_output += f"**- Description:** {result.get('metadata', {}).get('description', '')}\n" - crawl_output += f"**- URL:** {result.get('metadata', {}).get('ogUrl', '')}\n\n" - crawl_output += f"**- Web Content:**\n{result.get('markdown', '')}\n\n" - crawl_output += "---\n\n" - except Exception as e: - crawl_output += f"An error occurred: {str(e)}\n" - crawl_output += f"**- Title:** {result.get('metadata', {}).get('title', '')}\n" - crawl_output += f"**- Description:** {result.get('metadata', {}).get('description','')}\n" - crawl_output += f"**- URL:** {result.get('metadata', {}).get('ogUrl', '')}\n\n" - crawl_output += f"**- Web Content:**\n{result.get('markdown', '')}\n\n" - crawl_output += "---\n\n" + if not isinstance(crawl_result, str): + crawl_result = json.dumps(crawl_result, ensure_ascii=False, indent=4) - return self.create_text_message(crawl_output) \ No newline at end of file + if not crawl_result: + return self.create_text_message("Crawl request failed.") + + return self.create_text_message(crawl_result) diff --git a/api/core/tools/provider/builtin/firecrawl/tools/scrape.py b/api/core/tools/provider/builtin/firecrawl/tools/scrape.py new file mode 100644 index 0000000000..3a78dce8d0 --- /dev/null +++ b/api/core/tools/provider/builtin/firecrawl/tools/scrape.py @@ -0,0 +1,26 @@ +import json +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp +from core.tools.tool.builtin_tool import BuiltinTool + + +class ScrapeTool(BuiltinTool): + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url']) + + crawl_result = app.scrape_url( + url=tool_parameters['url'], + wait=True + ) + + if isinstance(crawl_result, dict): + result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4) + else: + result_message = str(crawl_result) + + if not crawl_result: + return self.create_text_message("Scrape request failed.") + + return self.create_text_message(result_message) diff --git a/api/core/tools/provider/builtin/firecrawl/tools/scrape.yaml b/api/core/tools/provider/builtin/firecrawl/tools/scrape.yaml new file mode 100644 index 0000000000..29aa5991aa --- /dev/null +++ b/api/core/tools/provider/builtin/firecrawl/tools/scrape.yaml @@ -0,0 +1,23 @@ +identity: + name: scrape + author: ahasasjeb + label: + en_US: Scrape + zh_Hans: 抓取 +description: + human: + en_US: Extract data from a single URL. + zh_Hans: 从单个URL抓取数据。 + llm: This tool is designed to scrape URL and output the content in Markdown format. +parameters: + - name: url + type: string + required: true + label: + en_US: URL to scrape + zh_Hans: 要抓取的URL + human_description: + en_US: The URL of the website to scrape and extract data from. + zh_Hans: 要抓取并提取数据的网站URL。 + llm_description: The URL of the website that needs to be crawled. This is a required parameter. + form: llm diff --git a/api/core/tools/provider/builtin/firecrawl/tools/search.py b/api/core/tools/provider/builtin/firecrawl/tools/search.py new file mode 100644 index 0000000000..0b118aa5f1 --- /dev/null +++ b/api/core/tools/provider/builtin/firecrawl/tools/search.py @@ -0,0 +1,26 @@ +import json +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp +from core.tools.tool.builtin_tool import BuiltinTool + + +class SearchTool(BuiltinTool): + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url']) + + crawl_result = app.search( + query=tool_parameters['keyword'], + wait=True + ) + + if isinstance(crawl_result, dict): + result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4) + else: + result_message = str(crawl_result) + + if not crawl_result: + return self.create_text_message("Search request failed.") + + return self.create_text_message(result_message) diff --git a/api/core/tools/provider/builtin/firecrawl/tools/search.yaml b/api/core/tools/provider/builtin/firecrawl/tools/search.yaml new file mode 100644 index 0000000000..b1513c914e --- /dev/null +++ b/api/core/tools/provider/builtin/firecrawl/tools/search.yaml @@ -0,0 +1,23 @@ +identity: + name: search + author: ahasasjeb + label: + en_US: Search + zh_Hans: 搜索 +description: + human: + en_US: Search, and output in Markdown format + zh_Hans: 搜索,并且以Markdown格式输出 + llm: This tool can perform online searches and convert the results to Markdown format. +parameters: + - name: keyword + type: string + required: true + label: + en_US: keyword + zh_Hans: 关键词 + human_description: + en_US: Input keywords to use Firecrawl API for search. + zh_Hans: 输入关键词即可使用Firecrawl API进行搜索。 + llm_description: Efficiently extract keywords from user text. + form: llm diff --git a/api/core/tools/provider/builtin/gaode/tools/gaode_weather.py b/api/core/tools/provider/builtin/gaode/tools/gaode_weather.py index 028da946d1..efd11cedce 100644 --- a/api/core/tools/provider/builtin/gaode/tools/gaode_weather.py +++ b/api/core/tools/provider/builtin/gaode/tools/gaode_weather.py @@ -37,10 +37,10 @@ class GaodeRepositoriesTool(BuiltinTool): apikey=self.runtime.credentials.get('api_key'))) weatherInfo_data = weatherInfo_response.json() if weatherInfo_response.status_code == 200 and weatherInfo_data.get('info') == 'OK': - contents = list() + contents = [] if len(weatherInfo_data.get('forecasts')) > 0: for item in weatherInfo_data['forecasts'][0]['casts']: - content = dict() + content = {} content['date'] = item.get('date') content['week'] = item.get('week') content['dayweather'] = item.get('dayweather') diff --git a/api/core/tools/provider/builtin/github/tools/github_repositories.py b/api/core/tools/provider/builtin/github/tools/github_repositories.py index 8a006f885f..a2f1e07fd4 100644 --- a/api/core/tools/provider/builtin/github/tools/github_repositories.py +++ b/api/core/tools/provider/builtin/github/tools/github_repositories.py @@ -39,10 +39,10 @@ class GihubRepositoriesTool(BuiltinTool): f"q={quote(query)}&sort=stars&per_page={top_n}&order=desc") response_data = response.json() if response.status_code == 200 and isinstance(response_data.get('items'), list): - contents = list() + contents = [] if len(response_data.get('items')) > 0: for item in response_data.get('items'): - content = dict() + content = {} updated_at_object = datetime.strptime(item['updated_at'], "%Y-%m-%dT%H:%M:%SZ") content['owner'] = item['owner']['login'] content['name'] = item['name'] diff --git a/api/core/tools/provider/builtin/google/tools/google_search.py b/api/core/tools/provider/builtin/google/tools/google_search.py index 87c2cc5796..09d0326fb4 100644 --- a/api/core/tools/provider/builtin/google/tools/google_search.py +++ b/api/core/tools/provider/builtin/google/tools/google_search.py @@ -8,99 +8,36 @@ from core.tools.tool.builtin_tool import BuiltinTool SERP_API_URL = "https://serpapi.com/search" -class SerpAPI: - """ - SerpAPI tool provider. - """ - def __init__(self, api_key: str) -> None: - """Initialize SerpAPI tool provider.""" - self.serpapi_api_key = api_key +class GoogleSearchTool(BuiltinTool): - def run(self, query: str, **kwargs: Any) -> str: - """Run query through SerpAPI and parse result.""" - typ = kwargs.get("result_type", "text") - return self._process_response(self.results(query), typ=typ) - - def results(self, query: str) -> dict: - """Run query through SerpAPI and return the raw result.""" - params = self.get_params(query) - response = requests.get(url=SERP_API_URL, params=params) - response.raise_for_status() - return response.json() - - def get_params(self, query: str) -> dict[str, str]: - """Get parameters for SerpAPI.""" + def _parse_response(self, response: dict) -> dict: + result = {} + if "knowledge_graph" in response: + result["title"] = response["knowledge_graph"].get("title", "") + result["description"] = response["knowledge_graph"].get("description", "") + if "organic_results" in response: + result["organic_results"] = [ + { + "title": item.get("title", ""), + "link": item.get("link", ""), + "snippet": item.get("snippet", "") + } + for item in response["organic_results"] + ] + return result + def _invoke(self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: params = { - "api_key": self.serpapi_api_key, - "q": query, + "api_key": self.runtime.credentials['serpapi_api_key'], + "q": tool_parameters['query'], "engine": "google", "google_domain": "google.com", "gl": "us", "hl": "en" } - return params - - @staticmethod - def _process_response(res: dict, typ: str) -> str: - """ - Process response from SerpAPI. - SerpAPI doc: https://serpapi.com/search-api - Google search main results are called organic results - """ - if "error" in res: - raise ValueError(f"Got error from SerpAPI: {res['error']}") - toret = "" - if typ == "text": - if "knowledge_graph" in res and "description" in res["knowledge_graph"]: - toret += res["knowledge_graph"]["description"] + "\n" - if "organic_results" in res: - snippets = [ - f"content: {item.get('snippet')}\nlink: {item.get('link')}" - for item in res["organic_results"] - if "snippet" in item - ] - toret += "\n".join(snippets) - elif typ == "link": - if "knowledge_graph" in res and "source" in res["knowledge_graph"]: - toret += res["knowledge_graph"]["source"]["link"] - elif "organic_results" in res: - links = [ - f"[{item['title']}]({item['link']})\n" - for item in res["organic_results"] - if "title" in item and "link" in item - ] - toret += "\n".join(links) - elif "related_questions" in res: - questions = [ - f"[{item['question']}]({item['link']})\n" - for item in res["related_questions"] - if "question" in item and "link" in item - ] - toret += "\n".join(questions) - elif "related_searches" in res: - searches = [ - f"[{item['query']}]({item['link']})\n" - for item in res["related_searches"] - if "query" in item and "link" in item - ] - toret += "\n".join(searches) - if not toret: - toret = "No good search result found" - return toret - - -class GoogleSearchTool(BuiltinTool): - def _invoke(self, - user_id: str, - tool_parameters: dict[str, Any], - ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: - """ - invoke tools - """ - query = tool_parameters['query'] - result_type = tool_parameters['result_type'] - api_key = self.runtime.credentials['serpapi_api_key'] - result = SerpAPI(api_key).run(query, result_type=result_type) - if result_type == 'text': - return self.create_text_message(text=result) - return self.create_link_message(link=result) + response = requests.get(url=SERP_API_URL, params=params) + response.raise_for_status() + valuable_res = self._parse_response(response.json()) + return self.create_json_message(valuable_res) diff --git a/api/core/tools/provider/builtin/google/tools/google_search.yaml b/api/core/tools/provider/builtin/google/tools/google_search.yaml index 9dc5023992..72db3839eb 100644 --- a/api/core/tools/provider/builtin/google/tools/google_search.yaml +++ b/api/core/tools/provider/builtin/google/tools/google_search.yaml @@ -25,27 +25,3 @@ parameters: pt_BR: used for searching llm_description: key words for searching form: llm - - name: result_type - type: select - required: true - options: - - value: text - label: - en_US: text - zh_Hans: 文本 - pt_BR: texto - - value: link - label: - en_US: link - zh_Hans: 链接 - pt_BR: link - default: link - label: - en_US: Result type - zh_Hans: 结果类型 - pt_BR: Result type - human_description: - en_US: used for selecting the result type, text or link - zh_Hans: 用于选择结果类型,使用文本还是链接进行展示 - pt_BR: used for selecting the result type, text or link - form: form diff --git a/api/core/tools/provider/builtin/jina/tools/jina_reader.py b/api/core/tools/provider/builtin/jina/tools/jina_reader.py index b0bd478846..ac06688c18 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_reader.py +++ b/api/core/tools/provider/builtin/jina/tools/jina_reader.py @@ -10,10 +10,10 @@ from core.tools.tool.builtin_tool import BuiltinTool class JinaReaderTool(BuiltinTool): _jina_reader_endpoint = 'https://r.jina.ai/' - def _invoke(self, + def _invoke(self, user_id: str, - tool_parameters: dict[str, Any], - ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: """ invoke tools """ @@ -26,15 +26,24 @@ class JinaReaderTool(BuiltinTool): if 'api_key' in self.runtime.credentials and self.runtime.credentials.get('api_key'): headers['Authorization'] = "Bearer " + self.runtime.credentials.get('api_key') - target_selector = tool_parameters.get('target_selector', None) + target_selector = tool_parameters.get('target_selector') if target_selector is not None and target_selector != '': headers['X-Target-Selector'] = target_selector - wait_for_selector = tool_parameters.get('wait_for_selector', None) + wait_for_selector = tool_parameters.get('wait_for_selector') if wait_for_selector is not None and wait_for_selector != '': headers['X-Wait-For-Selector'] = wait_for_selector - proxy_server = tool_parameters.get('proxy_server', None) + if tool_parameters.get('image_caption', False): + headers['X-With-Generated-Alt'] = 'true' + + if tool_parameters.get('gather_all_links_at_the_end', False): + headers['X-With-Links-Summary'] = 'true' + + if tool_parameters.get('gather_all_images_at_the_end', False): + headers['X-With-Images-Summary'] = 'true' + + proxy_server = tool_parameters.get('proxy_server') if proxy_server is not None and proxy_server != '': headers['X-Proxy-Url'] = proxy_server @@ -42,12 +51,12 @@ class JinaReaderTool(BuiltinTool): headers['X-No-Cache'] = 'true' response = ssrf_proxy.get( - str(URL(self._jina_reader_endpoint + url)), + str(URL(self._jina_reader_endpoint + url)), headers=headers, timeout=(10, 60) ) if tool_parameters.get('summary', False): return self.create_text_message(self.summary(user_id, response.text)) - + return self.create_text_message(response.text) diff --git a/api/core/tools/provider/builtin/jina/tools/jina_reader.yaml b/api/core/tools/provider/builtin/jina/tools/jina_reader.yaml index 703fa3d389..5eb2692ea5 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_reader.yaml +++ b/api/core/tools/provider/builtin/jina/tools/jina_reader.yaml @@ -51,6 +51,48 @@ parameters: pt_BR: css selector for waiting for specific elements llm_description: css selector of the target element to wait for form: form + - name: image_caption + type: boolean + required: false + default: false + label: + en_US: Image caption + zh_Hans: 图片说明 + pt_BR: Legenda da imagem + human_description: + en_US: "Captions all images at the specified URL, adding 'Image [idx]: [caption]' as an alt tag for those without one. This allows downstream LLMs to interact with the images in activities such as reasoning and summarizing." + zh_Hans: "为指定 URL 上的所有图像添加标题,为没有标题的图像添加“Image [idx]: [caption]”作为 alt 标签。这允许下游 LLM 在推理和总结等活动中与图像进行交互。" + pt_BR: "Captions all images at the specified URL, adding 'Image [idx]: [caption]' as an alt tag for those without one. This allows downstream LLMs to interact with the images in activities such as reasoning and summarizing." + llm_description: Captions all images at the specified URL + form: form + - name: gather_all_links_at_the_end + type: boolean + required: false + default: false + label: + en_US: Gather all links at the end + zh_Hans: 将所有链接集中到最后 + pt_BR: Coletar todos os links ao final + human_description: + en_US: A "Buttons & Links" section will be created at the end. This helps the downstream LLMs or web agents navigating the page or take further actions. + zh_Hans: 最后会创建一个“按钮和链接”部分。这可以帮助下游 LLM 或 Web 代理浏览页面或采取进一步的行动。 + pt_BR: A "Buttons & Links" section will be created at the end. This helps the downstream LLMs or web agents navigating the page or take further actions. + llm_description: Gather all links at the end + form: form + - name: gather_all_images_at_the_end + type: boolean + required: false + default: false + label: + en_US: Gather all images at the end + zh_Hans: 将所有图片集中到最后 + pt_BR: Coletar todas as imagens ao final + human_description: + en_US: An "Images" section will be created at the end. This gives the downstream LLMs an overview of all visuals on the page, which may improve reasoning. + zh_Hans: 最后会创建一个“图像”部分。这可以让下游的 LLM 概览页面上的所有视觉效果,从而提高推理能力。 + pt_BR: An "Images" section will be created at the end. This gives the downstream LLMs an overview of all visuals on the page, which may improve reasoning. + llm_description: Gather all images at the end + form: form - name: proxy_server type: string required: false diff --git a/api/core/tools/provider/builtin/jina/tools/jina_search.py b/api/core/tools/provider/builtin/jina/tools/jina_search.py index c13f58d0cd..e6bc08147f 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_search.py +++ b/api/core/tools/provider/builtin/jina/tools/jina_search.py @@ -24,7 +24,16 @@ class JinaSearchTool(BuiltinTool): if 'api_key' in self.runtime.credentials and self.runtime.credentials.get('api_key'): headers['Authorization'] = "Bearer " + self.runtime.credentials.get('api_key') - proxy_server = tool_parameters.get('proxy_server', None) + if tool_parameters.get('image_caption', False): + headers['X-With-Generated-Alt'] = 'true' + + if tool_parameters.get('gather_all_links_at_the_end', False): + headers['X-With-Links-Summary'] = 'true' + + if tool_parameters.get('gather_all_images_at_the_end', False): + headers['X-With-Images-Summary'] = 'true' + + proxy_server = tool_parameters.get('proxy_server') if proxy_server is not None and proxy_server != '': headers['X-Proxy-Url'] = proxy_server diff --git a/api/core/tools/provider/builtin/jina/tools/jina_search.yaml b/api/core/tools/provider/builtin/jina/tools/jina_search.yaml index f3b6c0737a..da0a300c6c 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_search.yaml +++ b/api/core/tools/provider/builtin/jina/tools/jina_search.yaml @@ -22,6 +22,48 @@ parameters: zh_Hans: 在网络上搜索信息 llm_description: simple question to ask on the web form: llm + - name: image_caption + type: boolean + required: false + default: false + label: + en_US: Image caption + zh_Hans: 图片说明 + pt_BR: Legenda da imagem + human_description: + en_US: "Captions all images at the specified URL, adding 'Image [idx]: [caption]' as an alt tag for those without one. This allows downstream LLMs to interact with the images in activities such as reasoning and summarizing." + zh_Hans: "为指定 URL 上的所有图像添加标题,为没有标题的图像添加“Image [idx]: [caption]”作为 alt 标签。这允许下游 LLM 在推理和总结等活动中与图像进行交互。" + pt_BR: "Captions all images at the specified URL, adding 'Image [idx]: [caption]' as an alt tag for those without one. This allows downstream LLMs to interact with the images in activities such as reasoning and summarizing." + llm_description: Captions all images at the specified URL + form: form + - name: gather_all_links_at_the_end + type: boolean + required: false + default: false + label: + en_US: Gather all links at the end + zh_Hans: 将所有链接集中到最后 + pt_BR: Coletar todos os links ao final + human_description: + en_US: A "Buttons & Links" section will be created at the end. This helps the downstream LLMs or web agents navigating the page or take further actions. + zh_Hans: 最后会创建一个“按钮和链接”部分。这可以帮助下游 LLM 或 Web 代理浏览页面或采取进一步的行动。 + pt_BR: A "Buttons & Links" section will be created at the end. This helps the downstream LLMs or web agents navigating the page or take further actions. + llm_description: Gather all links at the end + form: form + - name: gather_all_images_at_the_end + type: boolean + required: false + default: false + label: + en_US: Gather all images at the end + zh_Hans: 将所有图片集中到最后 + pt_BR: Coletar todas as imagens ao final + human_description: + en_US: An "Images" section will be created at the end. This gives the downstream LLMs an overview of all visuals on the page, which may improve reasoning. + zh_Hans: 最后会创建一个“图像”部分。这可以让下游的 LLM 概览页面上的所有视觉效果,从而提高推理能力。 + pt_BR: An "Images" section will be created at the end. This gives the downstream LLMs an overview of all visuals on the page, which may improve reasoning. + llm_description: Gather all images at the end + form: form - name: proxy_server type: string required: false diff --git a/api/core/tools/provider/builtin/json_process/_assets/icon.svg b/api/core/tools/provider/builtin/json_process/_assets/icon.svg new file mode 100644 index 0000000000..b123983836 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/_assets/icon.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/json_process.py b/api/core/tools/provider/builtin/json_process/json_process.py new file mode 100644 index 0000000000..f6eed3c628 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/json_process.py @@ -0,0 +1,17 @@ +from typing import Any + +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin.json_process.tools.parse import JSONParseTool +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class JsonExtractProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + try: + JSONParseTool().invoke(user_id='', + tool_parameters={ + 'content': '{"name": "John", "age": 30, "city": "New York"}', + 'json_filter': '$.name' + }) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/json_process.yaml b/api/core/tools/provider/builtin/json_process/json_process.yaml new file mode 100644 index 0000000000..c7896bbea7 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/json_process.yaml @@ -0,0 +1,14 @@ +identity: + author: Mingwei Zhang + name: json_process + label: + en_US: JSON Process + zh_Hans: JSON 处理 + pt_BR: JSON Process + description: + en_US: Tools for processing JSON content using jsonpath_ng + zh_Hans: 利用 jsonpath_ng 处理 JSON 内容的工具 + pt_BR: Tools for processing JSON content using jsonpath_ng + icon: icon.svg + tags: + - utilities diff --git a/api/core/tools/provider/builtin/json_process/tools/delete.py b/api/core/tools/provider/builtin/json_process/tools/delete.py new file mode 100644 index 0000000000..b09e494881 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/delete.py @@ -0,0 +1,59 @@ +import json +from typing import Any, Union + +from jsonpath_ng import parse + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class JSONDeleteTool(BuiltinTool): + def _invoke(self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """ + Invoke the JSON delete tool + """ + # Get content + content = tool_parameters.get('content', '') + if not content: + return self.create_text_message('Invalid parameter content') + + # Get query + query = tool_parameters.get('query', '') + if not query: + return self.create_text_message('Invalid parameter query') + + try: + result = self._delete(content, query) + return self.create_text_message(str(result)) + except Exception as e: + return self.create_text_message(f'Failed to delete JSON content: {str(e)}') + + def _delete(self, origin_json: str, query: str) -> str: + try: + input_data = json.loads(origin_json) + expr = parse('$.' + query.lstrip('$.')) # Ensure query path starts with $ + + matches = expr.find(input_data) + + if not matches: + return json.dumps(input_data, ensure_ascii=True) # No changes if no matches found + + for match in matches: + if isinstance(match.context.value, dict): + # Delete key from dictionary + del match.context.value[match.path.fields[-1]] + elif isinstance(match.context.value, list): + # Remove item from list + match.context.value.remove(match.value) + else: + # For other cases, we might want to set to None or remove the parent key + parent = match.context.parent + if parent: + del parent.value[match.path.fields[-1]] + + return json.dumps(input_data, ensure_ascii=True) + except Exception as e: + raise Exception(f"Delete operation failed: {str(e)}") \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/tools/delete.yaml b/api/core/tools/provider/builtin/json_process/tools/delete.yaml new file mode 100644 index 0000000000..4cfa90b861 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/delete.yaml @@ -0,0 +1,40 @@ +identity: + name: json_delete + author: Mingwei Zhang + label: + en_US: JSON Delete + zh_Hans: JSON 删除 + pt_BR: JSON Delete +description: + human: + en_US: A tool for deleting JSON content + zh_Hans: 一个删除 JSON 内容的工具 + pt_BR: A tool for deleting JSON content + llm: A tool for deleting JSON content +parameters: + - name: content + type: string + required: true + label: + en_US: JSON content + zh_Hans: JSON 内容 + pt_BR: JSON content + human_description: + en_US: JSON content to be processed + zh_Hans: 待处理的 JSON 内容 + pt_BR: JSON content to be processed + llm_description: JSON content to be processed + form: llm + - name: query + type: string + required: true + label: + en_US: Query + zh_Hans: 查询 + pt_BR: Query + human_description: + en_US: JSONPath query to locate the element to delete + zh_Hans: 用于定位要删除元素的 JSONPath 查询 + pt_BR: JSONPath query to locate the element to delete + llm_description: JSONPath query to locate the element to delete + form: llm diff --git a/api/core/tools/provider/builtin/json_process/tools/insert.py b/api/core/tools/provider/builtin/json_process/tools/insert.py new file mode 100644 index 0000000000..aa5986e2b4 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/insert.py @@ -0,0 +1,97 @@ +import json +from typing import Any, Union + +from jsonpath_ng import parse + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class JSONParseTool(BuiltinTool): + def _invoke(self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """ + invoke tools + """ + # get content + content = tool_parameters.get('content', '') + if not content: + return self.create_text_message('Invalid parameter content') + + # get query + query = tool_parameters.get('query', '') + if not query: + return self.create_text_message('Invalid parameter query') + + # get new value + new_value = tool_parameters.get('new_value', '') + if not new_value: + return self.create_text_message('Invalid parameter new_value') + + # get insert position + index = tool_parameters.get('index') + + # get create path + create_path = tool_parameters.get('create_path', False) + + try: + result = self._insert(content, query, new_value, index, create_path) + return self.create_text_message(str(result)) + except Exception: + return self.create_text_message('Failed to insert JSON content') + + + def _insert(self, origin_json, query, new_value, index=None, create_path=False): + try: + input_data = json.loads(origin_json) + expr = parse(query) + try: + new_value = json.loads(new_value) + except json.JSONDecodeError: + new_value = new_value + + matches = expr.find(input_data) + + if not matches and create_path: + # create new path + path_parts = query.strip('$').strip('.').split('.') + current = input_data + for i, part in enumerate(path_parts): + if '[' in part and ']' in part: + # process array index + array_name, index = part.split('[') + index = int(index.rstrip(']')) + if array_name not in current: + current[array_name] = [] + while len(current[array_name]) <= index: + current[array_name].append({}) + current = current[array_name][index] + else: + if i == len(path_parts) - 1: + current[part] = new_value + elif part not in current: + current[part] = {} + current = current[part] + else: + for match in matches: + if isinstance(match.value, dict): + # insert new value into dict + if isinstance(new_value, dict): + match.value.update(new_value) + else: + raise ValueError("Cannot insert non-dict value into dict") + elif isinstance(match.value, list): + # insert new value into list + if index is None: + match.value.append(new_value) + else: + match.value.insert(int(index), new_value) + else: + # replace old value with new value + match.full_path.update(input_data, new_value) + + return json.dumps(input_data, ensure_ascii=True) + except Exception as e: + return str(e) \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/tools/insert.yaml b/api/core/tools/provider/builtin/json_process/tools/insert.yaml new file mode 100644 index 0000000000..66a6ff9929 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/insert.yaml @@ -0,0 +1,77 @@ +identity: + name: json_insert + author: Mingwei Zhang + label: + en_US: JSON Insert + zh_Hans: JSON 插入 + pt_BR: JSON Insert +description: + human: + en_US: A tool for inserting JSON content + zh_Hans: 一个插入 JSON 内容的工具 + pt_BR: A tool for inserting JSON content + llm: A tool for inserting JSON content +parameters: + - name: content + type: string + required: true + label: + en_US: JSON content + zh_Hans: JSON 内容 + pt_BR: JSON content + human_description: + en_US: JSON content + zh_Hans: JSON 内容 + pt_BR: JSON content + llm_description: JSON content to be processed + form: llm + - name: query + type: string + required: true + label: + en_US: Query + zh_Hans: 查询 + pt_BR: Query + human_description: + en_US: Object to insert + zh_Hans: 待插入的对象 + pt_BR: Object to insert + llm_description: JSONPath query to locate the element to insert + form: llm + - name: new_value + type: string + required: true + label: + en_US: New Value + zh_Hans: 新值 + pt_BR: New Value + human_description: + en_US: New Value + zh_Hans: 新值 + pt_BR: New Value + llm_description: New Value to insert + form: llm + - name: create_path + type: select + required: true + default: "False" + label: + en_US: Whether to create a path + zh_Hans: 是否创建路径 + pt_BR: Whether to create a path + human_description: + en_US: Whether to create a path when the path does not exist + zh_Hans: 查询路径不存在时是否创建路径 + pt_BR: Whether to create a path when the path does not exist + options: + - value: "True" + label: + en_US: "Yes" + zh_Hans: 是 + pt_BR: "Yes" + - value: "False" + label: + en_US: "No" + zh_Hans: 否 + pt_BR: "No" + form: form diff --git a/api/core/tools/provider/builtin/json_process/tools/parse.py b/api/core/tools/provider/builtin/json_process/tools/parse.py new file mode 100644 index 0000000000..b246afc07e --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/parse.py @@ -0,0 +1,51 @@ +import json +from typing import Any, Union + +from jsonpath_ng import parse + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class JSONParseTool(BuiltinTool): + def _invoke(self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """ + invoke tools + """ + # get content + content = tool_parameters.get('content', '') + if not content: + return self.create_text_message('Invalid parameter content') + + # get json filter + json_filter = tool_parameters.get('json_filter', '') + if not json_filter: + return self.create_text_message('Invalid parameter json_filter') + + try: + result = self._extract(content, json_filter) + return self.create_text_message(str(result)) + except Exception: + return self.create_text_message('Failed to extract JSON content') + + # Extract data from JSON content + def _extract(self, content: str, json_filter: str) -> str: + try: + input_data = json.loads(content) + expr = parse(json_filter) + result = [match.value for match in expr.find(input_data)] + + if len(result) == 1: + result = result[0] + + if isinstance(result, dict | list): + return json.dumps(result, ensure_ascii=True) + elif isinstance(result, str | int | float | bool) or result is None: + return str(result) + else: + return repr(result) + except Exception as e: + return str(e) \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/tools/parse.yaml b/api/core/tools/provider/builtin/json_process/tools/parse.yaml new file mode 100644 index 0000000000..b619dcde94 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/parse.yaml @@ -0,0 +1,40 @@ +identity: + name: parse + author: Mingwei Zhang + label: + en_US: JSON Parse + zh_Hans: JSON 解析 + pt_BR: JSON Parse +description: + human: + en_US: A tool for extracting JSON objects + zh_Hans: 一个解析JSON对象的工具 + pt_BR: A tool for extracting JSON objects + llm: A tool for extracting JSON objects +parameters: + - name: content + type: string + required: true + label: + en_US: JSON data + zh_Hans: JSON数据 + pt_BR: JSON data + human_description: + en_US: JSON data + zh_Hans: JSON数据 + pt_BR: JSON数据 + llm_description: JSON data to be processed + form: llm + - name: json_filter + type: string + required: true + label: + en_US: JSON filter + zh_Hans: JSON解析对象 + pt_BR: JSON filter + human_description: + en_US: JSON fields to be parsed + zh_Hans: 需要解析的 JSON 字段 + pt_BR: JSON fields to be parsed + llm_description: JSON fields to be parsed + form: llm diff --git a/api/core/tools/provider/builtin/json_process/tools/replace.py b/api/core/tools/provider/builtin/json_process/tools/replace.py new file mode 100644 index 0000000000..9f127b9d06 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/replace.py @@ -0,0 +1,106 @@ +import json +from typing import Any, Union + +from jsonpath_ng import parse + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class JSONReplaceTool(BuiltinTool): + def _invoke(self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """ + invoke tools + """ + # get content + content = tool_parameters.get('content', '') + if not content: + return self.create_text_message('Invalid parameter content') + + # get query + query = tool_parameters.get('query', '') + if not query: + return self.create_text_message('Invalid parameter query') + + # get replace value + replace_value = tool_parameters.get('replace_value', '') + if not replace_value: + return self.create_text_message('Invalid parameter replace_value') + + # get replace model + replace_model = tool_parameters.get('replace_model', '') + if not replace_model: + return self.create_text_message('Invalid parameter replace_model') + + try: + if replace_model == 'pattern': + # get replace pattern + replace_pattern = tool_parameters.get('replace_pattern', '') + if not replace_pattern: + return self.create_text_message('Invalid parameter replace_pattern') + result = self._replace_pattern(content, query, replace_pattern, replace_value) + elif replace_model == 'key': + result = self._replace_key(content, query, replace_value) + elif replace_model == 'value': + result = self._replace_value(content, query, replace_value) + return self.create_text_message(str(result)) + except Exception: + return self.create_text_message('Failed to replace JSON content') + + # Replace pattern + def _replace_pattern(self, content: str, query: str, replace_pattern: str, replace_value: str) -> str: + try: + input_data = json.loads(content) + expr = parse(query) + + matches = expr.find(input_data) + + for match in matches: + new_value = match.value.replace(replace_pattern, replace_value) + match.full_path.update(input_data, new_value) + + return json.dumps(input_data, ensure_ascii=True) + except Exception as e: + return str(e) + + # Replace key + def _replace_key(self, content: str, query: str, replace_value: str) -> str: + try: + input_data = json.loads(content) + expr = parse(query) + + matches = expr.find(input_data) + + for match in matches: + parent = match.context.value + if isinstance(parent, dict): + old_key = match.path.fields[0] + if old_key in parent: + value = parent.pop(old_key) + parent[replace_value] = value + elif isinstance(parent, list): + for item in parent: + if isinstance(item, dict) and old_key in item: + value = item.pop(old_key) + item[replace_value] = value + return json.dumps(input_data, ensure_ascii=True) + except Exception as e: + return str(e) + + # Replace value + def _replace_value(self, content: str, query: str, replace_value: str) -> str: + try: + input_data = json.loads(content) + expr = parse(query) + + matches = expr.find(input_data) + + for match in matches: + match.full_path.update(input_data, replace_value) + + return json.dumps(input_data, ensure_ascii=True) + except Exception as e: + return str(e) \ No newline at end of file diff --git a/api/core/tools/provider/builtin/json_process/tools/replace.yaml b/api/core/tools/provider/builtin/json_process/tools/replace.yaml new file mode 100644 index 0000000000..556be5e8b2 --- /dev/null +++ b/api/core/tools/provider/builtin/json_process/tools/replace.yaml @@ -0,0 +1,95 @@ +identity: + name: json_replace + author: Mingwei Zhang + label: + en_US: JSON Replace + zh_Hans: JSON 替换 + pt_BR: JSON Replace +description: + human: + en_US: A tool for replacing JSON content + zh_Hans: 一个替换 JSON 内容的工具 + pt_BR: A tool for replacing JSON content + llm: A tool for replacing JSON content +parameters: + - name: content + type: string + required: true + label: + en_US: JSON content + zh_Hans: JSON 内容 + pt_BR: JSON content + human_description: + en_US: JSON content + zh_Hans: JSON 内容 + pt_BR: JSON content + llm_description: JSON content to be processed + form: llm + - name: query + type: string + required: true + label: + en_US: Query + zh_Hans: 查询 + pt_BR: Query + human_description: + en_US: Query + zh_Hans: 查询 + pt_BR: Query + llm_description: JSONPath query to locate the element to replace + form: llm + - name: replace_pattern + type: string + required: false + label: + en_US: String to be replaced + zh_Hans: 待替换字符串 + pt_BR: String to be replaced + human_description: + en_US: String to be replaced + zh_Hans: 待替换字符串 + pt_BR: String to be replaced + llm_description: String to be replaced + form: llm + - name: replace_value + type: string + required: true + label: + en_US: Replace Value + zh_Hans: 替换值 + pt_BR: Replace Value + human_description: + en_US: New Value + zh_Hans: New Value + pt_BR: New Value + llm_description: New Value to replace + form: llm + - name: replace_model + type: select + required: true + default: pattern + label: + en_US: Replace Model + zh_Hans: 替换模式 + pt_BR: Replace Model + human_description: + en_US: Replace Model + zh_Hans: 替换模式 + pt_BR: Replace Model + options: + - value: key + label: + en_US: replace key + zh_Hans: 键替换 + pt_BR: replace key + - value: value + label: + en_US: replace value + zh_Hans: 值替换 + pt_BR: replace value + - value: pattern + label: + en_US: replace string + zh_Hans: 字符串替换 + pt_BR: replace string + form: form diff --git a/api/core/tools/provider/builtin/nominatim/_assets/icon.svg b/api/core/tools/provider/builtin/nominatim/_assets/icon.svg new file mode 100644 index 0000000000..db5a4eb868 --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/_assets/icon.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 010110010011010110010011 + 010110010011010110010011 + + \ No newline at end of file diff --git a/api/core/tools/provider/builtin/nominatim/nominatim.py b/api/core/tools/provider/builtin/nominatim/nominatim.py new file mode 100644 index 0000000000..b6f29b5feb --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/nominatim.py @@ -0,0 +1,23 @@ +from typing import Any + +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin.nominatim.tools.nominatim_search import NominatimSearchTool +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class NominatimProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + try: + result = NominatimSearchTool().fork_tool_runtime( + runtime={ + "credentials": credentials, + } + ).invoke( + user_id='', + tool_parameters={ + 'query': 'London', + 'limit': 1, + }, + ) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) diff --git a/api/core/tools/provider/builtin/nominatim/nominatim.yaml b/api/core/tools/provider/builtin/nominatim/nominatim.yaml new file mode 100644 index 0000000000..7d014bd78c --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/nominatim.yaml @@ -0,0 +1,43 @@ +identity: + author: Charles Zhou + name: nominatim + label: + en_US: Nominatim + zh_Hans: Nominatim + de_DE: Nominatim + ja_JP: Nominatim + description: + en_US: Nominatim is a search engine for OpenStreetMap data + zh_Hans: Nominatim是OpenStreetMap数据的搜索引擎 + de_DE: Nominatim ist eine Suchmaschine für OpenStreetMap-Daten + ja_JP: NominatimはOpenStreetMapデータの検索エンジンです + icon: icon.svg + tags: + - search + - utilities +credentials_for_provider: + base_url: + type: text-input + required: false + default: https://nominatim.openstreetmap.org + label: + en_US: Nominatim Base URL + zh_Hans: Nominatim 基础 URL + de_DE: Nominatim Basis-URL + ja_JP: Nominatim ベースURL + placeholder: + en_US: "Enter your Nominatim instance URL (default: + https://nominatim.openstreetmap.org)" + zh_Hans: 输入您的Nominatim实例URL(默认:https://nominatim.openstreetmap.org) + de_DE: "Geben Sie Ihre Nominatim-Instanz-URL ein (Standard: + https://nominatim.openstreetmap.org)" + ja_JP: NominatimインスタンスのURLを入力してください(デフォルト:https://nominatim.openstreetmap.org) + help: + en_US: The base URL for the Nominatim instance. Use the default for the public + service or enter your self-hosted instance URL. + zh_Hans: Nominatim实例的基础URL。使用默认值可访问公共服务,或输入您的自托管实例URL。 + de_DE: Die Basis-URL für die Nominatim-Instanz. Verwenden Sie den Standardwert + für den öffentlichen Dienst oder geben Sie die URL Ihrer selbst + gehosteten Instanz ein. + ja_JP: NominatimインスタンスのベースURL。公共サービスにはデフォルトを使用するか、自己ホスティングインスタンスのURLを入力してください。 + url: https://nominatim.org/ diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.py b/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.py new file mode 100644 index 0000000000..e21ce14f54 --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.py @@ -0,0 +1,47 @@ +import json +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class NominatimLookupTool(BuiltinTool): + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + osm_ids = tool_parameters.get('osm_ids', '') + + if not osm_ids: + return self.create_text_message('Please provide OSM IDs') + + params = { + 'osm_ids': osm_ids, + 'format': 'json', + 'addressdetails': 1 + } + + return self._make_request(user_id, 'lookup', params) + + def _make_request(self, user_id: str, endpoint: str, params: dict) -> ToolInvokeMessage: + base_url = self.runtime.credentials.get('base_url', 'https://nominatim.openstreetmap.org') + + try: + headers = { + "User-Agent": "DifyNominatimTool/1.0" + } + s = requests.session() + response = s.request( + method='GET', + headers=headers, + url=f"{base_url}/{endpoint}", + params=params + ) + response_data = response.json() + + if response.status_code == 200: + s.close() + return self.create_text_message(self.summary(user_id=user_id, content=json.dumps(response_data, ensure_ascii=False))) + else: + return self.create_text_message(f"Error: {response.status_code} - {response.text}") + except Exception as e: + return self.create_text_message(f"An error occurred: {str(e)}") \ No newline at end of file diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.yaml b/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.yaml new file mode 100644 index 0000000000..508c4dcd88 --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_lookup.yaml @@ -0,0 +1,31 @@ +identity: + name: nominatim_lookup + author: Charles Zhou + label: + en_US: Nominatim OSM Lookup + zh_Hans: Nominatim OSM 对象查找 + de_DE: Nominatim OSM-Objektsuche + ja_JP: Nominatim OSM ルックアップ +description: + human: + en_US: Look up OSM objects using their IDs with Nominatim + zh_Hans: 使用Nominatim通过ID查找OSM对象 + de_DE: Suchen Sie OSM-Objekte anhand ihrer IDs mit Nominatim + ja_JP: Nominatimを使用してIDでOSMオブジェクトを検索 + llm: A tool for looking up OpenStreetMap objects using their IDs with Nominatim. +parameters: + - name: osm_ids + type: string + required: true + label: + en_US: OSM IDs + zh_Hans: OSM ID + de_DE: OSM-IDs + ja_JP: OSM ID + human_description: + en_US: Comma-separated list of OSM IDs to lookup (e.g., N123,W456,R789) + zh_Hans: 要查找的OSM ID的逗号分隔列表(例如:N123,W456,R789) + de_DE: Kommagetrennte Liste von OSM-IDs für die Suche (z.B. N123,W456,R789) + ja_JP: 検索するOSM IDのカンマ区切りリスト(例:N123,W456,R789) + llm_description: A comma-separated list of OSM IDs (prefixed with N, W, or R) for lookup. + form: llm diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.py b/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.py new file mode 100644 index 0000000000..438d5219e9 --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.py @@ -0,0 +1,49 @@ +import json +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class NominatimReverseTool(BuiltinTool): + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + lat = tool_parameters.get('lat') + lon = tool_parameters.get('lon') + + if lat is None or lon is None: + return self.create_text_message('Please provide both latitude and longitude') + + params = { + 'lat': lat, + 'lon': lon, + 'format': 'json', + 'addressdetails': 1 + } + + return self._make_request(user_id, 'reverse', params) + + def _make_request(self, user_id: str, endpoint: str, params: dict) -> ToolInvokeMessage: + base_url = self.runtime.credentials.get('base_url', 'https://nominatim.openstreetmap.org') + + try: + headers = { + "User-Agent": "DifyNominatimTool/1.0" + } + s = requests.session() + response = s.request( + method='GET', + headers=headers, + url=f"{base_url}/{endpoint}", + params=params + ) + response_data = response.json() + + if response.status_code == 200: + s.close() + return self.create_text_message(self.summary(user_id=user_id, content=json.dumps(response_data, ensure_ascii=False))) + else: + return self.create_text_message(f"Error: {response.status_code} - {response.text}") + except Exception as e: + return self.create_text_message(f"An error occurred: {str(e)}") \ No newline at end of file diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.yaml b/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.yaml new file mode 100644 index 0000000000..f1a2dd09fb --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_reverse.yaml @@ -0,0 +1,47 @@ +identity: + name: nominatim_reverse + author: Charles Zhou + label: + en_US: Nominatim Reverse Geocoding + zh_Hans: Nominatim 反向地理编码 + de_DE: Nominatim Rückwärts-Geocodierung + ja_JP: Nominatim リバースジオコーディング +description: + human: + en_US: Convert coordinates to addresses using Nominatim + zh_Hans: 使用Nominatim将坐标转换为地址 + de_DE: Konvertieren Sie Koordinaten in Adressen mit Nominatim + ja_JP: Nominatimを使用して座標を住所に変換 + llm: A tool for reverse geocoding using Nominatim, which can convert latitude + and longitude coordinates to an address. +parameters: + - name: lat + type: number + required: true + label: + en_US: Latitude + zh_Hans: 纬度 + de_DE: Breitengrad + ja_JP: 緯度 + human_description: + en_US: Latitude coordinate for reverse geocoding + zh_Hans: 用于反向地理编码的纬度坐标 + de_DE: Breitengrad-Koordinate für die Rückwärts-Geocodierung + ja_JP: リバースジオコーディングの緯度座標 + llm_description: The latitude coordinate for reverse geocoding. + form: llm + - name: lon + type: number + required: true + label: + en_US: Longitude + zh_Hans: 经度 + de_DE: Längengrad + ja_JP: 経度 + human_description: + en_US: Longitude coordinate for reverse geocoding + zh_Hans: 用于反向地理编码的经度坐标 + de_DE: Längengrad-Koordinate für die Rückwärts-Geocodierung + ja_JP: リバースジオコーディングの経度座標 + llm_description: The longitude coordinate for reverse geocoding. + form: llm diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.py b/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.py new file mode 100644 index 0000000000..983cbc0e34 --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.py @@ -0,0 +1,49 @@ +import json +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class NominatimSearchTool(BuiltinTool): + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + query = tool_parameters.get('query', '') + limit = tool_parameters.get('limit', 10) + + if not query: + return self.create_text_message('Please input a search query') + + params = { + 'q': query, + 'format': 'json', + 'limit': limit, + 'addressdetails': 1 + } + + return self._make_request(user_id, 'search', params) + + def _make_request(self, user_id: str, endpoint: str, params: dict) -> ToolInvokeMessage: + base_url = self.runtime.credentials.get('base_url', 'https://nominatim.openstreetmap.org') + + try: + headers = { + "User-Agent": "DifyNominatimTool/1.0" + } + s = requests.session() + response = s.request( + method='GET', + headers=headers, + url=f"{base_url}/{endpoint}", + params=params + ) + response_data = response.json() + + if response.status_code == 200: + s.close() + return self.create_text_message(self.summary(user_id=user_id, content=json.dumps(response_data, ensure_ascii=False))) + else: + return self.create_text_message(f"Error: {response.status_code} - {response.text}") + except Exception as e: + return self.create_text_message(f"An error occurred: {str(e)}") \ No newline at end of file diff --git a/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.yaml b/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.yaml new file mode 100644 index 0000000000..e0c53c046a --- /dev/null +++ b/api/core/tools/provider/builtin/nominatim/tools/nominatim_search.yaml @@ -0,0 +1,51 @@ +identity: + name: nominatim_search + author: Charles Zhou + label: + en_US: Nominatim Search + zh_Hans: Nominatim 搜索 + de_DE: Nominatim Suche + ja_JP: Nominatim 検索 +description: + human: + en_US: Search for locations using Nominatim + zh_Hans: 使用Nominatim搜索位置 + de_DE: Suche nach Orten mit Nominatim + ja_JP: Nominatimを使用して場所を検索 + llm: A tool for geocoding using Nominatim, which can search for locations based + on addresses or place names. +parameters: + - name: query + type: string + required: true + label: + en_US: Search Query + zh_Hans: 搜索查询 + de_DE: Suchanfrage + ja_JP: 検索クエリ + human_description: + en_US: Enter an address or place name to search for + zh_Hans: 输入要搜索的地址或地名 + de_DE: Geben Sie eine Adresse oder einen Ortsnamen für die Suche ein + ja_JP: 検索する住所または場所の名前を入力してください + llm_description: The search query for Nominatim, which can be an address or place name. + form: llm + - name: limit + type: number + default: 10 + min: 1 + max: 40 + required: false + label: + en_US: Result Limit + zh_Hans: 结果限制 + de_DE: Ergebnislimit + ja_JP: 結果の制限 + human_description: + en_US: "Maximum number of results to return (default: 10, max: 40)" + zh_Hans: 要返回的最大结果数(默认:10,最大:40) + de_DE: "Maximale Anzahl der zurückzugebenden Ergebnisse (Standard: 10, max: 40)" + ja_JP: 返す結果の最大数(デフォルト:10、最大:40) + llm_description: Limit the number of returned results. The default is 10, and + the maximum is 40. + form: form diff --git a/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.py b/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.py index 637dd70e55..ec2927675e 100644 --- a/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.py +++ b/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.py @@ -42,17 +42,18 @@ class NovitaAiModelQueryTool(BuiltinTool): result_str = '' if result_type == 'first sd_name': - result_str = models_data[0]['sd_name_in_api'] + result_str = models_data[0]['sd_name_in_api'] if len(models_data) > 0 else '' elif result_type == 'first name sd_name pair': - result_str = json.dumps({'name': models_data[0]['name'], 'sd_name': models_data[0]['sd_name_in_api']}) + result_str = json.dumps({'name': models_data[0]['name'], 'sd_name': models_data[0]['sd_name_in_api']}) if len(models_data) > 0 else '' elif result_type == 'sd_name array': - sd_name_array = [model['sd_name_in_api'] for model in models_data] + sd_name_array = [model['sd_name_in_api'] for model in models_data] if len(models_data) > 0 else [] result_str = json.dumps(sd_name_array) elif result_type == 'name array': - name_array = [model['name'] for model in models_data] + name_array = [model['name'] for model in models_data] if len(models_data) > 0 else [] result_str = json.dumps(name_array) elif result_type == 'name sd_name pair array': - name_sd_name_pair_array = [{'name': model['name'], 'sd_name': model['sd_name_in_api']} for model in models_data] + name_sd_name_pair_array = [{'name': model['name'], 'sd_name': model['sd_name_in_api']} + for model in models_data] if len(models_data) > 0 else [] result_str = json.dumps(name_sd_name_pair_array) elif result_type == 'whole info array': result_str = json.dumps(models_data) diff --git a/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.yaml b/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.yaml index a933f76d0e..a14795e45e 100644 --- a/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.yaml +++ b/api/core/tools/provider/builtin/novitaai/tools/novitaai_modelquery.yaml @@ -19,7 +19,8 @@ parameters: human_description: en_US: Seaching the content of sd_name, name, tags. zh_Hans: 搜索 sd_name、name、tags 中的内容 - form: form + llm_description: Enter the content to search + form: llm - name: result_type type: select default: "first sd_name" diff --git a/api/core/tools/provider/builtin/searchapi/tools/google.py b/api/core/tools/provider/builtin/searchapi/tools/google.py index d019fe7134..dd780aeadc 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google.py @@ -94,7 +94,7 @@ class GoogleTool(BuiltinTool): google_domain = tool_parameters.get("google_domain", "google.com") gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials['searchapi_api_key'] result = SearchAPI(api_key).run(query, result_type=result_type, num=num, google_domain=google_domain, gl=gl, hl=hl, location=location) diff --git a/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py b/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py index 1b8cfa7e30..81c67c51a9 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py @@ -72,11 +72,11 @@ class GoogleJobsTool(BuiltinTool): """ query = tool_parameters['query'] result_type = tool_parameters['result_type'] - is_remote = tool_parameters.get("is_remote", None) + is_remote = tool_parameters.get("is_remote") google_domain = tool_parameters.get("google_domain", "google.com") gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") ltype = 1 if is_remote else None diff --git a/api/core/tools/provider/builtin/searchapi/tools/google_news.py b/api/core/tools/provider/builtin/searchapi/tools/google_news.py index d592dc25aa..5d2657dddd 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google_news.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google_news.py @@ -82,7 +82,7 @@ class GoogleNewsTool(BuiltinTool): google_domain = tool_parameters.get("google_domain", "google.com") gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials['searchapi_api_key'] result = SearchAPI(api_key).run(query, result_type=result_type, num=num, google_domain=google_domain, gl=gl, hl=hl, location=location) diff --git a/api/core/tools/provider/builtin/searxng/tools/searxng_search.py b/api/core/tools/provider/builtin/searxng/tools/searxng_search.py index 3e46916b9b..5d12553629 100644 --- a/api/core/tools/provider/builtin/searxng/tools/searxng_search.py +++ b/api/core/tools/provider/builtin/searxng/tools/searxng_search.py @@ -107,7 +107,7 @@ class SearXNGSearchTool(BuiltinTool): if not host: raise Exception('SearXNG api is required') - query = tool_parameters.get('query', None) + query = tool_parameters.get('query') if not query: return self.create_text_message('Please input query') diff --git a/api/core/tools/provider/builtin/slack/tools/slack_webhook.py b/api/core/tools/provider/builtin/slack/tools/slack_webhook.py index 18e4eb86c8..f47557f2ef 100644 --- a/api/core/tools/provider/builtin/slack/tools/slack_webhook.py +++ b/api/core/tools/provider/builtin/slack/tools/slack_webhook.py @@ -20,7 +20,7 @@ class SlackWebhookTool(BuiltinTool): webhook_url = tool_parameters.get('webhook_url', '') - if not webhook_url.startswith('https://hooks.slack.com/services/'): + if not webhook_url.startswith('https://hooks.slack.com/'): return self.create_text_message( f'Invalid parameter webhook_url ${webhook_url}, not a valid Slack webhook URL') diff --git a/api/core/tools/provider/builtin/time/tools/current_time.yaml b/api/core/tools/provider/builtin/time/tools/current_time.yaml index dbdd39e223..52705ace4c 100644 --- a/api/core/tools/provider/builtin/time/tools/current_time.yaml +++ b/api/core/tools/provider/builtin/time/tools/current_time.yaml @@ -69,6 +69,11 @@ parameters: en_US: Asia/Shanghai zh_Hans: 亚洲/上海 pt_BR: Asia/Shanghai + - value: Asia/Ho_Chi_Minh + label: + en_US: Asia/Ho_Chi_Minh + zh_Hans: 亚洲/胡志明市 + pt_BR: Ásia/Ho Chi Minh - value: Asia/Tokyo label: en_US: Asia/Tokyo diff --git a/api/core/tools/provider/builtin/websearch/tools/get_markdown.py b/api/core/tools/provider/builtin/websearch/tools/get_markdown.py index 92d7d1addc..043879deea 100644 --- a/api/core/tools/provider/builtin/websearch/tools/get_markdown.py +++ b/api/core/tools/provider/builtin/websearch/tools/get_markdown.py @@ -43,7 +43,7 @@ class GetMarkdownTool(BuiltinTool): Invoke the SerplyApi tool. """ url = tool_parameters["url"] - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials["serply_api_key"] result = SerplyApi(api_key).run(url, location=location) diff --git a/api/core/tools/provider/builtin/websearch/tools/job_search.py b/api/core/tools/provider/builtin/websearch/tools/job_search.py index 347b4eb4c4..9128305922 100644 --- a/api/core/tools/provider/builtin/websearch/tools/job_search.py +++ b/api/core/tools/provider/builtin/websearch/tools/job_search.py @@ -55,7 +55,7 @@ class SerplyApi: f"Employer: {job['employer']}", f"Location: {job['location']}", f"Link: {job['link']}", - f"""Highest: {", ".join([h for h in job["highlights"]])}""", + f"""Highest: {", ".join(list(job["highlights"]))}""", "---", ]) ) @@ -78,7 +78,7 @@ class JobSearchTool(BuiltinTool): query = tool_parameters["query"] gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials["serply_api_key"] result = SerplyApi(api_key).run(query, gl=gl, hl=hl, location=location) diff --git a/api/core/tools/provider/builtin/websearch/tools/news_search.py b/api/core/tools/provider/builtin/websearch/tools/news_search.py index 886ea47765..e9c0744f05 100644 --- a/api/core/tools/provider/builtin/websearch/tools/news_search.py +++ b/api/core/tools/provider/builtin/websearch/tools/news_search.py @@ -80,7 +80,7 @@ class NewsSearchTool(BuiltinTool): query = tool_parameters["query"] gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials["serply_api_key"] result = SerplyApi(api_key).run(query, gl=gl, hl=hl, location=location) diff --git a/api/core/tools/provider/builtin/websearch/tools/scholar_search.py b/api/core/tools/provider/builtin/websearch/tools/scholar_search.py index 19df455231..0030a03c06 100644 --- a/api/core/tools/provider/builtin/websearch/tools/scholar_search.py +++ b/api/core/tools/provider/builtin/websearch/tools/scholar_search.py @@ -83,7 +83,7 @@ class ScholarSearchTool(BuiltinTool): query = tool_parameters["query"] gl = tool_parameters.get("gl", "us") hl = tool_parameters.get("hl", "en") - location = tool_parameters.get("location", None) + location = tool_parameters.get("location") api_key = self.runtime.credentials["serply_api_key"] result = SerplyApi(api_key).run(query, gl=gl, hl=hl, location=location) diff --git a/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.py b/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.py index aca10e6a7f..fb44b70f4e 100644 --- a/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.py +++ b/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.py @@ -22,7 +22,21 @@ class WecomGroupBotTool(BuiltinTool): return self.create_text_message( f'Invalid parameter hook_key ${hook_key}, not a valid UUID') - msgtype = 'text' + message_type = tool_parameters.get('message_type', 'text') + if message_type == 'markdown': + payload = { + "msgtype": 'markdown', + "markdown": { + "content": content, + } + } + else: + payload = { + "msgtype": 'text', + "text": { + "content": content, + } + } api_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send' headers = { 'Content-Type': 'application/json', @@ -30,12 +44,6 @@ class WecomGroupBotTool(BuiltinTool): params = { 'key': hook_key, } - payload = { - "msgtype": msgtype, - "text": { - "content": content, - } - } try: res = httpx.post(api_url, headers=headers, params=params, json=payload) diff --git a/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.yaml b/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.yaml index ece1bbc927..379005a102 100644 --- a/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.yaml +++ b/api/core/tools/provider/builtin/wecom/tools/wecom_group_bot.yaml @@ -38,3 +38,27 @@ parameters: pt_BR: Content to sent to the group. llm_description: Content of the message form: llm + - name: message_type + type: select + default: text + required: true + label: + en_US: Wecom Group bot message type + zh_Hans: 群机器人webhook的消息类型 + pt_BR: Wecom Group bot message type + human_description: + en_US: Wecom Group bot message type + zh_Hans: 群机器人webhook的消息类型 + pt_BR: Wecom Group bot message type + options: + - value: text + label: + en_US: Text + zh_Hans: 文本 + pt_BR: Text + - value: markdown + label: + en_US: Markdown + zh_Hans: Markdown + pt_BR: Markdown + form: form diff --git a/api/core/tools/provider/builtin_tool_provider.py b/api/core/tools/provider/builtin_tool_provider.py index d076cb384f..47e33b70c9 100644 --- a/api/core/tools/provider/builtin_tool_provider.py +++ b/api/core/tools/provider/builtin_tool_provider.py @@ -38,7 +38,7 @@ class BuiltinToolProviderController(ToolProviderController): super().__init__(**{ 'identity': provider_yaml['identity'], - 'credentials_schema': provider_yaml['credentials_for_provider'] if 'credentials_for_provider' in provider_yaml else None, + 'credentials_schema': provider_yaml.get('credentials_for_provider', None), }) def _get_builtin_tools(self) -> list[Tool]: diff --git a/api/core/tools/tool/api_tool.py b/api/core/tools/tool/api_tool.py index c39f4aa3b7..c8b683f9ef 100644 --- a/api/core/tools/tool/api_tool.py +++ b/api/core/tools/tool/api_tool.py @@ -1,11 +1,9 @@ import json -from json import dumps from os import getenv -from typing import Any, Union +from typing import Any from urllib.parse import urlencode import httpx -import requests import core.helper.ssrf_proxy as ssrf_proxy from core.tools.entities.tool_bundle import ApiToolBundle @@ -18,12 +16,14 @@ API_TOOL_DEFAULT_TIMEOUT = ( int(getenv('API_TOOL_DEFAULT_READ_TIMEOUT', '60')) ) + class ApiTool(Tool): api_bundle: ApiToolBundle - + """ Api tool """ + def fork_tool_runtime(self, runtime: dict[str, Any]) -> 'Tool': """ fork a new tool with meta data @@ -38,8 +38,9 @@ class ApiTool(Tool): api_bundle=self.api_bundle.model_copy() if self.api_bundle else None, runtime=Tool.Runtime(**runtime) ) - - def validate_credentials(self, credentials: dict[str, Any], parameters: dict[str, Any], format_only: bool = False) -> str: + + def validate_credentials(self, credentials: dict[str, Any], parameters: dict[str, Any], + format_only: bool = False) -> str: """ validate the credentials for Api tool """ @@ -47,7 +48,7 @@ class ApiTool(Tool): headers = self.assembling_request(parameters) if format_only: - return + return '' response = self.do_http_request(self.api_bundle.server_url, self.api_bundle.method, headers, parameters) # validate response @@ -68,12 +69,12 @@ class ApiTool(Tool): if 'api_key_header' in credentials: api_key_header = credentials['api_key_header'] - + if 'api_key_value' not in credentials: raise ToolProviderCredentialValidationError('Missing api_key_value') elif not isinstance(credentials['api_key_value'], str): raise ToolProviderCredentialValidationError('api_key_value must be a string') - + if 'api_key_header_prefix' in credentials: api_key_header_prefix = credentials['api_key_header_prefix'] if api_key_header_prefix == 'basic' and credentials['api_key_value']: @@ -82,20 +83,20 @@ class ApiTool(Tool): credentials['api_key_value'] = f'Bearer {credentials["api_key_value"]}' elif api_key_header_prefix == 'custom': pass - + headers[api_key_header] = credentials['api_key_value'] needed_parameters = [parameter for parameter in self.api_bundle.parameters if parameter.required] for parameter in needed_parameters: if parameter.required and parameter.name not in parameters: raise ToolParameterValidationError(f"Missing required parameter {parameter.name}") - + if parameter.default is not None and parameter.name not in parameters: parameters[parameter.name] = parameter.default return headers - def validate_and_parse_response(self, response: Union[httpx.Response, requests.Response]) -> str: + def validate_and_parse_response(self, response: httpx.Response) -> str: """ validate the response """ @@ -112,23 +113,20 @@ class ApiTool(Tool): return json.dumps(response) except Exception as e: return response.text - elif isinstance(response, requests.Response): - if not response.ok: - raise ToolInvokeError(f"Request failed with status code {response.status_code} and {response.text}") - if not response.content: - return 'Empty response from the tool, please check your parameters and try again.' - try: - response = response.json() - try: - return json.dumps(response, ensure_ascii=False) - except Exception as e: - return json.dumps(response) - except Exception as e: - return response.text else: raise ValueError(f'Invalid response type {type(response)}') - - def do_http_request(self, url: str, method: str, headers: dict[str, Any], parameters: dict[str, Any]) -> httpx.Response: + + @staticmethod + def get_parameter_value(parameter, parameters): + if parameter['name'] in parameters: + return parameters[parameter['name']] + elif parameter.get('required', False): + raise ToolParameterValidationError(f"Missing required parameter {parameter['name']}") + else: + return (parameter.get('schema', {}) or {}).get('default', '') + + def do_http_request(self, url: str, method: str, headers: dict[str, Any], + parameters: dict[str, Any]) -> httpx.Response: """ do http request depending on api bundle """ @@ -141,44 +139,17 @@ class ApiTool(Tool): # check parameters for parameter in self.api_bundle.openapi.get('parameters', []): + value = self.get_parameter_value(parameter, parameters) if parameter['in'] == 'path': - value = '' - if parameter['name'] in parameters: - value = parameters[parameter['name']] - elif parameter['required']: - raise ToolParameterValidationError(f"Missing required parameter {parameter['name']}") - else: - value = (parameter.get('schema', {}) or {}).get('default', '') path_params[parameter['name']] = value elif parameter['in'] == 'query': - value = '' - if parameter['name'] in parameters: - value = parameters[parameter['name']] - elif parameter.get('required', False): - raise ToolParameterValidationError(f"Missing required parameter {parameter['name']}") - else: - value = (parameter.get('schema', {}) or {}).get('default', '') params[parameter['name']] = value elif parameter['in'] == 'cookie': - value = '' - if parameter['name'] in parameters: - value = parameters[parameter['name']] - elif parameter.get('required', False): - raise ToolParameterValidationError(f"Missing required parameter {parameter['name']}") - else: - value = (parameter.get('schema', {}) or {}).get('default', '') cookies[parameter['name']] = value elif parameter['in'] == 'header': - value = '' - if parameter['name'] in parameters: - value = parameters[parameter['name']] - elif parameter.get('required', False): - raise ToolParameterValidationError(f"Missing required parameter {parameter['name']}") - else: - value = (parameter.get('schema', {}) or {}).get('default', '') headers[parameter['name']] = value # check if there is a request body and handle it @@ -188,8 +159,8 @@ class ApiTool(Tool): for content_type in self.api_bundle.openapi['requestBody']['content']: headers['Content-Type'] = content_type body_schema = self.api_bundle.openapi['requestBody']['content'][content_type]['schema'] - required = body_schema['required'] if 'required' in body_schema else [] - properties = body_schema['properties'] if 'properties' in body_schema else {} + required = body_schema.get('required', []) + properties = body_schema.get('properties', {}) for name, property in properties.items(): if name in parameters: # convert type @@ -203,7 +174,7 @@ class ApiTool(Tool): else: body[name] = None break - + # replace path parameters for name, value in path_params.items(): url = url.replace(f'{{{name}}}', f'{value}') @@ -211,33 +182,21 @@ class ApiTool(Tool): # parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored if 'Content-Type' in headers: if headers['Content-Type'] == 'application/json': - body = dumps(body) + body = json.dumps(body) elif headers['Content-Type'] == 'application/x-www-form-urlencoded': body = urlencode(body) else: body = body - - # do http request - if method == 'get': - response = ssrf_proxy.get(url, params=params, headers=headers, cookies=cookies, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) - elif method == 'post': - response = ssrf_proxy.post(url, params=params, headers=headers, cookies=cookies, data=body, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) - elif method == 'put': - response = ssrf_proxy.put(url, params=params, headers=headers, cookies=cookies, data=body, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) - elif method == 'delete': - response = ssrf_proxy.delete(url, params=params, headers=headers, cookies=cookies, data=body, timeout=API_TOOL_DEFAULT_TIMEOUT, allow_redirects=True) - elif method == 'patch': - response = ssrf_proxy.patch(url, params=params, headers=headers, cookies=cookies, data=body, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) - elif method == 'head': - response = ssrf_proxy.head(url, params=params, headers=headers, cookies=cookies, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) - elif method == 'options': - response = ssrf_proxy.options(url, params=params, headers=headers, cookies=cookies, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) + + if method in ('get', 'head', 'post', 'put', 'delete', 'patch'): + response = getattr(ssrf_proxy, method)(url, params=params, headers=headers, cookies=cookies, data=body, + timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True) + return response else: - raise ValueError(f'Invalid http method {method}') - - return response - - def _convert_body_property_any_of(self, property: dict[str, Any], value: Any, any_of: list[dict[str, Any]], max_recursive=10) -> Any: + raise ValueError(f'Invalid http method {self.method}') + + def _convert_body_property_any_of(self, property: dict[str, Any], value: Any, any_of: list[dict[str, Any]], + max_recursive=10) -> Any: if max_recursive <= 0: raise Exception("Max recursion depth reached") for option in any_of or []: @@ -290,9 +249,12 @@ class ApiTool(Tool): elif property['type'] == 'null': if value is None: return None - elif property['type'] == 'object': + elif property['type'] == 'object' or property['type'] == 'array': if isinstance(value, str): try: + # an array str like '[1,2]' also can convert to list [1,2] through json.loads + # json not support single quote, but we can support it + value = value.replace("'", '"') return json.loads(value) except ValueError: return value @@ -322,4 +284,3 @@ class ApiTool(Tool): # assemble invoke message return self.create_text_message(response) - \ No newline at end of file diff --git a/api/core/tools/tool/dataset_retriever_tool.py b/api/core/tools/tool/dataset_retriever_tool.py index e52981b2d1..1170e1b7a5 100644 --- a/api/core/tools/tool/dataset_retriever_tool.py +++ b/api/core/tools/tool/dataset_retriever_tool.py @@ -90,7 +90,7 @@ class DatasetRetrieverTool(Tool): """ invoke dataset retriever tool """ - query = tool_parameters.get('query', None) + query = tool_parameters.get('query') if not query: return self.create_text_message(text='please input query') diff --git a/api/core/tools/tool/tool.py b/api/core/tools/tool/tool.py index 6eba5bcb7f..04c09c7f5b 100644 --- a/api/core/tools/tool/tool.py +++ b/api/core/tools/tool/tool.py @@ -207,30 +207,7 @@ class Tool(BaseModel, ABC): result = [result] return result - - def _convert_tool_response_to_str(self, tool_response: list[ToolInvokeMessage]) -> str: - """ - Handle tool response - """ - result = '' - for response in tool_response: - if response.type == ToolInvokeMessage.MessageType.TEXT: - result += response.message - elif response.type == ToolInvokeMessage.MessageType.LINK: - result += f"result link: {response.message}. please tell user to check it. \n" - elif response.type == ToolInvokeMessage.MessageType.IMAGE_LINK or \ - response.type == ToolInvokeMessage.MessageType.IMAGE: - result += "image has been created and sent to user already, you do not need to create it, just tell the user to check it now. \n" - elif response.type == ToolInvokeMessage.MessageType.BLOB: - if len(response.message) > 114: - result += str(response.message[:114]) + '...' - else: - result += str(response.message) - else: - result += f"tool response: {response.message}. \n" - return result - def _transform_tool_parameters_type(self, tool_parameters: dict[str, Any]) -> dict[str, Any]: """ Transform tool parameters type @@ -355,3 +332,12 @@ class Tool(BaseModel, ABC): message=blob, meta=meta, save_as=save_as ) + + def create_json_message(self, object: dict) -> ToolInvokeMessage: + """ + create a json message + """ + return ToolInvokeMessage( + type=ToolInvokeMessage.MessageType.JSON, + message=object + ) diff --git a/api/core/tools/tool/workflow_tool.py b/api/core/tools/tool/workflow_tool.py index 122b663f94..071081303c 100644 --- a/api/core/tools/tool/workflow_tool.py +++ b/api/core/tools/tool/workflow_tool.py @@ -31,9 +31,10 @@ class WorkflowTool(Tool): :return: the tool provider type """ return ToolProviderType.WORKFLOW - - def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) \ - -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: """ invoke the tool """ diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 16fe9051e3..7615368934 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -1,7 +1,8 @@ +import json from copy import deepcopy from datetime import datetime, timezone from mimetypes import guess_type -from typing import Union +from typing import Any, Optional, Union from yarl import URL @@ -9,6 +10,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.callback_handler.agent_tool_callback_handler import DifyAgentCallbackHandler from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler from core.file.file_obj import FileTransferMethod +from core.ops.ops_trace_manager import TraceQueueManager from core.tools.entities.tool_entities import ToolInvokeMessage, ToolInvokeMessageBinary, ToolInvokeMeta, ToolParameter from core.tools.errors import ( ToolEngineInvokeError, @@ -31,10 +33,12 @@ class ToolEngine: Tool runtime engine take care of the tool executions. """ @staticmethod - def agent_invoke(tool: Tool, tool_parameters: Union[str, dict], - user_id: str, tenant_id: str, message: Message, invoke_from: InvokeFrom, - agent_tool_callback: DifyAgentCallbackHandler) \ - -> tuple[str, list[tuple[MessageFile, bool]], ToolInvokeMeta]: + def agent_invoke( + tool: Tool, tool_parameters: Union[str, dict], + user_id: str, tenant_id: str, message: Message, invoke_from: InvokeFrom, + agent_tool_callback: DifyAgentCallbackHandler, + trace_manager: Optional[TraceQueueManager] = None + ) -> tuple[str, list[tuple[MessageFile, bool]], ToolInvokeMeta]: """ Agent invokes the tool with the given arguments. """ @@ -82,9 +86,11 @@ class ToolEngine: # hit the callback handler agent_tool_callback.on_tool_end( - tool_name=tool.identity.name, - tool_inputs=tool_parameters, - tool_outputs=plain_text + tool_name=tool.identity.name, + tool_inputs=tool_parameters, + tool_outputs=plain_text, + message_id=message.id, + trace_manager=trace_manager ) # transform tool invoke message to get LLM friendly message @@ -120,8 +126,8 @@ class ToolEngine: def workflow_invoke(tool: Tool, tool_parameters: dict, user_id: str, workflow_id: str, workflow_tool_callback: DifyWorkflowCallbackHandler, - workflow_call_depth: int) \ - -> list[ToolInvokeMessage]: + workflow_call_depth: int, + ) -> list[ToolInvokeMessage]: """ Workflow invokes the tool with the given arguments. """ @@ -139,9 +145,9 @@ class ToolEngine: # hit the callback handler workflow_tool_callback.on_tool_end( - tool_name=tool.identity.name, - tool_inputs=tool_parameters, - tool_outputs=response + tool_name=tool.identity.name, + tool_inputs=tool_parameters, + tool_outputs=response, ) return response @@ -188,6 +194,8 @@ class ToolEngine: elif response.type == ToolInvokeMessage.MessageType.IMAGE_LINK or \ response.type == ToolInvokeMessage.MessageType.IMAGE: result += "image has been created and sent to user already, you do not need to create it, just tell the user to check it now." + elif response.type == ToolInvokeMessage.MessageType.JSON: + result += f"tool response: {json.dumps(response.message, ensure_ascii=False)}." else: result += f"tool response: {response.message}." @@ -247,7 +255,7 @@ class ToolEngine: agent_message: Message, invoke_from: InvokeFrom, user_id: str - ) -> list[tuple[MessageFile, bool]]: + ) -> list[tuple[Any, str]]: """ Create message file @@ -288,7 +296,7 @@ class ToolEngine: db.session.refresh(message_file) result.append(( - message_file, + message_file.id, message.save_as )) diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index aa184176a1..e30a905cbc 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -154,7 +154,7 @@ class ToolManager: 'invoke_from': invoke_from, 'tool_invoke_from': tool_invoke_from, }) - + elif provider_type == 'api': if tenant_id is None: raise ValueError('tenant id is required for api provider') @@ -201,7 +201,7 @@ class ToolManager: init runtime parameter """ parameter_value = parameters.get(parameter_rule.name) - if not parameter_value: + if not parameter_value and parameter_value != 0: # get default value parameter_value = parameter_rule.default if not parameter_value and parameter_rule.required: @@ -209,7 +209,7 @@ class ToolManager: if parameter_rule.type == ToolParameter.ToolParameterType.SELECT: # check if tool_parameter_config in options - options = list(map(lambda x: x.value, parameter_rule.options)) + options = [x.value for x in parameter_rule.options] if parameter_value is not None and parameter_value not in options: raise ValueError( f"tool parameter {parameter_rule.name} value {parameter_value} not in options {options}") @@ -321,14 +321,14 @@ class ToolManager: if cls._builtin_providers_loaded: yield from list(cls._builtin_providers.values()) return - + with cls._builtin_provider_lock: if cls._builtin_providers_loaded: yield from list(cls._builtin_providers.values()) return - + yield from cls._list_builtin_providers() - + @classmethod def _list_builtin_providers(cls) -> Generator[BuiltinToolProviderController, None, None]: """ @@ -492,7 +492,7 @@ class ToolManager: controller = ApiToolProviderController.from_db( provider, - ApiProviderAuthType.API_KEY if provider.credentials['auth_type'] == 'api_key' else + ApiProviderAuthType.API_KEY if provider.credentials['auth_type'] == 'api_key' else ApiProviderAuthType.NONE ) controller.load_bundled_tools(provider.tools) diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py index 40ae6c66d5..f711f7c9f3 100644 --- a/api/core/tools/utils/parser.py +++ b/api/core/tools/utils/parser.py @@ -21,10 +21,7 @@ class ApiBasedToolSchemaParser: extra_info = extra_info if extra_info is not None else {} # set description to extra_info - if 'description' in openapi['info']: - extra_info['description'] = openapi['info']['description'] - else: - extra_info['description'] = '' + extra_info['description'] = openapi['info'].get('description', '') if len(openapi['servers']) == 0: raise ToolProviderNotFoundError('No server found in the openapi yaml.') @@ -95,8 +92,8 @@ class ApiBasedToolSchemaParser: # parse body parameters if 'schema' in interface['operation']['requestBody']['content'][content_type]: body_schema = interface['operation']['requestBody']['content'][content_type]['schema'] - required = body_schema['required'] if 'required' in body_schema else [] - properties = body_schema['properties'] if 'properties' in body_schema else {} + required = body_schema.get('required', []) + properties = body_schema.get('properties', {}) for name, property in properties.items(): tool = ToolParameter( name=name, @@ -105,14 +102,14 @@ class ApiBasedToolSchemaParser: zh_Hans=name ), human_description=I18nObject( - en_US=property['description'] if 'description' in property else '', - zh_Hans=property['description'] if 'description' in property else '' + en_US=property.get('description', ''), + zh_Hans=property.get('description', '') ), type=ToolParameter.ToolParameterType.STRING, required=name in required, form=ToolParameter.ToolParameterForm.LLM, - llm_description=property['description'] if 'description' in property else '', - default=property['default'] if 'default' in property else None, + llm_description=property.get('description', ''), + default=property.get('default', None), ) # check if there is a type @@ -149,7 +146,7 @@ class ApiBasedToolSchemaParser: server_url=server_url + interface['path'], method=interface['method'], summary=interface['operation']['description'] if 'description' in interface['operation'] else - interface['operation']['summary'] if 'summary' in interface['operation'] else None, + interface['operation'].get('summary', None), operation_id=interface['operation']['operationId'], parameters=parameters, author='', diff --git a/api/core/tools/utils/tool_parameter_converter.py b/api/core/tools/utils/tool_parameter_converter.py index 55535be930..0c4ec00ec6 100644 --- a/api/core/tools/utils/tool_parameter_converter.py +++ b/api/core/tools/utils/tool_parameter_converter.py @@ -58,7 +58,8 @@ class ToolParameterConverter: return float(value) else: return int(value) - + case ToolParameter.ToolParameterType.FILE: + return value case _: return str(value) diff --git a/api/core/tools/utils/web_reader_tool.py b/api/core/tools/utils/web_reader_tool.py index 4c69c6eddc..1e7eb129a7 100644 --- a/api/core/tools/utils/web_reader_tool.py +++ b/api/core/tools/utils/web_reader_tool.py @@ -283,7 +283,7 @@ def strip_control_characters(text): # [Cn]: Other, Not Assigned # [Co]: Other, Private Use # [Cs]: Other, Surrogate - control_chars = set(['Cc', 'Cf', 'Cn', 'Co', 'Cs']) + control_chars = {'Cc', 'Cf', 'Cn', 'Co', 'Cs'} retained_chars = ['\t', '\n', '\r', '\f'] # Remove non-printing control characters diff --git a/api/core/workflow/nodes/code/code_node.py b/api/core/workflow/nodes/code/code_node.py index 610a23e704..e15c1c6f87 100644 --- a/api/core/workflow/nodes/code/code_node.py +++ b/api/core/workflow/nodes/code/code_node.py @@ -1,6 +1,6 @@ -import os from typing import Optional, Union, cast +from configs import dify_config from core.helper.code_executor.code_executor import CodeExecutionException, CodeExecutor, CodeLanguage from core.helper.code_executor.code_node_provider import CodeNodeProvider from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider @@ -11,14 +11,14 @@ from core.workflow.nodes.base_node import BaseNode from core.workflow.nodes.code.entities import CodeNodeData from models.workflow import WorkflowNodeExecutionStatus -MAX_NUMBER = int(os.environ.get('CODE_MAX_NUMBER', '9223372036854775807')) -MIN_NUMBER = int(os.environ.get('CODE_MIN_NUMBER', '-9223372036854775808')) +MAX_NUMBER = dify_config.CODE_MAX_NUMBER +MIN_NUMBER = dify_config.CODE_MIN_NUMBER MAX_PRECISION = 20 MAX_DEPTH = 5 -MAX_STRING_LENGTH = int(os.environ.get('CODE_MAX_STRING_LENGTH', '80000')) -MAX_STRING_ARRAY_LENGTH = int(os.environ.get('CODE_MAX_STRING_ARRAY_LENGTH', '30')) -MAX_OBJECT_ARRAY_LENGTH = int(os.environ.get('CODE_MAX_OBJECT_ARRAY_LENGTH', '30')) -MAX_NUMBER_ARRAY_LENGTH = int(os.environ.get('CODE_MAX_NUMBER_ARRAY_LENGTH', '1000')) +MAX_STRING_LENGTH = dify_config.CODE_MAX_STRING_LENGTH +MAX_STRING_ARRAY_LENGTH = dify_config.CODE_MAX_STRING_ARRAY_LENGTH +MAX_OBJECT_ARRAY_LENGTH = dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH +MAX_NUMBER_ARRAY_LENGTH = dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH class CodeNode(BaseNode): diff --git a/api/core/workflow/nodes/http_request/entities.py b/api/core/workflow/nodes/http_request/entities.py index 00d72a8b0a..65451452c8 100644 --- a/api/core/workflow/nodes/http_request/entities.py +++ b/api/core/workflow/nodes/http_request/entities.py @@ -1,57 +1,61 @@ -import os from typing import Literal, Optional, Union from pydantic import BaseModel, ValidationInfo, field_validator +from configs import dify_config from core.workflow.entities.base_node_data_entities import BaseNodeData -MAX_CONNECT_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_CONNECT_TIMEOUT', '300')) -MAX_READ_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_READ_TIMEOUT', '600')) -MAX_WRITE_TIMEOUT = int(os.environ.get('HTTP_REQUEST_MAX_WRITE_TIMEOUT', '600')) +MAX_CONNECT_TIMEOUT = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT +MAX_READ_TIMEOUT = dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT +MAX_WRITE_TIMEOUT = dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT + + +class HttpRequestNodeAuthorizationConfig(BaseModel): + type: Literal[None, 'basic', 'bearer', 'custom'] + api_key: Union[None, str] = None + header: Union[None, str] = None + + +class HttpRequestNodeAuthorization(BaseModel): + type: Literal['no-auth', 'api-key'] + config: Optional[HttpRequestNodeAuthorizationConfig] = None + + @field_validator('config', mode='before') + @classmethod + def check_config(cls, v: HttpRequestNodeAuthorizationConfig, values: ValidationInfo): + """ + Check config, if type is no-auth, config should be None, otherwise it should be a dict. + """ + if values.data['type'] == 'no-auth': + return None + else: + if not v or not isinstance(v, dict): + raise ValueError('config should be a dict') + + return v + + +class HttpRequestNodeBody(BaseModel): + type: Literal['none', 'form-data', 'x-www-form-urlencoded', 'raw-text', 'json'] + data: Union[None, str] = None + + +class HttpRequestNodeTimeout(BaseModel): + connect: int = MAX_CONNECT_TIMEOUT + read: int = MAX_READ_TIMEOUT + write: int = MAX_WRITE_TIMEOUT + class HttpRequestNodeData(BaseNodeData): """ Code Node Data. """ - class Authorization(BaseModel): - # TODO[pydantic]: The `Config` class inherits from another class, please create the `model_config` manually. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - class Config(BaseModel): - type: Literal[None, 'basic', 'bearer', 'custom'] - api_key: Union[None, str] = None - header: Union[None, str] = None - - type: Literal['no-auth', 'api-key'] - config: Optional[Config] = None - - @field_validator('config', mode='before') - @classmethod - def check_config(cls, v: Config, values: ValidationInfo): - """ - Check config, if type is no-auth, config should be None, otherwise it should be a dict. - """ - if values.data['type'] == 'no-auth': - return None - else: - if not v or not isinstance(v, dict): - raise ValueError('config should be a dict') - - return v - - class Body(BaseModel): - type: Literal['none', 'form-data', 'x-www-form-urlencoded', 'raw-text', 'json'] - data: Union[None, str] = None - - class Timeout(BaseModel): - connect: Optional[int] = MAX_CONNECT_TIMEOUT - read: Optional[int] = MAX_READ_TIMEOUT - write: Optional[int] = MAX_WRITE_TIMEOUT method: Literal['get', 'post', 'put', 'patch', 'delete', 'head'] url: str - authorization: Authorization + authorization: HttpRequestNodeAuthorization headers: str params: str - body: Optional[Body] = None - timeout: Optional[Timeout] = None + body: Optional[HttpRequestNodeBody] = None + timeout: Optional[HttpRequestNodeTimeout] = None mask_authorization_header: Optional[bool] = True diff --git a/api/core/workflow/nodes/http_request/http_executor.py b/api/core/workflow/nodes/http_request/http_executor.py index 74a6c5b9de..902d821e40 100644 --- a/api/core/workflow/nodes/http_request/http_executor.py +++ b/api/core/workflow/nodes/http_request/http_executor.py @@ -1,35 +1,36 @@ import json -import os from copy import deepcopy from random import randint from typing import Any, Optional, Union from urllib.parse import urlencode import httpx -import requests import core.helper.ssrf_proxy as ssrf_proxy +from configs import dify_config from core.workflow.entities.variable_entities import VariableSelector from core.workflow.entities.variable_pool import ValueType, VariablePool -from core.workflow.nodes.http_request.entities import HttpRequestNodeData +from core.workflow.nodes.http_request.entities import ( + HttpRequestNodeAuthorization, + HttpRequestNodeBody, + HttpRequestNodeData, + HttpRequestNodeTimeout, +) from core.workflow.utils.variable_template_parser import VariableTemplateParser -MAX_BINARY_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_BINARY_SIZE', 1024 * 1024 * 10)) # 10MB -READABLE_MAX_BINARY_SIZE = f'{MAX_BINARY_SIZE / 1024 / 1024:.2f}MB' -MAX_TEXT_SIZE = int(os.environ.get('HTTP_REQUEST_NODE_MAX_TEXT_SIZE', 1024 * 1024)) # 1MB -READABLE_MAX_TEXT_SIZE = f'{MAX_TEXT_SIZE / 1024 / 1024:.2f}MB' +MAX_BINARY_SIZE = dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE +READABLE_MAX_BINARY_SIZE = dify_config.HTTP_REQUEST_NODE_READABLE_MAX_BINARY_SIZE +MAX_TEXT_SIZE = dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE +READABLE_MAX_TEXT_SIZE = dify_config.HTTP_REQUEST_NODE_READABLE_MAX_TEXT_SIZE class HttpExecutorResponse: headers: dict[str, str] - response: Union[httpx.Response, requests.Response] + response: httpx.Response - def __init__(self, response: Union[httpx.Response, requests.Response] = None): - self.headers = {} - if isinstance(response, httpx.Response | requests.Response): - for k, v in response.headers.items(): - self.headers[k] = v + def __init__(self, response: httpx.Response): self.response = response + self.headers = dict(response.headers) if isinstance(self.response, httpx.Response) else {} @property def is_file(self) -> bool: @@ -42,10 +43,7 @@ class HttpExecutorResponse: return any(v in content_type for v in file_content_types) def get_content_type(self) -> str: - if 'content-type' in self.headers: - return self.headers.get('content-type') - else: - return self.headers.get('Content-Type') or "" + return self.headers.get('content-type', '') def extract_file(self) -> tuple[str, bytes]: """ @@ -58,46 +56,31 @@ class HttpExecutorResponse: @property def content(self) -> str: - """ - get content - """ - if isinstance(self.response, httpx.Response | requests.Response): + if isinstance(self.response, httpx.Response): return self.response.text else: raise ValueError(f'Invalid response type {type(self.response)}') @property def body(self) -> bytes: - """ - get body - """ - if isinstance(self.response, httpx.Response | requests.Response): + if isinstance(self.response, httpx.Response): return self.response.content else: raise ValueError(f'Invalid response type {type(self.response)}') @property def status_code(self) -> int: - """ - get status code - """ - if isinstance(self.response, httpx.Response | requests.Response): + if isinstance(self.response, httpx.Response): return self.response.status_code else: raise ValueError(f'Invalid response type {type(self.response)}') @property def size(self) -> int: - """ - get size - """ return len(self.body) @property def readable_size(self) -> str: - """ - get readable size - """ if self.size < 1024: return f'{self.size} bytes' elif self.size < 1024 * 1024: @@ -109,17 +92,21 @@ class HttpExecutorResponse: class HttpExecutor: server_url: str method: str - authorization: HttpRequestNodeData.Authorization + authorization: HttpRequestNodeAuthorization params: dict[str, Any] headers: dict[str, Any] body: Union[None, str] files: Union[None, dict[str, Any]] boundary: str variable_selectors: list[VariableSelector] - timeout: HttpRequestNodeData.Timeout + timeout: HttpRequestNodeTimeout - def __init__(self, node_data: HttpRequestNodeData, timeout: HttpRequestNodeData.Timeout, - variable_pool: Optional[VariablePool] = None): + def __init__( + self, + node_data: HttpRequestNodeData, + timeout: HttpRequestNodeTimeout, + variable_pool: Optional[VariablePool] = None, + ): self.server_url = node_data.url self.method = node_data.method self.authorization = node_data.authorization @@ -134,11 +121,11 @@ class HttpExecutor: self._init_template(node_data, variable_pool) @staticmethod - def _is_json_body(body: HttpRequestNodeData.Body): + def _is_json_body(body: HttpRequestNodeBody): """ check if body is json """ - if body and body.type == 'json': + if body and body.type == 'json' and body.data: try: json.loads(body.data) return True @@ -148,13 +135,9 @@ class HttpExecutor: return False @staticmethod - def _to_dict(convert_item: str, convert_text: str, maxsplit: int = -1): + def _to_dict(convert_text: str): """ Convert the string like `aa:bb\n cc:dd` to dict `{aa:bb, cc:dd}` - :param convert_item: A label for what item to be converted, params, headers or body. - :param convert_text: The string containing key-value pairs separated by '\n'. - :param maxsplit: The maximum number of splits allowed for the ':' character in each key-value pair. Default is -1 (no limit). - :return: A dictionary containing the key-value pairs from the input string. """ kv_paris = convert_text.split('\n') result = {} @@ -162,30 +145,25 @@ class HttpExecutor: if not kv.strip(): continue - kv = kv.split(':', maxsplit=maxsplit) - if len(kv) >= 3: - k, v = kv[0], ":".join(kv[1:]) - elif len(kv) == 2: - k, v = kv - elif len(kv) == 1: + kv = kv.split(':', maxsplit=1) + if len(kv) == 1: k, v = kv[0], '' else: - raise ValueError(f'Invalid {convert_item} {kv}') + k, v = kv result[k.strip()] = v return result def _init_template(self, node_data: HttpRequestNodeData, variable_pool: Optional[VariablePool] = None): - # extract all template in url self.server_url, server_url_variable_selectors = self._format_template(node_data.url, variable_pool) # extract all template in params params, params_variable_selectors = self._format_template(node_data.params, variable_pool) - self.params = self._to_dict("params", params) + self.params = self._to_dict(params) # extract all template in headers headers, headers_variable_selectors = self._format_template(node_data.headers, variable_pool) - self.headers = self._to_dict("headers", headers) + self.headers = self._to_dict(headers) # extract all template in body body_data_variable_selectors = [] @@ -197,18 +175,17 @@ class HttpExecutor: if body_data: body_data, body_data_variable_selectors = self._format_template(body_data, variable_pool, is_valid_json) - if node_data.body.type == 'json': + content_type_is_set = any(key.lower() == 'content-type' for key in self.headers) + if node_data.body.type == 'json' and not content_type_is_set: self.headers['Content-Type'] = 'application/json' - elif node_data.body.type == 'x-www-form-urlencoded': + elif node_data.body.type == 'x-www-form-urlencoded' and not content_type_is_set: self.headers['Content-Type'] = 'application/x-www-form-urlencoded' if node_data.body.type in ['form-data', 'x-www-form-urlencoded']: - body = self._to_dict("body", body_data, 1) + body = self._to_dict(body_data) if node_data.body.type == 'form-data': - self.files = { - k: ('', v) for k, v in body.items() - } + self.files = {k: ('', v) for k, v in body.items()} random_str = lambda n: ''.join([chr(randint(97, 122)) for _ in range(n)]) self.boundary = f'----WebKitFormBoundary{random_str(16)}' @@ -220,13 +197,24 @@ class HttpExecutor: elif node_data.body.type == 'none': self.body = '' - self.variable_selectors = (server_url_variable_selectors + params_variable_selectors - + headers_variable_selectors + body_data_variable_selectors) + self.variable_selectors = ( + server_url_variable_selectors + + params_variable_selectors + + headers_variable_selectors + + body_data_variable_selectors + ) def _assembling_headers(self) -> dict[str, Any]: authorization = deepcopy(self.authorization) headers = deepcopy(self.headers) or {} if self.authorization.type == 'api-key': + if self.authorization.config is None: + raise ValueError('self.authorization config is required') + if authorization.config is None: + raise ValueError('authorization config is required') + if authorization.config.header is None: + raise ValueError('authorization config header is required') + if self.authorization.config.api_key is None: raise ValueError('api_key is required') @@ -242,11 +230,11 @@ class HttpExecutor: return headers - def _validate_and_parse_response(self, response: Union[httpx.Response, requests.Response]) -> HttpExecutorResponse: + def _validate_and_parse_response(self, response: httpx.Response) -> HttpExecutorResponse: """ - validate the response + validate the response """ - if isinstance(response, httpx.Response | requests.Response): + if isinstance(response, httpx.Response): executor_response = HttpExecutorResponse(response) else: raise ValueError(f'Invalid response type {type(response)}') @@ -254,29 +242,29 @@ class HttpExecutor: if executor_response.is_file: if executor_response.size > MAX_BINARY_SIZE: raise ValueError( - f'File size is too large, max size is {READABLE_MAX_BINARY_SIZE}, but current size is {executor_response.readable_size}.') + f'File size is too large, max size is {READABLE_MAX_BINARY_SIZE}, but current size is {executor_response.readable_size}.' + ) else: if executor_response.size > MAX_TEXT_SIZE: raise ValueError( - f'Text size is too large, max size is {READABLE_MAX_TEXT_SIZE}, but current size is {executor_response.readable_size}.') + f'Text size is too large, max size is {READABLE_MAX_TEXT_SIZE}, but current size is {executor_response.readable_size}.' + ) return executor_response def _do_http_request(self, headers: dict[str, Any]) -> httpx.Response: """ - do http request depending on api bundle + do http request depending on api bundle """ kwargs = { 'url': self.server_url, 'headers': headers, 'params': self.params, 'timeout': (self.timeout.connect, self.timeout.read, self.timeout.write), - 'follow_redirects': True + 'follow_redirects': True, } - if self.method in ('get', 'head', 'options'): - response = getattr(ssrf_proxy, self.method)(**kwargs) - elif self.method in ('post', 'put', 'delete', 'patch'): + if self.method in ('get', 'head', 'post', 'put', 'delete', 'patch'): response = getattr(ssrf_proxy, self.method)(data=self.body, files=self.files, **kwargs) else: raise ValueError(f'Invalid http method {self.method}') @@ -336,8 +324,9 @@ class HttpExecutor: return raw_request - def _format_template(self, template: str, variable_pool: VariablePool, escape_quotes: bool = False) \ - -> tuple[str, list[VariableSelector]]: + def _format_template( + self, template: str, variable_pool: Optional[VariablePool], escape_quotes: bool = False + ) -> tuple[str, list[VariableSelector]]: """ format template """ @@ -348,14 +337,13 @@ class HttpExecutor: variable_value_mapping = {} for variable_selector in variable_selectors: value = variable_pool.get_variable_value( - variable_selector=variable_selector.value_selector, - target_value_type=ValueType.STRING + variable_selector=variable_selector.value_selector, target_value_type=ValueType.STRING ) if value is None: raise ValueError(f'Variable {variable_selector.variable} not found') - if escape_quotes: + if escape_quotes and isinstance(value, str): value = value.replace('"', '\\"') variable_value_mapping[variable_selector.variable] = value diff --git a/api/core/workflow/nodes/http_request/http_request_node.py b/api/core/workflow/nodes/http_request/http_request_node.py index 276c02f62d..24acf984f2 100644 --- a/api/core/workflow/nodes/http_request/http_request_node.py +++ b/api/core/workflow/nodes/http_request/http_request_node.py @@ -5,6 +5,7 @@ from typing import cast from core.file.file_obj import FileTransferMethod, FileType, FileVar from core.tools.tool_file_manager import ToolFileManager +from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.node_entities import NodeRunResult, NodeType from core.workflow.entities.variable_pool import VariablePool from core.workflow.nodes.base_node import BaseNode @@ -13,49 +14,50 @@ from core.workflow.nodes.http_request.entities import ( MAX_READ_TIMEOUT, MAX_WRITE_TIMEOUT, HttpRequestNodeData, + HttpRequestNodeTimeout, ) from core.workflow.nodes.http_request.http_executor import HttpExecutor, HttpExecutorResponse from models.workflow import WorkflowNodeExecutionStatus -HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeData.Timeout(connect=min(10, MAX_CONNECT_TIMEOUT), - read=min(60, MAX_READ_TIMEOUT), - write=min(20, MAX_WRITE_TIMEOUT)) +HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout( + connect=min(10, MAX_CONNECT_TIMEOUT), + read=min(60, MAX_READ_TIMEOUT), + write=min(20, MAX_WRITE_TIMEOUT), +) class HttpRequestNode(BaseNode): _node_data_cls = HttpRequestNodeData - node_type = NodeType.HTTP_REQUEST + _node_type = NodeType.HTTP_REQUEST @classmethod - def get_default_config(cls) -> dict: + def get_default_config(cls, filters: dict | None = None) -> dict: return { - "type": "http-request", - "config": { - "method": "get", - "authorization": { - "type": "no-auth", + 'type': 'http-request', + 'config': { + 'method': 'get', + 'authorization': { + 'type': 'no-auth', }, - "body": { - "type": "none" - }, - "timeout": { + 'body': {'type': 'none'}, + 'timeout': { **HTTP_REQUEST_DEFAULT_TIMEOUT.model_dump(), - "max_connect_timeout": MAX_CONNECT_TIMEOUT, - "max_read_timeout": MAX_READ_TIMEOUT, - "max_write_timeout": MAX_WRITE_TIMEOUT, - } + 'max_connect_timeout': MAX_CONNECT_TIMEOUT, + 'max_read_timeout': MAX_READ_TIMEOUT, + 'max_write_timeout': MAX_WRITE_TIMEOUT, + }, }, } def _run(self, variable_pool: VariablePool) -> NodeRunResult: - node_data: HttpRequestNodeData = cast(self._node_data_cls, self.node_data) + node_data: HttpRequestNodeData = cast(HttpRequestNodeData, self.node_data) # init http executor http_executor = None try: - http_executor = HttpExecutor(node_data=node_data, - timeout=self._get_request_timeout(node_data), - variable_pool=variable_pool) + http_executor = HttpExecutor( + node_data=node_data, timeout=self._get_request_timeout(node_data), variable_pool=variable_pool + ) # invoke http executor response = http_executor.invoke() @@ -70,7 +72,7 @@ class HttpRequestNode(BaseNode): return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, error=str(e), - process_data=process_data + process_data=process_data, ) files = self.extract_files(http_executor.server_url, response) @@ -85,34 +87,32 @@ class HttpRequestNode(BaseNode): }, process_data={ 'request': http_executor.to_raw_request( - mask_authorization_header=node_data.mask_authorization_header + mask_authorization_header=node_data.mask_authorization_header, ), - } + }, ) - def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeData.Timeout: + def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout: timeout = node_data.timeout if timeout is None: return HTTP_REQUEST_DEFAULT_TIMEOUT - if timeout.connect is None: - timeout.connect = HTTP_REQUEST_DEFAULT_TIMEOUT.connect + timeout.connect = timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect timeout.connect = min(timeout.connect, MAX_CONNECT_TIMEOUT) - if timeout.read is None: - timeout.read = HTTP_REQUEST_DEFAULT_TIMEOUT.read + timeout.read = timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read timeout.read = min(timeout.read, MAX_READ_TIMEOUT) - if timeout.write is None: - timeout.write = HTTP_REQUEST_DEFAULT_TIMEOUT.write + timeout.write = timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write timeout.write = min(timeout.write, MAX_WRITE_TIMEOUT) return timeout @classmethod - def _extract_variable_selector_to_variable_mapping(cls, node_data: HttpRequestNodeData) -> dict[str, list[str]]: + def _extract_variable_selector_to_variable_mapping(cls, node_data: BaseNodeData) -> dict[str, list[str]]: """ Extract variable selector to variable mapping :param node_data: node data :return: """ + node_data = cast(HttpRequestNodeData, node_data) try: http_executor = HttpExecutor(node_data=node_data, timeout=HTTP_REQUEST_DEFAULT_TIMEOUT) @@ -124,7 +124,7 @@ class HttpRequestNode(BaseNode): return variable_mapping except Exception as e: - logging.exception(f"Failed to extract variable selector to variable mapping: {e}") + logging.exception(f'Failed to extract variable selector to variable mapping: {e}') return {} def extract_files(self, url: str, response: HttpExecutorResponse) -> list[FileVar]: @@ -144,21 +144,23 @@ class HttpRequestNode(BaseNode): extension = guess_extension(mimetype) or '.bin' tool_file = ToolFileManager.create_file_by_raw( - user_id=self.user_id, - tenant_id=self.tenant_id, - conversation_id=None, - file_binary=file_binary, + user_id=self.user_id, + tenant_id=self.tenant_id, + conversation_id=None, + file_binary=file_binary, mimetype=mimetype, ) - files.append(FileVar( - tenant_id=self.tenant_id, - type=FileType.IMAGE, - transfer_method=FileTransferMethod.TOOL_FILE, - related_id=tool_file.id, - filename=filename, - extension=extension, - mime_type=mimetype, - )) + files.append( + FileVar( + tenant_id=self.tenant_id, + type=FileType.IMAGE, + transfer_method=FileTransferMethod.TOOL_FILE, + related_id=tool_file.id, + filename=filename, + extension=extension, + mime_type=mimetype, + ) + ) return files 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 8fceb3404a..d219156026 100644 --- a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py +++ b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py @@ -66,36 +66,34 @@ class ParameterExtractorNode(LLMNode): } } - def _run(self, variable_pool: VariablePool) -> NodeRunResult: """ Run the node. """ - node_data = cast(ParameterExtractorNodeData, self.node_data) query = variable_pool.get_variable_value(node_data.query) if not query: - raise ValueError("Query not found") - - inputs={ + raise ValueError("Input variable content not found or is empty") + + inputs = { 'query': query, 'parameters': jsonable_encoder(node_data.parameters), 'instruction': jsonable_encoder(node_data.instruction), } - + model_instance, model_config = self._fetch_model_config(node_data.model) if not isinstance(model_instance.model_type_instance, LargeLanguageModel): raise ValueError("Model is not a Large Language Model") - + llm_model = model_instance.model_type_instance model_schema = llm_model.get_model_schema(model_config.model, model_config.credentials) if not model_schema: raise ValueError("Model schema not found") - + # fetch memory memory = self._fetch_memory(node_data.memory, variable_pool, model_instance) - - if set(model_schema.features or []) & set([ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL]) \ + + if set(model_schema.features or []) & {ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL} \ and node_data.reasoning_mode == 'function_call': # use function call prompt_messages, prompt_message_tools = self._generate_function_call_prompt( @@ -103,7 +101,8 @@ class ParameterExtractorNode(LLMNode): ) else: # use prompt engineering - prompt_messages = self._generate_prompt_engineering_prompt(node_data, query, variable_pool, model_config, memory) + prompt_messages = self._generate_prompt_engineering_prompt(node_data, query, variable_pool, model_config, + memory) prompt_message_tools = [] process_data = { @@ -132,7 +131,7 @@ class ParameterExtractorNode(LLMNode): return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, inputs=inputs, - process_data={}, + process_data=process_data, outputs={ '__is_success': 0, '__reason': str(e) @@ -202,7 +201,7 @@ class ParameterExtractorNode(LLMNode): # handle invoke result if not isinstance(invoke_result, LLMResult): raise ValueError(f"Invalid invoke result: {invoke_result}") - + text = invoke_result.message.content usage = invoke_result.usage tool_call = invoke_result.message.tool_calls[0] if invoke_result.message.tool_calls else None @@ -212,21 +211,23 @@ class ParameterExtractorNode(LLMNode): return text, usage, tool_call - def _generate_function_call_prompt(self, - node_data: ParameterExtractorNodeData, - query: str, - variable_pool: VariablePool, - model_config: ModelConfigWithCredentialsEntity, - memory: Optional[TokenBufferMemory], - ) -> tuple[list[PromptMessage], list[PromptMessageTool]]: + def _generate_function_call_prompt(self, + node_data: ParameterExtractorNodeData, + query: str, + variable_pool: VariablePool, + model_config: ModelConfigWithCredentialsEntity, + memory: Optional[TokenBufferMemory], + ) -> tuple[list[PromptMessage], list[PromptMessageTool]]: """ Generate function call prompt. """ - query = FUNCTION_CALLING_EXTRACTOR_USER_TEMPLATE.format(content=query, structure=json.dumps(node_data.get_parameter_json_schema())) + query = FUNCTION_CALLING_EXTRACTOR_USER_TEMPLATE.format(content=query, structure=json.dumps( + node_data.get_parameter_json_schema())) prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '') - prompt_template = self._get_function_calling_prompt_template(node_data, query, variable_pool, memory, rest_token) + prompt_template = self._get_function_calling_prompt_template(node_data, query, variable_pool, memory, + rest_token) prompt_messages = prompt_transform.get_prompt( prompt_template=prompt_template, inputs={}, @@ -259,8 +260,8 @@ class ParameterExtractorNode(LLMNode): function=AssistantPromptMessage.ToolCall.ToolCallFunction( name=example['assistant']['function_call']['name'], arguments=json.dumps(example['assistant']['function_call']['parameters'] - ) - )) + ) + )) ] ), ToolPromptMessage( @@ -273,8 +274,8 @@ class ParameterExtractorNode(LLMNode): ]) prompt_messages = prompt_messages[:last_user_message_idx] + \ - example_messages + prompt_messages[last_user_message_idx:] - + example_messages + prompt_messages[last_user_message_idx:] + # generate tool tool = PromptMessageTool( name=FUNCTION_CALLING_EXTRACTOR_NAME, @@ -284,13 +285,13 @@ class ParameterExtractorNode(LLMNode): return prompt_messages, [tool] - def _generate_prompt_engineering_prompt(self, - data: ParameterExtractorNodeData, - query: str, - variable_pool: VariablePool, - model_config: ModelConfigWithCredentialsEntity, - memory: Optional[TokenBufferMemory], - ) -> list[PromptMessage]: + def _generate_prompt_engineering_prompt(self, + data: ParameterExtractorNodeData, + query: str, + variable_pool: VariablePool, + model_config: ModelConfigWithCredentialsEntity, + memory: Optional[TokenBufferMemory], + ) -> list[PromptMessage]: """ Generate prompt engineering prompt. """ @@ -308,18 +309,19 @@ class ParameterExtractorNode(LLMNode): raise ValueError(f"Invalid model mode: {model_mode}") def _generate_prompt_engineering_completion_prompt(self, - node_data: ParameterExtractorNodeData, - query: str, - variable_pool: VariablePool, - model_config: ModelConfigWithCredentialsEntity, - memory: Optional[TokenBufferMemory], - ) -> list[PromptMessage]: + node_data: ParameterExtractorNodeData, + query: str, + variable_pool: VariablePool, + model_config: ModelConfigWithCredentialsEntity, + memory: Optional[TokenBufferMemory], + ) -> list[PromptMessage]: """ Generate completion prompt. """ prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '') - prompt_template = self._get_prompt_engineering_prompt_template(node_data, query, variable_pool, memory, rest_token) + prompt_template = self._get_prompt_engineering_prompt_template(node_data, query, variable_pool, memory, + rest_token) prompt_messages = prompt_transform.get_prompt( prompt_template=prompt_template, inputs={ @@ -336,23 +338,23 @@ class ParameterExtractorNode(LLMNode): return prompt_messages def _generate_prompt_engineering_chat_prompt(self, - node_data: ParameterExtractorNodeData, - query: str, - variable_pool: VariablePool, - model_config: ModelConfigWithCredentialsEntity, - memory: Optional[TokenBufferMemory], - ) -> list[PromptMessage]: + node_data: ParameterExtractorNodeData, + query: str, + variable_pool: VariablePool, + model_config: ModelConfigWithCredentialsEntity, + memory: Optional[TokenBufferMemory], + ) -> list[PromptMessage]: """ Generate chat prompt. """ prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) rest_token = self._calculate_rest_token(node_data, query, variable_pool, model_config, '') prompt_template = self._get_prompt_engineering_prompt_template( - node_data, + node_data, CHAT_GENERATE_JSON_USER_MESSAGE_TEMPLATE.format( structure=json.dumps(node_data.get_parameter_json_schema()), text=query - ), + ), variable_pool, memory, rest_token ) @@ -363,7 +365,7 @@ class ParameterExtractorNode(LLMNode): files=[], context='', memory_config=node_data.memory, - memory=memory, + memory=None, model_config=model_config ) @@ -387,7 +389,7 @@ class ParameterExtractorNode(LLMNode): ]) prompt_messages = prompt_messages[:last_user_message_idx] + \ - example_messages + prompt_messages[last_user_message_idx:] + example_messages + prompt_messages[last_user_message_idx:] return prompt_messages @@ -397,23 +399,23 @@ class ParameterExtractorNode(LLMNode): """ if len(data.parameters) != len(result): raise ValueError("Invalid number of parameters") - + for parameter in data.parameters: if parameter.required and parameter.name not in result: raise ValueError(f"Parameter {parameter.name} is required") - + if parameter.type == 'select' and parameter.options and result.get(parameter.name) not in parameter.options: raise ValueError(f"Invalid `select` value for parameter {parameter.name}") - + if parameter.type == 'number' and not isinstance(result.get(parameter.name), int | float): raise ValueError(f"Invalid `number` value for parameter {parameter.name}") - + if parameter.type == 'bool' and not isinstance(result.get(parameter.name), bool): raise ValueError(f"Invalid `bool` value for parameter {parameter.name}") - + if parameter.type == 'string' and not isinstance(result.get(parameter.name), str): raise ValueError(f"Invalid `string` value for parameter {parameter.name}") - + if parameter.type.startswith('array'): if not isinstance(result.get(parameter.name), list): raise ValueError(f"Invalid `array` value for parameter {parameter.name}") @@ -499,6 +501,7 @@ class ParameterExtractorNode(LLMNode): """ Extract complete json response. """ + def extract_json(text): """ From a given JSON started from '{' or '[' extract the complete JSON object. @@ -515,11 +518,11 @@ class ParameterExtractorNode(LLMNode): if (c == '}' and stack[-1] == '{') or (c == ']' and stack[-1] == '['): stack.pop() if not stack: - return text[:i+1] + return text[:i + 1] else: return text[:i] return None - + # extract json from the text for idx in range(len(result)): if result[idx] == '{' or result[idx] == '[': @@ -536,9 +539,9 @@ class ParameterExtractorNode(LLMNode): """ if not tool_call or not tool_call.function.arguments: return None - + return json.loads(tool_call.function.arguments) - + def _generate_default_result(self, data: ParameterExtractorNodeData) -> dict: """ Generate default result. @@ -551,7 +554,7 @@ class ParameterExtractorNode(LLMNode): result[parameter.name] = False elif parameter.type in ['string', 'select']: result[parameter.name] = '' - + return result def _render_instruction(self, instruction: str, variable_pool: VariablePool) -> str: @@ -562,13 +565,13 @@ class ParameterExtractorNode(LLMNode): inputs = {} for selector in variable_template_parser.extract_variable_selectors(): inputs[selector.variable] = variable_pool.get_variable_value(selector.value_selector) - + return variable_template_parser.format(inputs) def _get_function_calling_prompt_template(self, node_data: ParameterExtractorNodeData, query: str, - variable_pool: VariablePool, - memory: Optional[TokenBufferMemory], - max_token_limit: int = 2000) \ + variable_pool: VariablePool, + memory: Optional[TokenBufferMemory], + max_token_limit: int = 2000) \ -> list[ChatModelMessage]: model_mode = ModelMode.value_of(node_data.model.mode) input_text = query @@ -590,12 +593,12 @@ class ParameterExtractorNode(LLMNode): return [system_prompt_messages, user_prompt_message] else: raise ValueError(f"Model mode {model_mode} not support.") - + def _get_prompt_engineering_prompt_template(self, node_data: ParameterExtractorNodeData, query: str, variable_pool: VariablePool, memory: Optional[TokenBufferMemory], max_token_limit: int = 2000) \ - -> list[ChatModelMessage]: + -> list[ChatModelMessage]: model_mode = ModelMode.value_of(node_data.model.mode) input_text = query @@ -620,8 +623,8 @@ class ParameterExtractorNode(LLMNode): text=COMPLETION_GENERATE_JSON_PROMPT.format(histories=memory_str, text=input_text, instruction=instruction) - .replace('{γγγ', '') - .replace('}γγγ', '') + .replace('{γγγ', '') + .replace('}γγγ', '') ) else: raise ValueError(f"Model mode {model_mode} not support.") @@ -635,13 +638,13 @@ class ParameterExtractorNode(LLMNode): model_instance, model_config = self._fetch_model_config(node_data.model) if not isinstance(model_instance.model_type_instance, LargeLanguageModel): raise ValueError("Model is not a Large Language Model") - + llm_model = model_instance.model_type_instance model_schema = llm_model.get_model_schema(model_config.model, model_config.credentials) if not model_schema: raise ValueError("Model schema not found") - if set(model_schema.features or []) & set([ModelFeature.MULTI_TOOL_CALL, ModelFeature.MULTI_TOOL_CALL]): + if set(model_schema.features or []) & {ModelFeature.MULTI_TOOL_CALL, ModelFeature.MULTI_TOOL_CALL}: prompt_template = self._get_function_calling_prompt_template(node_data, query, variable_pool, None, 2000) else: prompt_template = self._get_prompt_engineering_prompt_template(node_data, query, variable_pool, None, 2000) @@ -667,7 +670,7 @@ class ParameterExtractorNode(LLMNode): model_config.model, model_config.credentials, prompt_messages - ) + 1000 # add 1000 to ensure tool call messages + ) + 1000 # add 1000 to ensure tool call messages max_tokens = 0 for parameter_rule in model_config.model_schema.parameter_rules: @@ -680,8 +683,9 @@ class ParameterExtractorNode(LLMNode): rest_tokens = max(rest_tokens, 0) return rest_tokens - - def _fetch_model_config(self, node_data_model: ModelConfig) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: + + def _fetch_model_config(self, node_data_model: ModelConfig) -> tuple[ + ModelInstance, ModelConfigWithCredentialsEntity]: """ Fetch model config. """ @@ -689,9 +693,10 @@ class ParameterExtractorNode(LLMNode): self._model_instance, self._model_config = super()._fetch_model_config(node_data_model) return self._model_instance, self._model_config - + @classmethod - def _extract_variable_selector_to_variable_mapping(cls, node_data: ParameterExtractorNodeData) -> dict[str, list[str]]: + def _extract_variable_selector_to_variable_mapping(cls, node_data: ParameterExtractorNodeData) -> dict[ + str, list[str]]: """ Extract variable selector to variable mapping :param node_data: node data @@ -708,4 +713,4 @@ class ParameterExtractorNode(LLMNode): for selector in variable_template_parser.extract_variable_selectors(): variable_mapping[selector.variable] = selector.value_selector - return variable_mapping \ No newline at end of file + return variable_mapping diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 2a472fc8d2..cddea03bf8 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -74,13 +74,14 @@ class ToolNode(BaseNode): ) # convert tool messages - plain_text, files = self._convert_tool_messages(messages) + plain_text, files, json = self._convert_tool_messages(messages) return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={ 'text': plain_text, - 'files': files + 'files': files, + 'json': json }, metadata={ NodeRunMetadataKey.TOOL_INFO: tool_info @@ -149,8 +150,9 @@ class ToolNode(BaseNode): # extract plain text and files files = self._extract_tool_response_binary(messages) plain_text = self._extract_tool_response_text(messages) + json = self._extract_tool_response_json(messages) - return plain_text, files + return plain_text, files, json def _extract_tool_response_binary(self, tool_response: list[ToolInvokeMessage]) -> list[FileVar]: """ @@ -172,6 +174,7 @@ class ToolNode(BaseNode): tenant_id=self.tenant_id, type=FileType.IMAGE, transfer_method=FileTransferMethod.TOOL_FILE, + url=url, related_id=tool_file_id, filename=filename, extension=ext, @@ -203,7 +206,9 @@ class ToolNode(BaseNode): f'Link: {message.message}' if message.type == ToolInvokeMessage.MessageType.LINK else '' for message in tool_response ]) - + + def _extract_tool_response_json(self, tool_response: list[ToolInvokeMessage]) -> list[dict]: + return [message.message for message in tool_response if message.type == ToolInvokeMessage.MessageType.JSON] @classmethod def _extract_variable_selector_to_variable_mapping(cls, node_data: ToolNodeData) -> dict[str, list[str]]: diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 7b1f1dfc03..e74c6c2406 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -9,7 +9,7 @@ fi if [[ "${MODE}" == "worker" ]]; then celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} -c ${CELERY_WORKER_AMOUNT:-1} --loglevel INFO \ - -Q ${CELERY_QUEUES:-dataset,generation,mail} + -Q ${CELERY_QUEUES:-dataset,generation,mail,ops_trace,app_deletion} elif [[ "${MODE}" == "beat" ]]; then celery -A app.celery beat --loglevel INFO else diff --git a/api/events/app_event.py b/api/events/app_event.py index 3a975958fc..67a5982527 100644 --- a/api/events/app_event.py +++ b/api/events/app_event.py @@ -3,9 +3,6 @@ from blinker import signal # sender: app app_was_created = signal('app-was-created') -# sender: app -app_was_deleted = signal('app-was-deleted') - # sender: app, kwargs: app_model_config app_model_config_was_updated = signal('app-model-config-was-updated') diff --git a/api/events/event_handlers/__init__.py b/api/events/event_handlers/__init__.py index c82b8a92d9..7ee7146d09 100644 --- a/api/events/event_handlers/__init__.py +++ b/api/events/event_handlers/__init__.py @@ -4,10 +4,7 @@ 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_messaeg_created import handle -from .delete_installed_app_when_app_deleted import handle -from .delete_site_record_when_app_deleted import handle from .delete_tool_parameters_cache_when_sync_draft_workflow import handle -from .delete_workflow_as_tool_when_app_deleted 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_messaeg_created import handle diff --git a/api/events/event_handlers/delete_installed_app_when_app_deleted.py b/api/events/event_handlers/delete_installed_app_when_app_deleted.py deleted file mode 100644 index 1d6271a466..0000000000 --- a/api/events/event_handlers/delete_installed_app_when_app_deleted.py +++ /dev/null @@ -1,12 +0,0 @@ -from events.app_event import app_was_deleted -from extensions.ext_database import db -from models.model import InstalledApp - - -@app_was_deleted.connect -def handle(sender, **kwargs): - app = sender - installed_apps = db.session.query(InstalledApp).filter(InstalledApp.app_id == app.id).all() - for installed_app in installed_apps: - db.session.delete(installed_app) - db.session.commit() diff --git a/api/events/event_handlers/delete_site_record_when_app_deleted.py b/api/events/event_handlers/delete_site_record_when_app_deleted.py deleted file mode 100644 index 2e476d3d53..0000000000 --- a/api/events/event_handlers/delete_site_record_when_app_deleted.py +++ /dev/null @@ -1,11 +0,0 @@ -from events.app_event import app_was_deleted -from extensions.ext_database import db -from models.model import Site - - -@app_was_deleted.connect -def handle(sender, **kwargs): - app = sender - site = db.session.query(Site).filter(Site.app_id == app.id).first() - db.session.delete(site) - db.session.commit() diff --git a/api/events/event_handlers/delete_workflow_as_tool_when_app_deleted.py b/api/events/event_handlers/delete_workflow_as_tool_when_app_deleted.py deleted file mode 100644 index 0c56688ff6..0000000000 --- a/api/events/event_handlers/delete_workflow_as_tool_when_app_deleted.py +++ /dev/null @@ -1,14 +0,0 @@ -from events.app_event import app_was_deleted -from extensions.ext_database import db -from models.tools import WorkflowToolProvider - - -@app_was_deleted.connect -def handle(sender, **kwargs): - app = sender - workflow_tools = db.session.query(WorkflowToolProvider).filter( - WorkflowToolProvider.app_id == app.id - ).all() - for workflow_tool in workflow_tools: - db.session.delete(workflow_tool) - db.session.commit() diff --git a/api/extensions/ext_storage.py b/api/extensions/ext_storage.py index 130f2ea69d..38db1c6ce1 100644 --- a/api/extensions/ext_storage.py +++ b/api/extensions/ext_storage.py @@ -7,6 +7,7 @@ from extensions.storage.aliyun_storage import AliyunStorage from extensions.storage.azure_storage import AzureStorage from extensions.storage.google_storage import GoogleStorage from extensions.storage.local_storage import LocalStorage +from extensions.storage.oci_storage import OCIStorage from extensions.storage.s3_storage import S3Storage from extensions.storage.tencent_storage import TencentStorage @@ -37,6 +38,10 @@ class Storage: self.storage_runner = TencentStorage( app=app ) + elif storage_type == 'oci-storage': + self.storage_runner = OCIStorage( + app=app + ) else: self.storage_runner = LocalStorage(app=app) diff --git a/api/extensions/storage/oci_storage.py b/api/extensions/storage/oci_storage.py new file mode 100644 index 0000000000..e78d870950 --- /dev/null +++ b/api/extensions/storage/oci_storage.py @@ -0,0 +1,64 @@ +from collections.abc import Generator +from contextlib import closing + +import boto3 +from botocore.exceptions import ClientError +from flask import Flask + +from extensions.storage.base_storage import BaseStorage + + +class OCIStorage(BaseStorage): + def __init__(self, app: Flask): + super().__init__(app) + app_config = self.app.config + self.bucket_name = app_config.get('OCI_BUCKET_NAME') + self.client = boto3.client( + 's3', + aws_secret_access_key=app_config.get('OCI_SECRET_KEY'), + aws_access_key_id=app_config.get('OCI_ACCESS_KEY'), + endpoint_url=app_config.get('OCI_ENDPOINT'), + region_name=app_config.get('OCI_REGION') + ) + + def save(self, filename, data): + self.client.put_object(Bucket=self.bucket_name, Key=filename, Body=data) + + def load_once(self, filename: str) -> bytes: + try: + with closing(self.client) as client: + data = client.get_object(Bucket=self.bucket_name, Key=filename)['Body'].read() + except ClientError as ex: + if ex.response['Error']['Code'] == 'NoSuchKey': + raise FileNotFoundError("File not found") + else: + raise + return data + + def load_stream(self, filename: str) -> Generator: + def generate(filename: str = filename) -> Generator: + try: + with closing(self.client) as client: + response = client.get_object(Bucket=self.bucket_name, Key=filename) + yield from response['Body'].iter_chunks() + except ClientError as ex: + if ex.response['Error']['Code'] == 'NoSuchKey': + raise FileNotFoundError("File not found") + else: + raise + return generate() + + def download(self, filename, target_filepath): + with closing(self.client) as client: + client.download_file(self.bucket_name, filename, target_filepath) + + def exists(self, filename): + with closing(self.client) as client: + try: + client.head_object(Bucket=self.bucket_name, Key=filename) + return True + except: + return False + + def delete(self, filename): + self.client.delete_object(Bucket=self.bucket_name, Key=filename) \ No newline at end of file diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index e314fa21a3..83045f5c64 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -50,6 +50,7 @@ app_detail_fields = { 'enable_site': fields.Boolean, 'enable_api': fields.Boolean, 'model_config': fields.Nested(model_config_fields, attribute='app_model_config', allow_null=True), + 'tracing': fields.Raw, 'created_at': TimestampField } @@ -110,6 +111,8 @@ site_fields = { 'icon_background': fields.String, 'description': fields.String, 'default_language': fields.String, + 'chat_color_theme': fields.String, + 'chat_color_theme_inverted': fields.Boolean, 'customize_domain': fields.String, 'copyright': fields.String, 'privacy_policy': fields.String, diff --git a/api/libs/helper.py b/api/libs/helper.py index ebabb2ea47..335c6688f4 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -1,18 +1,23 @@ import json +import logging import random import re import string import subprocess +import time import uuid from collections.abc import Generator from datetime import datetime from hashlib import sha256 -from typing import Union +from typing import Any, Optional, Union from zoneinfo import available_timezones -from flask import Response, stream_with_context +from flask import Response, current_app, stream_with_context from flask_restful import fields +from extensions.ext_redis import redis_client +from models.account import Account + def run(script): return subprocess.getstatusoutput('source /root/.bashrc && ' + script) @@ -46,12 +51,12 @@ def uuid_value(value): error = ('{value} is not a valid uuid.' .format(value=value)) raise ValueError(error) - + def alphanumeric(value: str): # check if the value is alphanumeric and underlined if re.match(r'^[a-zA-Z0-9_]+$', value): return value - + raise ValueError(f'{value} is not a valid alphanumeric value') def timestamp_value(timestamp): @@ -163,3 +168,97 @@ def compact_generate_response(response: Union[dict, Generator]) -> Response: return Response(stream_with_context(generate()), status=200, mimetype='text/event-stream') + + +class TokenManager: + + @classmethod + def generate_token(cls, account: Account, token_type: str, additional_data: dict = None) -> str: + old_token = cls._get_current_token_for_account(account.id, token_type) + if old_token: + if isinstance(old_token, bytes): + old_token = old_token.decode('utf-8') + cls.revoke_token(old_token, token_type) + + token = str(uuid.uuid4()) + token_data = { + 'account_id': account.id, + 'email': account.email, + 'token_type': token_type + } + if additional_data: + token_data.update(additional_data) + + expiry_hours = current_app.config[f'{token_type.upper()}_TOKEN_EXPIRY_HOURS'] + token_key = cls._get_token_key(token, token_type) + redis_client.setex( + token_key, + expiry_hours * 60 * 60, + json.dumps(token_data) + ) + + cls._set_current_token_for_account(account.id, token, token_type, expiry_hours) + return token + + @classmethod + def _get_token_key(cls, token: str, token_type: str) -> str: + return f'{token_type}:token:{token}' + + @classmethod + def revoke_token(cls, token: str, token_type: str): + token_key = cls._get_token_key(token, token_type) + redis_client.delete(token_key) + + @classmethod + def get_token_data(cls, token: str, token_type: str) -> Optional[dict[str, Any]]: + key = cls._get_token_key(token, token_type) + token_data_json = redis_client.get(key) + if token_data_json is None: + logging.warning(f"{token_type} token {token} not found with key {key}") + return None + token_data = json.loads(token_data_json) + return token_data + + @classmethod + def _get_current_token_for_account(cls, account_id: str, token_type: str) -> Optional[str]: + key = cls._get_account_token_key(account_id, token_type) + current_token = redis_client.get(key) + return current_token + + @classmethod + def _set_current_token_for_account(cls, account_id: str, token: str, token_type: str, expiry_hours: int): + key = cls._get_account_token_key(account_id, token_type) + redis_client.setex(key, expiry_hours * 60 * 60, token) + + @classmethod + def _get_account_token_key(cls, account_id: str, token_type: str) -> str: + return f'{token_type}:account:{account_id}' + + +class RateLimiter: + def __init__(self, prefix: str, max_attempts: int, time_window: int): + self.prefix = prefix + self.max_attempts = max_attempts + self.time_window = time_window + + def _get_key(self, email: str) -> str: + return f"{self.prefix}:{email}" + + def is_rate_limited(self, email: str) -> bool: + key = self._get_key(email) + current_time = int(time.time()) + window_start_time = current_time - self.time_window + + redis_client.zremrangebyscore(key, '-inf', window_start_time) + attempts = redis_client.zcard(key) + + if attempts and int(attempts) >= self.max_attempts: + return True + return False + + def increment_rate_limit(self, email: str): + key = self._get_key(email) + current_time = int(time.time()) + + redis_client.zadd(key, {current_time: current_time}) + redis_client.expire(key, self.time_window * 2) diff --git a/api/libs/oauth_data_source.py b/api/libs/oauth_data_source.py index 3f2889adbe..a5c7814a54 100644 --- a/api/libs/oauth_data_source.py +++ b/api/libs/oauth_data_source.py @@ -246,10 +246,7 @@ class NotionOAuth(OAuthDataSource): } response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) response_json = response.json() - if 'results' in response_json: - results = response_json['results'] - else: - results = [] + results = response_json.get('results', []) return results def notion_block_parent_page_id(self, access_token: str, block_id: str): @@ -293,8 +290,5 @@ class NotionOAuth(OAuthDataSource): } response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) response_json = response.json() - if 'results' in response_json: - results = response_json['results'] - else: - results = [] + results = response_json.get('results', []) return results diff --git a/api/migrations/versions/04c602f5dc9b_update_appmodelconfig_and_add_table_.py b/api/migrations/versions/04c602f5dc9b_update_appmodelconfig_and_add_table_.py new file mode 100644 index 0000000000..be2c615525 --- /dev/null +++ b/api/migrations/versions/04c602f5dc9b_update_appmodelconfig_and_add_table_.py @@ -0,0 +1,43 @@ +"""update AppModelConfig and add table TracingAppConfig + +Revision ID: 04c602f5dc9b +Revises: 4e99a8df00ff +Create Date: 2024-06-12 07:49:07.666510 + +""" +import sqlalchemy as sa +from alembic import op + +import models as models + +# revision identifiers, used by Alembic. +revision = '04c602f5dc9b' +down_revision = '4ff534e1eb11' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tracing_app_configs', + sa.Column('id', models.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('app_id', models.StringUUID(), nullable=False), + sa.Column('tracing_provider', sa.String(length=255), nullable=True), + sa.Column('tracing_config', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id', name='tracing_app_config_pkey') + ) + with op.batch_alter_table('tracing_app_configs', schema=None) as batch_op: + batch_op.create_index('tracing_app_config_app_id_idx', ['app_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ## + with op.batch_alter_table('tracing_app_configs', schema=None) as batch_op: + batch_op.drop_index('tracing_app_config_app_id_idx') + + op.drop_table('tracing_app_configs') + # ### end Alembic commands ### diff --git a/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py b/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py new file mode 100644 index 0000000000..09ef5e186c --- /dev/null +++ b/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py @@ -0,0 +1,39 @@ +"""add app tracing + +Revision ID: 2a3aebbbf4bb +Revises: c031d46af369 +Create Date: 2024-06-17 10:08:54.803701 + +""" +import sqlalchemy as sa +from alembic import op + +import models as models + +# revision identifiers, used by Alembic. +revision = '2a3aebbbf4bb' +down_revision = 'c031d46af369' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('apps', schema=None) as batch_op: + batch_op.add_column(sa.Column('tracing', sa.Text(), nullable=True)) + + with op.batch_alter_table('trace_app_config', schema=None) as batch_op: + batch_op.create_index('tracing_app_config_app_id_idx', ['app_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('trace_app_config', schema=None) as batch_op: + batch_op.drop_index('tracing_app_config_app_id_idx') + + with op.batch_alter_table('apps', schema=None) as batch_op: + batch_op.drop_column('tracing') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/63f9175e515b_merge_branches.py b/api/migrations/versions/63f9175e515b_merge_branches.py new file mode 100644 index 0000000000..0623659941 --- /dev/null +++ b/api/migrations/versions/63f9175e515b_merge_branches.py @@ -0,0 +1,22 @@ +"""merge branches + +Revision ID: 63f9175e515b +Revises: 2a3aebbbf4bb, b69ca54b9208 +Create Date: 2024-06-26 09:46:36.573505 + +""" +import models as models + +# revision identifiers, used by Alembic. +revision = '63f9175e515b' +down_revision = ('2a3aebbbf4bb', 'b69ca54b9208') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/api/migrations/versions/b2602e131636_add_workflow_run_id_index_for_message.py b/api/migrations/versions/b2602e131636_add_workflow_run_id_index_for_message.py new file mode 100644 index 0000000000..c9a6a5a5a7 --- /dev/null +++ b/api/migrations/versions/b2602e131636_add_workflow_run_id_index_for_message.py @@ -0,0 +1,32 @@ +"""add workflow_run_id index for message + +Revision ID: b2602e131636 +Revises: 63f9175e515b +Create Date: 2024-06-29 12:16:51.646346 + +""" +from alembic import op + +import models as models + +# revision identifiers, used by Alembic. +revision = 'b2602e131636' +down_revision = '63f9175e515b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('messages', schema=None) as batch_op: + batch_op.create_index('message_workflow_run_id_idx', ['conversation_id', 'workflow_run_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('messages', schema=None) as batch_op: + batch_op.drop_index('message_workflow_run_id_idx') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py b/api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py new file mode 100644 index 0000000000..dd5a7495e4 --- /dev/null +++ b/api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py @@ -0,0 +1,35 @@ +"""add chatbot color theme + +Revision ID: b69ca54b9208 +Revises: 4ff534e1eb11 +Create Date: 2024-06-25 01:14:21.523873 + +""" +import sqlalchemy as sa +from alembic import op + +import models as models + +# revision identifiers, used by Alembic. +revision = 'b69ca54b9208' +down_revision = '4ff534e1eb11' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.add_column(sa.Column('chat_color_theme', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('chat_color_theme_inverted', sa.Boolean(), server_default=sa.text('false'), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.drop_column('chat_color_theme_inverted') + batch_op.drop_column('chat_color_theme') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py b/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py new file mode 100644 index 0000000000..1ac44d083a --- /dev/null +++ b/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py @@ -0,0 +1,59 @@ +"""remove app model config trace config and rename trace app config + +Revision ID: c031d46af369 +Revises: 04c602f5dc9b +Create Date: 2024-06-17 10:01:00.255189 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +import models as models + +# revision identifiers, used by Alembic. +revision = 'c031d46af369' +down_revision = '04c602f5dc9b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('trace_app_config', + sa.Column('id', models.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('app_id', models.StringUUID(), nullable=False), + sa.Column('tracing_provider', sa.String(length=255), nullable=True), + sa.Column('tracing_config', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('is_active', sa.Boolean(), server_default=sa.text('true'), nullable=False), + sa.PrimaryKeyConstraint('id', name='trace_app_config_pkey') + ) + with op.batch_alter_table('trace_app_config', schema=None) as batch_op: + batch_op.create_index('trace_app_config_app_id_idx', ['app_id'], unique=False) + + with op.batch_alter_table('tracing_app_configs', schema=None) as batch_op: + batch_op.drop_index('tracing_app_config_app_id_idx') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tracing_app_configs', + sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('app_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('tracing_provider', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('tracing_config', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('updated_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='trace_app_config_pkey') + ) + with op.batch_alter_table('tracing_app_configs', schema=None) as batch_op: + batch_op.create_index('trace_app_config_app_id_idx', ['app_id'], unique=False) + + with op.batch_alter_table('trace_app_config', schema=None) as batch_op: + batch_op.drop_index('trace_app_config_app_id_idx') + + op.drop_table('trace_app_config') + # ### end Alembic commands ### diff --git a/api/models/account.py b/api/models/account.py index 4911757b07..3b258c4c82 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -153,8 +153,7 @@ class Tenant(db.Model): created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) - def get_accounts(self) -> list[db.Model]: - Account = db.Model + def get_accounts(self) -> list[Account]: return db.session.query(Account).filter( Account.id == TenantAccountJoin.account_id, TenantAccountJoin.tenant_id == self.id diff --git a/api/models/dataset.py b/api/models/dataset.py index 757a5bf8de..672c2be8fa 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -352,6 +352,101 @@ class Document(db.Model): return DocumentSegment.query.with_entities(func.coalesce(func.sum(DocumentSegment.hit_count))) \ .filter(DocumentSegment.document_id == self.id).scalar() + def to_dict(self): + return { + 'id': self.id, + 'tenant_id': self.tenant_id, + 'dataset_id': self.dataset_id, + 'position': self.position, + 'data_source_type': self.data_source_type, + 'data_source_info': self.data_source_info, + 'dataset_process_rule_id': self.dataset_process_rule_id, + 'batch': self.batch, + 'name': self.name, + 'created_from': self.created_from, + 'created_by': self.created_by, + 'created_api_request_id': self.created_api_request_id, + 'created_at': self.created_at, + 'processing_started_at': self.processing_started_at, + 'file_id': self.file_id, + 'word_count': self.word_count, + 'parsing_completed_at': self.parsing_completed_at, + 'cleaning_completed_at': self.cleaning_completed_at, + 'splitting_completed_at': self.splitting_completed_at, + 'tokens': self.tokens, + 'indexing_latency': self.indexing_latency, + 'completed_at': self.completed_at, + 'is_paused': self.is_paused, + 'paused_by': self.paused_by, + 'paused_at': self.paused_at, + 'error': self.error, + 'stopped_at': self.stopped_at, + 'indexing_status': self.indexing_status, + 'enabled': self.enabled, + 'disabled_at': self.disabled_at, + 'disabled_by': self.disabled_by, + 'archived': self.archived, + 'archived_reason': self.archived_reason, + 'archived_by': self.archived_by, + 'archived_at': self.archived_at, + 'updated_at': self.updated_at, + 'doc_type': self.doc_type, + 'doc_metadata': self.doc_metadata, + 'doc_form': self.doc_form, + 'doc_language': self.doc_language, + 'display_status': self.display_status, + 'data_source_info_dict': self.data_source_info_dict, + 'average_segment_length': self.average_segment_length, + 'dataset_process_rule': self.dataset_process_rule.to_dict() if self.dataset_process_rule else None, + 'dataset': self.dataset.to_dict() if self.dataset else None, + 'segment_count': self.segment_count, + 'hit_count': self.hit_count + } + + @classmethod + def from_dict(cls, data: dict): + return cls( + id=data.get('id'), + tenant_id=data.get('tenant_id'), + dataset_id=data.get('dataset_id'), + position=data.get('position'), + data_source_type=data.get('data_source_type'), + data_source_info=data.get('data_source_info'), + dataset_process_rule_id=data.get('dataset_process_rule_id'), + batch=data.get('batch'), + name=data.get('name'), + created_from=data.get('created_from'), + created_by=data.get('created_by'), + created_api_request_id=data.get('created_api_request_id'), + created_at=data.get('created_at'), + processing_started_at=data.get('processing_started_at'), + file_id=data.get('file_id'), + word_count=data.get('word_count'), + parsing_completed_at=data.get('parsing_completed_at'), + cleaning_completed_at=data.get('cleaning_completed_at'), + splitting_completed_at=data.get('splitting_completed_at'), + tokens=data.get('tokens'), + indexing_latency=data.get('indexing_latency'), + completed_at=data.get('completed_at'), + is_paused=data.get('is_paused'), + paused_by=data.get('paused_by'), + paused_at=data.get('paused_at'), + error=data.get('error'), + stopped_at=data.get('stopped_at'), + indexing_status=data.get('indexing_status'), + enabled=data.get('enabled'), + disabled_at=data.get('disabled_at'), + disabled_by=data.get('disabled_by'), + archived=data.get('archived'), + archived_reason=data.get('archived_reason'), + archived_by=data.get('archived_by'), + archived_at=data.get('archived_at'), + updated_at=data.get('updated_at'), + doc_type=data.get('doc_type'), + doc_metadata=data.get('doc_metadata'), + doc_form=data.get('doc_form'), + doc_language=data.get('doc_language') + ) class DocumentSegment(db.Model): __tablename__ = 'document_segments' diff --git a/api/models/model.py b/api/models/model.py index 3024be0b4c..f59e8ebb7c 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -6,7 +6,7 @@ from typing import Optional from flask import current_app, request from flask_login import UserMixin -from sqlalchemy import Float, text +from sqlalchemy import Float, func, text from core.file.tool_file_parser import ToolFileParser from core.file.upload_file_parser import UploadFileParser @@ -73,6 +73,7 @@ class App(db.Model): is_demo = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) is_public = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) is_universal = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) + tracing = db.Column(db.Text, nullable=True) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) @@ -265,7 +266,7 @@ class AppModelConfig(db.Model): @property def retriever_resource_dict(self) -> dict: return json.loads(self.retriever_resource) if self.retriever_resource \ - else {"enabled": False} + else {"enabled": True} @property def annotation_reply_dict(self) -> dict: @@ -625,6 +626,7 @@ class Message(db.Model): db.Index('message_conversation_id_idx', 'conversation_id'), db.Index('message_end_user_idx', 'app_id', 'from_source', 'from_end_user_id'), db.Index('message_account_idx', 'app_id', 'from_source', 'from_account_id'), + db.Index('message_workflow_run_id_idx', 'conversation_id', 'workflow_run_id') ) id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()')) @@ -837,6 +839,49 @@ class Message(db.Model): return None + def to_dict(self) -> dict: + return { + 'id': self.id, + 'app_id': self.app_id, + 'conversation_id': self.conversation_id, + 'inputs': self.inputs, + 'query': self.query, + 'message': self.message, + 'answer': self.answer, + 'status': self.status, + 'error': self.error, + 'message_metadata': self.message_metadata_dict, + 'from_source': self.from_source, + 'from_end_user_id': self.from_end_user_id, + 'from_account_id': self.from_account_id, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'agent_based': self.agent_based, + 'workflow_run_id': self.workflow_run_id + } + + @classmethod + def from_dict(cls, data: dict): + return cls( + id=data['id'], + app_id=data['app_id'], + conversation_id=data['conversation_id'], + inputs=data['inputs'], + query=data['query'], + message=data['message'], + answer=data['answer'], + status=data['status'], + error=data['error'], + message_metadata=json.dumps(data['message_metadata']), + from_source=data['from_source'], + from_end_user_id=data['from_end_user_id'], + from_account_id=data['from_account_id'], + created_at=data['created_at'], + updated_at=data['updated_at'], + agent_based=data['agent_based'], + workflow_run_id=data['workflow_run_id'] + ) + class MessageFeedback(db.Model): __tablename__ = 'message_feedbacks' @@ -1041,6 +1086,8 @@ class Site(db.Model): icon_background = db.Column(db.String(255)) description = db.Column(db.Text) default_language = db.Column(db.String(255), nullable=False) + chat_color_theme = db.Column(db.String(255)) + chat_color_theme_inverted = db.Column(db.Boolean, nullable=False, server_default=db.text('false')) copyright = db.Column(db.String(255)) privacy_policy = db.Column(db.String(255)) show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text('true')) @@ -1328,3 +1375,38 @@ class TagBinding(db.Model): target_id = db.Column(StringUUID, nullable=True) created_by = db.Column(StringUUID, nullable=False) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) + + +class TraceAppConfig(db.Model): + __tablename__ = 'trace_app_config' + __table_args__ = ( + db.PrimaryKeyConstraint('id', name='tracing_app_config_pkey'), + db.Index('tracing_app_config_app_id_idx', 'app_id'), + ) + + id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()')) + app_id = db.Column(StringUUID, nullable=False) + tracing_provider = db.Column(db.String(255), nullable=True) + tracing_config = db.Column(db.JSON, nullable=True) + created_at = db.Column(db.DateTime, nullable=False, server_default=func.now()) + updated_at = db.Column(db.DateTime, nullable=False, server_default=func.now(), onupdate=func.now()) + is_active = db.Column(db.Boolean, nullable=False, server_default=db.text('true')) + + @property + def tracing_config_dict(self): + return self.tracing_config if self.tracing_config else {} + + @property + def tracing_config_str(self): + return json.dumps(self.tracing_config_dict) + + def to_dict(self): + return { + 'id': self.id, + 'app_id': self.app_id, + 'tracing_provider': self.tracing_provider, + 'tracing_config': self.tracing_config_dict, + "is_active": self.is_active, + "created_at": self.created_at.__str__() if self.created_at else None, + 'updated_at': self.updated_at.__str__() if self.updated_at else None, + } diff --git a/api/models/workflow.py b/api/models/workflow.py index d9bc784878..2d6491032b 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -324,6 +324,55 @@ class WorkflowRun(db.Model): def workflow(self): return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first() + def to_dict(self): + return { + '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, + 'version': self.version, + 'graph': self.graph_dict, + 'inputs': self.inputs_dict, + 'status': self.status, + 'outputs': self.outputs_dict, + 'error': self.error, + 'elapsed_time': self.elapsed_time, + 'total_tokens': self.total_tokens, + 'total_steps': self.total_steps, + 'created_by_role': self.created_by_role, + 'created_by': self.created_by, + 'created_at': self.created_at, + 'finished_at': self.finished_at, + } + + @classmethod + def from_dict(cls, data: dict) -> 'WorkflowRun': + return cls( + 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'), + version=data.get('version'), + graph=json.dumps(data.get('graph')), + inputs=json.dumps(data.get('inputs')), + status=data.get('status'), + outputs=json.dumps(data.get('outputs')), + error=data.get('error'), + elapsed_time=data.get('elapsed_time'), + total_tokens=data.get('total_tokens'), + total_steps=data.get('total_steps'), + created_by_role=data.get('created_by_role'), + created_by=data.get('created_by'), + created_at=data.get('created_at'), + finished_at=data.get('finished_at'), + ) + class WorkflowNodeExecutionTriggeredFrom(Enum): """ diff --git a/api/poetry.lock b/api/poetry.lock index 89140ce75e..ea99ae09d5 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -126,13 +126,13 @@ frozenlist = ">=1.1.0" [[package]] name = "alembic" -version = "1.13.1" +version = "1.13.2" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.8" files = [ - {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, - {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, ] [package.dependencies] @@ -534,41 +534,41 @@ files = [ [[package]] name = "boto3" -version = "1.28.17" +version = "1.34.136" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.7" +python-versions = ">=3.8" files = [ - {file = "boto3-1.28.17-py3-none-any.whl", hash = "sha256:bca0526f819e0f19c0f1e6eba3e2d1d6b6a92a45129f98c0d716e5aab6d9444b"}, - {file = "boto3-1.28.17.tar.gz", hash = "sha256:90f7cfb5e1821af95b1fc084bc50e6c47fa3edc99f32de1a2591faa0c546bea7"}, + {file = "boto3-1.34.136-py3-none-any.whl", hash = "sha256:d41037e2c680ab8d6c61a0a4ee6bf1fdd9e857f43996672830a95d62d6f6fa79"}, + {file = "boto3-1.34.136.tar.gz", hash = "sha256:0314e6598f59ee0f34eb4e6d1a0f69fa65c146d2b88a6e837a527a9956ec2731"}, ] [package.dependencies] -botocore = ">=1.31.17,<1.32.0" +botocore = ">=1.34.136,<1.35.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.6.0,<0.7.0" +s3transfer = ">=0.10.0,<0.11.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.85" +version = "1.34.139" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.7" +python-versions = ">=3.8" files = [ - {file = "botocore-1.31.85-py3-none-any.whl", hash = "sha256:b8f35d65f2b45af50c36fc25cc1844d6bd61d38d2148b2ef133b8f10e198555d"}, - {file = "botocore-1.31.85.tar.gz", hash = "sha256:ce58e688222df73ec5691f934be1a2122a52c9d11d3037b586b3fff16ed6d25f"}, + {file = "botocore-1.34.139-py3-none-any.whl", hash = "sha256:dd1e085d4caa2a4c1b7d83e3bc51416111c8238a35d498e9d3b04f3b63b086ba"}, + {file = "botocore-1.34.139.tar.gz", hash = "sha256:df023d8cf8999d574214dad4645cb90f9d2ccd1494f6ee2b57b1ab7522f6be77"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.12)"] +crt = ["awscrt (==0.20.11)"] [[package]] name = "bottleneck" @@ -865,13 +865,13 @@ zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -1840,33 +1840,32 @@ files = [ [[package]] name = "duckduckgo-search" -version = "6.1.7" +version = "6.1.9" description = "Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine." optional = false python-versions = ">=3.8" files = [ - {file = "duckduckgo_search-6.1.7-py3-none-any.whl", hash = "sha256:ec7d5becb8c392c0293ff9464938c1014896e1e14725c05adc306290a636fab2"}, - {file = "duckduckgo_search-6.1.7.tar.gz", hash = "sha256:c6fd8ba17fe9cd0a4f32e5b96984e959c3da865f9c2864bfcf82bf7ff9b7e8f0"}, + {file = "duckduckgo_search-6.1.9-py3-none-any.whl", hash = "sha256:a208babf87b971290b1afed9908bc5ab6ac6c1738b90b48ad613267f7630cb77"}, + {file = "duckduckgo_search-6.1.9.tar.gz", hash = "sha256:0d7d746e003d6b3bcd0d0dc11927c9a69b6fa271f3b3f65df6f01ea4d9d2689d"}, ] [package.dependencies] click = ">=8.1.7" -orjson = ">=3.10.5" -pyreqwest-impersonate = ">=0.4.8" +pyreqwest-impersonate = ">=0.4.9" [package.extras] -dev = ["mypy (>=1.10.0)", "pytest (>=8.2.2)", "pytest-asyncio (>=0.23.7)", "ruff (>=0.4.8)"] +dev = ["mypy (>=1.10.1)", "pytest (>=8.2.2)", "pytest-asyncio (>=0.23.7)", "ruff (>=0.5.0)"] lxml = ["lxml (>=5.2.2)"] [[package]] name = "email-validator" -version = "2.1.2" +version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, - {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, ] [package.dependencies] @@ -2057,13 +2056,13 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.15.3" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, - {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] @@ -2082,20 +2081,6 @@ files = [ {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, ] -[[package]] -name = "firecrawl-py" -version = "0.0.5" -description = "Python SDK for Firecrawl API" -optional = false -python-versions = "*" -files = [ - {file = "firecrawl-py-0.0.5.tar.gz", hash = "sha256:3d1cc30b7d86c12aa06e6434ebb526072cd70ab9a0c8b145008efe044a1cd09c"}, - {file = "firecrawl_py-0.0.5-py3-none-any.whl", hash = "sha256:476694345141c0145a1bee9c01a8ad0103f75892c12a122dc511a3adad0785e7"}, -] - -[package.dependencies] -requests = "*" - [[package]] name = "flask" version = "3.0.3" @@ -2245,53 +2230,53 @@ files = [ [[package]] name = "fonttools" -version = "4.53.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, - {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, - {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, - {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, - {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, - {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, - {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, - {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, - {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, - {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, - {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, - {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, - {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, - {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, ] [package.extras] @@ -2438,13 +2423,13 @@ files = [ [[package]] name = "fsspec" -version = "2024.6.0" +version = "2024.6.1" description = "File-system specification" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2024.6.0-py3-none-any.whl", hash = "sha256:58d7122eb8a1a46f7f13453187bfea4972d66bf01618d37366521b1998034cee"}, - {file = "fsspec-2024.6.0.tar.gz", hash = "sha256:f579960a56e6d8038a9efc8f9c77279ec12e6299aa86b0769a7e9c46b94527c2"}, + {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, + {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, ] [package.extras] @@ -2753,13 +2738,13 @@ xai = ["tensorflow (>=2.3.0,<3.0.0dev)"] [[package]] name = "google-cloud-bigquery" -version = "3.24.0" +version = "3.25.0" description = "Google BigQuery API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-bigquery-3.24.0.tar.gz", hash = "sha256:e95e6f6e0aa32e6c453d44e2b3298931fdd7947c309ea329a31b6ff1f939e17e"}, - {file = "google_cloud_bigquery-3.24.0-py2.py3-none-any.whl", hash = "sha256:bc08323ce99dee4e811b7c3d0cde8929f5bf0b1aeaed6bcd75fc89796dd87652"}, + {file = "google-cloud-bigquery-3.25.0.tar.gz", hash = "sha256:5b2aff3205a854481117436836ae1403f11f2594e6810a98886afd57eda28509"}, + {file = "google_cloud_bigquery-3.25.0-py2.py3-none-any.whl", hash = "sha256:7f0c371bc74d2a7fb74dacbc00ac0f90c8c2bec2289b51dd6685a275873b1ce9"}, ] [package.dependencies] @@ -3052,19 +3037,19 @@ test = ["objgraph", "psutil"] [[package]] name = "grpc-google-iam-v1" -version = "0.13.0" +version = "0.13.1" description = "IAM API client library" optional = false python-versions = ">=3.7" files = [ - {file = "grpc-google-iam-v1-0.13.0.tar.gz", hash = "sha256:fad318608b9e093258fbf12529180f400d1c44453698a33509cc6ecf005b294e"}, - {file = "grpc_google_iam_v1-0.13.0-py2.py3-none-any.whl", hash = "sha256:53902e2af7de8df8c1bd91373d9be55b0743ec267a7428ea638db3775becae89"}, + {file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"}, + {file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"}, ] [package.dependencies] googleapis-common-protos = {version = ">=1.56.0,<2.0.0dev", extras = ["grpc"]} grpcio = ">=1.44.0,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" [[package]] name = "grpcio" @@ -3701,6 +3686,20 @@ files = [ {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, ] +[[package]] +name = "jsonpath-ng" +version = "1.6.1" +description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +optional = false +python-versions = "*" +files = [ + {file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"}, + {file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"}, +] + +[package.dependencies] +ply = "*" + [[package]] name = "kaleido" version = "0.2.1" @@ -3901,6 +3900,50 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "langfuse" +version = "2.38.0" +description = "A client library for accessing langfuse" +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "langfuse-2.38.0-py3-none-any.whl", hash = "sha256:9e81757b88d26acb8949dbd1d25153df49f4ce8da39e5347c4aa23c3b9d4559c"}, + {file = "langfuse-2.38.0.tar.gz", hash = "sha256:0022a805a167d2e436759ac48b5efb45db2910b02e2f934e47eaf481ad3b21f5"}, +] + +[package.dependencies] +anyio = ">=4.4.0,<5.0.0" +backoff = ">=1.10.0" +httpx = ">=0.15.4,<1.0" +idna = ">=3.7,<4.0" +packaging = ">=23.2,<24.0" +pydantic = ">=1.10.7,<3.0" +wrapt = ">=1.14,<2.0" + +[package.extras] +langchain = ["langchain (>=0.0.309)"] +llama-index = ["llama-index (>=0.10.12,<2.0.0)"] +openai = ["openai (>=0.27.8)"] + +[[package]] +name = "langsmith" +version = "0.1.83" +description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "langsmith-0.1.83-py3-none-any.whl", hash = "sha256:f54d8cd8479b648b6339f3f735d19292c3516d080f680933ecdca3eab4b67ed3"}, + {file = "langsmith-0.1.83.tar.gz", hash = "sha256:5cdd947212c8ad19adb992c06471c860185a777daa6859bb47150f90daf64bf3"}, +] + +[package.dependencies] +orjson = ">=3.9.14,<4.0.0" +pydantic = [ + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, + {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, +] +requests = ">=2,<3" + [[package]] name = "llvmlite" version = "0.43.0" @@ -4386,13 +4429,13 @@ tests = ["pytest (>=4.6)"] [[package]] name = "msal" -version = "1.28.1" +version = "1.29.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false python-versions = ">=3.7" files = [ - {file = "msal-1.28.1-py3-none-any.whl", hash = "sha256:563c2d70de77a2ca9786aab84cb4e133a38a6897e6676774edc23d610bfc9e7b"}, - {file = "msal-1.28.1.tar.gz", hash = "sha256:d72bbfe2d5c2f2555f4bc6205be4450ddfd12976610dd9a16a9ab0f05c68b64d"}, + {file = "msal-1.29.0-py3-none-any.whl", hash = "sha256:6b301e63f967481f0cc1a3a3bac0cf322b276855bc1b0955468d9deb3f33d511"}, + {file = "msal-1.29.0.tar.gz", hash = "sha256:8f6725f099752553f9b2fe84125e2a5ebe47b49f92eacca33ebedd3a9ebaae25"}, ] [package.dependencies] @@ -4405,22 +4448,18 @@ broker = ["pymsalruntime (>=0.13.2,<0.17)"] [[package]] name = "msal-extensions" -version = "1.1.0" +version = "1.2.0" description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." optional = false python-versions = ">=3.7" files = [ - {file = "msal-extensions-1.1.0.tar.gz", hash = "sha256:6ab357867062db7b253d0bd2df6d411c7891a0ee7308d54d1e4317c1d1c54252"}, - {file = "msal_extensions-1.1.0-py3-none-any.whl", hash = "sha256:01be9711b4c0b1a151450068eeb2c4f0997df3bba085ac299de3a66f585e382f"}, + {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, + {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, ] [package.dependencies] -msal = ">=0.4.1,<2.0.0" -packaging = "*" -portalocker = [ - {version = ">=1.0,<3", markers = "platform_system != \"Windows\""}, - {version = ">=1.6,<3", markers = "platform_system == \"Windows\""}, -] +msal = ">=1.29,<2" +portalocker = ">=1.4,<3" [[package]] name = "msg-parser" @@ -4813,42 +4852,42 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "onnxruntime" -version = "1.18.0" +version = "1.18.1" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.18.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:5a3b7993a5ecf4a90f35542a4757e29b2d653da3efe06cdd3164b91167bbe10d"}, - {file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15b944623b2cdfe7f7945690bfb71c10a4531b51997c8320b84e7b0bb59af902"}, - {file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e61ce5005118064b1a0ed73ebe936bc773a102f067db34108ea6c64dd62a179"}, - {file = "onnxruntime-1.18.0-cp310-cp310-win32.whl", hash = "sha256:a4fc8a2a526eb442317d280610936a9f73deece06c7d5a91e51570860802b93f"}, - {file = "onnxruntime-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:71ed219b768cab004e5cd83e702590734f968679bf93aa488c1a7ffbe6e220c3"}, - {file = "onnxruntime-1.18.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:3d24bd623872a72a7fe2f51c103e20fcca2acfa35d48f2accd6be1ec8633d960"}, - {file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f15e41ca9b307a12550bfd2ec93f88905d9fba12bab7e578f05138ad0ae10d7b"}, - {file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f45ca2887f62a7b847d526965686b2923efa72538c89b7703c7b3fe970afd59"}, - {file = "onnxruntime-1.18.0-cp311-cp311-win32.whl", hash = "sha256:9e24d9ecc8781323d9e2eeda019b4b24babc4d624e7d53f61b1fe1a929b0511a"}, - {file = "onnxruntime-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8608398976ed18aef450d83777ff6f77d0b64eced1ed07a985e1a7db8ea3771"}, - {file = "onnxruntime-1.18.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f1d79941f15fc40b1ee67738b2ca26b23e0181bf0070b5fb2984f0988734698f"}, - {file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e8caf3a8565c853a22d323a3eebc2a81e3de7591981f085a4f74f7a60aab2d"}, - {file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:498d2b8380635f5e6ebc50ec1b45f181588927280f32390fb910301d234f97b8"}, - {file = "onnxruntime-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ba7cc0ce2798a386c082aaa6289ff7e9bedc3dee622eef10e74830cff200a72e"}, - {file = "onnxruntime-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:1fa175bd43f610465d5787ae06050c81f7ce09da2bf3e914eb282cb8eab363ef"}, - {file = "onnxruntime-1.18.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0284c579c20ec8b1b472dd190290a040cc68b6caec790edb960f065d15cf164a"}, - {file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47353d036d8c380558a5643ea5f7964d9d259d31c86865bad9162c3e916d1f6"}, - {file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:885509d2b9ba4b01f08f7fa28d31ee54b6477953451c7ccf124a84625f07c803"}, - {file = "onnxruntime-1.18.0-cp38-cp38-win32.whl", hash = "sha256:8614733de3695656411d71fc2f39333170df5da6c7efd6072a59962c0bc7055c"}, - {file = "onnxruntime-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:47af3f803752fce23ea790fd8d130a47b2b940629f03193f780818622e856e7a"}, - {file = "onnxruntime-1.18.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:9153eb2b4d5bbab764d0aea17adadffcfc18d89b957ad191b1c3650b9930c59f"}, - {file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c7fd86eca727c989bb8d9c5104f3c45f7ee45f445cc75579ebe55d6b99dfd7c"}, - {file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac67a4de9c1326c4d87bcbfb652c923039b8a2446bb28516219236bec3b494f5"}, - {file = "onnxruntime-1.18.0-cp39-cp39-win32.whl", hash = "sha256:6ffb445816d06497df7a6dd424b20e0b2c39639e01e7fe210e247b82d15a23b9"}, - {file = "onnxruntime-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:46de6031cb6745f33f7eca9e51ab73e8c66037fb7a3b6b4560887c5b55ab5d5d"}, + {file = "onnxruntime-1.18.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ef7683312393d4ba04252f1b287d964bd67d5e6048b94d2da3643986c74d80"}, + {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc706eb1df06ddf55776e15a30519fb15dda7697f987a2bbda4962845e3cec05"}, + {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7de69f5ced2a263531923fa68bbec52a56e793b802fcd81a03487b5e292bc3a"}, + {file = "onnxruntime-1.18.1-cp310-cp310-win32.whl", hash = "sha256:221e5b16173926e6c7de2cd437764492aa12b6811f45abd37024e7cf2ae5d7e3"}, + {file = "onnxruntime-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:75211b619275199c861ee94d317243b8a0fcde6032e5a80e1aa9ded8ab4c6060"}, + {file = "onnxruntime-1.18.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f26582882f2dc581b809cfa41a125ba71ad9e715738ec6402418df356969774a"}, + {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36f3a8b768506d02be349ac303fd95d92813ba3ba70304d40c3cd5c25d6a4c"}, + {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:170e711393e0618efa8ed27b59b9de0ee2383bd2a1f93622a97006a5ad48e434"}, + {file = "onnxruntime-1.18.1-cp311-cp311-win32.whl", hash = "sha256:9b6a33419b6949ea34e0dc009bc4470e550155b6da644571ecace4b198b0d88f"}, + {file = "onnxruntime-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c1380a9f1b7788da742c759b6a02ba771fe1ce620519b2b07309decbd1a2fe1"}, + {file = "onnxruntime-1.18.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:31bd57a55e3f983b598675dfc7e5d6f0877b70ec9864b3cc3c3e1923d0a01919"}, + {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9e03c4ba9f734500691a4d7d5b381cd71ee2f3ce80a1154ac8f7aed99d1ecaa"}, + {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:781aa9873640f5df24524f96f6070b8c550c66cb6af35710fd9f92a20b4bfbf6"}, + {file = "onnxruntime-1.18.1-cp312-cp312-win32.whl", hash = "sha256:3a2d9ab6254ca62adbb448222e630dc6883210f718065063518c8f93a32432be"}, + {file = "onnxruntime-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:ad93c560b1c38c27c0275ffd15cd7f45b3ad3fc96653c09ce2931179982ff204"}, + {file = "onnxruntime-1.18.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3b55dc9d3c67626388958a3eb7ad87eb7c70f75cb0f7ff4908d27b8b42f2475c"}, + {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80dbcfb6763cc0177a31168b29b4bd7662545b99a19e211de8c734b657e0669"}, + {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1ff2c61a16d6c8631796c54139bafea41ee7736077a0fc64ee8ae59432f5c58"}, + {file = "onnxruntime-1.18.1-cp38-cp38-win32.whl", hash = "sha256:219855bd272fe0c667b850bf1a1a5a02499269a70d59c48e6f27f9c8bcb25d02"}, + {file = "onnxruntime-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:afdf16aa607eb9a2c60d5ca2d5abf9f448e90c345b6b94c3ed14f4fb7e6a2d07"}, + {file = "onnxruntime-1.18.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:128df253ade673e60cea0955ec9d0e89617443a6d9ce47c2d79eb3f72a3be3de"}, + {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9839491e77e5c5a175cab3621e184d5a88925ee297ff4c311b68897197f4cde9"}, + {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3187c1faff3ac15f7f0e7373ef4788c582cafa655a80fdbb33eaec88976c66"}, + {file = "onnxruntime-1.18.1-cp39-cp39-win32.whl", hash = "sha256:34657c78aa4e0b5145f9188b550ded3af626651b15017bf43d280d7e23dbf195"}, + {file = "onnxruntime-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c14fd97c3ddfa97da5feef595e2c73f14c2d0ec1d4ecbea99c8d96603c89589"}, ] [package.dependencies] coloredlogs = "*" flatbuffers = "*" -numpy = ">=1.21.6" +numpy = ">=1.21.6,<2.0" packaging = "*" protobuf = "*" sympy = "*" @@ -4878,13 +4917,13 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "openpyxl" -version = "3.1.4" +version = "3.1.5" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" optional = false python-versions = ">=3.8" files = [ - {file = "openpyxl-3.1.4-py2.py3-none-any.whl", hash = "sha256:ec17f6483f2b8f7c88c57e5e5d3b0de0e3fb9ac70edc084d28e864f5b33bbefd"}, - {file = "openpyxl-3.1.4.tar.gz", hash = "sha256:8d2c8adf5d20d6ce8f9bca381df86b534835e974ed0156dacefa76f68c1d69fb"}, + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, ] [package.dependencies] @@ -5121,57 +5160,62 @@ cryptography = ">=3.2.1" [[package]] name = "orjson" -version = "3.10.5" +version = "3.10.6" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, - {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, - {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, - {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, - {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, - {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, - {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, - {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, - {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, - {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, - {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, - {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, - {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, - {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, - {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, - {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, + {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, + {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, + {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, + {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, + {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, + {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, + {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, + {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, + {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, + {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, + {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, + {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, + {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, + {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, + {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, ] [[package]] @@ -5205,13 +5249,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "23.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -5341,84 +5385,95 @@ numpy = "*" [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -5484,13 +5539,13 @@ files = [ [[package]] name = "portalocker" -version = "2.8.2" +version = "2.10.0" description = "Wraps the portalocker recipe for easy usage" optional = false python-versions = ">=3.8" files = [ - {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, - {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, + {file = "portalocker-2.10.0-py3-none-any.whl", hash = "sha256:48944147b2cd42520549bc1bb8fe44e220296e56f7c3d551bc6ecce69d9b0de1"}, + {file = "portalocker-2.10.0.tar.gz", hash = "sha256:49de8bc0a2f68ca98bf9e219c81a3e6b27097c7bf505a87c5a112ce1aaeb9b81"}, ] [package.dependencies] @@ -5925,13 +5980,13 @@ python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] [[package]] name = "pydantic-settings" -version = "2.3.3" +version = "2.3.4" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.3.3-py3-none-any.whl", hash = "sha256:e4ed62ad851670975ec11285141db888fd24947f9440bd4380d7d8788d4965de"}, - {file = "pydantic_settings-2.3.3.tar.gz", hash = "sha256:87fda838b64b5039b970cd47c3e8a1ee460ce136278ff672980af21516f6e6ce"}, + {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, + {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, ] [package.dependencies] @@ -6113,59 +6168,59 @@ files = [ [[package]] name = "pyreqwest-impersonate" -version = "0.4.8" +version = "0.4.9" description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints" optional = false python-versions = ">=3.8" files = [ - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:45cad57afe4e6f56078ed9a7a90d0dc839d19d3e7a70175c80af21017f383bfb"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1986600253baf38f25fd07b8bdc1903359c26e5d34beb7d7d084845554b5664d"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cca4e6e59b9ad0cd20bad6caed3ac96992cd9c1d3126ecdfcab2c0ac2b75376"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab6b32544491ee655264dab86fc8a58e47c4f87d196b28022d4007faf971a50"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64bd6299e7fc888bb7f7292cf3e29504c406e5d5d04afd37ca994ab8142d8ee4"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e914b650dd953b8d9b24ef56aa4ecbfc16e399227b68accd818f8bf159e0c558"}, - {file = "pyreqwest_impersonate-0.4.8-cp310-none-win_amd64.whl", hash = "sha256:cb56a2149b0c4548a8e0158b071a943f33dae9b717f92b5c9ac34ccd1f5a958c"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f62620e023490902feca0109f306e122e427feff7d59e03ecd22c69a89452367"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08d4c01d76da88cfe3d7d03b311b375ce3fb5a59130f93f0637bb755d6e56ff1"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524e276bc460176c79d7ba4b9131d9db73c534586660371ebdf067749252a33"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22863bc0aaf02ca2f5d76c8130929ae680b7d82dfc1c28c1ed5f306ff626928"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cc82d57f6a91037e64a7aa9122f909576ef2a141a42ce599958ef9f8c4bc033"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da8a053308210e44fd8349f07f45442a0691ac932f2881e98b05cf9ac404b091"}, - {file = "pyreqwest_impersonate-0.4.8-cp311-none-win_amd64.whl", hash = "sha256:4baf3916c14364a815a64ead7f728afb61b37541933b2771f18dbb245029bb55"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:78db05deed0b32c9c75f2b3168a3a9b7d5e36487b218cb839bfe7e2a143450cb"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9af9446d605903c2b4e94621a9093f8d8a403729bc9cbfbcb62929f8238c838f"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c55890181d8d81e66cac25a95e215dc9680645d01e9091b64449d5407ad9bc6"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69344e7ae9964502a8693da7ad77ebc3e1418ee197e2e394bc23c5d4970772a"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5db5c957a10d8cc2815085ba0b8fe09245b2f94c2225d9653a854a03b4217e1"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03c19c21f63f9c91c590c4bbcc32cc2d8066b508c683a1d163b8c7d9816a01d5"}, - {file = "pyreqwest_impersonate-0.4.8-cp312-none-win_amd64.whl", hash = "sha256:0230610779129f74ff802c744643ce7589b1d07cba21d046fe3b574281c29581"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b8cb9471ab4b2fa7e80d3ac4e580249ff988d782f2938ad1f0428433652b170d"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8081a5ace2658be91519902bde9ddc5f94e1f850a39be196007a25e3da5bbfdc"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69eababfa3200459276acd780a0f3eaf41d1fe7c02bd169e714cba422055b5b9"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:632957fa671ebb841166e40913015de457225cb73600ef250c436c280e68bf45"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2ce7ddef334b4e5c68f5ea1da1d65f686b8d84f4443059d128e0f069d3fa499a"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6ce333d450b158d582e36317089a006440b4e66739a8e8849d170e4cb15e8c8d"}, - {file = "pyreqwest_impersonate-0.4.8-cp38-none-win_amd64.whl", hash = "sha256:9d9c85ce19db92362854f534807e470f03e905f283a7de6826dc79b790a8788e"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2503277f2a95a30e28e498570e2ed03ef4302f873054e8e21d6c0e607cbbc1d1"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8260395ef4ddae325e8b30cef0391adde7bd35e1a1decf8c729e26391f09b52d"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d8066b46d82bbaff5402d767e2f13d3449b8191c37bf8283e91d301a7159869"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c42f6343cfbd6663fb53edc9eb9feb4ebf6186b284e22368adc1eeb6a33854"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff534f491a059e74fb7f994876df86078b4b125dbecc53c098a298ecd55fa9c6"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b8fbf73b3ac513ddadafd338d61f79cd2370f0691d9175b2b92a45920920d6b"}, - {file = "pyreqwest_impersonate-0.4.8-cp39-none-win_amd64.whl", hash = "sha256:a26447c82665d0e361207c1a15e56b0ca54974aa6c1fdfa18c68f908dec78cbe"}, - {file = "pyreqwest_impersonate-0.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24a16b8d55309f0af0db9d04ff442b0c91afccf078a94809e7c3a71747a5c214"}, - {file = "pyreqwest_impersonate-0.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c8fada56465fc19179404cc9d5d5e1064f5dfe27405cb052f57a5b4fe06aed1"}, - {file = "pyreqwest_impersonate-0.4.8-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a3d48d5abc146fd804395713427d944757a99254350e6a651e7d776818074aee"}, - {file = "pyreqwest_impersonate-0.4.8-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:475829fe9994c66258157a8d4adb1c038f44f79f901208ba656d547842337227"}, - {file = "pyreqwest_impersonate-0.4.8-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef1ec0e97623bc0e18469418cc4dd2c59a2d5fddcae944de61e13c0b46f910e"}, - {file = "pyreqwest_impersonate-0.4.8-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91857b196de89e9b36d3f8629aa8772c0bbe7efef8334fe266956b1c192ec31c"}, - {file = "pyreqwest_impersonate-0.4.8-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:63831e407487b8a21bb51f97cd86a616c291d5138f8caec16ab6019cf6423935"}, - {file = "pyreqwest_impersonate-0.4.8-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c30e61de93bcd0a9d3ca226b1ae5475002afde61e9d85018a6a4a040eeb86567"}, - {file = "pyreqwest_impersonate-0.4.8-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c72c37b03bce9900f5dbb4f476af17253ec60c13bf7a7259f71a8dc1b036cb"}, - {file = "pyreqwest_impersonate-0.4.8-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1f1096165741b5c2178ab15b0eb09b5de16dd39b1cc135767d72471f0a69ce"}, - {file = "pyreqwest_impersonate-0.4.8-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:70c940c0e4ef335e22a6c705b01f286ee44780b5909065d212d94d82ea2580cb"}, - {file = "pyreqwest_impersonate-0.4.8-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81c06f21757602d85f16dbc1cbaee1121cd65455f65aed4c048b7dcda7be85c4"}, - {file = "pyreqwest_impersonate-0.4.8.tar.gz", hash = "sha256:1eba11d47bd17244c64fec1502cc26ee66cc5c8a3be131e408101ae2b455e5bc"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a229f56575d992df0c520d93408b4b6b660b304387af06208e7b97d739cce2ff"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c00dbfd0ed878bed231384cd0c823d71a42220ae73c6d982b6fe77d2870338ca"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d4e6ce0e48b73740f08b1aa69cdbded5d66f4eec327d5eaf2ac42a4fce1a008"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690a5c5615b33cbab340e3a4247256ede157ebf39a02f563cff5356bf61c0c51"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7231511ee14faee27b90a84ec74e15515b7e2d1c389710698660765eaed6e2fd"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2fdbe8e146e595c02fa0afb776d70f9e3b351122e2de9af15934b83f3a548ecd"}, + {file = "pyreqwest_impersonate-0.4.9-cp310-none-win_amd64.whl", hash = "sha256:982b0e53db24c084675a056944dd769aa07cd1378abf972927f3f1afb23e08b0"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:60b1102b8aec7bbf91e0f7b8bbc3507776731a9acc6844de764911e8d64f7dd2"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37150478157e683374517d4c0eae0f991b8f5280067a8ee042b6a72fec088843"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc77cd1cdae22dad7549a4e9a1a4630619c2ff443add1b28c7d607accda81eb"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83e99e627d13f1f60d71ce2c2a2b03e1c7f57e8f6a73bde2827ff97cb96f1683"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72d1adb73264db8c5e24d073d558a895d6690d13a5e38fd857b8b01c33fcbabf"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6253bd8a104316bbece0e6c658d28292f0bf37a99cccfaf476470b98252d185b"}, + {file = "pyreqwest_impersonate-0.4.9-cp311-none-win_amd64.whl", hash = "sha256:7e25628a900236fc76320e790fce90e5502371994523c476af2b1c938382f5fa"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:57e1e7f3bfc175c3229947cdd2b26564bcea2923135b8dec8ab157609e201a7c"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3aeb1c834f54fe685d3c7c0bec65dd981bd988fa3725ee3c7b5656eb7c44a1f7"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27bc384f18099573817d7ed68d12eb67d33dfc5d2b30ab2ac5a69cdf19c22b6f"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd604444ddf86ed222b49dd5e3f050c4c0e980dd7be0b3ea0f208fb70544c4b6"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5206dc7311081accf5b7d021c9e0e68219fd7bb35b0cd755b2d72c3ebfa41842"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76802f0738c2d00bb4e057938ec048c4c7c4efc5c44f04b9d877ad4419b21ee8"}, + {file = "pyreqwest_impersonate-0.4.9-cp312-none-win_amd64.whl", hash = "sha256:7cf94f6365bc144f787658e844f94dad38107fb9ff61d65079fb6510447777fe"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:66e92bd868146028dac1ef9cd2b4aac57e7e6cbd8806fa8a4c77ac5becf396e1"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc3ff7ac332879e40301d737b3ec1f3691b1de7492728bea26e75e26d05f89ec"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9e9eba83620852d4253023e50e3436726aee16e2de94afbd468da4373830dc"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d6b47d403c63b461a97efa2aed668f0f06ed26cf61c23d7d6dab4f5a0c81ffc"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:88f695a01e8699ec3a1547d793617b9fd00f810c05c2b4dc0d1472c7f12eed97"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:abb4fbfaa1a3c3adeb7f46baa1d67663af85ab24f2b4cdd15a668ddc6be3a375"}, + {file = "pyreqwest_impersonate-0.4.9-cp38-none-win_amd64.whl", hash = "sha256:884c1399fe0157dcd0a5a71e3600910df50faa0108c64602d47c15e75b32e60b"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bf5cd99276207510d64b48eff5602e12f049754d3b0f1194a024e1a080a61d3d"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:029eea1d386d12856da767d685169835f0b0c025ae312c1ee7bc0d8cb47a7d3d"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1bfb8795fe0a46aee883abcf510a9ecdb4e9acf75c3a5a23571276f555f5e88"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe35ce48e7e6b570304ee15915da0e6fab82dcae2b7a1d1a92593b522ebe852"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dfa377a842bd2e73d1f201bfc33194dd98c148372409d376f6d57efe338ff0eb"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d46880e68eb779cd071648e94a7ec50b3b77a28210f218be407eda1b0c8df343"}, + {file = "pyreqwest_impersonate-0.4.9-cp39-none-win_amd64.whl", hash = "sha256:ac431e4a94f8529a19a396750d78c66cc4fa11a8cc61d4bed7f0e0295a9394a9"}, + {file = "pyreqwest_impersonate-0.4.9-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12fd04d8da4d23ab5720402fd9f3b6944fb388c19952f2ec9121b46ac1f74616"}, + {file = "pyreqwest_impersonate-0.4.9-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b52df560d78681cde2fbc39bee547a42a79c8fd33655b79618835ecc412e6933"}, + {file = "pyreqwest_impersonate-0.4.9-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e1a2828942f9d589ee6161496444a380d3305e78bda25ff63e4f993b0545b193"}, + {file = "pyreqwest_impersonate-0.4.9-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:beebedf6d8c0d5fdee9ae15bc64a74e51b35f98eb0d049bf2db067702fbf4e53"}, + {file = "pyreqwest_impersonate-0.4.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a3d47dea1f46410b58ab60795b5818c8c99d901f6c93fbb6a9d23fa55adb2b1"}, + {file = "pyreqwest_impersonate-0.4.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb871adc5d12b2bcbb5af167384d49fc4e7e5e07d12cf912b931149163df724"}, + {file = "pyreqwest_impersonate-0.4.9-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d1b0b5556d2bd14a4ffa32654291fe2a9ef1eaac35b5514d9220e7e333a6c727"}, + {file = "pyreqwest_impersonate-0.4.9-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d50feaec78c06d51e1dd65cdbe80a1fc62ff93c8114555482f8a8cc5fe14895"}, + {file = "pyreqwest_impersonate-0.4.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2a9cfc41917200d8eee61b87a5668abe7d1f924a55b7437065540edf613beed"}, + {file = "pyreqwest_impersonate-0.4.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8106e3df0c1dca4df99e0f998f0e085ea3e1facfaa5afc268160a496ddf7256f"}, + {file = "pyreqwest_impersonate-0.4.9-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ff66bb7dc6b1f52cf950b5e9cb0e53baffd1a15da595fd1ef933cd9e36396403"}, + {file = "pyreqwest_impersonate-0.4.9-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39f2a3ed17cf08098dc637459e88fb86d3fa7bdf9502659c82218be75651649c"}, + {file = "pyreqwest_impersonate-0.4.9.tar.gz", hash = "sha256:4ec8df7fe813e89f61e814c5ef75f6fd71164c8e26299c1a42dcd0d42f0bc96c"}, ] [package.extras] @@ -6598,104 +6653,104 @@ test = ["coverage", "pytest"] [[package]] name = "rapidfuzz" -version = "3.9.3" +version = "3.9.4" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.8" files = [ - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdb8c5b8e29238ec80727c2ba3b301efd45aa30c6a7001123a6647b8e6f77ea4"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3bd0d9632088c63a241f217742b1cf86e2e8ae573e01354775bd5016d12138c"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153f23c03d4917f6a1fc2fb56d279cc6537d1929237ff08ee7429d0e40464a18"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96c5225e840f1587f1bac8fa6f67562b38e095341576e82b728a82021f26d62"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b777cd910ceecd738adc58593d6ed42e73f60ad04ecdb4a841ae410b51c92e0e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53e06e4b81f552da04940aa41fc556ba39dee5513d1861144300c36c33265b76"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7ca5b6050f18fdcacdada2dc5fb7619ff998cd9aba82aed2414eee74ebe6cd"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:87bb8d84cb41446a808c4b5f746e29d8a53499381ed72f6c4e456fe0f81c80a8"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:959a15186d18425d19811bea86a8ffbe19fd48644004d29008e636631420a9b7"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a24603dd05fb4e3c09d636b881ce347e5f55f925a6b1b4115527308a323b9f8e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d055da0e801c71dd74ba81d72d41b2fa32afa182b9fea6b4b199d2ce937450d"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:875b581afb29a7213cf9d98cb0f98df862f1020bce9d9b2e6199b60e78a41d14"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win32.whl", hash = "sha256:6073a46f61479a89802e3f04655267caa6c14eb8ac9d81a635a13805f735ebc1"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:119c010e20e561249b99ca2627f769fdc8305b07193f63dbc07bca0a6c27e892"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:790b0b244f3213581d42baa2fed8875f9ee2b2f9b91f94f100ec80d15b140ba9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f57e8305c281e8c8bc720515540e0580355100c0a7a541105c6cafc5de71daae"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4fc7b784cf987dbddc300cef70e09a92ed1bce136f7bb723ea79d7e297fe76d"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b422c0a6fe139d5447a0766268e68e6a2a8c2611519f894b1f31f0a392b9167"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f50fed4a9b0c9825ff37cf0bccafd51ff5792090618f7846a7650f21f85579c9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b80eb7cbe62348c61d3e67e17057cddfd6defab168863028146e07d5a8b24a89"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f45be77ec82da32ce5709a362e236ccf801615cc7163b136d1778cf9e31b14"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd84b7f652a5610733400307dc732f57c4a907080bef9520412e6d9b55bc9adc"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e6d27dad8c990218b8cd4a5c99cbc8834f82bb46ab965a7265d5aa69fc7ced7"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05ee0696ebf0dfe8f7c17f364d70617616afc7dafe366532730ca34056065b8a"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2bc8391749e5022cd9e514ede5316f86e332ffd3cfceeabdc0b17b7e45198a8c"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93981895602cf5944d89d317ae3b1b4cc684d175a8ae2a80ce5b65615e72ddd0"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:754b719a4990735f66653c9e9261dcf52fd4d925597e43d6b9069afcae700d21"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win32.whl", hash = "sha256:14c9f268ade4c88cf77ab007ad0fdf63699af071ee69378de89fff7aa3cae134"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc1991b4cde6c9d3c0bbcb83d5581dc7621bec8c666c095c65b4277233265a82"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:0c34139df09a61b1b557ab65782ada971b4a3bce7081d1b2bee45b0a52231adb"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d6a210347d6e71234af5c76d55eeb0348b026c9bb98fe7c1cca89bac50fb734"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b300708c917ce52f6075bdc6e05b07c51a085733650f14b732c087dc26e0aaad"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83ea7ca577d76778250421de61fb55a719e45b841deb769351fc2b1740763050"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8319838fb5b7b5f088d12187d91d152b9386ce3979ed7660daa0ed1bff953791"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:505d99131afd21529293a9a7b91dfc661b7e889680b95534756134dc1cc2cd86"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c52970f7784518d7c82b07a62a26e345d2de8c2bd8ed4774e13342e4b3ff4200"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:143caf7247449055ecc3c1e874b69e42f403dfc049fc2f3d5f70e1daf21c1318"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b8ab0fa653d9225195a8ff924f992f4249c1e6fa0aea563f685e71b81b9fcccf"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57e7c5bf7b61c7320cfa5dde1e60e678d954ede9bb7da8e763959b2138391401"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51fa1ba84653ab480a2e2044e2277bd7f0123d6693051729755addc0d015c44f"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:17ff7f7eecdb169f9236e3b872c96dbbaf116f7787f4d490abd34b0116e3e9c8"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afe7c72d3f917b066257f7ff48562e5d462d865a25fbcabf40fca303a9fa8d35"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win32.whl", hash = "sha256:e53ed2e9b32674ce96eed80b3b572db9fd87aae6742941fb8e4705e541d861ce"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:35b7286f177e4d8ba1e48b03612f928a3c4bdac78e5651379cec59f95d8651e6"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:e6e4b9380ed4758d0cb578b0d1970c3f32dd9e87119378729a5340cb3169f879"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a39890013f6d5b056cc4bfdedc093e322462ece1027a57ef0c636537bdde7531"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b5bc0fdbf419493163c5c9cb147c5fbe95b8e25844a74a8807dcb1a125e630cf"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe6e200a75a792d37b960457904c4fce7c928a96ae9e5d21d2bd382fe39066e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de077c468c225d4c18f7188c47d955a16d65f21aab121cbdd98e3e2011002c37"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f917eaadf5388466a95f6a236f678a1588d231e52eda85374077101842e794e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858ba57c05afd720db8088a8707079e8d024afe4644001fe0dbd26ef7ca74a65"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36447d21b05f90282a6f98c5a33771805f9222e5d0441d03eb8824e33e5bbb4"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:acbe4b6f1ccd5b90c29d428e849aa4242e51bb6cab0448d5f3c022eb9a25f7b1"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:53c7f27cdf899e94712972237bda48cfd427646aa6f5d939bf45d084780e4c16"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6175682a829c6dea4d35ed707f1dadc16513270ef64436568d03b81ccb6bdb74"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5276df395bd8497397197fca2b5c85f052d2e6a66ffc3eb0544dd9664d661f95"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:77b5c4f3e72924d7845f0e189c304270066d0f49635cf8a3938e122c437e58de"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win32.whl", hash = "sha256:8add34061e5cd561c72ed4febb5c15969e7b25bda2bb5102d02afc3abc1f52d0"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:604e0502a39cf8e67fa9ad239394dddad4cdef6d7008fdb037553817d420e108"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21047f55d674614eb4b0ab34e35c3dc66f36403b9fbfae645199c4a19d4ed447"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a56da3aff97cb56fe85d9ca957d1f55dbac7c27da927a86a2a86d8a7e17f80aa"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c08481aec2fe574f0062e342924db2c6b321391aeb73d68853ed42420fd6d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e2b827258beefbe5d3f958243caa5a44cf46187eff0c20e0b2ab62d1550327a"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6e65a301fcd19fbfbee3a514cc0014ff3f3b254b9fd65886e8a9d6957fb7bca"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe93ba1725a8d47d2b9dca6c1f435174859427fbc054d83de52aea5adc65729"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca21c0a34adee582775da997a600283e012a608a107398d80a42f9a57ad323d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:256e07d3465173b2a91c35715a2277b1ee3ae0b9bbab4e519df6af78570741d0"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:802ca2cc8aa6b8b34c6fdafb9e32540c1ba05fca7ad60b3bbd7ec89ed1797a87"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:dd789100fc852cffac1449f82af0da139d36d84fd9faa4f79fc4140a88778343"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d0abbacdb06e27ff803d7ae0bd0624020096802758068ebdcab9bd49cf53115"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:378d1744828e27490a823fc6fe6ebfb98c15228d54826bf4e49e4b76eb5f5579"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win32.whl", hash = "sha256:5d0cb272d43e6d3c0dedefdcd9d00007471f77b52d2787a4695e9dd319bb39d2"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:15e4158ac4b3fb58108072ec35b8a69165f651ba1c8f43559a36d518dbf9fb3f"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_arm64.whl", hash = "sha256:58c6a4936190c558d5626b79fc9e16497e5df7098589a7e80d8bff68148ff096"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5410dc848c947a603792f4f51b904a3331cf1dc60621586bfbe7a6de72da1091"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:282d55700a1a3d3a7980746eb2fcd48c9bbc1572ebe0840d0340d548a54d01fe"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc1037507810833646481f5729901a154523f98cbebb1157ba3a821012e16402"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e33f779391caedcba2ba3089fb6e8e557feab540e9149a5c3f7fea7a3a7df37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41a81a9f311dc83d22661f9b1a1de983b201322df0c4554042ffffd0f2040c37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a93250bd8fae996350c251e1752f2c03335bb8a0a5b0c7e910a593849121a435"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3617d1aa7716c57d120b6adc8f7c989f2d65bc2b0cbd5f9288f1fc7bf469da11"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad04a3f5384b82933213bba2459f6424decc2823df40098920856bdee5fd6e88"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8709918da8a88ad73c9d4dd0ecf24179a4f0ceba0bee21efc6ea21a8b5290349"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b770f85eab24034e6ef7df04b2bfd9a45048e24f8a808e903441aa5abde8ecdd"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930b4e6fdb4d914390141a2b99a6f77a52beacf1d06aa4e170cba3a98e24c1bc"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c8444e921bfc3757c475c4f4d7416a7aa69b2d992d5114fe55af21411187ab0d"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c1d3ef3878f871abe6826e386c3d61b5292ef5f7946fe646f4206b85836b5da"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d861bf326ee7dabc35c532a40384541578cd1ec1e1b7db9f9ecbba56eb76ca22"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cde6b9d9ba5007077ee321ec722fa714ebc0cbd9a32ccf0f4dd3cc3f20952d71"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb6546e7b6bed1aefbe24f68a5fb9b891cc5aef61bca6c1a7b1054b7f0359bb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8a57261ef7996d5ced7c8cba9189ada3fbeffd1815f70f635e4558d93766cb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:67201c02efc596923ad950519e0b75ceb78d524177ea557134d6567b9ac2c283"}, - {file = "rapidfuzz-3.9.3.tar.gz", hash = "sha256:b398ea66e8ed50451bce5997c430197d5e4b06ac4aa74602717f792d8d8d06e2"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b9793c19bdf38656c8eaefbcf4549d798572dadd70581379e666035c9df781"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:015b5080b999404fe06ec2cb4f40b0be62f0710c926ab41e82dfbc28e80675b4"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc5ceca9c1e1663f3e6c23fb89a311f69b7615a40ddd7645e3435bf3082688a"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1424e238bc3f20e1759db1e0afb48a988a9ece183724bef91ea2a291c0b92a95"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed01378f605aa1f449bee82cd9c83772883120d6483e90aa6c5a4ce95dc5c3aa"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb26d412271e5a76cdee1c2d6bf9881310665d3fe43b882d0ed24edfcb891a84"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f37e9e1f17be193c41a31c864ad4cd3ebd2b40780db11cd5c04abf2bcf4201b"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d070ec5cf96b927c4dc5133c598c7ff6db3b833b363b2919b13417f1002560bc"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:10e61bb7bc807968cef09a0e32ce253711a2d450a4dce7841d21d45330ffdb24"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:31a2fc60bb2c7face4140010a7aeeafed18b4f9cdfa495cc644a68a8c60d1ff7"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fbebf1791a71a2e89f5c12b78abddc018354d5859e305ec3372fdae14f80a826"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aee9fc9e3bb488d040afc590c0a7904597bf4ccd50d1491c3f4a5e7e67e6cd2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win32.whl", hash = "sha256:005a02688a51c7d2451a2d41c79d737aa326ff54167211b78a383fc2aace2c2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:3a2e75e41ee3274754d3b2163cc6c82cd95b892a85ab031f57112e09da36455f"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_arm64.whl", hash = "sha256:2c99d355f37f2b289e978e761f2f8efeedc2b14f4751d9ff7ee344a9a5ca98d9"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:07141aa6099e39d48637ce72a25b893fc1e433c50b3e837c75d8edf99e0c63e1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db1664eaff5d7d0f2542dd9c25d272478deaf2c8412e4ad93770e2e2d828e175"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc01a223f6605737bec3202e94dcb1a449b6c76d46082cfc4aa980f2a60fd40e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1869c42e73e2a8910b479be204fa736418741b63ea2325f9cc583c30f2ded41a"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62ea7007941fb2795fff305ac858f3521ec694c829d5126e8f52a3e92ae75526"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:698e992436bf7f0afc750690c301215a36ff952a6dcd62882ec13b9a1ebf7a39"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b76f611935f15a209d3730c360c56b6df8911a9e81e6a38022efbfb96e433bab"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129627d730db2e11f76169344a032f4e3883d34f20829419916df31d6d1338b1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:90a82143c14e9a14b723a118c9ef8d1bbc0c5a16b1ac622a1e6c916caff44dd8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ded58612fe3b0e0d06e935eaeaf5a9fd27da8ba9ed3e2596307f40351923bf72"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f16f5d1c4f02fab18366f2d703391fcdbd87c944ea10736ca1dc3d70d8bd2d8b"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26aa7eece23e0df55fb75fbc2a8fb678322e07c77d1fd0e9540496e6e2b5f03e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win32.whl", hash = "sha256:f187a9c3b940ce1ee324710626daf72c05599946bd6748abe9e289f1daa9a077"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8e9130fe5d7c9182990b366ad78fd632f744097e753e08ace573877d67c32f8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_arm64.whl", hash = "sha256:40419e98b10cd6a00ce26e4837a67362f658fc3cd7a71bd8bd25c99f7ee8fea5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b5d5072b548db1b313a07d62d88fe0b037bd2783c16607c647e01b070f6cf9e5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf5bcf22e1f0fd273354462631d443ef78d677f7d2fc292de2aec72ae1473e66"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8fc973adde8ed52810f590410e03fb6f0b541bbaeb04c38d77e63442b2df4c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2464bb120f135293e9a712e342c43695d3d83168907df05f8c4ead1612310c7"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d9d58689aca22057cf1a5851677b8a3ccc9b535ca008c7ed06dc6e1899f7844"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167e745f98baa0f3034c13583e6302fb69249a01239f1483d68c27abb841e0a1"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0bf0663b4b6da1507869722420ea9356b6195aa907228d6201303e69837af9"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd6ac61b74fdb9e23f04d5f068e6cf554f47e77228ca28aa2347a6ca8903972f"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:60ff67c690acecf381759c16cb06c878328fe2361ddf77b25d0e434ea48a29da"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cb934363380c60f3a57d14af94325125cd8cded9822611a9f78220444034e36e"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fe833493fb5cc5682c823ea3e2f7066b07612ee8f61ecdf03e1268f262106cdd"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2797fb847d89e04040d281cb1902cbeffbc4b5131a5c53fc0db490fd76b2a547"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win32.whl", hash = "sha256:52e3d89377744dae68ed7c84ad0ddd3f5e891c82d48d26423b9e066fc835cc7c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:c76da20481c906e08400ee9be230f9e611d5931a33707d9df40337c2655c84b5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_arm64.whl", hash = "sha256:f2d2846f3980445864c7e8b8818a29707fcaff2f0261159ef6b7bd27ba139296"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:355fc4a268ffa07bab88d9adee173783ec8d20136059e028d2a9135c623c44e6"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d81a78f90269190b568a8353d4ea86015289c36d7e525cd4d43176c88eff429"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e618625ffc4660b26dc8e56225f8b966d5842fa190e70c60db6cd393e25b86e"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b712336ad6f2bacdbc9f1452556e8942269ef71f60a9e6883ef1726b52d9228a"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc1ee19fdad05770c897e793836c002344524301501d71ef2e832847425707"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1950f8597890c0c707cb7e0416c62a1cf03dcdb0384bc0b2dbda7e05efe738ec"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6c35f272ec9c430568dc8c1c30cb873f6bc96be2c79795e0bce6db4e0e101d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1df0f9e9239132a231c86ae4f545ec2b55409fa44470692fcfb36b1bd00157ad"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d2c51955329bfccf99ae26f63d5928bf5be9fcfcd9f458f6847fd4b7e2b8986c"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:3c522f462d9fc504f2ea8d82e44aa580e60566acc754422c829ad75c752fbf8d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:d8a52fc50ded60d81117d7647f262c529659fb21d23e14ebfd0b35efa4f1b83d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:04dbdfb0f0bfd3f99cf1e9e24fadc6ded2736d7933f32f1151b0f2abb38f9a25"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win32.whl", hash = "sha256:4968c8bd1df84b42f382549e6226710ad3476f976389839168db3e68fd373298"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:3fe4545f89f8d6c27b6bbbabfe40839624873c08bd6700f63ac36970a179f8f5"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f256c8fb8f3125574c8c0c919ab0a1f75d7cba4d053dda2e762dcc36357969d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fdc09cf6e9d8eac3ce48a4615b3a3ee332ea84ac9657dbbefef913b13e632f"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d395d46b80063d3b5d13c0af43d2c2cedf3ab48c6a0c2aeec715aa5455b0c632"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa714fb96ce9e70c37e64c83b62fe8307030081a0bfae74a76fac7ba0f91715"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc1a0f29f9119be7a8d3c720f1d2068317ae532e39e4f7f948607c3a6de8396"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6022674aa1747d6300f699cd7c54d7dae89bfe1f84556de699c4ac5df0838082"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb72e5f9762fd469701a7e12e94b924af9004954f8c739f925cb19c00862e38"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad04ae301129f0eb5b350a333accd375ce155a0c1cec85ab0ec01f770214e2e4"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f46a22506f17c0433e349f2d1dc11907c393d9b3601b91d4e334fa9a439a6a4d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:01b42a8728c36011718da409aa86b84984396bf0ca3bfb6e62624f2014f6022c"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e590d5d5443cf56f83a51d3c4867bd1f6be8ef8cfcc44279522bcef3845b2a51"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c72078b5fdce34ba5753f9299ae304e282420e6455e043ad08e4488ca13a2b0"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win32.whl", hash = "sha256:f75639277304e9b75e6a7b3c07042d2264e16740a11e449645689ed28e9c2124"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:e81e27e8c32a1e1278a4bb1ce31401bfaa8c2cc697a053b985a6f8d013df83ec"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_arm64.whl", hash = "sha256:15bc397ee9a3ed1210b629b9f5f1da809244adc51ce620c504138c6e7095b7bd"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:20488ade4e1ddba3cfad04f400da7a9c1b91eff5b7bd3d1c50b385d78b587f4f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e61b03509b1a6eb31bc5582694f6df837d340535da7eba7bedb8ae42a2fcd0b9"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098d231d4e51644d421a641f4a5f2f151f856f53c252b03516e01389b2bfef99"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17ab8b7d10fde8dd763ad428aa961c0f30a1b44426e675186af8903b5d134fb0"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e272df61bee0a056a3daf99f9b1bd82cf73ace7d668894788139c868fdf37d6f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d6481e099ff8c4edda85b8b9b5174c200540fd23c8f38120016c765a86fa01f5"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad61676e9bdae677d577fe80ec1c2cea1d150c86be647e652551dcfe505b1113"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:af65020c0dd48d0d8ae405e7e69b9d8ae306eb9b6249ca8bf511a13f465fad85"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d38b4e026fcd580e0bda6c0ae941e0e9a52c6bc66cdce0b8b0da61e1959f5f8"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f74ed072c2b9dc6743fb19994319d443a4330b0e64aeba0aa9105406c7c5b9c2"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee5f6b8321f90615c184bd8a4c676e9becda69b8e4e451a90923db719d6857c"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a555e3c841d6efa350f862204bb0a3fea0c006b8acc9b152b374fa36518a1c6"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0772150d37bf018110351c01d032bf9ab25127b966a29830faa8ad69b7e2f651"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:addcdd3c3deef1bd54075bd7aba0a6ea9f1d01764a08620074b7a7b1e5447cb9"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fe86b82b776554add8f900b6af202b74eb5efe8f25acdb8680a5c977608727f"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0fc91ac59f4414d8542454dfd6287a154b8e6f1256718c898f695bdbb993467"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a944e546a296a5fdcaabb537b01459f1b14d66f74e584cb2a91448bffadc3c1"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fb96ba96d58c668a17a06b5b5e8340fedc26188e87b0d229d38104556f30cd8"}, + {file = "rapidfuzz-3.9.4.tar.gz", hash = "sha256:366bf8947b84e37f2f4cf31aaf5f37c39f620d8c0eddb8b633e6ba0129ca4a0a"}, ] [package.extras] @@ -6725,13 +6780,13 @@ test = ["coveralls", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-benc [[package]] name = "redis" -version = "5.0.6" +version = "5.0.7" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.6-py3-none-any.whl", hash = "sha256:c0d6d990850c627bbf7be01c5c4cbaadf67b48593e913bb71c9819c30df37eee"}, - {file = "redis-5.0.6.tar.gz", hash = "sha256:38473cd7c6389ad3e44a91f4c3eaf6bcb8a9f746007f29bf4fb20824ff0b2197"}, + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, ] [package.dependencies] @@ -6951,46 +7006,46 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.4.9" +version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"}, - {file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"}, - {file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"}, - {file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"}, - {file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"}, - {file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"}, + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] [[package]] name = "s3transfer" -version = "0.6.2" +version = "0.10.2" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.7" +python-versions = ">=3.8" files = [ - {file = "s3transfer-0.6.2-py3-none-any.whl", hash = "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084"}, - {file = "s3transfer-0.6.2.tar.gz", hash = "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"}, + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, ] [package.dependencies] -botocore = ">=1.12.36,<2.0a.0" +botocore = ">=1.33.2,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "safetensors" @@ -7158,45 +7213,45 @@ tests = ["black (>=22.3.0)", "flake8 (>=3.8.2)", "matplotlib (>=3.1.3)", "mypy ( [[package]] name = "scipy" -version = "1.13.1" +version = "1.14.0" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, + {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, + {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, + {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, + {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, + {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, + {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, + {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, ] [package.dependencies] -numpy = ">=1.22.4,<2.3" +numpy = ">=1.23.5,<2.3" [package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "sentry-sdk" @@ -7248,18 +7303,18 @@ tornado = ["tornado (>=5)"] [[package]] name = "setuptools" -version = "70.1.0" +version = "70.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, + {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, + {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sgmllib3k" @@ -7579,13 +7634,13 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tencentcloud-sdk-python-common" -version = "3.0.1172" +version = "3.0.1183" description = "Tencent Cloud Common SDK for Python" optional = false python-versions = "*" files = [ - {file = "tencentcloud-sdk-python-common-3.0.1172.tar.gz", hash = "sha256:37b3b9f4a53caa070379afb6910ac989823eacd35169701405ddafb12ea14e9e"}, - {file = "tencentcloud_sdk_python_common-3.0.1172-py2.py3-none-any.whl", hash = "sha256:8915ddc713bcd7512e9d528ec36ad3e527990ab06f5e89f63941f2e5c23f4675"}, + {file = "tencentcloud-sdk-python-common-3.0.1183.tar.gz", hash = "sha256:59f8175cd2be20badfbed035637794d1d827071dd4e9d746543689254a9eae47"}, + {file = "tencentcloud_sdk_python_common-3.0.1183-py2.py3-none-any.whl", hash = "sha256:9deb38d80f7d8fbaf45b46f201f8c0c324a78cc0cb6c5034c1da84a06116af88"}, ] [package.dependencies] @@ -7593,17 +7648,17 @@ requests = ">=2.16.0" [[package]] name = "tencentcloud-sdk-python-hunyuan" -version = "3.0.1172" +version = "3.0.1183" description = "Tencent Cloud Hunyuan SDK for Python" optional = false python-versions = "*" files = [ - {file = "tencentcloud-sdk-python-hunyuan-3.0.1172.tar.gz", hash = "sha256:ae83b39c9da7302b10c4bffb7672ae95be72945b43e06a0b1ae9ac23bac2d43b"}, - {file = "tencentcloud_sdk_python_hunyuan-3.0.1172-py2.py3-none-any.whl", hash = "sha256:443908059ef1a00a798b7387f85e210d89c65b4f9db73629e53b3ec609b8528b"}, + {file = "tencentcloud-sdk-python-hunyuan-3.0.1183.tar.gz", hash = "sha256:5648994f0124c694ee75dd498d991ca632c8dc8d55b6d349d8cc7fd5bc33b1bd"}, + {file = "tencentcloud_sdk_python_hunyuan-3.0.1183-py2.py3-none-any.whl", hash = "sha256:01bfdf33ea04ed791931636c3eafa569a0387f623967ef880ff220b3c548e6f5"}, ] [package.dependencies] -tencentcloud-sdk-python-common = "3.0.1172" +tencentcloud-sdk-python-common = "3.0.1183" [[package]] name = "threadpoolctl" @@ -7989,13 +8044,13 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "types-requests" -version = "2.32.0.20240602" +version = "2.32.0.20240622" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"}, - {file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"}, + {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"}, + {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"}, ] [package.dependencies] @@ -8238,18 +8293,18 @@ files = [ [[package]] name = "urllib3" -version = "2.0.7" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -9040,4 +9095,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "cac196b2ddb59d7873fb3380d87b622d002613d6dc1d271a5c15e46817a38c55" +content-hash = "a31e1524d35da47f63f5e8d24236cbe14585e6a5d9edf9b734d517d24f83e287" diff --git a/api/pyproject.toml b/api/pyproject.toml index 249113ddb9..3c633675e2 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -14,9 +14,11 @@ line-length = 120 preview = true select = [ "B", # flake8-bugbear rules + "C4", # flake8-comprehensions "F", # pyflakes rules "I", # isort rules - "UP", # pyupgrade rules + "UP", # pyupgrade rules + "B035", # static-key-dict-comprehension "E101", # mixed-spaces-and-tabs "E111", # indentation-with-invalid-multiple "E112", # no-indented-block @@ -28,8 +30,13 @@ select = [ "RUF100", # unused-noqa "RUF101", # redirected-noqa "S506", # unsafe-yaml-load + "SIM116", # if-else-block-instead-of-dict-lookup + "SIM401", # if-else-block-instead-of-dict-get + "SIM910", # dict-get-with-none-default "W191", # tab-indentation "W605", # invalid-escape-sequence + "F601", # multi-value-repeated-key-literal + "F602", # multi-value-repeated-key-variable ] ignore = [ "F403", # undefined-local-with-import-star @@ -61,6 +68,8 @@ ignore = [ "F811", # redefined-while-unused ] +[tool.ruff.format] +quote-style = "single" [tool.pytest_env] OPENAI_API_KEY = "sk-IamNotARealKeyJustForMockTestKawaiiiiiiiiii" @@ -80,114 +89,143 @@ HUGGINGFACE_TEXT2TEXT_GEN_ENDPOINT_URL = "b" HUGGINGFACE_EMBEDDINGS_ENDPOINT_URL = "c" MOCK_SWITCH = "true" CODE_MAX_STRING_LENGTH = "80000" -CODE_EXECUTION_ENDPOINT="http://127.0.0.1:8194" -CODE_EXECUTION_API_KEY="dify-sandbox" +CODE_EXECUTION_ENDPOINT = "http://127.0.0.1:8194" +CODE_EXECUTION_API_KEY = "dify-sandbox" FIRECRAWL_API_KEY = "fc-" [tool.poetry] name = "dify-api" package-mode = false +############################################################ +# Main dependencies +############################################################ + [tool.poetry.dependencies] -python = "^3.10" +anthropic = "~0.23.1" +authlib = "1.3.1" +azure-identity = "1.16.1" +azure-storage-blob = "12.13.0" beautifulsoup4 = "4.12.2" +boto3 = "1.34.136" +bs4 = "~0.0.1" +cachetools = "~5.3.0" +celery = "~5.3.6" +chardet = "~5.1.0" +cohere = "~5.2.4" +cos-python-sdk-v5 = "1.9.30" +dashscope = { version = "~1.17.0", extras = ["tokenizer"] } flask = "~3.0.1" -flask-sqlalchemy = "~3.0.5" -sqlalchemy = "~2.0.29" flask-compress = "~1.14" +flask-cors = "~4.0.0" flask-login = "~0.6.3" flask-migrate = "~4.0.5" flask-restful = "~0.3.10" -flask-cors = "~4.0.0" -gunicorn = "~22.0.0" +flask-sqlalchemy = "~3.0.5" gevent = "~23.9.1" -openai = "~1.29.0" -tiktoken = "~0.7.0" -psycopg2-binary = "~2.9.6" -pycryptodome = "3.19.1" -python-dotenv = "1.0.0" -authlib = "1.3.1" -boto3 = "1.28.17" -cachetools = "~5.3.0" -weaviate-client = "~3.21.0" -mailchimp-transactional = "~1.0.50" -scikit-learn = "1.2.2" -sentry-sdk = {version = "~1.39.2", extras = ["flask"]} -sympy = "1.12" -jieba = "0.42.1" -celery = "~5.3.6" -redis = {version = "~5.0.3", extras = ["hiredis"]} -chardet = "~5.1.0" -python-docx = "~1.1.0" -pypdfium2 = "~4.17.0" -resend = "~0.7.0" -pyjwt = "~2.8.0" -anthropic = "~0.23.1" -newspaper3k = "0.2.8" -wikipedia = "1.4.0" -readabilipy = "0.2.0" +gmpy2 = "~2.1.5" google-ai-generativelanguage = "0.6.1" google-api-core = "2.18.0" google-api-python-client = "2.90.0" google-auth = "2.29.0" google-auth-httplib2 = "0.2.0" +google-cloud-aiplatform = "1.49.0" +google-cloud-storage = "2.16.0" google-generativeai = "0.5.0" googleapis-common-protos = "1.63.0" -google-cloud-storage = "2.16.0" -replicate = "~0.22.0" -websocket-client = "~1.7.0" -dashscope = {version = "~1.17.0", extras = ["tokenizer"]} +gunicorn = "~22.0.0" +httpx = { version = "~0.27.0", extras = ["socks"] } huggingface-hub = "~0.16.4" -transformers = "~4.35.0" -tokenizers = "~0.15.0" -pandas = { version = "~2.2.2", extras = ["performance", "excel"] } -xinference-client = "0.9.4" -safetensors = "~0.4.3" -zhipuai = "1.0.7" -werkzeug = "~3.0.1" -pymilvus = "2.3.1" -qdrant-client = "1.7.3" -cohere = "~5.2.4" -pyyaml = "~6.0.1" -numpy = "~1.26.4" -unstructured = {version = "~0.10.27", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"]} -bs4 = "~0.0.1" +jieba = "0.42.1" +langfuse = "^2.36.1" +langsmith = "^0.1.77" +mailchimp-transactional = "~1.0.50" markdown = "~3.5.1" -httpx = {version = "~0.27.0", extras = ["socks"]} -matplotlib = "~3.8.2" -yfinance = "~0.2.40" -pydub = "~0.25.1" -gmpy2 = "~2.1.5" -numexpr = "~2.9.0" -duckduckgo-search = "~6.1.5" -arxiv = "2.1.0" -yarl = "~1.9.4" -twilio = "~9.0.4" -qrcode = "~7.4.2" -azure-storage-blob = "12.13.0" -azure-identity = "1.16.1" -lxml = "5.1.0" -xlrd = "~2.0.1" -pydantic = "~2.7.4" -pydantic_extra_types = "~2.8.1" -pydantic-settings = "~2.3.3" -pgvecto-rs = "0.1.4" -firecrawl-py = "0.0.5" -oss2 = "2.18.5" -pgvector = "0.2.5" -pymysql = "1.1.1" -tidb-vector = "0.0.9" -google-cloud-aiplatform = "1.49.0" -vanna = {version = "0.5.5", extras = ["postgres", "mysql", "clickhouse", "duckdb"]} -kaleido = "0.2.1" -tencentcloud-sdk-python-hunyuan = "~3.0.1158" -tcvectordb = "1.3.2" -chromadb = "~0.5.1" -tenacity = "~8.3.0" -cos-python-sdk-v5 = "1.9.30" novita-client = "^0.5.6" +numpy = "~1.26.4" +openai = "~1.29.0" +oss2 = "2.18.5" +pandas = { version = "~2.2.2", extras = ["performance", "excel"] } +psycopg2-binary = "~2.9.6" +pycryptodome = "3.19.1" +pydantic = "~2.7.4" +pydantic-settings = "~2.3.3" +pydantic_extra_types = "~2.8.1" +pydub = "~0.25.1" +pyjwt = "~2.8.0" +pypdfium2 = "~4.17.0" +python = "^3.10" +python-docx = "~1.1.0" +python-dotenv = "1.0.0" +pyyaml = "~6.0.1" +readabilipy = "0.2.0" +redis = { version = "~5.0.3", extras = ["hiredis"] } +replicate = "~0.22.0" +resend = "~0.7.0" +safetensors = "~0.4.3" +scikit-learn = "1.2.2" +sentry-sdk = { version = "~1.39.2", extras = ["flask"] } +sqlalchemy = "~2.0.29" +tencentcloud-sdk-python-hunyuan = "~3.0.1158" +tiktoken = "~0.7.0" +tokenizers = "~0.15.0" +transformers = "~4.35.0" +unstructured = { version = "~0.10.27", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] } +websocket-client = "~1.7.0" +werkzeug = "~3.0.1" +xinference-client = "0.9.4" +yarl = "~1.9.4" +zhipuai = "1.0.7" + +############################################################ +# Tool dependencies required by tool implementations +############################################################ + +[tool.poetry.group.tool.dependencies] +arxiv = "2.1.0" +matplotlib = "~3.8.2" +newspaper3k = "0.2.8" +duckduckgo-search = "^6.1.8" +jsonpath-ng = "1.6.1" +numexpr = "~2.9.0" opensearch-py = "2.4.0" +qrcode = "~7.4.2" +twilio = "~9.0.4" +vanna = { version = "0.5.5", extras = ["postgres", "mysql", "clickhouse", "duckdb"] } +wikipedia = "1.4.0" +yfinance = "~0.2.40" + +############################################################ +# VDB dependencies required by vector store clients +############################################################ + +[tool.poetry.group.vdb.dependencies] +chromadb = "~0.5.1" oracledb = "~2.2.1" +pgvecto-rs = "0.1.4" +pgvector = "0.2.5" +pymilvus = "2.3.1" +pymysql = "1.1.1" +tcvectordb = "1.3.2" +tidb-vector = "0.0.9" +qdrant-client = "1.7.3" +weaviate-client = "~3.21.0" + +############################################################ +# Transparent dependencies required by main dependencies +# for pinning versions +############################################################ + +[tool.poetry.group.transparent.dependencies] +kaleido = "0.2.1" +lxml = "5.1.0" +sympy = "1.12" +tenacity = "~8.3.0" +xlrd = "~2.0.1" + +############################################################ +# Dev dependencies for running tests +############################################################ [tool.poetry.group.dev] optional = true @@ -199,9 +237,13 @@ pytest-benchmark = "~4.0.0" pytest-env = "~1.1.3" pytest-mock = "~3.14.0" +############################################################ +# Lint dependencies for code style linting +############################################################ + [tool.poetry.group.lint] optional = true [tool.poetry.group.lint.dependencies] ruff = "~0.4.8" -dotenv-linter = "~0.5.0" +dotenv-linter = "~0.5.0" \ No newline at end of file diff --git a/api/services/account_service.py b/api/services/account_service.py index 2c401aad91..36c24ef7bf 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -13,10 +13,12 @@ from werkzeug.exceptions import Unauthorized from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created from extensions.ext_redis import redis_client +from libs.helper import RateLimiter, TokenManager from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair from models.account import * +from models.model import DifySetup from services.errors.account import ( AccountAlreadyInTenantError, AccountLoginError, @@ -28,14 +30,22 @@ from services.errors.account import ( LinkAccountIntegrateError, MemberNotInTenantError, NoPermissionError, + RateLimitExceededError, RoleAlreadyAssignedError, TenantNotFound, ) from tasks.mail_invite_member_task import send_invite_member_mail_task +from tasks.mail_reset_password_task import send_reset_password_mail_task class AccountService: + reset_password_rate_limiter = RateLimiter( + prefix="reset_password_rate_limit", + max_attempts=5, + time_window=60 * 60 + ) + @staticmethod def load_user(user_id: str) -> Account: account = Account.query.filter_by(id=user_id).first() @@ -119,10 +129,11 @@ class AccountService: return account @staticmethod - def create_account(email: str, name: str, interface_language: str, - password: str = None, - interface_theme: str = 'light', - timezone: str = 'America/New_York', ) -> Account: + def create_account(email: str, + name: str, + interface_language: str, + password: Optional[str] = None, + interface_theme: str = 'light') -> Account: """create account""" account = Account() account.email = email @@ -200,7 +211,6 @@ class AccountService: account.last_login_ip = ip_address db.session.add(account) db.session.commit() - logging.info(f'Account {account.id} logged in successfully.') @staticmethod def login(account: Account, *, ip_address: Optional[str] = None): @@ -221,9 +231,33 @@ class AccountService: return None return AccountService.load_user(account_id) + @classmethod + def send_reset_password_email(cls, account): + if cls.reset_password_rate_limiter.is_rate_limited(account.email): + raise RateLimitExceededError(f"Rate limit exceeded for email: {account.email}. Please try again later.") + + token = TokenManager.generate_token(account, 'reset_password') + send_reset_password_mail_task.delay( + language=account.interface_language, + to=account.email, + token=token + ) + cls.reset_password_rate_limiter.increment_rate_limit(account.email) + return token + + @classmethod + def revoke_reset_password_token(cls, token: str): + TokenManager.revoke_token(token, 'reset_password') + + @classmethod + def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]: + return TokenManager.get_token_data(token, 'reset_password') + + def _get_login_cache_key(*, account_id: str, token: str): return f"account_login:{account_id}:{token}" + class TenantService: @staticmethod @@ -444,8 +478,51 @@ class RegisterService: return f'member_invite:token:{token}' @classmethod - def register(cls, email, name, password: str = None, open_id: str = None, provider: str = None, - language: str = None, status: AccountStatus = None) -> Account: + def setup(cls, email: str, name: str, password: str, ip_address: str) -> None: + """ + Setup dify + + :param email: email + :param name: username + :param password: password + :param ip_address: ip address + """ + try: + # Register + account = AccountService.create_account( + email=email, + name=name, + interface_language=languages[0], + password=password, + ) + + account.last_login_ip = ip_address + account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) + + TenantService.create_owner_tenant_if_not_exist(account) + + dify_setup = DifySetup( + version=current_app.config['CURRENT_VERSION'] + ) + db.session.add(dify_setup) + db.session.commit() + except Exception as e: + db.session.query(DifySetup).delete() + db.session.query(TenantAccountJoin).delete() + db.session.query(Account).delete() + db.session.query(Tenant).delete() + db.session.commit() + + logging.exception(f'Setup failed: {e}') + raise ValueError(f'Setup failed: {e}') + + @classmethod + def register(cls, email, name, + password: Optional[str] = None, + open_id: Optional[str] = None, + provider: Optional[str] = None, + language: Optional[str] = None, + status: Optional[AccountStatus] = None) -> Account: db.session.begin_nested() """Register account""" try: diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index f73a6dcbb6..f73a88fdd1 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -18,7 +18,8 @@ class AppGenerateService: user: Union[Account, EndUser], args: Any, invoke_from: InvokeFrom, - streaming: bool = True) -> Union[dict, Generator[dict, None, None]]: + streaming: bool = True, + ) -> Union[dict, Generator[dict, None, None]]: """ App Content Generate :param app_model: app model diff --git a/api/services/app_service.py b/api/services/app_service.py index 23c00740c8..7f5b356772 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -16,13 +16,14 @@ from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelTy from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager -from events.app_event import app_model_config_was_updated, app_was_created, app_was_deleted +from events.app_event import app_model_config_was_updated, app_was_created from extensions.ext_database import db from models.account import Account from models.model import App, AppMode, AppModelConfig from models.tools import ApiToolProvider from services.tag_service import TagService from services.workflow_service import WorkflowService +from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task class AppService: @@ -395,16 +396,8 @@ class AppService: """ db.session.delete(app) db.session.commit() - - app_was_deleted.send(app) - - # todo async delete related data by event - # app_model_configs, site, api_tokens, installed_apps, recommended_apps BY app - # app_annotation_hit_histories, app_annotation_settings, app_dataset_joins BY app - # workflows, workflow_runs, workflow_node_executions, workflow_app_logs BY app - # conversations, pinned_conversations, messages BY app - # message_feedbacks, message_annotations, message_chains BY message - # message_agent_thoughts, message_files, saved_messages BY message + # Trigger asynchronous deletion of app and related data + remove_app_and_related_data_task.delay(app.id) def get_app_meta(self, app_model: App) -> dict: """ diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index 5c2fb83b72..82ee10ee78 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -96,7 +96,9 @@ class ConversationService: # generate conversation name try: - name = LLMGenerator.generate_conversation_name(app_model.tenant_id, message.query) + name = LLMGenerator.generate_conversation_name( + app_model.tenant_id, message.query, conversation.id, app_model.id + ) conversation.name = name except: pass diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index e8446da44c..6207a1a45c 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -34,7 +34,7 @@ from models.dataset import ( from models.model import UploadFile from models.source import DataSourceOauthBinding from services.errors.account import NoPermissionError -from services.errors.dataset import DatasetInUseError, DatasetNameDuplicateError +from services.errors.dataset import DatasetNameDuplicateError from services.errors.document import DocumentIndexingError from services.errors.file import FileNotExistsError from services.feature_service import FeatureModel, FeatureService @@ -234,9 +234,6 @@ class DatasetService: @staticmethod def delete_dataset(dataset_id, user): - count = AppDatasetJoin.query.filter_by(dataset_id=dataset_id).count() - if count > 0: - raise DatasetInUseError() dataset = DatasetService.get_dataset(dataset_id) @@ -251,6 +248,13 @@ class DatasetService: db.session.commit() return True + @staticmethod + def dataset_use_check(dataset_id) -> bool: + count = AppDatasetJoin.query.filter_by(dataset_id=dataset_id).count() + if count > 0: + return True + return False + @staticmethod def check_dataset_permission(dataset, user): if dataset.tenant_id != user.current_tenant_id: @@ -696,7 +700,7 @@ class DocumentService: elif document_data["data_source"]["type"] == "notion_import": notion_info_list = document_data["data_source"]['info_list']['notion_info_list'] exist_page_ids = [] - exist_document = dict() + exist_document = {} documents = Document.query.filter_by( dataset_id=dataset.id, tenant_id=current_user.current_tenant_id, diff --git a/api/services/errors/account.py b/api/services/errors/account.py index 14612eed75..ddc2dbdea8 100644 --- a/api/services/errors/account.py +++ b/api/services/errors/account.py @@ -51,3 +51,8 @@ class MemberNotInTenantError(BaseServiceError): class RoleAlreadyAssignedError(BaseServiceError): pass + + +class RateLimitExceededError(BaseServiceError): + pass + diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index 8ff96c7337..0378370d88 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -4,10 +4,6 @@ import time import numpy as np from sklearn.manifold import TSNE -from core.embedding.cached_embedding import CacheEmbedding -from core.model_manager import ModelManager -from core.model_runtime.entities.model_entities import ModelType -from core.rag.datasource.entity.embedding import Embeddings from core.rag.datasource.retrieval_service import RetrievalService from core.rag.models.document import Document from core.rag.retrieval.retrival_methods import RetrievalMethod @@ -45,17 +41,6 @@ class HitTestingService: if not retrieval_model: retrieval_model = dataset.retrieval_model if dataset.retrieval_model else default_retrieval_model - # get embedding model - model_manager = ModelManager() - embedding_model = model_manager.get_model_instance( - tenant_id=dataset.tenant_id, - model_type=ModelType.TEXT_EMBEDDING, - provider=dataset.embedding_model_provider, - model=dataset.embedding_model - ) - - embeddings = CacheEmbedding(embedding_model) - all_documents = RetrievalService.retrieve(retrival_method=retrieval_model['search_method'], dataset_id=dataset.id, query=query, @@ -80,20 +65,10 @@ class HitTestingService: db.session.add(dataset_query) db.session.commit() - return cls.compact_retrieve_response(dataset, embeddings, query, all_documents) + return cls.compact_retrieve_response(dataset, query, all_documents) @classmethod - def compact_retrieve_response(cls, dataset: Dataset, embeddings: Embeddings, query: str, documents: list[Document]): - text_embeddings = [ - embeddings.embed_query(query) - ] - - text_embeddings.extend(embeddings.embed_documents([document.page_content for document in documents])) - - tsne_position_data = cls.get_tsne_positions_from_embeddings(text_embeddings) - - query_position = tsne_position_data.pop(0) - + def compact_retrieve_response(cls, dataset: Dataset, query: str, documents: list[Document]): i = 0 records = [] for document in documents: @@ -113,7 +88,6 @@ class HitTestingService: record = { "segment": segment, "score": document.metadata.get('score', None), - "tsne_position": tsne_position_data[i] } records.append(record) @@ -123,7 +97,6 @@ class HitTestingService: return { "query": { "content": query, - "tsne_position": query_position, }, "records": records } diff --git a/api/services/message_service.py b/api/services/message_service.py index e826dcc6bf..e310d70d53 100644 --- a/api/services/message_service.py +++ b/api/services/message_service.py @@ -7,6 +7,8 @@ from core.llm_generator.llm_generator import LLMGenerator from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelManager from core.model_runtime.entities.model_entities import ModelType +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName +from core.ops.utils import measure_time from extensions.ext_database import db from libs.infinite_scroll_pagination import InfiniteScrollPagination from models.account import Account @@ -262,9 +264,21 @@ class MessageService: message_limit=3, ) - questions = LLMGenerator.generate_suggested_questions_after_answer( - tenant_id=app_model.tenant_id, - histories=histories + with measure_time() as timer: + questions = LLMGenerator.generate_suggested_questions_after_answer( + tenant_id=app_model.tenant_id, + histories=histories + ) + + # get tracing instance + trace_manager = TraceQueueManager(app_id=app_model.id) + trace_manager.add_trace_task( + TraceTask( + TraceTaskName.SUGGESTED_QUESTION_TRACE, + message_id=message_id, + suggested_question=questions, + timer=timer + ) ) return questions diff --git a/api/services/ops_service.py b/api/services/ops_service.py new file mode 100644 index 0000000000..ffc12a9acd --- /dev/null +++ b/api/services/ops_service.py @@ -0,0 +1,130 @@ +from core.ops.ops_trace_manager import OpsTraceManager, provider_config_map +from extensions.ext_database import db +from models.model import App, TraceAppConfig + + +class OpsService: + @classmethod + def get_tracing_app_config(cls, app_id: str, tracing_provider: str): + """ + Get tracing app config + :param app_id: app id + :param tracing_provider: tracing provider + :return: + """ + trace_config_data: TraceAppConfig = db.session.query(TraceAppConfig).filter( + TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider + ).first() + + if not trace_config_data: + return None + + # decrypt_token and obfuscated_token + tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id + decrypt_tracing_config = OpsTraceManager.decrypt_tracing_config(tenant_id, tracing_provider, trace_config_data.tracing_config) + decrypt_tracing_config = OpsTraceManager.obfuscated_decrypt_token(tracing_provider, decrypt_tracing_config) + + trace_config_data.tracing_config = decrypt_tracing_config + + return trace_config_data.to_dict() + + @classmethod + def create_tracing_app_config(cls, app_id: str, tracing_provider: str, tracing_config: dict): + """ + Create tracing app config + :param app_id: app id + :param tracing_provider: tracing provider + :param tracing_config: tracing config + :return: + """ + if tracing_provider not in provider_config_map.keys() and tracing_provider != None: + return {"error": f"Invalid tracing provider: {tracing_provider}"} + + config_class, other_keys = provider_config_map[tracing_provider]['config_class'], \ + provider_config_map[tracing_provider]['other_keys'] + default_config_instance = config_class(**tracing_config) + for key in other_keys: + if key in tracing_config and tracing_config[key] == "": + tracing_config[key] = getattr(default_config_instance, key, None) + + # api check + if not OpsTraceManager.check_trace_config_is_effective(tracing_config, tracing_provider): + return {"error": "Invalid Credentials"} + + # check if trace config already exists + trace_config_data: TraceAppConfig = db.session.query(TraceAppConfig).filter( + TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider + ).first() + + if trace_config_data: + return None + + # get tenant id + tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id + tracing_config = OpsTraceManager.encrypt_tracing_config(tenant_id, tracing_provider, tracing_config) + trace_config_data = TraceAppConfig( + app_id=app_id, + tracing_provider=tracing_provider, + tracing_config=tracing_config, + ) + db.session.add(trace_config_data) + db.session.commit() + + return {"result": "success"} + + @classmethod + def update_tracing_app_config(cls, app_id: str, tracing_provider: str, tracing_config: dict): + """ + Update tracing app config + :param app_id: app id + :param tracing_provider: tracing provider + :param tracing_config: tracing config + :return: + """ + if tracing_provider not in provider_config_map.keys(): + raise ValueError(f"Invalid tracing provider: {tracing_provider}") + + # check if trace config already exists + current_trace_config = db.session.query(TraceAppConfig).filter( + TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider + ).first() + + if not current_trace_config: + return None + + # get tenant id + tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id + tracing_config = OpsTraceManager.encrypt_tracing_config( + tenant_id, tracing_provider, tracing_config, current_trace_config.tracing_config + ) + + # api check + # decrypt_token + decrypt_tracing_config = OpsTraceManager.decrypt_tracing_config(tenant_id, tracing_provider, tracing_config) + if not OpsTraceManager.check_trace_config_is_effective(decrypt_tracing_config, tracing_provider): + raise ValueError("Invalid Credentials") + + current_trace_config.tracing_config = tracing_config + db.session.commit() + + return current_trace_config.to_dict() + + @classmethod + def delete_tracing_app_config(cls, app_id: str, tracing_provider: str): + """ + Delete tracing app config + :param app_id: app id + :param tracing_provider: tracing provider + :return: + """ + trace_config = db.session.query(TraceAppConfig).filter( + TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider + ).first() + + if not trace_config: + return None + + db.session.delete(trace_config) + db.session.commit() + + return True diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 6a155922b4..2c2c0efc7a 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -95,7 +95,7 @@ class RecommendedAppService: categories.add(recommended_app.category) # add category to categories - return {'recommended_apps': recommended_apps_result, 'categories': sorted(list(categories))} + return {'recommended_apps': recommended_apps_result, 'categories': sorted(categories)} @classmethod def _fetch_recommended_apps_from_dify_official(cls, language: str) -> dict: diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index d76cd4c7ff..010d53389a 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -514,8 +514,8 @@ class WorkflowConverter: prompt_rules = prompt_template_config['prompt_rules'] role_prefix = { - "user": prompt_rules['human_prefix'] if 'human_prefix' in prompt_rules else 'Human', - "assistant": prompt_rules['assistant_prefix'] if 'assistant_prefix' in prompt_rules else 'Assistant' + "user": prompt_rules.get('human_prefix', 'Human'), + "assistant": prompt_rules.get('assistant_prefix', 'Assistant') } else: advanced_completion_prompt_template = prompt_template.advanced_completion_prompt_template diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index ce0dba0885..17e92b876e 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -3,6 +3,8 @@ import time from datetime import datetime, timezone from typing import Optional +import yaml + from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager from core.model_runtime.utils.encoders import jsonable_encoder @@ -112,6 +114,56 @@ class WorkflowService: # return draft workflow return workflow + def import_draft_workflow(self, app_model: App, + data: str, + account: Account) -> Workflow: + """ + Import draft workflow + :param app_model: App instance + :param data: import data + :param account: Account instance + :return: + """ + try: + import_data = yaml.safe_load(data) + except yaml.YAMLError as e: + raise ValueError("Invalid YAML format in data argument.") + + app_data = import_data.get('app') + workflow = import_data.get('workflow') + + if not app_data: + raise ValueError("Missing app in data argument") + + app_mode = AppMode.value_of(app_data.get('mode')) + if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]: + raise ValueError("Only support import workflow in advanced-chat or workflow app.") + + if app_data.get('mode') != app_model.mode: + raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_model.mode}") + + if not workflow: + raise ValueError("Missing workflow in data argument " + "when app mode is advanced-chat or workflow") + + # fetch draft workflow by app_model + current_draft_workflow = self.get_draft_workflow(app_model=app_model) + if current_draft_workflow: + unique_hash = current_draft_workflow.unique_hash + else: + unique_hash = None + + # sync draft workflow + draft_workflow = self.sync_draft_workflow( + app_model=app_model, + graph=workflow.get('graph'), + features=workflow.get('features'), + unique_hash=unique_hash, + account=account + ) + + return draft_workflow + def publish_workflow(self, app_model: App, account: Account, draft_workflow: Optional[Workflow] = None) -> Workflow: diff --git a/api/tasks/mail_invite_member_task.py b/api/tasks/mail_invite_member_task.py index 3341f5f4b8..1f40c05077 100644 --- a/api/tasks/mail_invite_member_task.py +++ b/api/tasks/mail_invite_member_task.py @@ -39,16 +39,15 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content) else: html_content = render_template('invite_member_mail_template_en-US.html', - to=to, - inviter_name=inviter_name, - workspace_name=workspace_name, - url=url) + to=to, + inviter_name=inviter_name, + workspace_name=workspace_name, + url=url) mail.send(to=to, subject="Join Dify Workspace Now", html=html_content) - end_at = time.perf_counter() logging.info( click.style('Send invite member mail to {} succeeded: latency: {}'.format(to, end_at - start_at), fg='green')) except Exception: - logging.exception("Send invite member mail to {} failed".format(to)) + logging.exception("Send invite member mail to {} failed".format(to)) \ No newline at end of file diff --git a/api/tasks/mail_reset_password_task.py b/api/tasks/mail_reset_password_task.py new file mode 100644 index 0000000000..0e64c6f163 --- /dev/null +++ b/api/tasks/mail_reset_password_task.py @@ -0,0 +1,44 @@ +import logging +import time + +import click +from celery import shared_task +from flask import current_app, render_template + +from extensions.ext_mail import mail + + +@shared_task(queue='mail') +def send_reset_password_mail_task(language: str, to: str, token: str): + """ + Async Send reset password mail + :param language: Language in which the email should be sent (e.g., 'en', 'zh') + :param to: Recipient email address + :param token: Reset password token to be included in the email + """ + if not mail.is_inited(): + return + + logging.info(click.style('Start password reset mail to {}'.format(to), fg='green')) + start_at = time.perf_counter() + + # send reset password mail using different languages + try: + url = f'{current_app.config.get("CONSOLE_WEB_URL")}/forgot-password?token={token}' + if language == 'zh-Hans': + html_content = render_template('reset_password_mail_template_zh-CN.html', + to=to, + url=url) + mail.send(to=to, subject="重置您的 Dify 密码", html=html_content) + else: + html_content = render_template('reset_password_mail_template_en-US.html', + to=to, + url=url) + mail.send(to=to, subject="Reset Your Dify Password", html=html_content) + + end_at = time.perf_counter() + logging.info( + click.style('Send password reset mail to {} succeeded: latency: {}'.format(to, end_at - start_at), + fg='green')) + except Exception: + logging.exception("Send password reset mail to {} failed".format(to)) diff --git a/api/tasks/ops_trace_task.py b/api/tasks/ops_trace_task.py new file mode 100644 index 0000000000..1d33609205 --- /dev/null +++ b/api/tasks/ops_trace_task.py @@ -0,0 +1,46 @@ +import logging +import time + +from celery import shared_task +from flask import current_app + +from core.ops.entities.trace_entity import trace_info_info_map +from core.rag.models.document import Document +from models.model import Message +from models.workflow import WorkflowRun + + +@shared_task(queue='ops_trace') +def process_trace_tasks(tasks_data): + """ + Async process trace tasks + :param tasks_data: List of dictionaries containing task data + + Usage: process_trace_tasks.delay(tasks_data) + """ + from core.ops.ops_trace_manager import OpsTraceManager + + trace_info = tasks_data.get('trace_info') + app_id = tasks_data.get('app_id') + conversation_id = tasks_data.get('conversation_id') + message_id = tasks_data.get('message_id') + trace_info_type = tasks_data.get('trace_info_type') + trace_instance = OpsTraceManager.get_ops_trace_instance(app_id, conversation_id, message_id) + + if trace_info.get('message_data'): + trace_info['message_data'] = Message.from_dict(data=trace_info['message_data']) + if trace_info.get('workflow_data'): + trace_info['workflow_data'] = WorkflowRun.from_dict(data=trace_info['workflow_data']) + if trace_info.get('documents'): + trace_info['documents'] = [Document(**doc) for doc in trace_info['documents']] + + try: + if trace_instance: + with current_app.app_context(): + trace_type = trace_info_info_map.get(trace_info_type) + if trace_type: + trace_info = trace_type(**trace_info) + trace_instance.trace(trace_info) + end_at = time.perf_counter() + except Exception: + logging.exception("Processing trace tasks failed") diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py new file mode 100644 index 0000000000..117ce8d923 --- /dev/null +++ b/api/tasks/remove_app_and_related_data_task.py @@ -0,0 +1,153 @@ +import logging +import time + +import click +from celery import shared_task +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError + +from extensions.ext_database import db +from models.dataset import AppDatasetJoin +from models.model import ( + ApiToken, + AppAnnotationHitHistory, + AppAnnotationSetting, + AppModelConfig, + Conversation, + EndUser, + InstalledApp, + Message, + MessageAgentThought, + MessageAnnotation, + MessageChain, + MessageFeedback, + MessageFile, + RecommendedApp, + Site, + TagBinding, +) +from models.tools import WorkflowToolProvider +from models.web import PinnedConversation, SavedMessage +from models.workflow import Workflow, WorkflowAppLog, WorkflowNodeExecution, WorkflowRun + + +@shared_task(queue='app_deletion', bind=True, max_retries=3) +def remove_app_and_related_data_task(self, app_id: str): + logging.info(click.style(f'Start deleting app and related data: {app_id}', fg='green')) + start_at = time.perf_counter() + try: + # Use a transaction to ensure all deletions succeed or none do + with db.session.begin_nested(): + # Delete related data + _delete_app_model_configs(app_id) + _delete_app_site(app_id) + _delete_app_api_tokens(app_id) + _delete_installed_apps(app_id) + _delete_recommended_apps(app_id) + _delete_app_annotation_data(app_id) + _delete_app_dataset_joins(app_id) + _delete_app_workflows(app_id) + _delete_app_conversations(app_id) + _delete_app_messages(app_id) + _delete_workflow_tool_providers(app_id) + _delete_app_tag_bindings(app_id) + _delete_end_users(app_id) + + # If we reach here, the transaction was successful + db.session.commit() + + end_at = time.perf_counter() + logging.info(click.style(f'App and related data deleted: {app_id} latency: {end_at - start_at}', fg='green')) + + except SQLAlchemyError as e: + db.session.rollback() + logging.exception( + click.style(f"Database error occurred while deleting app {app_id} and related data", fg='red')) + raise self.retry(exc=e, countdown=60) # Retry after 60 seconds + + except Exception as e: + logging.exception(click.style(f"Error occurred while deleting app {app_id} and related data", fg='red')) + raise self.retry(exc=e, countdown=60) # Retry after 60 seconds + + +def _delete_app_model_configs(app_id: str): + db.session.query(AppModelConfig).filter(AppModelConfig.app_id == app_id).delete() + + +def _delete_app_site(app_id: str): + db.session.query(Site).filter(Site.app_id == app_id).delete() + + +def _delete_app_api_tokens(app_id: str): + db.session.query(ApiToken).filter(ApiToken.app_id == app_id).delete() + + +def _delete_installed_apps(app_id: str): + db.session.query(InstalledApp).filter(InstalledApp.app_id == app_id).delete() + + +def _delete_recommended_apps(app_id: str): + db.session.query(RecommendedApp).filter(RecommendedApp.app_id == app_id).delete() + + +def _delete_app_annotation_data(app_id: str): + db.session.query(AppAnnotationHitHistory).filter(AppAnnotationHitHistory.app_id == app_id).delete() + db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_id).delete() + + +def _delete_app_dataset_joins(app_id: str): + db.session.query(AppDatasetJoin).filter(AppDatasetJoin.app_id == app_id).delete() + + +def _delete_app_workflows(app_id: str): + db.session.query(WorkflowRun).filter( + WorkflowRun.workflow_id.in_( + db.session.query(Workflow.id).filter(Workflow.app_id == app_id) + ) + ).delete(synchronize_session=False) + db.session.query(WorkflowNodeExecution).filter( + WorkflowNodeExecution.workflow_id.in_( + db.session.query(Workflow.id).filter(Workflow.app_id == app_id) + ) + ).delete(synchronize_session=False) + db.session.query(WorkflowAppLog).filter(WorkflowAppLog.app_id == app_id).delete(synchronize_session=False) + db.session.query(Workflow).filter(Workflow.app_id == app_id).delete(synchronize_session=False) + + +def _delete_app_conversations(app_id: str): + db.session.query(PinnedConversation).filter( + PinnedConversation.conversation_id.in_( + db.session.query(Conversation.id).filter(Conversation.app_id == app_id) + ) + ).delete(synchronize_session=False) + db.session.query(Conversation).filter(Conversation.app_id == app_id).delete() + + +def _delete_app_messages(app_id: str): + message_ids = select(Message.id).filter(Message.app_id == app_id).scalar_subquery() + db.session.query(MessageFeedback).filter(MessageFeedback.message_id.in_(message_ids)).delete( + synchronize_session=False) + db.session.query(MessageAnnotation).filter(MessageAnnotation.message_id.in_(message_ids)).delete( + synchronize_session=False) + db.session.query(MessageChain).filter(MessageChain.message_id.in_(message_ids)).delete(synchronize_session=False) + db.session.query(MessageAgentThought).filter(MessageAgentThought.message_id.in_(message_ids)).delete( + synchronize_session=False) + db.session.query(MessageFile).filter(MessageFile.message_id.in_(message_ids)).delete(synchronize_session=False) + db.session.query(SavedMessage).filter(SavedMessage.message_id.in_(message_ids)).delete(synchronize_session=False) + db.session.query(Message).filter(Message.app_id == app_id).delete(synchronize_session=False) + + +def _delete_workflow_tool_providers(app_id: str): + db.session.query(WorkflowToolProvider).filter( + WorkflowToolProvider.app_id == app_id + ).delete(synchronize_session=False) + + +def _delete_app_tag_bindings(app_id: str): + db.session.query(TagBinding).filter( + TagBinding.target_id == app_id + ).delete(synchronize_session=False) + + +def _delete_end_users(app_id: str): + db.session.query(EndUser).filter(EndUser.app_id == app_id).delete() diff --git a/api/templates/reset_password_mail_template_en-US.html b/api/templates/reset_password_mail_template_en-US.html new file mode 100644 index 0000000000..ffc558ab66 --- /dev/null +++ b/api/templates/reset_password_mail_template_en-US.html @@ -0,0 +1,72 @@ + + + + + + + +
+
+ Dify Logo +
+
+

Dear {{ to }},

+

We have received a request to reset your password. If you initiated this request, please click the button below to reset your password:

+

Reset Password

+

If you did not request a password reset, please ignore this email and your account will remain secure.

+
+ +
+ + diff --git a/api/templates/reset_password_mail_template_zh-CN.html b/api/templates/reset_password_mail_template_zh-CN.html new file mode 100644 index 0000000000..b74b23ac3f --- /dev/null +++ b/api/templates/reset_password_mail_template_zh-CN.html @@ -0,0 +1,72 @@ + + + + + + + +
+
+ Dify Logo +
+
+

尊敬的 {{ to }},

+

我们收到了您关于重置密码的请求。如果是您本人操作,请点击以下按钮重置您的密码:

+

重置密码

+

如果您没有请求重置密码,请忽略此邮件,您的账户信息将保持安全。

+
+ +
+ + diff --git a/api/tests/integration_tests/model_runtime/__mock/openai_chat.py b/api/tests/integration_tests/model_runtime/__mock/openai_chat.py index c5af941d1e..ba902e32ea 100644 --- a/api/tests/integration_tests/model_runtime/__mock/openai_chat.py +++ b/api/tests/integration_tests/model_runtime/__mock/openai_chat.py @@ -73,17 +73,15 @@ class MockChatClass: return FunctionCall(name=function_name, arguments=dumps(parameters)) @staticmethod - def generate_tool_calls( - tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, - ) -> Optional[list[ChatCompletionMessageToolCall]]: + def generate_tool_calls(tools = NOT_GIVEN) -> Optional[list[ChatCompletionMessageToolCall]]: list_tool_calls = [] if not tools or len(tools) == 0: return None - tool: ChatCompletionToolParam = tools[0] + tool = tools[0] - if tools['type'] != 'function': + if 'type' in tools and tools['type'] != 'function': return None - + function = tool['function'] function_call = MockChatClass.generate_function_call(functions=[function]) diff --git a/api/tests/integration_tests/model_runtime/localai/test_rerank.py b/api/tests/integration_tests/model_runtime/localai/test_rerank.py index a75439337e..99847bc852 100644 --- a/api/tests/integration_tests/model_runtime/localai/test_rerank.py +++ b/api/tests/integration_tests/model_runtime/localai/test_rerank.py @@ -1,64 +1,8 @@ import os import pytest -from api.core.model_runtime.entities.rerank_entities import RerankResult - -from core.model_runtime.errors.validate import CredentialsValidateFailedError -from core.model_runtime.model_providers.localai.rerank.rerank import LocalaiRerankModel - - -def test_validate_credentials_for_chat_model(): - model = LocalaiRerankModel() - - with pytest.raises(CredentialsValidateFailedError): - model.validate_credentials( - model='bge-reranker-v2-m3', - credentials={ - 'server_url': 'hahahaha', - 'completion_type': 'completion', - } - ) - - model.validate_credentials( - model='bge-reranker-base', - credentials={ - 'server_url': os.environ.get('LOCALAI_SERVER_URL'), - 'completion_type': 'completion', - } - ) - -def test_invoke_rerank_model(): - model = LocalaiRerankModel() - - response = model.invoke( - model='bge-reranker-base', - credentials={ - 'server_url': os.environ.get('LOCALAI_SERVER_URL') - }, - query='Organic skincare products for sensitive skin', - docs=[ - "Eco-friendly kitchenware for modern homes", - "Biodegradable cleaning supplies for eco-conscious consumers", - "Organic cotton baby clothes for sensitive skin", - "Natural organic skincare range for sensitive skin", - "Tech gadgets for smart homes: 2024 edition", - "Sustainable gardening tools and compost solutions", - "Sensitive skin-friendly facial cleansers and toners", - "Organic food wraps and storage solutions", - "Yoga mats made from recycled materials" - ], - top_n=3, - score_threshold=0.75, - user="abc-123" - ) - - assert isinstance(response, RerankResult) - assert len(response.docs) == 3 -import os - -import pytest -from api.core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult +from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.model_providers.localai.rerank.rerank import LocalaiRerankModel diff --git a/api/tests/integration_tests/model_runtime/zhipuai/test_llm.py b/api/tests/integration_tests/model_runtime/zhipuai/test_llm.py index 393fe9fb2f..0f92b50cb0 100644 --- a/api/tests/integration_tests/model_runtime/zhipuai/test_llm.py +++ b/api/tests/integration_tests/model_runtime/zhipuai/test_llm.py @@ -152,4 +152,4 @@ def test_get_tools_num_tokens(): ] ) - assert num_tokens == 108 \ No newline at end of file + assert num_tokens == 88 diff --git a/api/tests/integration_tests/tools/__mock/http.py b/api/tests/integration_tests/tools/__mock/http.py new file mode 100644 index 0000000000..41bb3daeb5 --- /dev/null +++ b/api/tests/integration_tests/tools/__mock/http.py @@ -0,0 +1,36 @@ +import json +from typing import Literal + +import httpx +import pytest +from _pytest.monkeypatch import MonkeyPatch + + +class MockedHttp: + def httpx_request(method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], + url: str, **kwargs) -> httpx.Response: + """ + Mocked httpx.request + """ + request = httpx.Request( + method, + url, + params=kwargs.get('params'), + headers=kwargs.get('headers'), + cookies=kwargs.get('cookies') + ) + data = kwargs.get('data', None) + resp = json.dumps(data).encode('utf-8') if data else b'OK' + response = httpx.Response( + status_code=200, + request=request, + content=resp, + ) + return response + + +@pytest.fixture +def setup_http_mock(request, monkeypatch: MonkeyPatch): + monkeypatch.setattr(httpx, "request", MockedHttp.httpx_request) + yield + monkeypatch.undo() diff --git a/api/tests/integration_tests/tools/api_tool/__init__.py b/api/tests/integration_tests/tools/api_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/tools/api_tool/test_api_tool.py b/api/tests/integration_tests/tools/api_tool/test_api_tool.py new file mode 100644 index 0000000000..f6e7b153dd --- /dev/null +++ b/api/tests/integration_tests/tools/api_tool/test_api_tool.py @@ -0,0 +1,39 @@ +from core.tools.tool.api_tool import ApiTool +from core.tools.tool.tool import Tool +from tests.integration_tests.tools.__mock.http import setup_http_mock + +tool_bundle = { + 'server_url': 'http://www.example.com/{path_param}', + 'method': 'post', + 'author': '', + 'openapi': {'parameters': [{'in': 'path', 'name': 'path_param'}, + {'in': 'query', 'name': 'query_param'}, + {'in': 'cookie', 'name': 'cookie_param'}, + {'in': 'header', 'name': 'header_param'}, + ], + 'requestBody': { + 'content': {'application/json': {'schema': {'properties': {'body_param': {'type': 'string'}}}}}} + }, + 'parameters': [] +} +parameters = { + 'path_param': 'p_param', + 'query_param': 'q_param', + 'cookie_param': 'c_param', + 'header_param': 'h_param', + 'body_param': 'b_param', +} + + +def test_api_tool(setup_http_mock): + tool = ApiTool(api_bundle=tool_bundle, runtime=Tool.Runtime(credentials={'auth_type': 'none'})) + headers = tool.assembling_request(parameters) + response = tool.do_http_request(tool.api_bundle.server_url, tool.api_bundle.method, headers, parameters) + + assert response.status_code == 200 + assert '/p_param' == response.request.url.path + assert b'query_param=q_param' == response.request.url.query + assert 'h_param' == response.request.headers.get('header_param') + assert 'application/json' == response.request.headers.get('content-type') + assert 'cookie_param=c_param' == response.request.headers.get('cookie') + assert 'b_param' in response.content.decode() diff --git a/api/tests/integration_tests/vdb/milvus/test_milvus.py b/api/tests/integration_tests/vdb/milvus/test_milvus.py index 2ce85445fb..9c0917ef30 100644 --- a/api/tests/integration_tests/vdb/milvus/test_milvus.py +++ b/api/tests/integration_tests/vdb/milvus/test_milvus.py @@ -24,9 +24,6 @@ class MilvusVectorTest(AbstractVectorTest): hits_by_full_text = self.vector.search_by_full_text(query=get_example_text()) assert len(hits_by_full_text) == 0 - def delete_by_document_id(self): - self.vector.delete_by_document_id(document_id=self.example_doc_id) - 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 diff --git a/api/tests/integration_tests/vdb/opensearch/test_opensearch.py b/api/tests/integration_tests/vdb/opensearch/test_opensearch.py index e372c9b7ac..ea1e05da90 100644 --- a/api/tests/integration_tests/vdb/opensearch/test_opensearch.py +++ b/api/tests/integration_tests/vdb/opensearch/test_opensearch.py @@ -91,23 +91,6 @@ class TestOpenSearchVector: assert hits_by_vector[0].metadata['document_id'] == self.example_doc_id, \ f"Expected document ID {self.example_doc_id}, got {hits_by_vector[0].metadata['document_id']}" - def test_delete_by_document_id(self): - self.vector._client.delete_by_query.return_value = {'deleted': 1} - - doc = Document(page_content="Test content to delete", metadata={"document_id": self.example_doc_id}) - embedding = [0.1] * 128 - - with patch('opensearchpy.helpers.bulk') as mock_bulk: - mock_bulk.return_value = ([], []) - self.vector.add_texts([doc], [embedding]) - - self.vector.delete_by_document_id(document_id=self.example_doc_id) - - self.vector._client.search.return_value = {'hits': {'total': {'value': 0}, 'hits': []}} - - ids = self.vector.get_ids_by_metadata_field(key='document_id', value=self.example_doc_id) - assert ids is None or len(ids) == 0 - def test_get_ids_by_metadata_field(self): mock_response = { 'hits': { @@ -169,10 +152,6 @@ class TestOpenSearchVectorWithRedis: expected_doc_id = "example_doc_id" self.tester.test_search_by_full_text(search_response, expected_length, expected_doc_id) - def test_delete_by_document_id(self): - self.tester.setup_method() - self.tester.test_delete_by_document_id() - def test_get_ids_by_metadata_field(self): self.tester.setup_method() self.tester.test_get_ids_by_metadata_field() diff --git a/api/tests/integration_tests/vdb/pgvecto_rs/test_pgvecto_rs.py b/api/tests/integration_tests/vdb/pgvecto_rs/test_pgvecto_rs.py index 89a40a92be..e6ce8aab3d 100644 --- a/api/tests/integration_tests/vdb/pgvecto_rs/test_pgvecto_rs.py +++ b/api/tests/integration_tests/vdb/pgvecto_rs/test_pgvecto_rs.py @@ -26,9 +26,6 @@ class PGVectoRSVectorTest(AbstractVectorTest): hits_by_full_text = self.vector.search_by_full_text(query=get_example_text()) assert len(hits_by_full_text) == 0 - def delete_by_document_id(self): - self.vector.delete_by_document_id(document_id=self.example_doc_id) - 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 diff --git a/api/tests/integration_tests/vdb/test_vector_store.py b/api/tests/integration_tests/vdb/test_vector_store.py index 3930daf484..cb35822709 100644 --- a/api/tests/integration_tests/vdb/test_vector_store.py +++ b/api/tests/integration_tests/vdb/test_vector_store.py @@ -81,10 +81,6 @@ class AbstractVectorTest: def text_exists(self): assert self.vector.text_exists(self.example_doc_id) - def delete_by_document_id(self): - with pytest.raises(NotImplementedError): - self.vector.delete_by_document_id(document_id=self.example_doc_id) - def get_ids_by_metadata_field(self): with pytest.raises(NotImplementedError): self.vector.get_ids_by_metadata_field(key='key', value='value') @@ -95,7 +91,6 @@ class AbstractVectorTest: self.search_by_full_text() self.text_exists() self.get_ids_by_metadata_field() - self.delete_by_document_id() added_doc_ids = self.add_texts() self.delete_by_ids(added_doc_ids) self.delete_vector() diff --git a/api/tests/integration_tests/vdb/tidb_vector/test_tidb_vector.py b/api/tests/integration_tests/vdb/tidb_vector/test_tidb_vector.py index 837a228a55..18e00dbedd 100644 --- a/api/tests/integration_tests/vdb/tidb_vector/test_tidb_vector.py +++ b/api/tests/integration_tests/vdb/tidb_vector/test_tidb_vector.py @@ -16,7 +16,8 @@ def tidb_vector(): port="4000", user="xxx.root", password="xxxxxx", - database="dify" + database="dify", + program_name="langgenius/dify" ) ) @@ -42,9 +43,6 @@ class TiDBVectorTest(AbstractVectorTest): ids = self.vector.get_ids_by_metadata_field(key='document_id', value=self.example_doc_id) assert len(ids) == 0 - def delete_by_document_id(self): - self.vector.delete_by_document_id(document_id=self.example_doc_id) - def test_tidb_vector(setup_mock_redis, setup_tidbvector_mock, tidb_vector, mock_session): TiDBVectorTest(vector=tidb_vector).run_all_tests() diff --git a/api/tests/integration_tests/workflow/nodes/__mock/http.py b/api/tests/integration_tests/workflow/nodes/__mock/http.py index b74a49b640..beb5c04009 100644 --- a/api/tests/integration_tests/workflow/nodes/__mock/http.py +++ b/api/tests/integration_tests/workflow/nodes/__mock/http.py @@ -2,84 +2,52 @@ import os from json import dumps from typing import Literal -import httpx._api as httpx +import httpx import pytest -import requests.api as requests from _pytest.monkeypatch import MonkeyPatch -from httpx import Request as HttpxRequest -from requests import Response as RequestsResponse -from yarl import URL MOCK = os.getenv('MOCK_SWITCH', 'false') == 'true' + class MockedHttp: - def requests_request(method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], url: str, - **kwargs) -> RequestsResponse: - """ - Mocked requests.request - """ - response = RequestsResponse() - response.url = str(URL(url) % kwargs.get('params', {})) - response.headers = kwargs.get('headers', {}) - - if url == 'http://404.com': - response.status_code = 404 - response._content = b'Not Found' - return response - - # get data, files - data = kwargs.get('data', None) - files = kwargs.get('files', None) - - if data is not None: - resp = dumps(data).encode('utf-8') - if files is not None: - resp = dumps(files).encode('utf-8') - else: - resp = b'OK' - - response.status_code = 200 - response._content = resp - return response - - def httpx_request(method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + def httpx_request(method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], url: str, **kwargs) -> httpx.Response: """ Mocked httpx.request """ - response = httpx.Response( - status_code=200, - request=HttpxRequest(method, url) - ) - response.headers = kwargs.get('headers', {}) - if url == 'http://404.com': - response.status_code = 404 - response.content = b'Not Found' + response = httpx.Response( + status_code=404, + request=httpx.Request(method, url), + content=b'Not Found' + ) return response - + # get data, files data = kwargs.get('data', None) files = kwargs.get('files', None) - if data is not None: resp = dumps(data).encode('utf-8') - if files is not None: + elif files is not None: resp = dumps(files).encode('utf-8') else: resp = b'OK' - response.status_code = 200 - response._content = resp + response = httpx.Response( + status_code=200, + request=httpx.Request(method, url), + headers=kwargs.get('headers', {}), + content=resp + ) return response + @pytest.fixture def setup_http_mock(request, monkeypatch: MonkeyPatch): if not MOCK: yield return - monkeypatch.setattr(requests, "request", MockedHttp.requests_request) monkeypatch.setattr(httpx, "request", MockedHttp.httpx_request) yield - monkeypatch.undo() \ No newline at end of file + monkeypatch.undo() diff --git a/api/tests/integration_tests/workflow/nodes/test_http.py b/api/tests/integration_tests/workflow/nodes/test_http.py index ffa2741e55..eaed24e56c 100644 --- a/api/tests/integration_tests/workflow/nodes/test_http.py +++ b/api/tests/integration_tests/workflow/nodes/test_http.py @@ -1,3 +1,5 @@ +from urllib.parse import urlencode + import pytest from core.app.entities.app_invoke_entities import InvokeFrom @@ -20,6 +22,7 @@ pool = VariablePool(system_variables={}, user_inputs={}) pool.append_variable(node_id='a', variable_key_list=['b123', 'args1'], value=1) pool.append_variable(node_id='a', variable_key_list=['b123', 'args2'], value=2) + @pytest.mark.parametrize('setup_http_mock', [['none']], indirect=True) def test_get(setup_http_mock): node = HttpRequestNode(config={ @@ -33,7 +36,7 @@ def test_get(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -52,6 +55,7 @@ def test_get(setup_http_mock): assert 'api-key: Basic ak-xxx' in data assert 'X-Header: 123' in data + @pytest.mark.parametrize('setup_http_mock', [['none']], indirect=True) def test_no_auth(setup_http_mock): node = HttpRequestNode(config={ @@ -78,6 +82,7 @@ def test_no_auth(setup_http_mock): assert '?A=b' in data assert 'X-Header: 123' in data + @pytest.mark.parametrize('setup_http_mock', [['none']], indirect=True) def test_custom_authorization_header(setup_http_mock): node = HttpRequestNode(config={ @@ -110,6 +115,7 @@ def test_custom_authorization_header(setup_http_mock): assert 'X-Header: 123' in data assert 'X-Auth: Auth' in data + @pytest.mark.parametrize('setup_http_mock', [['none']], indirect=True) def test_template(setup_http_mock): node = HttpRequestNode(config={ @@ -123,7 +129,7 @@ def test_template(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -143,6 +149,7 @@ def test_template(setup_http_mock): assert 'X-Header: 123' in data assert 'X-Header2: 2' in data + @pytest.mark.parametrize('setup_http_mock', [['none']], indirect=True) def test_json(setup_http_mock): node = HttpRequestNode(config={ @@ -156,7 +163,7 @@ def test_json(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -177,6 +184,7 @@ def test_json(setup_http_mock): assert 'api-key: Basic ak-xxx' in data assert 'X-Header: 123' in data + def test_x_www_form_urlencoded(setup_http_mock): node = HttpRequestNode(config={ 'id': '1', @@ -189,7 +197,7 @@ def test_x_www_form_urlencoded(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -210,6 +218,7 @@ def test_x_www_form_urlencoded(setup_http_mock): assert 'api-key: Basic ak-xxx' in data assert 'X-Header: 123' in data + def test_form_data(setup_http_mock): node = HttpRequestNode(config={ 'id': '1', @@ -222,7 +231,7 @@ def test_form_data(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -246,6 +255,7 @@ def test_form_data(setup_http_mock): assert 'api-key: Basic ak-xxx' in data assert 'X-Header: 123' in data + def test_none_data(setup_http_mock): node = HttpRequestNode(config={ 'id': '1', @@ -258,7 +268,7 @@ def test_none_data(setup_http_mock): 'type': 'api-key', 'config': { 'type': 'basic', - 'api_key':'ak-xxx', + 'api_key': 'ak-xxx', 'header': 'api-key', } }, @@ -278,3 +288,59 @@ def test_none_data(setup_http_mock): assert 'api-key: Basic ak-xxx' in data assert 'X-Header: 123' in data assert '123123123' not in data + + +def test_mock_404(setup_http_mock): + node = HttpRequestNode(config={ + 'id': '1', + 'data': { + 'title': 'http', + 'desc': '', + 'method': 'get', + 'url': 'http://404.com', + 'authorization': { + 'type': 'no-auth', + 'config': None, + }, + 'body': None, + 'params': '', + 'headers': 'X-Header:123', + 'mask_authorization_header': False, + } + }, **BASIC_NODE_DATA) + + result = node.run(pool) + resp = result.outputs + + assert 404 == resp.get('status_code') + assert 'Not Found' in resp.get('body') + + +def test_multi_colons_parse(setup_http_mock): + node = HttpRequestNode(config={ + 'id': '1', + 'data': { + 'title': 'http', + 'desc': '', + 'method': 'get', + 'url': 'http://example.com', + 'authorization': { + 'type': 'no-auth', + 'config': None, + }, + 'params': 'Referer:http://example1.com\nRedirect:http://example2.com', + 'headers': 'Referer:http://example3.com\nRedirect:http://example4.com', + 'body': { + 'type': 'form-data', + 'data': 'Referer:http://example5.com\nRedirect:http://example6.com' + }, + 'mask_authorization_header': False, + } + }, **BASIC_NODE_DATA) + + result = node.run(pool) + resp = result.outputs + + assert urlencode({'Redirect': 'http://example2.com'}) in result.process_data.get('request') + assert 'form-data; name="Redirect"\n\nhttp://example6.com' in result.process_data.get('request') + assert 'http://example3.com' == resp.get('headers').get('referer') diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index a150be3c00..d7a6c1224f 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -112,7 +112,7 @@ def test_execute_llm(setup_openai_mock): # Mock db.session.close() db.session.close = MagicMock() - node._fetch_model_config = MagicMock(return_value=tuple([model_instance, model_config])) + node._fetch_model_config = MagicMock(return_value=(model_instance, model_config)) # execute node result = node.run(pool) @@ -229,7 +229,7 @@ def test_execute_llm_with_jinja2(setup_code_executor_mock, setup_openai_mock): # Mock db.session.close() db.session.close = MagicMock() - node._fetch_model_config = MagicMock(return_value=tuple([model_instance, model_config])) + node._fetch_model_config = MagicMock(return_value=(model_instance, model_config)) # execute node result = node.run(pool) 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 056c78441d..e5fd2bc1fd 100644 --- a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py +++ b/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py @@ -1,5 +1,6 @@ import json import os +from typing import Optional from unittest.mock import MagicMock import pytest @@ -7,6 +8,7 @@ import pytest from core.app.entities.app_invoke_entities import InvokeFrom, ModelConfigWithCredentialsEntity from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle from core.entities.provider_entities import CustomConfiguration, CustomProviderConfiguration, SystemConfiguration +from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.model_runtime.entities.model_entities import ModelType from core.model_runtime.model_providers.model_provider_factory import ModelProviderFactory @@ -59,7 +61,17 @@ def get_mocked_fetch_model_config( provider_model_bundle=provider_model_bundle ) - return MagicMock(return_value=tuple([model_instance, model_config])) + return MagicMock(return_value=(model_instance, model_config)) + +def get_mocked_fetch_memory(memory_text: str): + class MemoryMock: + def get_history_prompt_text(self, human_prefix: str = "Human", + ai_prefix: str = "Assistant", + max_token_limit: int = 2000, + message_limit: Optional[int] = None): + return memory_text + + return MagicMock(return_value=MemoryMock()) @pytest.mark.parametrize('setup_openai_mock', [['chat']], indirect=True) def test_function_calling_parameter_extractor(setup_openai_mock): @@ -354,4 +366,83 @@ def test_extract_json_response(): hello world. """) - assert result['location'] == 'kawaii' \ No newline at end of file + assert result['location'] == 'kawaii' + +@pytest.mark.parametrize('setup_anthropic_mock', [['none']], indirect=True) +def test_chat_parameter_extractor_with_memory(setup_anthropic_mock): + """ + Test chat parameter extractor with memory. + """ + node = ParameterExtractorNode( + tenant_id='1', + app_id='1', + workflow_id='1', + user_id='1', + invoke_from=InvokeFrom.WEB_APP, + user_from=UserFrom.ACCOUNT, + config={ + 'id': 'llm', + 'data': { + 'title': '123', + 'type': 'parameter-extractor', + 'model': { + 'provider': 'anthropic', + 'name': 'claude-2', + 'mode': 'chat', + 'completion_params': {} + }, + 'query': ['sys', 'query'], + 'parameters': [{ + 'name': 'location', + 'type': 'string', + 'description': 'location', + 'required': True + }], + 'reasoning_mode': 'prompt', + 'instruction': '', + 'memory': { + 'window': { + 'enabled': True, + 'size': 50 + } + }, + } + } + ) + + node._fetch_model_config = get_mocked_fetch_model_config( + provider='anthropic', model='claude-2', mode='chat', credentials={ + 'anthropic_api_key': os.environ.get('ANTHROPIC_API_KEY') + } + ) + node._fetch_memory = get_mocked_fetch_memory('customized memory') + db.session.close = MagicMock() + + # construct variable pool + pool = VariablePool(system_variables={ + SystemVariable.QUERY: 'what\'s the weather in SF', + SystemVariable.FILES: [], + SystemVariable.CONVERSATION_ID: 'abababa', + SystemVariable.USER_ID: 'aaa' + }, user_inputs={}) + + result = node.run(pool) + + assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert result.outputs.get('location') == '' + assert result.outputs.get('__reason') == 'Failed to extract result from function call or text response, using empty result.' + prompts = result.process_data.get('prompts') + + latest_role = None + for prompt in prompts: + if prompt.get('role') == 'user': + if '' in prompt.get('text'): + assert '\n{"type": "object"' in prompt.get('text') + elif prompt.get('role') == 'system': + assert 'customized memory' in prompt.get('text') + + if latest_role is not None: + assert latest_role != prompt.get('role') + + if prompt.get('role') in ['user', 'assistant']: + latest_role = prompt.get('role') \ No newline at end of file diff --git a/api/tests/unit_tests/configs/test_dify_config.py b/api/tests/unit_tests/configs/test_dify_config.py index 6a6fe35f66..50bb2b75ac 100644 --- a/api/tests/unit_tests/configs/test_dify_config.py +++ b/api/tests/unit_tests/configs/test_dify_config.py @@ -15,11 +15,13 @@ def example_env_file(tmp_path, monkeypatch) -> str: file_path.write_text(dedent( """ CONSOLE_API_URL=https://example.com + CONSOLE_WEB_URL=https://example.com """)) return str(file_path) def test_dify_config_undefined_entry(example_env_file): + # NOTE: See https://github.com/microsoft/pylance-release/issues/6099 for more details about this type error. # load dotenv file with pydantic-settings config = DifyConfig(_env_file=example_env_file) @@ -42,19 +44,20 @@ def test_dify_config(example_env_file): assert config.SENTRY_TRACES_SAMPLE_RATE == 1.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`. def test_flask_configs(example_env_file): flask_app = Flask('app') flask_app.config.from_mapping(DifyConfig(_env_file=example_env_file).model_dump()) config = flask_app.config - # configs read from dotenv directly - assert config['LOG_LEVEL'] == 'INFO' - # configs read from pydantic-settings + assert config['LOG_LEVEL'] == 'INFO' assert config['COMMIT_SHA'] == '' assert config['EDITION'] == 'SELF_HOSTED' assert config['API_COMPRESSION_ENABLED'] is False assert config['SENTRY_TRACES_SAMPLE_RATE'] == 1.0 + assert config['TESTING'] == False # value from env file assert config['CONSOLE_API_URL'] == 'https://example.com' @@ -71,3 +74,7 @@ def test_flask_configs(example_env_file): 'pool_recycle': 3600, 'pool_size': 30, } + + assert config['CONSOLE_WEB_URL']=='https://example.com' + assert config['CONSOLE_CORS_ALLOW_ORIGINS']==['https://example.com'] + assert config['WEB_API_CORS_ALLOW_ORIGINS'] == ['*'] diff --git a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py index 7e32ecbbdb..6d6363610b 100644 --- a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py @@ -238,8 +238,8 @@ def test__get_completion_model_prompt_messages(): prompt_rules = prompt_template['prompt_rules'] full_inputs = {**inputs, '#context#': context, '#query#': query, '#histories#': memory.get_history_prompt_text( max_token_limit=2000, - human_prefix=prompt_rules['human_prefix'] if 'human_prefix' in prompt_rules else 'Human', - ai_prefix=prompt_rules['assistant_prefix'] if 'assistant_prefix' in prompt_rules else 'Assistant' + human_prefix=prompt_rules.get("human_prefix", "Human"), + ai_prefix=prompt_rules.get("assistant_prefix", "Assistant") )} real_prompt = prompt_template['prompt_template'].format(full_inputs) diff --git a/dev/pytest/pytest_vdb.sh b/dev/pytest/pytest_vdb.sh index 7b212150a4..c954c528fb 100755 --- a/dev/pytest/pytest_vdb.sh +++ b/dev/pytest/pytest_vdb.sh @@ -1,4 +1,10 @@ #!/bin/bash set -x -pytest api/tests/integration_tests/vdb/ +pytest api/tests/integration_tests/vdb/chroma \ + api/tests/integration_tests/vdb/milvus \ + api/tests/integration_tests/vdb/pgvecto_rs \ + api/tests/integration_tests/vdb/pgvector \ + api/tests/integration_tests/vdb/qdrant \ + api/tests/integration_tests/vdb/weaviate \ + api/tests/integration_tests/vdb/test_vector_store.py \ No newline at end of file diff --git a/docker/docker-compose.chroma.yaml b/docker-legacy/docker-compose.chroma.yaml similarity index 96% rename from docker/docker-compose.chroma.yaml rename to docker-legacy/docker-compose.chroma.yaml index 4ba4ba2927..a943d620c0 100644 --- a/docker/docker-compose.chroma.yaml +++ b/docker-legacy/docker-compose.chroma.yaml @@ -1,4 +1,3 @@ -version: '3' services: # Chroma vector store. chroma: diff --git a/docker-legacy/docker-compose.middleware.yaml b/docker-legacy/docker-compose.middleware.yaml new file mode 100644 index 0000000000..38760901b1 --- /dev/null +++ b/docker-legacy/docker-compose.middleware.yaml @@ -0,0 +1,109 @@ +version: '3' +services: + # The postgres database. + db: + image: postgres:15-alpine + restart: always + environment: + # The password for the default postgres user. + POSTGRES_PASSWORD: difyai123456 + # The name of the default postgres database. + POSTGRES_DB: dify + # postgres data directory + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - ./volumes/db/data:/var/lib/postgresql/data + ports: + - "5432:5432" + + # The redis cache. + redis: + image: redis:6-alpine + restart: always + volumes: + # Mount the redis data directory to the container. + - ./volumes/redis/data:/data + # Set the redis password when startup redis server. + command: redis-server --requirepass difyai123456 + ports: + - "6379:6379" + + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + restart: always + volumes: + # Mount the Weaviate data directory to the container. + - ./volumes/weaviate:/var/lib/weaviate + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + QUERY_DEFAULTS_LIMIT: 25 + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false' + PERSISTENCE_DATA_PATH: '/var/lib/weaviate' + DEFAULT_VECTORIZER_MODULE: 'none' + CLUSTER_HOSTNAME: 'node1' + AUTHENTICATION_APIKEY_ENABLED: 'true' + AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih' + AUTHENTICATION_APIKEY_USERS: 'hello@dify.ai' + AUTHORIZATION_ADMINLIST_ENABLED: 'true' + AUTHORIZATION_ADMINLIST_USERS: 'hello@dify.ai' + ports: + - "8080:8080" + + # The DifySandbox + sandbox: + image: langgenius/dify-sandbox:0.2.1 + restart: always + environment: + # The DifySandbox configurations + # Make sure you are changing this key for your deployment with a strong key. + # You can generate a strong key using `openssl rand -base64 42`. + API_KEY: dify-sandbox + GIN_MODE: 'release' + WORKER_TIMEOUT: 15 + ENABLE_NETWORK: 'true' + HTTP_PROXY: 'http://ssrf_proxy:3128' + HTTPS_PROXY: 'http://ssrf_proxy:3128' + SANDBOX_PORT: 8194 + volumes: + - ./volumes/sandbox/dependencies:/dependencies + networks: + - ssrf_proxy_network + + # ssrf_proxy server + # for more information, please refer to + # https://docs.dify.ai/getting-started/install-self-hosted/install-faq#id-16.-why-is-ssrf_proxy-needed + ssrf_proxy: + image: ubuntu/squid:latest + restart: always + ports: + - "3128:3128" + - "8194:8194" + volumes: + # pls clearly modify the squid.conf file to fit your network environment. + - ./volumes/ssrf_proxy/squid.conf:/etc/squid/squid.conf + networks: + - ssrf_proxy_network + - default + # Qdrant vector store. + # uncomment to use qdrant as vector store. + # (if uncommented, you need to comment out the weaviate service above, + # and set VECTOR_STORE to qdrant in the api & worker service.) + # qdrant: + # image: qdrant/qdrant:1.7.3 + # restart: always + # volumes: + # - ./volumes/qdrant:/qdrant/storage + # environment: + # QDRANT_API_KEY: 'difyai123456' + # ports: + # - "6333:6333" + # - "6334:6334" + + +networks: + # create a network between sandbox, api and ssrf_proxy, and can not access outside. + ssrf_proxy_network: + driver: bridge + internal: true diff --git a/docker/docker-compose.milvus.yaml b/docker-legacy/docker-compose.milvus.yaml similarity index 100% rename from docker/docker-compose.milvus.yaml rename to docker-legacy/docker-compose.milvus.yaml diff --git a/docker/docker-compose.opensearch.yml b/docker-legacy/docker-compose.opensearch.yml similarity index 99% rename from docker/docker-compose.opensearch.yml rename to docker-legacy/docker-compose.opensearch.yml index 67ae156fff..ce72033180 100644 --- a/docker/docker-compose.opensearch.yml +++ b/docker-legacy/docker-compose.opensearch.yml @@ -1,4 +1,3 @@ -version: '3' services: opensearch: # This is also the hostname of the container within the Docker network (i.e. https://opensearch/) image: opensearchproject/opensearch:latest # Specifying the latest available image - modify if you want a specific version diff --git a/docker/docker-compose.oracle.yaml b/docker-legacy/docker-compose.oracle.yaml similarity index 97% rename from docker/docker-compose.oracle.yaml rename to docker-legacy/docker-compose.oracle.yaml index 527bd7f577..a10d2556b3 100644 --- a/docker/docker-compose.oracle.yaml +++ b/docker-legacy/docker-compose.oracle.yaml @@ -1,4 +1,3 @@ -version: '3' services: # oracle 23 ai vector store. oracle: diff --git a/docker/docker-compose.pgvecto-rs.yaml b/docker-legacy/docker-compose.pgvecto-rs.yaml similarity index 98% rename from docker/docker-compose.pgvecto-rs.yaml rename to docker-legacy/docker-compose.pgvecto-rs.yaml index a083302b1e..e383b75a83 100644 --- a/docker/docker-compose.pgvecto-rs.yaml +++ b/docker-legacy/docker-compose.pgvecto-rs.yaml @@ -1,4 +1,3 @@ -version: '3' services: # The pgvecto—rs database. pgvecto-rs: diff --git a/docker/docker-compose.pgvector.yaml b/docker-legacy/docker-compose.pgvector.yaml similarity index 98% rename from docker/docker-compose.pgvector.yaml rename to docker-legacy/docker-compose.pgvector.yaml index b584880abf..fce1cf9043 100644 --- a/docker/docker-compose.pgvector.yaml +++ b/docker-legacy/docker-compose.pgvector.yaml @@ -1,4 +1,3 @@ -version: '3' services: # Qdrant vector store. pgvector: diff --git a/docker-legacy/docker-compose.png b/docker-legacy/docker-compose.png new file mode 100644 index 0000000000..bdac113086 Binary files /dev/null and b/docker-legacy/docker-compose.png differ diff --git a/docker/docker-compose.qdrant.yaml b/docker-legacy/docker-compose.qdrant.yaml similarity index 95% rename from docker/docker-compose.qdrant.yaml rename to docker-legacy/docker-compose.qdrant.yaml index 04d34fac64..8e59287b28 100644 --- a/docker/docker-compose.qdrant.yaml +++ b/docker-legacy/docker-compose.qdrant.yaml @@ -1,4 +1,3 @@ -version: '3' services: # Qdrant vector store. qdrant: diff --git a/docker-legacy/docker-compose.yaml b/docker-legacy/docker-compose.yaml new file mode 100644 index 0000000000..eadaaced2c --- /dev/null +++ b/docker-legacy/docker-compose.yaml @@ -0,0 +1,588 @@ +version: '3' +services: + # API service + api: + image: langgenius/dify-api:0.6.12-fix1 + restart: always + environment: + # Startup mode, 'api' starts the API server. + MODE: api + # The log level for the application. Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` + LOG_LEVEL: INFO + # enable DEBUG mode to output more logs + # DEBUG : true + # A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`. + SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U + # The base URL of console application web frontend, refers to the Console base URL of WEB service if console domain is + # different from api or web app domain. + # example: http://cloud.dify.ai + CONSOLE_WEB_URL: '' + # Password for admin user initialization. + # If left unset, admin user will not be prompted for a password when creating the initial admin account. + INIT_PASSWORD: '' + # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is + # different from api or web app domain. + # example: http://cloud.dify.ai + CONSOLE_API_URL: '' + # The URL prefix for Service API endpoints, refers to the base URL of the current API service if api domain is + # different from console domain. + # example: http://api.dify.ai + SERVICE_API_URL: '' + # The URL prefix for Web APP frontend, refers to the Web App base URL of WEB service if web app domain is different from + # console or api domain. + # example: http://udify.app + APP_WEB_URL: '' + # File preview or download Url prefix. + # used to display File preview or download Url to the front-end or as Multi-model inputs; + # Url is signed and has expiration time. + FILES_URL: '' + # File Access Time specifies a time interval in seconds for the file to be accessed. + # The default value is 300 seconds. + FILES_ACCESS_TIMEOUT: 300 + # When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed. + MIGRATION_ENABLED: 'true' + # The configurations of postgres database connection. + # It is consistent with the configuration in the 'db' service below. + DB_USERNAME: postgres + DB_PASSWORD: difyai123456 + DB_HOST: db + DB_PORT: 5432 + DB_DATABASE: dify + # The configurations of redis connection. + # It is consistent with the configuration in the 'redis' service below. + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_USERNAME: '' + REDIS_PASSWORD: difyai123456 + REDIS_USE_SSL: 'false' + # use redis db 0 for redis cache + REDIS_DB: 0 + # The configurations of celery broker. + # Use redis as the broker, and redis db 1 for celery broker. + CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1 + # Specifies the allowed origins for cross-origin requests to the Web API, e.g. https://dify.app or * for all origins. + WEB_API_CORS_ALLOW_ORIGINS: '*' + # Specifies the allowed origins for cross-origin requests to the console API, e.g. https://cloud.dify.ai or * for all origins. + CONSOLE_CORS_ALLOW_ORIGINS: '*' + # CSRF Cookie settings + # Controls whether a cookie is sent with cross-site requests, + # providing some protection against cross-site request forgery attacks + # + # Default: `SameSite=Lax, Secure=false, HttpOnly=true` + # This default configuration supports same-origin requests using either HTTP or HTTPS, + # but does not support cross-origin requests. It is suitable for local debugging purposes. + # + # If you want to enable cross-origin support, + # you must use the HTTPS protocol and set the configuration to `SameSite=None, Secure=true, HttpOnly=true`. + # + # The type of storage to use for storing user files. Supported values are `local` and `s3` and `azure-blob` and `google-storage`, Default: `local` + STORAGE_TYPE: local + # The path to the local storage directory, the directory relative the root path of API service codes or absolute path. Default: `storage` or `/home/john/storage`. + # only available when STORAGE_TYPE is `local`. + STORAGE_LOCAL_PATH: storage + # The S3 storage configurations, only available when STORAGE_TYPE is `s3`. + S3_USE_AWS_MANAGED_IAM: 'false' + S3_ENDPOINT: 'https://xxx.r2.cloudflarestorage.com' + S3_BUCKET_NAME: 'difyai' + S3_ACCESS_KEY: 'ak-difyai' + S3_SECRET_KEY: 'sk-difyai' + S3_REGION: 'us-east-1' + # The Azure Blob storage configurations, only available when STORAGE_TYPE is `azure-blob`. + AZURE_BLOB_ACCOUNT_NAME: 'difyai' + AZURE_BLOB_ACCOUNT_KEY: 'difyai' + AZURE_BLOB_CONTAINER_NAME: 'difyai-container' + AZURE_BLOB_ACCOUNT_URL: 'https://.blob.core.windows.net' + # The Google storage configurations, only available when STORAGE_TYPE is `google-storage`. + GOOGLE_STORAGE_BUCKET_NAME: 'yout-bucket-name' + # if you want to use Application Default Credentials, you can leave GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64 empty. + GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: 'your-google-service-account-json-base64-string' + # The Alibaba Cloud OSS configurations, only available when STORAGE_TYPE is `aliyun-oss` + ALIYUN_OSS_BUCKET_NAME: 'your-bucket-name' + ALIYUN_OSS_ACCESS_KEY: 'your-access-key' + ALIYUN_OSS_SECRET_KEY: 'your-secret-key' + ALIYUN_OSS_ENDPOINT: 'https://oss-ap-southeast-1-internal.aliyuncs.com' + ALIYUN_OSS_REGION: 'ap-southeast-1' + ALIYUN_OSS_AUTH_VERSION: 'v4' + # The Tencent COS storage configurations, only available when STORAGE_TYPE is `tencent-cos`. + TENCENT_COS_BUCKET_NAME: 'your-bucket-name' + TENCENT_COS_SECRET_KEY: 'your-secret-key' + TENCENT_COS_SECRET_ID: 'your-secret-id' + TENCENT_COS_REGION: 'your-region' + TENCENT_COS_SCHEME: 'your-scheme' + # The type of vector store to use. Supported values are `weaviate`, `qdrant`, `milvus`, `relyt`,`pgvector`, `chroma`, 'opensearch', 'tidb_vector'. + VECTOR_STORE: weaviate + # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. + WEAVIATE_ENDPOINT: http://weaviate:8080 + # The Weaviate API key. + WEAVIATE_API_KEY: WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih + # The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. + QDRANT_URL: http://qdrant:6333 + # The Qdrant API key. + QDRANT_API_KEY: difyai123456 + # The Qdrant client timeout setting. + QDRANT_CLIENT_TIMEOUT: 20 + # The Qdrant client enable gRPC mode. + QDRANT_GRPC_ENABLED: 'false' + # The Qdrant server gRPC mode PORT. + QDRANT_GRPC_PORT: 6334 + # Milvus configuration Only available when VECTOR_STORE is `milvus`. + # The milvus host. + MILVUS_HOST: 127.0.0.1 + # The milvus host. + MILVUS_PORT: 19530 + # The milvus username. + MILVUS_USER: root + # The milvus password. + MILVUS_PASSWORD: Milvus + # The milvus tls switch. + MILVUS_SECURE: 'false' + # relyt configurations + RELYT_HOST: db + RELYT_PORT: 5432 + RELYT_USER: postgres + RELYT_PASSWORD: difyai123456 + RELYT_DATABASE: postgres + # pgvector configurations + PGVECTOR_HOST: pgvector + PGVECTOR_PORT: 5432 + PGVECTOR_USER: postgres + PGVECTOR_PASSWORD: difyai123456 + PGVECTOR_DATABASE: dify + # tidb vector configurations + TIDB_VECTOR_HOST: tidb + TIDB_VECTOR_PORT: 4000 + TIDB_VECTOR_USER: xxx.root + TIDB_VECTOR_PASSWORD: xxxxxx + TIDB_VECTOR_DATABASE: dify + # oracle configurations + ORACLE_HOST: oracle + ORACLE_PORT: 1521 + ORACLE_USER: dify + ORACLE_PASSWORD: dify + ORACLE_DATABASE: FREEPDB1 + # Chroma configuration + CHROMA_HOST: 127.0.0.1 + CHROMA_PORT: 8000 + CHROMA_TENANT: default_tenant + CHROMA_DATABASE: default_database + CHROMA_AUTH_PROVIDER: chromadb.auth.token_authn.TokenAuthClientProvider + CHROMA_AUTH_CREDENTIALS: xxxxxx + # Mail configuration, support: resend, smtp + MAIL_TYPE: '' + # default send from email address, if not specified + MAIL_DEFAULT_SEND_FROM: 'YOUR EMAIL FROM (eg: no-reply )' + SMTP_SERVER: '' + SMTP_PORT: 465 + SMTP_USERNAME: '' + SMTP_PASSWORD: '' + SMTP_USE_TLS: 'true' + SMTP_OPPORTUNISTIC_TLS: 'false' + # the api-key for resend (https://resend.com) + RESEND_API_KEY: '' + RESEND_API_URL: https://api.resend.com + # The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled. + SENTRY_DSN: '' + # The sample rate for Sentry events. Default: `1.0` + SENTRY_TRACES_SAMPLE_RATE: 1.0 + # The sample rate for Sentry profiles. Default: `1.0` + SENTRY_PROFILES_SAMPLE_RATE: 1.0 + # 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 + # The sandbox service endpoint. + CODE_EXECUTION_ENDPOINT: "http://sandbox: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 + # SSRF Proxy server + SSRF_PROXY_HTTP_URL: 'http://ssrf_proxy:3128' + SSRF_PROXY_HTTPS_URL: 'http://ssrf_proxy:3128' + # Indexing configuration + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: 1000 + depends_on: + - db + - redis + volumes: + # Mount the storage directory to the container, for storing user files. + - ./volumes/app/storage:/app/api/storage + # uncomment to expose dify-api port to host + # ports: + # - "5001:5001" + networks: + - ssrf_proxy_network + - default + + # worker service + # The Celery worker for processing the queue. + worker: + image: langgenius/dify-api:0.6.12-fix1 + restart: always + environment: + CONSOLE_WEB_URL: '' + # Startup mode, 'worker' starts the Celery worker for processing the queue. + MODE: worker + + # --- All the configurations below are the same as those in the 'api' service. --- + + # The log level for the application. Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` + LOG_LEVEL: INFO + # A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`. + # same as the API service + SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U + # The configurations of postgres database connection. + # It is consistent with the configuration in the 'db' service below. + DB_USERNAME: postgres + DB_PASSWORD: difyai123456 + DB_HOST: db + DB_PORT: 5432 + DB_DATABASE: dify + # The configurations of redis cache connection. + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_USERNAME: '' + REDIS_PASSWORD: difyai123456 + REDIS_DB: 0 + REDIS_USE_SSL: 'false' + # The configurations of celery broker. + CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1 + # The type of storage to use for storing user files. Supported values are `local` and `s3` and `azure-blob` and `google-storage`, Default: `local` + STORAGE_TYPE: local + STORAGE_LOCAL_PATH: storage + # The S3 storage configurations, only available when STORAGE_TYPE is `s3`. + S3_USE_AWS_MANAGED_IAM: 'false' + S3_ENDPOINT: 'https://xxx.r2.cloudflarestorage.com' + S3_BUCKET_NAME: 'difyai' + S3_ACCESS_KEY: 'ak-difyai' + S3_SECRET_KEY: 'sk-difyai' + S3_REGION: 'us-east-1' + # The Azure Blob storage configurations, only available when STORAGE_TYPE is `azure-blob`. + AZURE_BLOB_ACCOUNT_NAME: 'difyai' + AZURE_BLOB_ACCOUNT_KEY: 'difyai' + AZURE_BLOB_CONTAINER_NAME: 'difyai-container' + AZURE_BLOB_ACCOUNT_URL: 'https://.blob.core.windows.net' + # The Google storage configurations, only available when STORAGE_TYPE is `google-storage`. + GOOGLE_STORAGE_BUCKET_NAME: 'yout-bucket-name' + # if you want to use Application Default Credentials, you can leave GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64 empty. + GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: 'your-google-service-account-json-base64-string' + # The Alibaba Cloud OSS configurations, only available when STORAGE_TYPE is `aliyun-oss` + ALIYUN_OSS_BUCKET_NAME: 'your-bucket-name' + ALIYUN_OSS_ACCESS_KEY: 'your-access-key' + ALIYUN_OSS_SECRET_KEY: 'your-secret-key' + ALIYUN_OSS_ENDPOINT: 'https://oss-ap-southeast-1-internal.aliyuncs.com' + ALIYUN_OSS_REGION: 'ap-southeast-1' + ALIYUN_OSS_AUTH_VERSION: 'v4' + # The Tencent COS storage configurations, only available when STORAGE_TYPE is `tencent-cos`. + TENCENT_COS_BUCKET_NAME: 'your-bucket-name' + TENCENT_COS_SECRET_KEY: 'your-secret-key' + TENCENT_COS_SECRET_ID: 'your-secret-id' + TENCENT_COS_REGION: 'your-region' + TENCENT_COS_SCHEME: 'your-scheme' + # The type of vector store to use. Supported values are `weaviate`, `qdrant`, `milvus`, `relyt`, `pgvector`, `chroma`, 'opensearch', 'tidb_vector'. + VECTOR_STORE: weaviate + # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. + WEAVIATE_ENDPOINT: http://weaviate:8080 + # The Weaviate API key. + WEAVIATE_API_KEY: WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih + # The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. + QDRANT_URL: http://qdrant:6333 + # The Qdrant API key. + QDRANT_API_KEY: difyai123456 + # The Qdrant client timeout setting. + QDRANT_CLIENT_TIMEOUT: 20 + # The Qdrant client enable gRPC mode. + QDRANT_GRPC_ENABLED: 'false' + # The Qdrant server gRPC mode PORT. + QDRANT_GRPC_PORT: 6334 + # Milvus configuration Only available when VECTOR_STORE is `milvus`. + # The milvus host. + MILVUS_HOST: 127.0.0.1 + # The milvus host. + MILVUS_PORT: 19530 + # The milvus username. + MILVUS_USER: root + # The milvus password. + MILVUS_PASSWORD: Milvus + # The milvus tls switch. + MILVUS_SECURE: 'false' + # Mail configuration, support: resend + MAIL_TYPE: '' + # default send from email address, if not specified + MAIL_DEFAULT_SEND_FROM: 'YOUR EMAIL FROM (eg: no-reply )' + SMTP_SERVER: '' + SMTP_PORT: 465 + SMTP_USERNAME: '' + SMTP_PASSWORD: '' + SMTP_USE_TLS: 'true' + SMTP_OPPORTUNISTIC_TLS: 'false' + # the api-key for resend (https://resend.com) + RESEND_API_KEY: '' + RESEND_API_URL: https://api.resend.com + # relyt configurations + RELYT_HOST: db + RELYT_PORT: 5432 + RELYT_USER: postgres + RELYT_PASSWORD: difyai123456 + RELYT_DATABASE: postgres + # tencent configurations + TENCENT_VECTOR_DB_URL: http://127.0.0.1 + TENCENT_VECTOR_DB_API_KEY: dify + TENCENT_VECTOR_DB_TIMEOUT: 30 + TENCENT_VECTOR_DB_USERNAME: dify + TENCENT_VECTOR_DB_DATABASE: dify + TENCENT_VECTOR_DB_SHARD: 1 + TENCENT_VECTOR_DB_REPLICAS: 2 + # OpenSearch configuration + OPENSEARCH_HOST: 127.0.0.1 + OPENSEARCH_PORT: 9200 + OPENSEARCH_USER: admin + OPENSEARCH_PASSWORD: admin + OPENSEARCH_SECURE: 'true' + # pgvector configurations + PGVECTOR_HOST: pgvector + PGVECTOR_PORT: 5432 + PGVECTOR_USER: postgres + PGVECTOR_PASSWORD: difyai123456 + PGVECTOR_DATABASE: dify + # tidb vector configurations + TIDB_VECTOR_HOST: tidb + TIDB_VECTOR_PORT: 4000 + TIDB_VECTOR_USER: xxx.root + TIDB_VECTOR_PASSWORD: xxxxxx + TIDB_VECTOR_DATABASE: dify + # oracle configurations + ORACLE_HOST: oracle + ORACLE_PORT: 1521 + ORACLE_USER: dify + ORACLE_PASSWORD: dify + ORACLE_DATABASE: FREEPDB1 + # Chroma configuration + CHROMA_HOST: 127.0.0.1 + CHROMA_PORT: 8000 + CHROMA_TENANT: default_tenant + CHROMA_DATABASE: default_database + CHROMA_AUTH_PROVIDER: chromadb.auth.token_authn.TokenAuthClientProvider + CHROMA_AUTH_CREDENTIALS: xxxxxx + # 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 + # Indexing configuration + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: 1000 + depends_on: + - db + - redis + volumes: + # Mount the storage directory to the container, for storing user files. + - ./volumes/app/storage:/app/api/storage + networks: + - ssrf_proxy_network + - default + + # Frontend web application. + web: + image: langgenius/dify-web:0.6.12-fix1 + restart: always + environment: + # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is + # different from api or web app domain. + # example: http://cloud.dify.ai + CONSOLE_API_URL: '' + # The URL for Web APP api server, refers to the Web App base URL of WEB service if web app domain is different from + # console or api domain. + # example: http://udify.app + APP_API_URL: '' + # The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled. + SENTRY_DSN: '' + # uncomment to expose dify-web port to host + # ports: + # - "3000:3000" + + # The postgres database. + db: + image: postgres:15-alpine + restart: always + environment: + PGUSER: postgres + # The password for the default postgres user. + POSTGRES_PASSWORD: difyai123456 + # The name of the default postgres database. + POSTGRES_DB: dify + # postgres data directory + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - ./volumes/db/data:/var/lib/postgresql/data + # notice!: if you use windows-wsl2, postgres may not work properly due to the ntfs issue.you can use volumes to mount the data directory to the host. + # if you use the following config, you need to uncomment the volumes configuration below at the end of the file. + # - postgres:/var/lib/postgresql/data + # uncomment to expose db(postgresql) port to host + # ports: + # - "5432:5432" + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 1s + timeout: 3s + retries: 30 + + # The redis cache. + redis: + image: redis:6-alpine + restart: always + volumes: + # Mount the redis data directory to the container. + - ./volumes/redis/data:/data + # Set the redis password when startup redis server. + command: redis-server --requirepass difyai123456 + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + # uncomment to expose redis port to host + # ports: + # - "6379:6379" + + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + restart: always + volumes: + # Mount the Weaviate data directory to the container. + - ./volumes/weaviate:/var/lib/weaviate + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + QUERY_DEFAULTS_LIMIT: 25 + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false' + PERSISTENCE_DATA_PATH: '/var/lib/weaviate' + DEFAULT_VECTORIZER_MODULE: 'none' + CLUSTER_HOSTNAME: 'node1' + AUTHENTICATION_APIKEY_ENABLED: 'true' + AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih' + AUTHENTICATION_APIKEY_USERS: 'hello@dify.ai' + AUTHORIZATION_ADMINLIST_ENABLED: 'true' + AUTHORIZATION_ADMINLIST_USERS: 'hello@dify.ai' + # uncomment to expose weaviate port to host + # ports: + # - "8080:8080" + + # The DifySandbox + sandbox: + image: langgenius/dify-sandbox:0.2.1 + restart: always + environment: + # The DifySandbox configurations + # Make sure you are changing this key for your deployment with a strong key. + # You can generate a strong key using `openssl rand -base64 42`. + API_KEY: dify-sandbox + GIN_MODE: 'release' + WORKER_TIMEOUT: 15 + ENABLE_NETWORK: 'true' + HTTP_PROXY: 'http://ssrf_proxy:3128' + HTTPS_PROXY: 'http://ssrf_proxy:3128' + SANDBOX_PORT: 8194 + volumes: + - ./volumes/sandbox/dependencies:/dependencies + networks: + - ssrf_proxy_network + + # ssrf_proxy server + # for more information, please refer to + # https://docs.dify.ai/getting-started/install-self-hosted/install-faq#id-16.-why-is-ssrf_proxy-needed + ssrf_proxy: + image: ubuntu/squid:latest + restart: always + volumes: + # pls clearly modify the squid.conf file to fit your network environment. + - ./volumes/ssrf_proxy/squid.conf:/etc/squid/squid.conf + networks: + - ssrf_proxy_network + - default + # Qdrant vector store. + # uncomment to use qdrant as vector store. + # (if uncommented, you need to comment out the weaviate service above, + # and set VECTOR_STORE to qdrant in the api & worker service.) + # qdrant: + # image: langgenius/qdrant:v1.7.3 + # restart: always + # volumes: + # - ./volumes/qdrant:/qdrant/storage + # environment: + # QDRANT_API_KEY: 'difyai123456' + # # uncomment to expose qdrant port to host + # # ports: + # # - "6333:6333" + # # - "6334:6334" + + # The pgvector vector database. + # Uncomment to use qdrant as vector store. + # pgvector: + # image: pgvector/pgvector:pg16 + # restart: always + # environment: + # PGUSER: postgres + # # The password for the default postgres user. + # POSTGRES_PASSWORD: difyai123456 + # # The name of the default postgres database. + # POSTGRES_DB: dify + # # postgres data directory + # PGDATA: /var/lib/postgresql/data/pgdata + # volumes: + # - ./volumes/pgvector/data:/var/lib/postgresql/data + # # uncomment to expose db(postgresql) port to host + # # ports: + # # - "5433:5432" + # healthcheck: + # test: [ "CMD", "pg_isready" ] + # interval: 1s + # timeout: 3s + # retries: 30 + + # The oracle vector database. + # Uncomment to use oracle23ai as vector store. Also need to Uncomment volumes block + # oracle: + # image: container-registry.oracle.com/database/free:latest + # restart: always + # ports: + # - 1521:1521 + # volumes: + # - type: volume + # source: oradata + # target: /opt/oracle/oradata + # - ./startupscripts:/opt/oracle/scripts/startup + # environment: + # - ORACLE_PWD=Dify123456 + # - ORACLE_CHARACTERSET=AL32UTF8 + + + # The nginx reverse proxy. + # used for reverse proxying the API service and Web service. + nginx: + image: nginx:latest + restart: always + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/proxy.conf:/etc/nginx/proxy.conf + - ./nginx/conf.d:/etc/nginx/conf.d + #- ./nginx/ssl:/etc/ssl + depends_on: + - api + - web + ports: + - "80:80" + #- "443:443" +# notice: if you use windows-wsl2, postgres may not work properly due to the ntfs issue.you can use volumes to mount the data directory to the host. +# volumes: +#   postgres: +networks: + # create a network between sandbox, api and ssrf_proxy, and can not access outside. + ssrf_proxy_network: + driver: bridge + internal: true + +#volumes: +# oradata: diff --git a/docker/nginx/conf.d/default.conf b/docker-legacy/nginx/conf.d/default.conf similarity index 100% rename from docker/nginx/conf.d/default.conf rename to docker-legacy/nginx/conf.d/default.conf diff --git a/docker/nginx/nginx.conf b/docker-legacy/nginx/nginx.conf similarity index 100% rename from docker/nginx/nginx.conf rename to docker-legacy/nginx/nginx.conf diff --git a/docker/nginx/proxy.conf b/docker-legacy/nginx/proxy.conf similarity index 100% rename from docker/nginx/proxy.conf rename to docker-legacy/nginx/proxy.conf diff --git a/docker-legacy/nginx/ssl/.gitkeep b/docker-legacy/nginx/ssl/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/docker-legacy/nginx/ssl/.gitkeep @@ -0,0 +1 @@ + diff --git a/docker-legacy/startupscripts/create_user.sql b/docker-legacy/startupscripts/create_user.sql new file mode 100755 index 0000000000..b80e19c3b0 --- /dev/null +++ b/docker-legacy/startupscripts/create_user.sql @@ -0,0 +1,5 @@ +show pdbs; +ALTER SYSTEM SET PROCESSES=500 SCOPE=SPFILE; +alter session set container= freepdb1; +create user dify identified by dify DEFAULT TABLESPACE users quota unlimited on users; +grant DB_DEVELOPER_ROLE to dify; diff --git a/docker-legacy/volumes/opensearch/opensearch_dashboards.yml b/docker-legacy/volumes/opensearch/opensearch_dashboards.yml new file mode 100644 index 0000000000..f50d63bbb9 --- /dev/null +++ b/docker-legacy/volumes/opensearch/opensearch_dashboards.yml @@ -0,0 +1,222 @@ +--- +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 + +# Description: +# Default configuration for OpenSearch Dashboards + +# OpenSearch Dashboards is served by a back end server. This setting specifies the port to use. +# server.port: 5601 + +# Specifies the address to which the OpenSearch Dashboards server will bind. IP addresses and host names are both valid values. +# The default is 'localhost', which usually means remote machines will not be able to connect. +# To allow connections from remote users, set this parameter to a non-loopback address. +# server.host: "localhost" + +# Enables you to specify a path to mount OpenSearch Dashboards at if you are running behind a proxy. +# Use the `server.rewriteBasePath` setting to tell OpenSearch Dashboards if it should remove the basePath +# from requests it receives, and to prevent a deprecation warning at startup. +# This setting cannot end in a slash. +# server.basePath: "" + +# Specifies whether OpenSearch Dashboards should rewrite requests that are prefixed with +# `server.basePath` or require that they are rewritten by your reverse proxy. +# server.rewriteBasePath: false + +# The maximum payload size in bytes for incoming server requests. +# server.maxPayloadBytes: 1048576 + +# The OpenSearch Dashboards server's name. This is used for display purposes. +# server.name: "your-hostname" + +# The URLs of the OpenSearch instances to use for all your queries. +# opensearch.hosts: ["http://localhost:9200"] + +# OpenSearch Dashboards uses an index in OpenSearch to store saved searches, visualizations and +# dashboards. OpenSearch Dashboards creates a new index if the index doesn't already exist. +# opensearchDashboards.index: ".opensearch_dashboards" + +# The default application to load. +# opensearchDashboards.defaultAppId: "home" + +# Setting for an optimized healthcheck that only uses the local OpenSearch node to do Dashboards healthcheck. +# This settings should be used for large clusters or for clusters with ingest heavy nodes. +# It allows Dashboards to only healthcheck using the local OpenSearch node rather than fan out requests across all nodes. +# +# It requires the user to create an OpenSearch node attribute with the same name as the value used in the setting +# This node attribute should assign all nodes of the same cluster an integer value that increments with each new cluster that is spun up +# e.g. in opensearch.yml file you would set the value to a setting using node.attr.cluster_id: +# Should only be enabled if there is a corresponding node attribute created in your OpenSearch config that matches the value here +# opensearch.optimizedHealthcheckId: "cluster_id" + +# If your OpenSearch is protected with basic authentication, these settings provide +# the username and password that the OpenSearch Dashboards server uses to perform maintenance on the OpenSearch Dashboards +# index at startup. Your OpenSearch Dashboards users still need to authenticate with OpenSearch, which +# is proxied through the OpenSearch Dashboards server. +# opensearch.username: "opensearch_dashboards_system" +# opensearch.password: "pass" + +# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. +# These settings enable SSL for outgoing requests from the OpenSearch Dashboards server to the browser. +# server.ssl.enabled: false +# server.ssl.certificate: /path/to/your/server.crt +# server.ssl.key: /path/to/your/server.key + +# Optional settings that provide the paths to the PEM-format SSL certificate and key files. +# These files are used to verify the identity of OpenSearch Dashboards to OpenSearch and are required when +# xpack.security.http.ssl.client_authentication in OpenSearch is set to required. +# opensearch.ssl.certificate: /path/to/your/client.crt +# opensearch.ssl.key: /path/to/your/client.key + +# Optional setting that enables you to specify a path to the PEM file for the certificate +# authority for your OpenSearch instance. +# opensearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] + +# To disregard the validity of SSL certificates, change this setting's value to 'none'. +# opensearch.ssl.verificationMode: full + +# Time in milliseconds to wait for OpenSearch to respond to pings. Defaults to the value of +# the opensearch.requestTimeout setting. +# opensearch.pingTimeout: 1500 + +# Time in milliseconds to wait for responses from the back end or OpenSearch. This value +# must be a positive integer. +# opensearch.requestTimeout: 30000 + +# List of OpenSearch Dashboards client-side headers to send to OpenSearch. To send *no* client-side +# headers, set this value to [] (an empty list). +# opensearch.requestHeadersWhitelist: [ authorization ] + +# Header names and values that are sent to OpenSearch. Any custom headers cannot be overwritten +# by client-side headers, regardless of the opensearch.requestHeadersWhitelist configuration. +# opensearch.customHeaders: {} + +# Time in milliseconds for OpenSearch to wait for responses from shards. Set to 0 to disable. +# opensearch.shardTimeout: 30000 + +# Logs queries sent to OpenSearch. Requires logging.verbose set to true. +# opensearch.logQueries: false + +# Specifies the path where OpenSearch Dashboards creates the process ID file. +# pid.file: /var/run/opensearchDashboards.pid + +# Enables you to specify a file where OpenSearch Dashboards stores log output. +# logging.dest: stdout + +# Set the value of this setting to true to suppress all logging output. +# logging.silent: false + +# Set the value of this setting to true to suppress all logging output other than error messages. +# logging.quiet: false + +# Set the value of this setting to true to log all events, including system usage information +# and all requests. +# logging.verbose: false + +# Set the interval in milliseconds to sample system and process performance +# metrics. Minimum is 100ms. Defaults to 5000. +# ops.interval: 5000 + +# Specifies locale to be used for all localizable strings, dates and number formats. +# Supported languages are the following: English - en , by default , Chinese - zh-CN . +# i18n.locale: "en" + +# Set the allowlist to check input graphite Url. Allowlist is the default check list. +# vis_type_timeline.graphiteAllowedUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'] + +# Set the blocklist to check input graphite Url. Blocklist is an IP list. +# Below is an example for reference +# vis_type_timeline.graphiteBlockedIPs: [ +# //Loopback +# '127.0.0.0/8', +# '::1/128', +# //Link-local Address for IPv6 +# 'fe80::/10', +# //Private IP address for IPv4 +# '10.0.0.0/8', +# '172.16.0.0/12', +# '192.168.0.0/16', +# //Unique local address (ULA) +# 'fc00::/7', +# //Reserved IP address +# '0.0.0.0/8', +# '100.64.0.0/10', +# '192.0.0.0/24', +# '192.0.2.0/24', +# '198.18.0.0/15', +# '192.88.99.0/24', +# '198.51.100.0/24', +# '203.0.113.0/24', +# '224.0.0.0/4', +# '240.0.0.0/4', +# '255.255.255.255/32', +# '::/128', +# '2001:db8::/32', +# 'ff00::/8', +# ] +# vis_type_timeline.graphiteBlockedIPs: [] + +# opensearchDashboards.branding: +# logo: +# defaultUrl: "" +# darkModeUrl: "" +# mark: +# defaultUrl: "" +# darkModeUrl: "" +# loadingLogo: +# defaultUrl: "" +# darkModeUrl: "" +# faviconUrl: "" +# applicationTitle: "" + +# Set the value of this setting to true to capture region blocked warnings and errors +# for your map rendering services. +# map.showRegionBlockedWarning: false% + +# Set the value of this setting to false to suppress search usage telemetry +# for reducing the load of OpenSearch cluster. +# data.search.usageTelemetry.enabled: false + +# 2.4 renames 'wizard.enabled: false' to 'vis_builder.enabled: false' +# Set the value of this setting to false to disable VisBuilder +# functionality in Visualization. +# vis_builder.enabled: false + +# 2.4 New Experimental Feature +# Set the value of this setting to true to enable the experimental multiple data source +# support feature. Use with caution. +# data_source.enabled: false +# Set the value of these settings to customize crypto materials to encryption saved credentials +# in data sources. +# data_source.encryption.wrappingKeyName: 'changeme' +# data_source.encryption.wrappingKeyNamespace: 'changeme' +# data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + +# 2.6 New ML Commons Dashboards Feature +# Set the value of this setting to true to enable the ml commons dashboards +# ml_commons_dashboards.enabled: false + +# 2.12 New experimental Assistant Dashboards Feature +# Set the value of this setting to true to enable the assistant dashboards +# assistant.chat.enabled: false + +# 2.13 New Query Assistant Feature +# Set the value of this setting to false to disable the query assistant +# observability.query_assist.enabled: false + +# 2.14 Enable Ui Metric Collectors in Usage Collector +# Set the value of this setting to true to enable UI Metric collections +# usageCollection.uiMetric.enabled: false + +opensearch.hosts: [https://localhost:9200] +opensearch.ssl.verificationMode: none +opensearch.username: admin +opensearch.password: 'Qazwsxedc!@#123' +opensearch.requestHeadersWhitelist: [authorization, securitytenant] + +opensearch_security.multitenancy.enabled: true +opensearch_security.multitenancy.tenants.preferred: [Private, Global] +opensearch_security.readonly_mode.roles: [kibana_read_only] +# Use this setting if you are running opensearch-dashboards without https +opensearch_security.cookie.secure: false +server.host: '0.0.0.0' diff --git a/docker-legacy/volumes/sandbox/dependencies/python-requirements.txt b/docker-legacy/volumes/sandbox/dependencies/python-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/volumes/ssrf_proxy/squid.conf b/docker-legacy/volumes/ssrf_proxy/squid.conf similarity index 100% rename from docker/volumes/ssrf_proxy/squid.conf rename to docker-legacy/volumes/ssrf_proxy/squid.conf diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000000..fd2cbe2b9d --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,606 @@ +# ------------------------------ +# Environment Variables for API service & worker +# ------------------------------ + +# ------------------------------ +# Common Variables +# ------------------------------ + +# The backend URL of the console API, +# used to concatenate the authorization callback. +# If empty, it is the same domain. +# Example: https://api.console.dify.ai +CONSOLE_API_URL= + +# The front-end URL of the console web, +# used to concatenate some front-end addresses and for CORS configuration use. +# If empty, it is the same domain. +# Example: https://console.dify.ai +CONSOLE_WEB_URL= + +# Service API Url, +# used to display Service API Base Url to the front-end. +# If empty, it is the same domain. +# Example: https://api.dify.ai +SERVICE_API_URL= + +# WebApp API backend Url, +# used to declare the back-end URL for the front-end API. +# If empty, it is the same domain. +# Example: https://api.app.dify.ai +APP_API_URL= + +# WebApp Url, +# used to display WebAPP API Base Url to the front-end. +# If empty, it is the same domain. +# Example: https://app.dify.ai +APP_WEB_URL= + +# File preview or download Url prefix. +# used to display File preview or download Url to the front-end or as Multi-model inputs; +# Url is signed and has expiration time. +FILES_URL= + +# ------------------------------ +# Server Configuration +# ------------------------------ + +# The log level for the application. +# Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` +LOG_LEVEL=INFO + +# Debug mode, default is false. +# It is recommended to turn on this configuration for local development +# to prevent some problems caused by monkey patch. +DEBUG=false + +# Flask debug mode, it can output trace information at the interface when turned on, +# which is convenient for debugging. +FLASK_DEBUG=false + +# A secretkey that is used for securely signing the session cookie +# and encrypting sensitive information on the database. +# You can generate a strong key using `openssl rand -base64 42`. +SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U + +# Password for admin user initialization. +# If left unset, admin user will not be prompted for a password +# when creating the initial admin account. +INIT_PASSWORD= + +# Deployment environment. +# Supported values are `PRODUCTION`, `TESTING`. Default is `PRODUCTION`. +# Testing environment. There will be a distinct color label on the front-end page, +# indicating that this environment is a testing environment. +DEPLOY_ENV=PRODUCTION + +# Whether to enable the version check policy. +# If set to empty, https://updates.dify.ai will not be called for version check. +CHECK_UPDATE_URL=https://updates.dify.ai + +# Used to change the OpenAI base address, default is https://api.openai.com/v1. +# When OpenAI cannot be accessed in China, replace it with a domestic mirror address, +# or when a local model provides OpenAI compatible API, it can be replaced. +OPENAI_API_BASE=https://api.openai.com/v1 + +# When enabled, migrations will be executed prior to application startup +# and the application will start after the migrations have completed. +MIGRATION_ENABLED=true + +# File Access Time specifies a time interval in seconds for the file to be accessed. +# The default value is 300 seconds. +FILES_ACCESS_TIMEOUT=300 + +# ------------------------------ +# Container Startup Related Configuration +# Only effective when starting with docker image or docker-compose. +# ------------------------------ + +# API service binding address, default: 0.0.0.0, i.e., all addresses can be accessed. +DIFY_BIND_ADDRESS=0.0.0.0 + +# API service binding port number, default 5001. +DIFY_PORT=5001 + +# The number of API server workers, i.e., the number of gevent workers. +# Formula: number of cpu cores x 2 + 1 +# Reference: https://docs.gunicorn.org/en/stable/design.html#how-many-workers +SERVER_WORKER_AMOUNT= + +# Defaults to gevent. If using windows, it can be switched to sync or solo. +SERVER_WORKER_CLASS= + +# Similar to SERVER_WORKER_CLASS. Default is gevent. +# If using windows, it can be switched to sync or solo. +CELERY_WORKER_CLASS= + +# Request handling timeout. The default is 200, +# it is recommended to set it to 360 to support a longer sse connection time. +GUNICORN_TIMEOUT=360 + +# The number of Celery workers. The default is 1, and can be set as needed. +CELERY_WORKER_AMOUNT= + +# ------------------------------ +# Database Configuration +# The database uses PostgreSQL. Please use the public schema. +# It is consistent with the configuration in the 'db' service below. +# ------------------------------ + +DB_USERNAME=postgres +DB_PASSWORD=difyai123456 +DB_HOST=db +DB_PORT=5432 +DB_DATABASE=dify +# The size of the database connection pool. +# The default is 30 connections, which can be appropriately increased. +SQLALCHEMY_POOL_SIZE=30 +# Database connection pool recycling time, the default is 3600 seconds. +SQLALCHEMY_POOL_RECYCLE=3600 +# Whether to print SQL, default is false. +SQLALCHEMY_ECHO=false + +# ------------------------------ +# Redis Configuration +# This Redis configuration is used for caching and for pub/sub during conversation. +# ------------------------------ + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_USERNAME= +REDIS_PASSWORD=difyai123456 +REDIS_USE_SSL=false + +# ------------------------------ +# Celery Configuration +# ------------------------------ + +# Use redis as the broker, and redis db 1 for celery broker. +# Format as follows: `redis://:@:/` +# Example: redis://:difyai123456@redis:6379/1 +CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1 +BROKER_USE_SSL=false + +# ------------------------------ +# CORS Configuration +# Used to set the front-end cross-domain access policy. +# ------------------------------ + +# Specifies the allowed origins for cross-origin requests to the Web API, +# e.g. https://dify.app or * for all origins. +WEB_API_CORS_ALLOW_ORIGINS=* + +# Specifies the allowed origins for cross-origin requests to the console API, +# e.g. https://cloud.dify.ai or * for all origins. +CONSOLE_CORS_ALLOW_ORIGINS=* + +# ------------------------------ +# File Storage Configuration +# ------------------------------ + +# The type of storage to use for storing user files. +# Supported values are `local` and `s3` and `azure-blob` and `google-storage` and `tencent-cos`, +# Default: `local` +STORAGE_TYPE=local + +# S3 Configuration +# Whether to use AWS managed IAM roles for authenticating with the S3 service. +# If set to false, the access key and secret key must be provided. +S3_USE_AWS_MANAGED_IAM=false +# The endpoint of the S3 service. +S3_ENDPOINT= +# The region of the S3 service. +S3_REGION=us-east-1 +# The name of the S3 bucket to use for storing files. +S3_BUCKET_NAME=difyai +# The access key to use for authenticating with the S3 service. +S3_ACCESS_KEY= +# The secret key to use for authenticating with the S3 service. +S3_SECRET_KEY= + +# Azure Blob Configuration +# The name of the Azure Blob Storage account to use for storing files. +AZURE_BLOB_ACCOUNT_NAME=difyai +# The access key to use for authenticating with the Azure Blob Storage account. +AZURE_BLOB_ACCOUNT_KEY=difyai +# The name of the Azure Blob Storage container to use for storing files. +AZURE_BLOB_CONTAINER_NAME=difyai-container +# The URL of the Azure Blob Storage account. +AZURE_BLOB_ACCOUNT_URL=https://.blob.core.windows.net + +# Google Storage Configuration +# The name of the Google Storage bucket to use for storing files. +GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name +# The service account JSON key to use for authenticating with the Google Storage service. +GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string + +# The Alibaba Cloud OSS configurations, +# only available when STORAGE_TYPE is `aliyun-oss` +ALIYUN_OSS_BUCKET_NAME=your-bucket-name +ALIYUN_OSS_ACCESS_KEY=your-access-key +ALIYUN_OSS_SECRET_KEY=your-secret-key +ALIYUN_OSS_ENDPOINT=https://oss-ap-southeast-1-internal.aliyuncs.com +ALIYUN_OSS_REGION=ap-southeast-1 +ALIYUN_OSS_AUTH_VERSION=v4 + +# Tencent COS Configuration +# The name of the Tencent COS bucket to use for storing files. +TENCENT_COS_BUCKET_NAME=your-bucket-name +# The secret key to use for authenticating with the Tencent COS service. +TENCENT_COS_SECRET_KEY=your-secret-key +# The secret id to use for authenticating with the Tencent COS service. +TENCENT_COS_SECRET_ID=your-secret-id +# The region of the Tencent COS service. +TENCENT_COS_REGION=your-region +# The scheme of the Tencent COS service. +TENCENT_COS_SCHEME=your-scheme + +# ------------------------------ +# Vector Database Configuration +# ------------------------------ + +# The type of vector store to use. +# Supported values are `weaviate`, `qdrant`, `milvus`, `relyt`, `pgvector`, `chroma`, `opensearch`, `tidb_vector`, `oracle`, `tencent`. +VECTOR_STORE=weaviate + +# The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. +WEAVIATE_ENDPOINT=http://weaviate:8080 +# The Weaviate API key. +WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih + +# The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. +QDRANT_URL=http://qdrant:6333 +# The Qdrant API key. +QDRANT_API_KEY=difyai123456 +# The Qdrant client timeout setting. +QDRANT_CLIENT_TIMEOUT=20 +# The Qdrant client enable gRPC mode. +QDRANT_GRPC_ENABLED=false +# The Qdrant server gRPC mode PORT. +QDRANT_GRPC_PORT=6334 + +# Milvus configuration Only available when VECTOR_STORE is `milvus`. +# The milvus host. +MILVUS_HOST=127.0.0.1 +# The milvus host. +MILVUS_PORT=19530 +# The milvus username. +MILVUS_USER=root +# The milvus password. +MILVUS_PASSWORD=Milvus +# The milvus tls switch. +MILVUS_SECURE=false + +# pgvector configurations, only available when VECTOR_STORE is `pgvecto-rs or pgvector` +PGVECTOR_HOST=pgvector +PGVECTOR_PORT=5432 +PGVECTOR_USER=postgres +PGVECTOR_PASSWORD=difyai123456 +PGVECTOR_DATABASE=dify + +# TiDB vector configurations, only available when VECTOR_STORE is `tidb` +TIDB_VECTOR_HOST=tidb +TIDB_VECTOR_PORT=4000 +TIDB_VECTOR_USER=xxx.root +TIDB_VECTOR_PASSWORD=xxxxxx +TIDB_VECTOR_DATABASE=dify + +# Chroma configuration, only available when VECTOR_STORE is `chroma` +CHROMA_HOST=127.0.0.1 +CHROMA_PORT=8000 +CHROMA_TENANT=default_tenant +CHROMA_DATABASE=default_database +CHROMA_AUTH_PROVIDER=chromadb.auth.token_authn.TokenAuthClientProvider +CHROMA_AUTH_CREDENTIALS=xxxxxx + +# Oracle configuration, only available when VECTOR_STORE is `oracle` +ORACLE_HOST=oracle +ORACLE_PORT=1521 +ORACLE_USER=dify +ORACLE_PASSWORD=dify +ORACLE_DATABASE=FREEPDB1 + +# relyt configurations, only available when VECTOR_STORE is `relyt` +RELYT_HOST=db +RELYT_PORT=5432 +RELYT_USER=postgres +RELYT_PASSWORD=difyai123456 +RELYT_DATABASE=postgres + +# open search configuration, only available when VECTOR_STORE is `opensearch` +OPENSEARCH_HOST=opensearch +OPENSEARCH_PORT=9200 +OPENSEARCH_USER=admin +OPENSEARCH_PASSWORD=admin +OPENSEARCH_SECURE=true + +# tencent vector configurations, only available when VECTOR_STORE is `tencent` +TENCENT_VECTOR_DB_URL=http://127.0.0.1 +TENCENT_VECTOR_DB_API_KEY=dify +TENCENT_VECTOR_DB_TIMEOUT=30 +TENCENT_VECTOR_DB_USERNAME=dify +TENCENT_VECTOR_DB_DATABASE=dify +TENCENT_VECTOR_DB_SHARD=1 +TENCENT_VECTOR_DB_REPLICAS=2 + +# ------------------------------ +# Knowledge Configuration +# ------------------------------ + +# Upload file size limit, default 15M. +UPLOAD_FILE_SIZE_LIMIT=15 + +# The maximum number of files that can be uploaded at a time, default 5. +UPLOAD_FILE_BATCH_LIMIT=5 + +# ETl type, support: `dify`, `Unstructured` +# `dify` Dify's proprietary file extraction scheme +# `Unstructured` Unstructured.io file extraction scheme +ETL_TYPE=dify + +# Unstructured API path, needs to be configured when ETL_TYPE is Unstructured. +# For example: http://unstructured:8000/general/v0/general +UNSTRUCTURED_API_URL= + +# ------------------------------ +# Multi-modal Configuration +# ------------------------------ + +# The format of the image sent when the multi-modal model is input, +# the default is base64, optional url. +# The delay of the call in url mode will be lower than that in base64 mode. +# It is generally recommended to use the more compatible base64 mode. +# If configured as url, you need to configure FILES_URL as an externally accessible address so that the multi-modal model can access the image. +MULTIMODAL_SEND_IMAGE_FORMAT=base64 + +# Upload image file size limit, default 10M. +UPLOAD_IMAGE_FILE_SIZE_LIMIT=10 + +# ------------------------------ +# Sentry Configuration +# Used for application monitoring and error log tracking. +# ------------------------------ + +# API Service Sentry DSN address, default is empty, when empty, +# all monitoring information is not reported to Sentry. +# If not set, Sentry error reporting will be disabled. +API_SENTRY_DSN= + +# API Service The reporting ratio of Sentry events, if it is 0.01, it is 1%. +API_SENTRY_TRACES_SAMPLE_RATE=1.0 + +# API Service The reporting ratio of Sentry profiles, if it is 0.01, it is 1%. +API_SENTRY_PROFILES_SAMPLE_RATE=1.0 + +# Web Service Sentry DSN address, default is empty, when empty, +# all monitoring information is not reported to Sentry. +# If not set, Sentry error reporting will be disabled. +WEB_SENTRY_DSN= + +# ------------------------------ +# Notion Integration Configuration +# Variables can be obtained by applying for Notion integration: https://www.notion.so/my-integrations +# ------------------------------ + +# Configure as "public" or "internal". +# Since Notion's OAuth redirect URL only supports HTTPS, +# if deploying locally, please use Notion's internal integration. +NOTION_INTEGRATION_TYPE=public +# Notion OAuth client secret (used for public integration type) +NOTION_CLIENT_SECRET= +# Notion OAuth client id (used for public integration type) +NOTION_CLIENT_ID= +# Notion internal integration secret. +# If the value of NOTION_INTEGRATION_TYPE is "internal", +# you need to configure this variable. +NOTION_INTERNAL_SECRET= + +# ------------------------------ +# Mail related configuration +# ------------------------------ + +# Mail type, support: resend, smtp +MAIL_TYPE=resend + +# Default send from email address, if not specified +MAIL_DEFAULT_SEND_FROM= + +# API-Key for the Resend email provider, used when MAIL_TYPE is `resend`. +RESEND_API_KEY=your-resend-api-key + +# SMTP server configuration, used when MAIL_TYPE is `smtp` +SMTP_SERVER= +SMTP_PORT=465 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_USE_TLS=true +SMTP_OPPORTUNISTIC_TLS=false + +# ------------------------------ +# Others Configuration +# ------------------------------ + +# Maximum length of segmentation tokens for indexing +INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=1000 + +# Member invitation link valid time (hours), +# Default: 72. +INVITE_EXPIRY_HOURS=72 + +# Reset password token valid time (hours), +# Default: 24. +RESET_PASSWORD_TOKEN_EXPIRY_HOURS=24 + +# The sandbox service endpoint. +CODE_EXECUTION_ENDPOINT=http://sandbox:8194 +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 + +# SSRF Proxy server HTTP URL +SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128 +# SSRF Proxy server HTTPS URL +SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128 + +# ------------------------------ +# Environment Variables for db Service +# ------------------------------ + +PGUSER=${DB_USERNAME} +# The password for the default postgres user. +POSTGRES_PASSWORD=${DB_PASSWORD} +# The name of the default postgres database. +POSTGRES_DB=${DB_DATABASE} +# postgres data directory +PGDATA=/var/lib/postgresql/data/pgdata + +# ------------------------------ +# Environment Variables for sandbox Service +# ------------------------------ + +# The API key for the sandbox service +SANDBOX_API_KEY=dify-sandbox +# The mode in which the Gin framework runs +SANDBOX_GIN_MODE=release +# The timeout for the worker in seconds +SANDBOX_WORKER_TIMEOUT=15 +# Enable network for the sandbox service +SANDBOX_ENABLE_NETWORK=true +# HTTP proxy URL for SSRF protection +SANDBOX_HTTP_PROXY=http://ssrf_proxy:3128 +# HTTPS proxy URL for SSRF protection +SANDBOX_HTTPS_PROXY=http://ssrf_proxy:3128 +# The port on which the sandbox service runs +SANDBOX_PORT=8194 + +# ------------------------------ +# Environment Variables for weaviate Service +# (only used when VECTOR_STORE is weaviate) +# ------------------------------ +WEAVIATE_PERSISTENCE_DATA_PATH='/var/lib/weaviate' +WEAVIATE_QUERY_DEFAULTS_LIMIT=25 +WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true +WEAVIATE_DEFAULT_VECTORIZER_MODULE=none +WEAVIATE_CLUSTER_HOSTNAME=node1 +WEAVIATE_AUTHENTICATION_APIKEY_ENABLED=true +WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih +WEAVIATE_AUTHENTICATION_APIKEY_USERS=hello@dify.ai +WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED=true +WEAVIATE_AUTHORIZATION_ADMINLIST_USERS=hello@dify.ai + +# ------------------------------ +# Environment Variables for Chroma +# (only used when VECTOR_STORE is chroma) +# ------------------------------ + +# Authentication credentials for Chroma server +CHROMA_SERVER_AUTHN_CREDENTIALS=difyai123456 +# Authentication provider for Chroma server +CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider +# Persistence setting for Chroma server +CHROMA_IS_PERSISTENT=TRUE + +# ------------------------------ +# Environment Variables for Oracle Service +# (only used when VECTOR_STORE is Oracle) +# ------------------------------ +ORACLE_PWD=Dify123456 +ORACLE_CHARACTERSET=AL32UTF8 + +# ------------------------------ +# Environment Variables for milvus Service +# (only used when VECTOR_STORE is milvus) +# ------------------------------ +# ETCD configuration for auto compaction mode +ETCD_AUTO_COMPACTION_MODE=revision +# ETCD configuration for auto compaction retention in terms of number of revisions +ETCD_AUTO_COMPACTION_RETENTION=1000 +# ETCD configuration for backend quota in bytes +ETCD_QUOTA_BACKEND_BYTES=4294967296 +# ETCD configuration for the number of changes before triggering a snapshot +ETCD_SNAPSHOT_COUNT=50000 +# MinIO access key for authentication +MINIO_ACCESS_KEY=minioadmin +# MinIO secret key for authentication +MINIO_SECRET_KEY=minioadmin +# ETCD service endpoints +ETCD_ENDPOINTS=etcd:2379 +# MinIO service address +MINIO_ADDRESS=minio:9000 +# Enable or disable security authorization +MILVUS_AUTHORIZATION_ENABLED=true + +# ------------------------------ +# Environment Variables for pgvector / pgvector-rs Service +# (only used when VECTOR_STORE is pgvector / pgvector-rs) +# ------------------------------ +PGVECTOR_PGUSER=postgres +# The password for the default postgres user. +PGVECTOR_POSTGRES_PASSWORD=difyai123456 +# The name of the default postgres database. +PGVECTOR_POSTGRES_DB=dify +# postgres data directory +PGVECTOR_PGDATA=/var/lib/postgresql/data/pgdata + +# ------------------------------ +# Environment Variables for opensearch +# (only used when VECTOR_STORE is opensearch) +# ------------------------------ +OPENSEARCH_DISCOVERY_TYPE=single-node +OPENSEARCH_BOOTSTRAP_MEMORY_LOCK=true +OPENSEARCH_JAVA_OPTS_MIN=512m +OPENSEARCH_JAVA_OPTS_MAX=1024m +OPENSEARCH_INITIAL_ADMIN_PASSWORD=Qazwsxedc!@#123 +OPENSEARCH_MEMLOCK_SOFT=-1 +OPENSEARCH_MEMLOCK_HARD=-1 +OPENSEARCH_NOFILE_SOFT=65536 +OPENSEARCH_NOFILE_HARD=65536 + +# ------------------------------ +# Environment Variables for Nginx reverse proxy +# ------------------------------ +NGINX_SERVER_NAME=_ +NGINX_HTTPS_ENABLED=false +# HTTP port +NGINX_PORT=80 +# SSL settings are only applied when HTTPS_ENABLED is true +NGINX_SSL_PORT=443 +# if HTTPS_ENABLED is true, you're required to add your own SSL certificates/keys to the `./nginx/ssl` directory +# and modify the env vars below accordingly. +NGINX_SSL_CERT_FILENAME=dify.crt +NGINX_SSL_CERT_KEY_FILENAME=dify.key +NGINX_SSL_PROTOCOLS=TLSv1.1 TLSv1.2 TLSv1.3 + +# Nginx performance tuning +NGINX_WORKER_PROCESSES=auto +NGINX_CLIENT_MAX_BODY_SIZE=15M +NGINX_KEEPALIVE_TIMEOUT=65 + +# Proxy settings +NGINX_PROXY_READ_TIMEOUT=3600s +NGINX_PROXY_SEND_TIMEOUT=3600s + +# ------------------------------ +# Environment Variables for SSRF Proxy +# ------------------------------ +SSRF_HTTP_PORT=3128 +SSRF_COREDUMP_DIR=/var/spool/squid +SSRF_REVERSE_PROXY_PORT=8194 +SSRF_SANDBOX_HOST=sandbox + +# ------------------------------ +# docker env var for specifying vector db type at startup +# (based on the vector db type, the corresponding docker +# compose profile will be used) +# ------------------------------ +COMPOSE_PROFILES=${VECTOR_STORE:-weaviate} + +# ------------------------------ +# Docker Compose Service Expose Host Port Configurations +# ------------------------------ +EXPOSE_NGINX_PORT=80 +EXPOSE_NGINX_SSL_PORT=443 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000000..6bff8bc314 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,88 @@ +## README for docker Deployment + +Welcome to the new `docker` directory for deploying Dify using Docker Compose. This README outlines the updates, deployment instructions, and migration details for existing users. + +### What's Updated +- **Persistent Environment Variables**: Environment variables are now managed through a `.env` file, ensuring that your configurations persist across deployments. + + > What is `.env`?

+ > The `.env` file is a crucial component in Docker and Docker Compose environments, serving as a centralized configuration file where you can define environment variables that are accessible to the containers at runtime. This file simplifies the management of environment settings across different stages of development, testing, and production, providing consistency and ease of configuration to deployments. + +- **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file. +- **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades. +- **Legacy Support**: Previous deployment files are now located in the `docker-legacy` directory and will no longer be maintained. + +### How to Deploy Dify with `docker-compose.yaml` +1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system. +2. **Environment Setup**: + - Navigate to the `docker` directory. + - Copy the `.env.example` file to a new file named `.env` by running `cp .env.example .env`. + - Customize the `.env` file as needed. Refer to the `.env.example` file for detailed configuration options. +3. **Running the Services**: + - Execute `docker compose up` from the `docker` directory to start the services. + - To specify a vector database, set the `VECTOR_store` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. + +### How to Deploy Middleware for Developing Dify +1. **Middleware Setup**: + - Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches. + - Navigate to the `docker` directory. + - Ensure the `middleware.env` file is created by running `cp middleware.env.example middleware.env` (refer to the `middleware.env.example` file). +2. **Running Middleware Services**: + - Execute `docker-compose -f docker-compose.middleware.yaml up -d` to start the middleware services. + +### Migration for Existing Users +For users migrating from the `docker-legacy` setup: +1. **Review Changes**: Familiarize yourself with the new `.env` configuration and Docker Compose setup. +2. **Transfer Customizations**: + - If you have customized configurations such as `docker-compose.yaml`, `ssrf_proxy/squid.conf`, or `nginx/conf.d/default.conf`, you will need to reflect these changes in the `.env` file you create. +3. **Data Migration**: + - Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary. + +### Overview of `.env` + +#### Key Modules and Customization + +- **Vector Database Services**: Depending on the type of vector database used (`VECTOR_STORE`), users can set specific endpoints, ports, and authentication details. +- **Storage Services**: Depending on the storage type (`STORAGE_TYPE`), users can configure specific settings for S3, Azure Blob, Google Storage, etc. +- **API and Web Services**: Users can define URLs and other settings that affect how the API and web frontends operate. + +#### Other notable variables +The `.env.example` file provided in the Docker setup is extensive and covers a wide range of configuration options. It is structured into several sections, each pertaining to different aspects of the application and its services. Here are some of the key sections and variables: + +1. **Common Variables**: + - `CONSOLE_API_URL`, `SERVICE_API_URL`: URLs for different API services. + - `APP_WEB_URL`: Frontend application URL. + - `FILES_URL`: Base URL for file downloads and previews. + +2. **Server Configuration**: + - `LOG_LEVEL`, `DEBUG`, `FLASK_DEBUG`: Logging and debug settings. + - `SECRET_KEY`: A key for encrypting session cookies and other sensitive data. + +3. **Database Configuration**: + - `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`: PostgreSQL database credentials and connection details. + +4. **Redis Configuration**: + - `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings. + +5. **Celery Configuration**: + - `CELERY_BROKER_URL`: Configuration for Celery message broker. + +6. **Storage Configuration**: + - `STORAGE_TYPE`, `S3_BUCKET_NAME`, `AZURE_BLOB_ACCOUNT_NAME`: Settings for file storage options like local, S3, Azure Blob, etc. + +7. **Vector Database Configuration**: + - `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). + - Specific settings for each vector store like `WEAVIATE_ENDPOINT`, `MILVUS_HOST`. + +8. **CORS Configuration**: + - `WEB_API_CORS_ALLOW_ORIGINS`, `CONSOLE_CORS_ALLOW_ORIGINS`: Settings for cross-origin resource sharing. + +9. **Other Service-Specific Environment Variables**: + - Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`. + + +### Additional Information +- **Continuous Improvement Phase**: We are actively seeking feedback from the community to refine and enhance the deployment process. As more users adopt this new method, we will continue to make improvements based on your experiences and suggestions. +- **Support**: For detailed configuration options and environment variable settings, refer to the `.env.example` file and the Docker Compose configuration files in the `docker` directory. + +This README aims to guide you through the deployment process using the new Docker Compose setup. For any issues or further assistance, please refer to the official documentation or contact support. \ No newline at end of file diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 38760901b1..3dee6efb7c 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -1,20 +1,18 @@ -version: '3' services: # The postgres database. db: image: postgres:15-alpine restart: always + env_file: + - ./middleware.env environment: - # The password for the default postgres user. - POSTGRES_PASSWORD: difyai123456 - # The name of the default postgres database. - POSTGRES_DB: dify - # postgres data directory - PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} + POSTGRES_DB: ${POSTGRES_DB:-dify} + PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} volumes: - ./volumes/db/data:/var/lib/postgresql/data ports: - - "5432:5432" + - "${EXPOSE_POSTGRES_PORT:-5432}:5432" # The redis cache. redis: @@ -26,30 +24,7 @@ services: # Set the redis password when startup redis server. command: redis-server --requirepass difyai123456 ports: - - "6379:6379" - - # The Weaviate vector store. - weaviate: - image: semitechnologies/weaviate:1.19.0 - restart: always - volumes: - # Mount the Weaviate data directory to the container. - - ./volumes/weaviate:/var/lib/weaviate - environment: - # The Weaviate configurations - # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. - QUERY_DEFAULTS_LIMIT: 25 - AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false' - PERSISTENCE_DATA_PATH: '/var/lib/weaviate' - DEFAULT_VECTORIZER_MODULE: 'none' - CLUSTER_HOSTNAME: 'node1' - AUTHENTICATION_APIKEY_ENABLED: 'true' - AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih' - AUTHENTICATION_APIKEY_USERS: 'hello@dify.ai' - AUTHORIZATION_ADMINLIST_ENABLED: 'true' - AUTHORIZATION_ADMINLIST_USERS: 'hello@dify.ai' - ports: - - "8080:8080" + - "${EXPOSE_REDIS_PORT:-6379}:6379" # The DifySandbox sandbox: @@ -59,13 +34,13 @@ services: # The DifySandbox configurations # Make sure you are changing this key for your deployment with a strong key. # You can generate a strong key using `openssl rand -base64 42`. - API_KEY: dify-sandbox - GIN_MODE: 'release' - WORKER_TIMEOUT: 15 - ENABLE_NETWORK: 'true' - HTTP_PROXY: 'http://ssrf_proxy:3128' - HTTPS_PROXY: 'http://ssrf_proxy:3128' - SANDBOX_PORT: 8194 + API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + GIN_MODE: ${SANDBOX_GIN_MODE:-release} + WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15} + ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true} + HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128} + HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} volumes: - ./volumes/sandbox/dependencies:/dependencies networks: @@ -77,30 +52,50 @@ services: ssrf_proxy: image: ubuntu/squid:latest restart: always - ports: - - "3128:3128" - - "8194:8194" volumes: - # pls clearly modify the squid.conf file to fit your network environment. - - ./volumes/ssrf_proxy/squid.conf:/etc/squid/squid.conf + - ./ssrf_proxy/squid.conf.template:/etc/squid/squid.conf.template + - ./ssrf_proxy/docker-entrypoint.sh:/docker-entrypoint-mount.sh + entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ] + environment: + # pls clearly modify the squid env vars to fit your network environment. + HTTP_PORT: ${SSRF_HTTP_PORT:-3128} + COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} + REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} + SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} + ports: + - "${EXPOSE_SSRF_PROXY_PORT:-3128}:${SSRF_HTTP_PORT:-3128}" + - "${EXPOSE_SANDBOX_PORT:-8194}:${SANDBOX_PORT:-8194}" networks: - ssrf_proxy_network - default - # Qdrant vector store. - # uncomment to use qdrant as vector store. - # (if uncommented, you need to comment out the weaviate service above, - # and set VECTOR_STORE to qdrant in the api & worker service.) - # qdrant: - # image: qdrant/qdrant:1.7.3 - # restart: always - # volumes: - # - ./volumes/qdrant:/qdrant/storage - # environment: - # QDRANT_API_KEY: 'difyai123456' - # ports: - # - "6333:6333" - # - "6334:6334" + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + profiles: + - weaviate + restart: always + volumes: + # Mount the Weaviate data directory to the container. + - ./volumes/weaviate:/var/lib/weaviate + env_file: + - ./middleware.env + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + PERSISTENCE_DATA_PATH: ${WEAVIATE_PERSISTENCE_DATA_PATH:-/var/lib/weaviate} + QUERY_DEFAULTS_LIMIT: ${WEAVIATE_QUERY_DEFAULTS_LIMIT:-25} + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: ${WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED:-false} + DEFAULT_VECTORIZER_MODULE: ${WEAVIATE_DEFAULT_VECTORIZER_MODULE:-none} + CLUSTER_HOSTNAME: ${WEAVIATE_CLUSTER_HOSTNAME:-node1} + AUTHENTICATION_APIKEY_ENABLED: ${WEAVIATE_AUTHENTICATION_APIKEY_ENABLED:-true} + AUTHENTICATION_APIKEY_ALLOWED_KEYS: ${WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} + AUTHENTICATION_APIKEY_USERS: ${WEAVIATE_AUTHENTICATION_APIKEY_USERS:-hello@dify.ai} + AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true} + AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai} + ports: + - "${EXPOSE_WEAVIATE_PORT:-8080}:8080" networks: # create a network between sandbox, api and ssrf_proxy, and can not access outside. diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0e0f997c97..d947532301 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,220 +1,179 @@ -version: '3' +x-shared-env: &shared-api-worker-env + LOG_LEVEL: ${LOG_LEVEL:-INFO} + DEBUG: ${DEBUG:-false} + FLASK_DEBUG: ${FLASK_DEBUG:-false} + SECRET_KEY: ${SECRET_KEY:-sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U} + INIT_PASSWORD: ${INIT_PASSWORD:-} + CONSOLE_WEB_URL: ${CONSOLE_WEB_URL:-} + CONSOLE_API_URL: ${CONSOLE_API_URL:-} + SERVICE_API_URL: ${SERVICE_API_URL:-} + APP_WEB_URL: ${APP_WEB_URL:-} + CHECK_UPDATE_URL: ${CHECK_UPDATE_URL:-https://updates.dify.ai} + OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1} + FILES_URL: ${FILES_URL:-} + FILES_ACCESS_TIMEOUT: ${FILES_ACCESS_TIMEOUT:-300} + MIGRATION_ENABLED: ${MIGRATION_ENABLED:-true} + DEPLOY_ENV: ${DEPLOY_ENV:-PRODUCTION} + DIFY_BIND_ADDRESS: ${DIFY_BIND_ADDRESS:-0.0.0.0} + DIFY_PORT: ${DIFY_PORT:-5001} + SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-} + SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-} + CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360} + CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-} + DB_USERNAME: ${DB_USERNAME:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-difyai123456} + DB_HOST: ${DB_HOST:-db} + DB_PORT: ${DB_PORT:-5432} + DB_DATABASE: ${DB_DATABASE:-dify} + SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} + SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} + SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_USERNAME: ${REDIS_USERNAME:-} + REDIS_PASSWORD: ${REDIS_PASSWORD:-difyai123456} + REDIS_USE_SSL: ${REDIS_USE_SSL:-false} + REDIS_DB: 0 + CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:difyai123456@redis:6379/1} + BROKER_USE_SSL: ${BROKER_USE_SSL:-false} + WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} + CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} + STORAGE_TYPE: ${STORAGE_TYPE:-local} + STORAGE_LOCAL_PATH: storage + S3_USE_AWS_MANAGED_IAM: ${S3_USE_AWS_MANAGED_IAM:-false} + S3_ENDPOINT: ${S3_ENDPOINT:-} + S3_BUCKET_NAME: ${S3_BUCKET_NAME:-} + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} + S3_SECRET_KEY: ${S3_SECRET_KEY:-} + S3_REGION: ${S3_REGION:-us-east-1} + AZURE_BLOB_ACCOUNT_NAME: ${AZURE_BLOB_ACCOUNT_NAME:-} + AZURE_BLOB_ACCOUNT_KEY: ${AZURE_BLOB_ACCOUNT_KEY:-} + AZURE_BLOB_CONTAINER_NAME: ${AZURE_BLOB_CONTAINER_NAME:-} + AZURE_BLOB_ACCOUNT_URL: ${AZURE_BLOB_ACCOUNT_URL:-} + GOOGLE_STORAGE_BUCKET_NAME: ${GOOGLE_STORAGE_BUCKET_NAME:-} + GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: ${GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64:-} + ALIYUN_OSS_BUCKET_NAME: ${ALIYUN_OSS_BUCKET_NAME:-} + ALIYUN_OSS_ACCESS_KEY: ${ALIYUN_OSS_ACCESS_KEY:-} + ALIYUN_OSS_SECRET_KEY: ${ALIYUN_OSS_SECRET_KEY:-} + ALIYUN_OSS_ENDPOINT: ${ALIYUN_OSS_ENDPOINT:-} + ALIYUN_OSS_REGION: ${ALIYUN_OSS_REGION:-} + ALIYUN_OSS_AUTH_VERSION: ${ALIYUN_OSS_AUTH_VERSION:-v4} + TENCENT_COS_BUCKET_NAME: ${TENCENT_COS_BUCKET_NAME:-} + TENCENT_COS_SECRET_KEY: ${TENCENT_COS_SECRET_KEY:-} + TENCENT_COS_SECRET_ID: ${TENCENT_COS_SECRET_ID:-} + TENCENT_COS_REGION: ${TENCENT_COS_REGION:-} + TENCENT_COS_SCHEME: ${TENCENT_COS_SCHEME:-} + OCI_ENDPOINT: ${OCI_ENDPOINT:-} + OCI_BUCKET_NAME: ${OCI_BUCKET_NAME:-} + OCI_ACCESS_KEY: ${OCI_ACCESS_KEY:-} + OCI_SECRET_KEY: ${OCI_SECRET_KEY:-} + OCI_REGION: ${OCI_REGION:-} + VECTOR_STORE: ${VECTOR_STORE:-weaviate} + WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} + WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} + QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333} + QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456} + QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20} + QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false} + QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334} + MILVUS_HOST: ${MILVUS_HOST:-127.0.0.1} + MILVUS_PORT: ${MILVUS_PORT:-19530} + MILVUS_USER: ${MILVUS_USER:-root} + MILVUS_PASSWORD: ${MILVUS_PASSWORD:-Milvus} + MILVUS_SECURE: ${MILVUS_SECURE:-false} + RELYT_HOST: ${RELYT_HOST:-db} + RELYT_PORT: ${RELYT_PORT:-5432} + RELYT_USER: ${RELYT_USER:-postgres} + RELYT_PASSWORD: ${RELYT_PASSWORD:-difyai123456} + RELYT_DATABASE: ${RELYT_DATABASE:-postgres} + PGVECTOR_HOST: ${PGVECTOR_HOST:-pgvector} + PGVECTOR_PORT: ${PGVECTOR_PORT:-5432} + PGVECTOR_USER: ${PGVECTOR_USER:-postgres} + PGVECTOR_PASSWORD: ${PGVECTOR_PASSWORD:-difyai123456} + PGVECTOR_DATABASE: ${PGVECTOR_DATABASE:-dify} + TIDB_VECTOR_HOST: ${TIDB_VECTOR_HOST:-tidb} + TIDB_VECTOR_PORT: ${TIDB_VECTOR_PORT:-4000} + TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-} + TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-} + TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} + ORACLE_HOST: ${ORACLE_HOST:-oracle} + ORACLE_PORT: ${ORACLE_PORT:-1521} + ORACLE_USER: ${ORACLE_USER:-dify} + ORACLE_PASSWORD: ${ORACLE_PASSWORD:-dify} + ORACLE_DATABASE: ${ORACLE_DATABASE:-FREEPDB1} + CHROMA_HOST: ${CHROMA_HOST:-127.0.0.1} + CHROMA_PORT: ${CHROMA_PORT:-8000} + CHROMA_TENANT: ${CHROMA_TENANT:-default_tenant} + CHROMA_DATABASE: ${CHROMA_DATABASE:-default_database} + CHROMA_AUTH_PROVIDER: ${CHROMA_AUTH_PROVIDER:-chromadb.auth.token_authn.TokenAuthClientProvider} + CHROMA_AUTH_CREDENTIALS: ${CHROMA_AUTH_CREDENTIALS:-} + OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch} + OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200} + OPENSEARCH_USER: ${OPENSEARCH_USER:-admin} + OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin} + OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true} + TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1} + TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify} + TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30} + TENCENT_VECTOR_DB_USERNAME: ${TENCENT_VECTOR_DB_USERNAME:-dify} + TENCENT_VECTOR_DB_DATABASE: ${TENCENT_VECTOR_DB_DATABASE:-dify} + TENCENT_VECTOR_DB_SHARD: ${TENCENT_VECTOR_DB_SHARD:-1} + TENCENT_VECTOR_DB_REPLICAS: ${TENCENT_VECTOR_DB_REPLICAS:-2} + UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15} + UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5} + ETL_TYPE: ${ETL_TYPE:-dify} + UNSTRUCTURED_API_URL: ${UNSTRUCTURED_API_URL:-} + MULTIMODAL_SEND_IMAGE_FORMAT: ${MULTIMODAL_SEND_IMAGE_FORMAT:-base64} + UPLOAD_IMAGE_FILE_SIZE_LIMIT: ${UPLOAD_IMAGE_FILE_SIZE_LIMIT:-10} + SENTRY_DSN: ${API_SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0} + SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0} + NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public} + NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-} + NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-} + NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-} + MAIL_TYPE: ${MAIL_TYPE:-resend} + MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-} + SMTP_SERVER: ${SMTP_SERVER:-} + SMTP_PORT: ${SMTP_PORT:-465} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_USE_TLS: ${SMTP_USE_TLS:-true} + SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} + RESEND_API_KEY: ${RESEND_API_KEY:-your-resend-api-key} + RESEND_API_URL: https://api.resend.com + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-1000} + INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} + RESET_PASSWORD_TOKEN_EXPIRY_HOURS: ${RESET_PASSWORD_TOKEN_EXPIRY_HOURS:-24} + CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} + CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807} + CODE_MIN_NUMBER: ${CODE_MIN_NUMBER:--9223372036854775808} + CODE_MAX_STRING_LENGTH: ${CODE_MAX_STRING_LENGTH:-80000} + TEMPLATE_TRANSFORM_MAX_LENGTH: ${TEMPLATE_TRANSFORM_MAX_LENGTH:-80000} + CODE_MAX_STRING_ARRAY_LENGTH: ${CODE_MAX_STRING_ARRAY_LENGTH:-30} + CODE_MAX_OBJECT_ARRAY_LENGTH: ${CODE_MAX_OBJECT_ARRAY_LENGTH:-30} + CODE_MAX_NUMBER_ARRAY_LENGTH: ${CODE_MAX_NUMBER_ARRAY_LENGTH:-1000} + SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} + SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} + services: # API service api: - image: langgenius/dify-api:0.6.11 + image: langgenius/dify-api:0.6.12-fix1 restart: always environment: + # Use the shared environment variables. + <<: *shared-api-worker-env # Startup mode, 'api' starts the API server. MODE: api - # The log level for the application. Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` - LOG_LEVEL: INFO - # enable DEBUG mode to output more logs - # DEBUG : true - # A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`. - SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U - # The base URL of console application web frontend, refers to the Console base URL of WEB service if console domain is - # different from api or web app domain. - # example: http://cloud.dify.ai - CONSOLE_WEB_URL: '' - # Password for admin user initialization. - # If left unset, admin user will not be prompted for a password when creating the initial admin account. - INIT_PASSWORD: '' - # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is - # different from api or web app domain. - # example: http://cloud.dify.ai - CONSOLE_API_URL: '' - # The URL prefix for Service API endpoints, refers to the base URL of the current API service if api domain is - # different from console domain. - # example: http://api.dify.ai - SERVICE_API_URL: '' - # The URL prefix for Web APP frontend, refers to the Web App base URL of WEB service if web app domain is different from - # console or api domain. - # example: http://udify.app - APP_WEB_URL: '' - # File preview or download Url prefix. - # used to display File preview or download Url to the front-end or as Multi-model inputs; - # Url is signed and has expiration time. - FILES_URL: '' - # File Access Time specifies a time interval in seconds for the file to be accessed. - # The default value is 300 seconds. - FILES_ACCESS_TIMEOUT: 300 - # When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed. - MIGRATION_ENABLED: 'true' - # The configurations of postgres database connection. - # It is consistent with the configuration in the 'db' service below. - DB_USERNAME: postgres - DB_PASSWORD: difyai123456 - DB_HOST: db - DB_PORT: 5432 - DB_DATABASE: dify - # The configurations of redis connection. - # It is consistent with the configuration in the 'redis' service below. - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_USERNAME: '' - REDIS_PASSWORD: difyai123456 - REDIS_USE_SSL: 'false' - # use redis db 0 for redis cache - REDIS_DB: 0 - # The configurations of celery broker. - # Use redis as the broker, and redis db 1 for celery broker. - CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1 - # Specifies the allowed origins for cross-origin requests to the Web API, e.g. https://dify.app or * for all origins. - WEB_API_CORS_ALLOW_ORIGINS: '*' - # Specifies the allowed origins for cross-origin requests to the console API, e.g. https://cloud.dify.ai or * for all origins. - CONSOLE_CORS_ALLOW_ORIGINS: '*' - # CSRF Cookie settings - # Controls whether a cookie is sent with cross-site requests, - # providing some protection against cross-site request forgery attacks - # - # Default: `SameSite=Lax, Secure=false, HttpOnly=true` - # This default configuration supports same-origin requests using either HTTP or HTTPS, - # but does not support cross-origin requests. It is suitable for local debugging purposes. - # - # If you want to enable cross-origin support, - # you must use the HTTPS protocol and set the configuration to `SameSite=None, Secure=true, HttpOnly=true`. - # - # The type of storage to use for storing user files. Supported values are `local` and `s3` and `azure-blob` and `google-storage`, Default: `local` - STORAGE_TYPE: local - # The path to the local storage directory, the directory relative the root path of API service codes or absolute path. Default: `storage` or `/home/john/storage`. - # only available when STORAGE_TYPE is `local`. - STORAGE_LOCAL_PATH: storage - # The S3 storage configurations, only available when STORAGE_TYPE is `s3`. - S3_USE_AWS_MANAGED_IAM: 'false' - S3_ENDPOINT: 'https://xxx.r2.cloudflarestorage.com' - S3_BUCKET_NAME: 'difyai' - S3_ACCESS_KEY: 'ak-difyai' - S3_SECRET_KEY: 'sk-difyai' - S3_REGION: 'us-east-1' - # The Azure Blob storage configurations, only available when STORAGE_TYPE is `azure-blob`. - AZURE_BLOB_ACCOUNT_NAME: 'difyai' - AZURE_BLOB_ACCOUNT_KEY: 'difyai' - AZURE_BLOB_CONTAINER_NAME: 'difyai-container' - AZURE_BLOB_ACCOUNT_URL: 'https://.blob.core.windows.net' - # The Google storage configurations, only available when STORAGE_TYPE is `google-storage`. - GOOGLE_STORAGE_BUCKET_NAME: 'yout-bucket-name' - # if you want to use Application Default Credentials, you can leave GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64 empty. - GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: 'your-google-service-account-json-base64-string' - # The Alibaba Cloud OSS configurations, only available when STORAGE_TYPE is `aliyun-oss` - ALIYUN_OSS_BUCKET_NAME: 'your-bucket-name' - ALIYUN_OSS_ACCESS_KEY: 'your-access-key' - ALIYUN_OSS_SECRET_KEY: 'your-secret-key' - ALIYUN_OSS_ENDPOINT: 'https://oss-ap-southeast-1-internal.aliyuncs.com' - ALIYUN_OSS_REGION: 'ap-southeast-1' - ALIYUN_OSS_AUTH_VERSION: 'v4' - # The Tencent COS storage configurations, only available when STORAGE_TYPE is `tencent-cos`. - TENCENT_COS_BUCKET_NAME: 'your-bucket-name' - TENCENT_COS_SECRET_KEY: 'your-secret-key' - TENCENT_COS_SECRET_ID: 'your-secret-id' - TENCENT_COS_REGION: 'your-region' - TENCENT_COS_SCHEME: 'your-scheme' - # The type of vector store to use. Supported values are `weaviate`, `qdrant`, `milvus`, `relyt`. - VECTOR_STORE: weaviate - # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. - WEAVIATE_ENDPOINT: http://weaviate:8080 - # The Weaviate API key. - WEAVIATE_API_KEY: WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih - # The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. - QDRANT_URL: http://qdrant:6333 - # The Qdrant API key. - QDRANT_API_KEY: difyai123456 - # The Qdrant client timeout setting. - QDRANT_CLIENT_TIMEOUT: 20 - # The Qdrant client enable gRPC mode. - QDRANT_GRPC_ENABLED: 'false' - # The Qdrant server gRPC mode PORT. - QDRANT_GRPC_PORT: 6334 - # Milvus configuration Only available when VECTOR_STORE is `milvus`. - # The milvus host. - MILVUS_HOST: 127.0.0.1 - # The milvus host. - MILVUS_PORT: 19530 - # The milvus username. - MILVUS_USER: root - # The milvus password. - MILVUS_PASSWORD: Milvus - # The milvus tls switch. - MILVUS_SECURE: 'false' - # relyt configurations - RELYT_HOST: db - RELYT_PORT: 5432 - RELYT_USER: postgres - RELYT_PASSWORD: difyai123456 - RELYT_DATABASE: postgres - # pgvector configurations - PGVECTOR_HOST: pgvector - PGVECTOR_PORT: 5432 - PGVECTOR_USER: postgres - PGVECTOR_PASSWORD: difyai123456 - PGVECTOR_DATABASE: dify - # tidb vector configurations - TIDB_VECTOR_HOST: tidb - TIDB_VECTOR_PORT: 4000 - TIDB_VECTOR_USER: xxx.root - TIDB_VECTOR_PASSWORD: xxxxxx - TIDB_VECTOR_DATABASE: dify - # oracle configurations - ORACLE_HOST: oracle - ORACLE_PORT: 1521 - ORACLE_USER: dify - ORACLE_PASSWORD: dify - ORACLE_DATABASE: FREEPDB1 - # Chroma configuration - CHROMA_HOST: 127.0.0.1 - CHROMA_PORT: 8000 - CHROMA_TENANT: default_tenant - CHROMA_DATABASE: default_database - CHROMA_AUTH_PROVIDER: chromadb.auth.token_authn.TokenAuthClientProvider - CHROMA_AUTH_CREDENTIALS: xxxxxx - # Mail configuration, support: resend, smtp - MAIL_TYPE: '' - # default send from email address, if not specified - MAIL_DEFAULT_SEND_FROM: 'YOUR EMAIL FROM (eg: no-reply )' - SMTP_SERVER: '' - SMTP_PORT: 465 - SMTP_USERNAME: '' - SMTP_PASSWORD: '' - SMTP_USE_TLS: 'true' - SMTP_OPPORTUNISTIC_TLS: 'false' - # the api-key for resend (https://resend.com) - RESEND_API_KEY: '' - RESEND_API_URL: https://api.resend.com - # The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled. - SENTRY_DSN: '' - # The sample rate for Sentry events. Default: `1.0` - SENTRY_TRACES_SAMPLE_RATE: 1.0 - # The sample rate for Sentry profiles. Default: `1.0` - SENTRY_PROFILES_SAMPLE_RATE: 1.0 - # 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 - # The sandbox service endpoint. - CODE_EXECUTION_ENDPOINT: "http://sandbox: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 - # SSRF Proxy server - SSRF_PROXY_HTTP_URL: 'http://ssrf_proxy:3128' - SSRF_PROXY_HTTPS_URL: 'http://ssrf_proxy:3128' - # Indexing configuration - INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: 1000 depends_on: - db - redis volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage - # uncomment to expose dify-api port to host - # ports: - # - "5001:5001" networks: - ssrf_proxy_network - default @@ -222,160 +181,13 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.6.11 + image: langgenius/dify-api:0.6.12-fix1 restart: always environment: - CONSOLE_WEB_URL: '' + # Use the shared environment variables. + <<: *shared-api-worker-env # Startup mode, 'worker' starts the Celery worker for processing the queue. MODE: worker - - # --- All the configurations below are the same as those in the 'api' service. --- - - # The log level for the application. Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` - LOG_LEVEL: INFO - # A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`. - # same as the API service - SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U - # The configurations of postgres database connection. - # It is consistent with the configuration in the 'db' service below. - DB_USERNAME: postgres - DB_PASSWORD: difyai123456 - DB_HOST: db - DB_PORT: 5432 - DB_DATABASE: dify - # The configurations of redis cache connection. - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_USERNAME: '' - REDIS_PASSWORD: difyai123456 - REDIS_DB: 0 - REDIS_USE_SSL: 'false' - # The configurations of celery broker. - CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1 - # The type of storage to use for storing user files. Supported values are `local` and `s3` and `azure-blob` and `google-storage`, Default: `local` - STORAGE_TYPE: local - STORAGE_LOCAL_PATH: storage - # The S3 storage configurations, only available when STORAGE_TYPE is `s3`. - S3_USE_AWS_MANAGED_IAM: 'false' - S3_ENDPOINT: 'https://xxx.r2.cloudflarestorage.com' - S3_BUCKET_NAME: 'difyai' - S3_ACCESS_KEY: 'ak-difyai' - S3_SECRET_KEY: 'sk-difyai' - S3_REGION: 'us-east-1' - # The Azure Blob storage configurations, only available when STORAGE_TYPE is `azure-blob`. - AZURE_BLOB_ACCOUNT_NAME: 'difyai' - AZURE_BLOB_ACCOUNT_KEY: 'difyai' - AZURE_BLOB_CONTAINER_NAME: 'difyai-container' - AZURE_BLOB_ACCOUNT_URL: 'https://.blob.core.windows.net' - # The Google storage configurations, only available when STORAGE_TYPE is `google-storage`. - GOOGLE_STORAGE_BUCKET_NAME: 'yout-bucket-name' - # if you want to use Application Default Credentials, you can leave GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64 empty. - GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: 'your-google-service-account-json-base64-string' - # The Alibaba Cloud OSS configurations, only available when STORAGE_TYPE is `aliyun-oss` - ALIYUN_OSS_BUCKET_NAME: 'your-bucket-name' - ALIYUN_OSS_ACCESS_KEY: 'your-access-key' - ALIYUN_OSS_SECRET_KEY: 'your-secret-key' - ALIYUN_OSS_ENDPOINT: 'https://oss-ap-southeast-1-internal.aliyuncs.com' - ALIYUN_OSS_REGION: 'ap-southeast-1' - ALIYUN_OSS_AUTH_VERSION: 'v4' - # The Tencent COS storage configurations, only available when STORAGE_TYPE is `tencent-cos`. - TENCENT_COS_BUCKET_NAME: 'your-bucket-name' - TENCENT_COS_SECRET_KEY: 'your-secret-key' - TENCENT_COS_SECRET_ID: 'your-secret-id' - TENCENT_COS_REGION: 'your-region' - TENCENT_COS_SCHEME: 'your-scheme' - # The type of vector store to use. Supported values are `weaviate`, `qdrant`, `milvus`, `relyt`, `pgvector`. - VECTOR_STORE: weaviate - # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. - WEAVIATE_ENDPOINT: http://weaviate:8080 - # The Weaviate API key. - WEAVIATE_API_KEY: WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih - # The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. - QDRANT_URL: http://qdrant:6333 - # The Qdrant API key. - QDRANT_API_KEY: difyai123456 - # The Qdrant client timeout setting. - QDRANT_CLIENT_TIMEOUT: 20 - # The Qdrant client enable gRPC mode. - QDRANT_GRPC_ENABLED: 'false' - # The Qdrant server gRPC mode PORT. - QDRANT_GRPC_PORT: 6334 - # Milvus configuration Only available when VECTOR_STORE is `milvus`. - # The milvus host. - MILVUS_HOST: 127.0.0.1 - # The milvus host. - MILVUS_PORT: 19530 - # The milvus username. - MILVUS_USER: root - # The milvus password. - MILVUS_PASSWORD: Milvus - # The milvus tls switch. - MILVUS_SECURE: 'false' - # Mail configuration, support: resend - MAIL_TYPE: '' - # default send from email address, if not specified - MAIL_DEFAULT_SEND_FROM: 'YOUR EMAIL FROM (eg: no-reply )' - SMTP_SERVER: '' - SMTP_PORT: 465 - SMTP_USERNAME: '' - SMTP_PASSWORD: '' - SMTP_USE_TLS: 'true' - SMTP_OPPORTUNISTIC_TLS: 'false' - # the api-key for resend (https://resend.com) - RESEND_API_KEY: '' - RESEND_API_URL: https://api.resend.com - # relyt configurations - RELYT_HOST: db - RELYT_PORT: 5432 - RELYT_USER: postgres - RELYT_PASSWORD: difyai123456 - RELYT_DATABASE: postgres - # tencent configurations - TENCENT_VECTOR_DB_URL: http://127.0.0.1 - TENCENT_VECTOR_DB_API_KEY: dify - TENCENT_VECTOR_DB_TIMEOUT: 30 - TENCENT_VECTOR_DB_USERNAME: dify - TENCENT_VECTOR_DB_DATABASE: dify - TENCENT_VECTOR_DB_SHARD: 1 - TENCENT_VECTOR_DB_REPLICAS: 2 - # OpenSearch configuration - OPENSEARCH_HOST: 127.0.0.1 - OPENSEARCH_PORT: 9200 - OPENSEARCH_USER: admin - OPENSEARCH_PASSWORD: admin - OPENSEARCH_SECURE: 'true' - # pgvector configurations - PGVECTOR_HOST: pgvector - PGVECTOR_PORT: 5432 - PGVECTOR_USER: postgres - PGVECTOR_PASSWORD: difyai123456 - PGVECTOR_DATABASE: dify - # tidb vector configurations - TIDB_VECTOR_HOST: tidb - TIDB_VECTOR_PORT: 4000 - TIDB_VECTOR_USER: xxx.root - TIDB_VECTOR_PASSWORD: xxxxxx - TIDB_VECTOR_DATABASE: dify - # oracle configurations - ORACLE_HOST: oracle - ORACLE_PORT: 1521 - ORACLE_USER: dify - ORACLE_PASSWORD: dify - ORACLE_DATABASE: FREEPDB1 - # Chroma configuration - CHROMA_HOST: 127.0.0.1 - CHROMA_PORT: 8000 - CHROMA_TENANT: default_tenant - CHROMA_DATABASE: default_database - CHROMA_AUTH_PROVIDER: chromadb.auth.token_authn.TokenAuthClientProvider - CHROMA_AUTH_CREDENTIALS: xxxxxx - # 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 - # Indexing configuration - INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: 1000 depends_on: - db - redis @@ -388,43 +200,24 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.6.11 + image: langgenius/dify-web:0.6.12-fix1 restart: always environment: - # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is - # different from api or web app domain. - # example: http://cloud.dify.ai - CONSOLE_API_URL: '' - # The URL for Web APP api server, refers to the Web App base URL of WEB service if web app domain is different from - # console or api domain. - # example: http://udify.app - APP_API_URL: '' - # The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled. - SENTRY_DSN: '' - # uncomment to expose dify-web port to host - # ports: - # - "3000:3000" + CONSOLE_API_URL: ${CONSOLE_API_URL:-} + APP_API_URL: ${APP_API_URL:-} + SENTRY_DSN: ${WEB_SENTRY_DSN:-} # The postgres database. db: image: postgres:15-alpine restart: always environment: - PGUSER: postgres - # The password for the default postgres user. - POSTGRES_PASSWORD: difyai123456 - # The name of the default postgres database. - POSTGRES_DB: dify - # postgres data directory - PGDATA: /var/lib/postgresql/data/pgdata + PGUSER: ${PGUSER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} + POSTGRES_DB: ${POSTGRES_DB:-dify} + PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} volumes: - ./volumes/db/data:/var/lib/postgresql/data - # notice!: if you use windows-wsl2, postgres may not work properly due to the ntfs issue.you can use volumes to mount the data directory to the host. - # if you use the following config, you need to uncomment the volumes configuration below at the end of the file. - # - postgres:/var/lib/postgresql/data - # uncomment to expose db(postgresql) port to host - # ports: - # - "5432:5432" healthcheck: test: [ "CMD", "pg_isready" ] interval: 1s @@ -439,36 +232,9 @@ services: # Mount the redis data directory to the container. - ./volumes/redis/data:/data # Set the redis password when startup redis server. - command: redis-server --requirepass difyai123456 + command: redis-server --requirepass ${REDIS_PASSWORD:-difyai123456} healthcheck: test: [ "CMD", "redis-cli", "ping" ] - # uncomment to expose redis port to host - # ports: - # - "6379:6379" - - # The Weaviate vector store. - weaviate: - image: semitechnologies/weaviate:1.19.0 - restart: always - volumes: - # Mount the Weaviate data directory to the container. - - ./volumes/weaviate:/var/lib/weaviate - environment: - # The Weaviate configurations - # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. - QUERY_DEFAULTS_LIMIT: 25 - AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false' - PERSISTENCE_DATA_PATH: '/var/lib/weaviate' - DEFAULT_VECTORIZER_MODULE: 'none' - CLUSTER_HOSTNAME: 'node1' - AUTHENTICATION_APIKEY_ENABLED: 'true' - AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih' - AUTHENTICATION_APIKEY_USERS: 'hello@dify.ai' - AUTHORIZATION_ADMINLIST_ENABLED: 'true' - AUTHORIZATION_ADMINLIST_USERS: 'hello@dify.ai' - # uncomment to expose weaviate port to host - # ports: - # - "8080:8080" # The DifySandbox sandbox: @@ -478,13 +244,13 @@ services: # The DifySandbox configurations # Make sure you are changing this key for your deployment with a strong key. # You can generate a strong key using `openssl rand -base64 42`. - API_KEY: dify-sandbox - GIN_MODE: 'release' - WORKER_TIMEOUT: 15 - ENABLE_NETWORK: 'true' - HTTP_PROXY: 'http://ssrf_proxy:3128' - HTTPS_PROXY: 'http://ssrf_proxy:3128' - SANDBOX_PORT: 8194 + API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + GIN_MODE: ${SANDBOX_GIN_MODE:-release} + WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15} + ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true} + HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128} + HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} volumes: - ./volumes/sandbox/dependencies:/dependencies networks: @@ -497,67 +263,19 @@ services: image: ubuntu/squid:latest restart: always volumes: - # pls clearly modify the squid.conf file to fit your network environment. - - ./volumes/ssrf_proxy/squid.conf:/etc/squid/squid.conf + - ./ssrf_proxy/squid.conf.template:/etc/squid/squid.conf.template + - ./ssrf_proxy/docker-entrypoint.sh:/docker-entrypoint-mount.sh + entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ] + environment: + # pls clearly modify the squid env vars to fit your network environment. + HTTP_PORT: ${SSRF_HTTP_PORT:-3128} + COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} + REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} + SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} networks: - ssrf_proxy_network - default - # Qdrant vector store. - # uncomment to use qdrant as vector store. - # (if uncommented, you need to comment out the weaviate service above, - # and set VECTOR_STORE to qdrant in the api & worker service.) - # qdrant: - # image: langgenius/qdrant:v1.7.3 - # restart: always - # volumes: - # - ./volumes/qdrant:/qdrant/storage - # environment: - # QDRANT_API_KEY: 'difyai123456' - # # uncomment to expose qdrant port to host - # # ports: - # # - "6333:6333" - # # - "6334:6334" - - # The pgvector vector database. - # Uncomment to use qdrant as vector store. - # pgvector: - # image: pgvector/pgvector:pg16 - # restart: always - # environment: - # PGUSER: postgres - # # The password for the default postgres user. - # POSTGRES_PASSWORD: difyai123456 - # # The name of the default postgres database. - # POSTGRES_DB: dify - # # postgres data directory - # PGDATA: /var/lib/postgresql/data/pgdata - # volumes: - # - ./volumes/pgvector/data:/var/lib/postgresql/data - # # uncomment to expose db(postgresql) port to host - # # ports: - # # - "5433:5432" - # healthcheck: - # test: [ "CMD", "pg_isready" ] - # interval: 1s - # timeout: 3s - # retries: 30 - - # The oracle vector database. - # Uncomment to use oracle23ai as vector store. Also need to Uncomment volumes block - # oracle: - # image: container-registry.oracle.com/database/free:latest - # restart: always - # ports: - # - 1521:1521 - # volumes: - # - type: volume - # source: oradata - # target: /opt/oracle/oradata - # - ./startupscripts:/opt/oracle/scripts/startup - # environment: - # - ORACLE_PWD=Dify123456 - # - ORACLE_CHARACTERSET=AL32UTF8 - # The nginx reverse proxy. # used for reverse proxying the API service and Web service. @@ -565,24 +283,255 @@ services: image: nginx:latest restart: always volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf - - ./nginx/proxy.conf:/etc/nginx/proxy.conf + - ./nginx/nginx.conf.template:/etc/nginx/nginx.conf.template + - ./nginx/proxy.conf.template:/etc/nginx/proxy.conf.template + - ./nginx/https.conf.template:/etc/nginx/https.conf.template - ./nginx/conf.d:/etc/nginx/conf.d - #- ./nginx/ssl:/etc/ssl + - ./nginx/docker-entrypoint.sh:/docker-entrypoint-mount.sh + - ./nginx/ssl:/etc/ssl + entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ] + environment: + NGINX_SERVER_NAME: ${NGINX_SERVER_NAME:-_} + NGINX_HTTPS_ENABLED: ${NGINX_HTTPS_ENABLED:-false} + NGINX_SSL_PORT: ${NGINX_SSL_PORT:-443} + NGINX_PORT: ${NGINX_PORT:-80} + # You're required to add your own SSL certificates/keys to the `./nginx/ssl` directory + # and modify the env vars below in .env if HTTPS_ENABLED is true. + NGINX_SSL_CERT_FILENAME: ${NGINX_SSL_CERT_FILENAME:-dify.crt} + 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_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} + NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} + NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} depends_on: - api - web ports: - - "80:80" - #- "443:443" -# notice: if you use windows-wsl2, postgres may not work properly due to the ntfs issue.you can use volumes to mount the data directory to the host. -# volumes: -#   postgres: + - "${EXPOSE_NGINX_PORT:-80}:${NGINX_PORT:-80}" + - "${EXPOSE_NGINX_SSL_PORT:-443}:${NGINX_SSL_PORT:-443}" + + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + profiles: + - '' + - weaviate + restart: always + volumes: + # Mount the Weaviate data directory to the con tainer. + - ./volumes/weaviate:/var/lib/weaviate + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + PERSISTENCE_DATA_PATH: ${WEAVIATE_PERSISTENCE_DATA_PATH:-/var/lib/weaviate} + QUERY_DEFAULTS_LIMIT: ${WEAVIATE_QUERY_DEFAULTS_LIMIT:-25} + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: ${WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED:-false} + DEFAULT_VECTORIZER_MODULE: ${WEAVIATE_DEFAULT_VECTORIZER_MODULE:-none} + CLUSTER_HOSTNAME: ${WEAVIATE_CLUSTER_HOSTNAME:-node1} + AUTHENTICATION_APIKEY_ENABLED: ${WEAVIATE_AUTHENTICATION_APIKEY_ENABLED:-true} + AUTHENTICATION_APIKEY_ALLOWED_KEYS: ${WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} + AUTHENTICATION_APIKEY_USERS: ${WEAVIATE_AUTHENTICATION_APIKEY_USERS:-hello@dify.ai} + AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true} + AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai} + + # Qdrant vector store. + # (if used, you need to set VECTOR_STORE to qdrant in the api & worker service.) + qdrant: + image: langgenius/qdrant:v1.7.3 + profiles: + - qdrant + restart: always + volumes: + - ./volumes/qdrant:/qdrant/storage + environment: + QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456} + + # The pgvector vector database. + pgvector: + image: pgvector/pgvector:pg16 + profiles: + - pgvector + restart: always + environment: + PGUSER: ${PGVECTOR_PGUSER:-postgres} + # The password for the default postgres user. + POSTGRES_PASSWORD: ${PGVECTOR_POSTGRES_PASSWORD:-difyai123456} + # The name of the default postgres database. + POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify} + # postgres data directory + PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata} + volumes: + - ./volumes/pgvector/data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 1s + timeout: 3s + retries: 30 + + # pgvecto-rs vector store + pgvecto-rs: + image: tensorchord/pgvecto-rs:pg16-v0.2.0 + profiles: + - pgvecto-rs + restart: always + environment: + PGUSER: ${PGVECTOR_PGUSER:-postgres} + # The password for the default postgres user. + POSTGRES_PASSWORD: ${PGVECTOR_POSTGRES_PASSWORD:-difyai123456} + # The name of the default postgres database. + POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify} + # postgres data directory + PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata} + volumes: + - ./volumes/pgvecto_rs/data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 1s + timeout: 3s + retries: 30 + + # Chroma vector database + chroma: + image: ghcr.io/chroma-core/chroma:0.5.1 + profiles: + - chroma + restart: always + volumes: + - ./volumes/chroma:/chroma/chroma + environment: + CHROMA_SERVER_AUTHN_CREDENTIALS: ${CHROMA_SERVER_AUTHN_CREDENTIALS:-difyai123456} + CHROMA_SERVER_AUTHN_PROVIDER: ${CHROMA_SERVER_AUTHN_PROVIDER:-chromadb.auth.token_authn.TokenAuthenticationServerProvider} + IS_PERSISTENT: ${CHROMA_IS_PERSISTENT:-TRUE} + + # Oracle vector database + oracle: + image: container-registry.oracle.com/database/free:latest + profiles: + - oracle + restart: always + volumes: + - type: volume + source: oradata + target: /opt/oracle/oradata + - ./startupscripts:/opt/oracle/scripts/startup + environment: + - ORACLE_PWD=${ORACLE_PWD:-Dify123456} + - ORACLE_CHARACTERSET=${ORACLE_CHARACTERSET:-AL32UTF8} + + # Milvus vector database services + etcd: + container_name: milvus-etcd + image: quay.io/coreos/etcd:v3.5.5 + profiles: + - milvus + environment: + - ETCD_AUTO_COMPACTION_MODE=${ETCD_AUTO_COMPACTION_MODE:-revision} + - ETCD_AUTO_COMPACTION_RETENTION=${ETCD_AUTO_COMPACTION_RETENTION:-1000} + - ETCD_QUOTA_BACKEND_BYTES=${ETCD_QUOTA_BACKEND_BYTES:-4294967296} + - ETCD_SNAPSHOT_COUNT=${ETCD_SNAPSHOT_COUNT:-50000} + volumes: + - ./volumes/milvus/etcd:/etcd + command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd + healthcheck: + test: ["CMD", "etcdctl", "endpoint", "health"] + interval: 30s + timeout: 20s + retries: 3 + networks: + - milvus + + minio: + container_name: milvus-minio + image: minio/minio:RELEASE.2023-03-20T20-16-18Z + profiles: + - milvus + environment: + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} + volumes: + - ./volumes/milvus/minio:/minio_data + command: minio server /minio_data --console-address ":9001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + networks: + - milvus + + milvus-standalone: + container_name: milvus-standalone + image: milvusdb/milvus:v2.3.1 + profiles: + - milvus + command: ["milvus", "run", "standalone"] + environment: + ETCD_ENDPOINTS: ${ETCD_ENDPOINTS:-etcd:2379} + MINIO_ADDRESS: ${MINIO_ADDRESS:-minio:9000} + common.security.authorizationEnabled: ${MILVUS_AUTHORIZATION_ENABLED:-true} + volumes: + - ./volumes/milvus/milvus:/var/lib/milvus + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"] + interval: 30s + start_period: 90s + timeout: 20s + retries: 3 + depends_on: + - "etcd" + - "minio" + networks: + - milvus + + # Opensearch vector database + opensearch: + container_name: opensearch + image: opensearchproject/opensearch:latest + profiles: + - opensearch + environment: + - discovery.type=${OPENSEARCH_DISCOVERY_TYPE:-single-node} + - bootstrap.memory_lock=${OPENSEARCH_BOOTSTRAP_MEMORY_LOCK:-true} + - OPENSEARCH_JAVA_OPTS=-Xms${OPENSEARCH_JAVA_OPTS_MIN:-512m} -Xmx${OPENSEARCH_JAVA_OPTS_MAX:-1024m} + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD:-Qazwsxedc!@#123} + ulimits: + memlock: + soft: ${OPENSEARCH_MEMLOCK_SOFT:--1} + hard: ${OPENSEARCH_MEMLOCK_HARD:--1} + nofile: + soft: ${OPENSEARCH_NOFILE_SOFT:-65536} + hard: ${OPENSEARCH_NOFILE_HARD:-65536} + volumes: + - ./volumes/opensearch/data:/usr/share/opensearch/data + networks: + - opensearch-net + + opensearch-dashboards: + container_name: opensearch-dashboards + image: opensearchproject/opensearch-dashboards:latest + profiles: + - opensearch + environment: + OPENSEARCH_HOSTS: '["https://opensearch:9200"]' + volumes: + - ./volumes/opensearch/opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml + networks: + - opensearch-net + depends_on: + - opensearch + networks: # create a network between sandbox, api and ssrf_proxy, and can not access outside. ssrf_proxy_network: driver: bridge internal: true + milvus: + driver: bridge + opensearch-net: + driver: bridge + internal: true -#volumes: -# oradata: +volumes: + oradata: diff --git a/docker/middleware.env.example b/docker/middleware.env.example new file mode 100644 index 0000000000..750dcfe950 --- /dev/null +++ b/docker/middleware.env.example @@ -0,0 +1,52 @@ +# ------------------------------ +# Environment Variables for db Service +# ------------------------------ +PGUSER=postgres +# The password for the default postgres user. +POSTGRES_PASSWORD=difyai123456 +# The name of the default postgres database. +POSTGRES_DB=dify +# postgres data directory +PGDATA=/var/lib/postgresql/data/pgdata + + +# ------------------------------ +# Environment Variables for sandbox Service +SANDBOX_API_KEY=dify-sandbox +SANDBOX_GIN_MODE=release +SANDBOX_WORKER_TIMEOUT=15 +SANDBOX_ENABLE_NETWORK=true +SANDBOX_HTTP_PROXY=http://ssrf_proxy:3128 +SANDBOX_HTTPS_PROXY=http://ssrf_proxy:3128 +SANDBOX_PORT=8194 +# ------------------------------ + +# ------------------------------ +# Environment Variables for ssrf_proxy Service +# ------------------------------ +SSRF_HTTP_PORT=3128 +SSRF_COREDUMP_DIR=/var/spool/squid +SSRF_REVERSE_PROXY_PORT=8194 +SSRF_SANDBOX_HOST=sandbox + +# ------------------------------ +# Environment Variables for weaviate Service +# ------------------------------ +WEAVIATE_QUERY_DEFAULTS_LIMIT=25 +WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true +WEAVIATE_DEFAULT_VECTORIZER_MODULE=none +WEAVIATE_CLUSTER_HOSTNAME=node1 +WEAVIATE_AUTHENTICATION_APIKEY_ENABLED=true +WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih +WEAVIATE_AUTHENTICATION_APIKEY_USERS=hello@dify.ai +WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED=true +WEAVIATE_AUTHORIZATION_ADMINLIST_USERS=hello@dify.ai + +# ------------------------------ +# Docker Compose Service Expose Host Port Configurations +# ------------------------------ +EXPOSE_POSTGRES_PORT=5432 +EXPOSE_REDIS_PORT=6379 +EXPOSE_SANDBOX_PORT=8194 +EXPOSE_SSRF_PROXY_PORT=3128 +EXPOSE_WEAVIATE_PORT=8080 diff --git a/docker/nginx/conf.d/default.conf.template b/docker/nginx/conf.d/default.conf.template new file mode 100644 index 0000000000..9f6e99af51 --- /dev/null +++ b/docker/nginx/conf.d/default.conf.template @@ -0,0 +1,34 @@ +# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + +server { + listen ${NGINX_PORT}; + server_name ${NGINX_SERVER_NAME}; + + location /console/api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /v1 { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /files { + proxy_pass http://api:5001; + include proxy.conf; + } + + location / { + proxy_pass http://web:3000; + include proxy.conf; + } + + # placeholder for https config defined in https.conf.template + ${HTTPS_CONFIG} +} diff --git a/docker/nginx/docker-entrypoint.sh b/docker/nginx/docker-entrypoint.sh new file mode 100755 index 0000000000..df432a0213 --- /dev/null +++ b/docker/nginx/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if [ "${NGINX_HTTPS_ENABLED}" = "true" ]; then + # set the HTTPS_CONFIG environment variable to the content of the https.conf.template + HTTPS_CONFIG=$(envsubst < /etc/nginx/https.conf.template) + export HTTPS_CONFIG + # Substitute the HTTPS_CONFIG in the default.conf.template with content from https.conf.template + envsubst '${HTTPS_CONFIG}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf +fi + +env_vars=$(printenv | cut -d= -f1 | sed 's/^/$/g' | paste -sd, -) + +envsubst "$env_vars" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +envsubst "$env_vars" < /etc/nginx/proxy.conf.template > /etc/nginx/proxy.conf + +envsubst < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + +# Start Nginx using the default entrypoint +exec nginx -g 'daemon off;' \ No newline at end of file diff --git a/docker/nginx/https.conf.template b/docker/nginx/https.conf.template new file mode 100644 index 0000000000..12a6f56e3b --- /dev/null +++ b/docker/nginx/https.conf.template @@ -0,0 +1,9 @@ +# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + +listen ${NGINX_SSL_PORT} ssl; +ssl_certificate ./../ssl/${NGINX_SSL_CERT_FILENAME}; +ssl_certificate_key ./../ssl/${NGINX_SSL_CERT_KEY_FILENAME}; +ssl_protocols ${NGINX_SSL_PROTOCOLS}; +ssl_prefer_server_ciphers on; +ssl_session_cache shared:SSL:10m; +ssl_session_timeout 10m; \ No newline at end of file diff --git a/docker/nginx/nginx.conf.template b/docker/nginx/nginx.conf.template new file mode 100644 index 0000000000..32a571653e --- /dev/null +++ b/docker/nginx/nginx.conf.template @@ -0,0 +1,34 @@ +# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + +user nginx; +worker_processes ${NGINX_WORKER_PROCESSES}; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout ${NGINX_KEEPALIVE_TIMEOUT}; + + #gzip on; + client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE}; + + include /etc/nginx/conf.d/*.conf; +} \ No newline at end of file diff --git a/docker/nginx/proxy.conf.template b/docker/nginx/proxy.conf.template new file mode 100644 index 0000000000..6b52d23512 --- /dev/null +++ b/docker/nginx/proxy.conf.template @@ -0,0 +1,10 @@ +# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + +proxy_set_header Host $host; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_buffering off; +proxy_read_timeout ${NGINX_PROXY_READ_TIMEOUT}; +proxy_send_timeout ${NGINX_PROXY_SEND_TIMEOUT}; diff --git a/docker/nginx/ssl/.gitkeep b/docker/nginx/ssl/.gitkeep index 8b13789179..e69de29bb2 100644 --- a/docker/nginx/ssl/.gitkeep +++ b/docker/nginx/ssl/.gitkeep @@ -1 +0,0 @@ - diff --git a/docker/ssrf_proxy/docker-entrypoint.sh b/docker/ssrf_proxy/docker-entrypoint.sh new file mode 100755 index 0000000000..613897bb7d --- /dev/null +++ b/docker/ssrf_proxy/docker-entrypoint.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Modified based on Squid OCI image entrypoint + +# This entrypoint aims to forward the squid logs to stdout to assist users of +# common container related tooling (e.g., kubernetes, docker-compose, etc) to +# access the service logs. + +# Moreover, it invokes the squid binary, leaving all the desired parameters to +# be provided by the "command" passed to the spawned container. If no command +# is provided by the user, the default behavior (as per the CMD statement in +# the Dockerfile) will be to use Ubuntu's default configuration [1] and run +# squid with the "-NYC" options to mimic the behavior of the Ubuntu provided +# systemd unit. + +# [1] The default configuration is changed in the Dockerfile to allow local +# network connections. See the Dockerfile for further information. + +echo "[ENTRYPOINT] re-create snakeoil self-signed certificate removed in the build process" +if [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then + /usr/sbin/make-ssl-cert generate-default-snakeoil --force-overwrite > /dev/null 2>&1 +fi + +tail -F /var/log/squid/access.log 2>/dev/null & +tail -F /var/log/squid/error.log 2>/dev/null & +tail -F /var/log/squid/store.log 2>/dev/null & +tail -F /var/log/squid/cache.log 2>/dev/null & + +# Replace environment variables in the template and output to the squid.conf +echo "[ENTRYPOINT] replacing environment variables in the template" +awk '{ + while(match($0, /\${[A-Za-z_][A-Za-z_0-9]*}/)) { + var = substr($0, RSTART+2, RLENGTH-3) + val = ENVIRON[var] + $0 = substr($0, 1, RSTART-1) val substr($0, RSTART+RLENGTH) + } + print +}' /etc/squid/squid.conf.template > /etc/squid/squid.conf + +/usr/sbin/squid -Nz +echo "[ENTRYPOINT] starting squid" +/usr/sbin/squid -f /etc/squid/squid.conf -NYC 1 diff --git a/docker/ssrf_proxy/squid.conf.template b/docker/ssrf_proxy/squid.conf.template new file mode 100644 index 0000000000..a0875a8826 --- /dev/null +++ b/docker/ssrf_proxy/squid.conf.template @@ -0,0 +1,50 @@ +acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) +acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) +acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) +acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines +acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) +acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) +acl localnet src fc00::/7 # RFC 4193 local private network range +acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 21 # ftp +acl Safe_ports port 443 # https +acl Safe_ports port 70 # gopher +acl Safe_ports port 210 # wais +acl Safe_ports port 1025-65535 # unregistered ports +acl Safe_ports port 280 # http-mgmt +acl Safe_ports port 488 # gss-http +acl Safe_ports port 591 # filemaker +acl Safe_ports port 777 # multiling http +acl CONNECT method CONNECT +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access allow localhost +include /etc/squid/conf.d/*.conf +http_access deny all + +################################## Proxy Server ################################ +http_port ${HTTP_PORT} +coredump_dir ${COREDUMP_DIR} +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims +refresh_pattern \/InRelease$ 0 0% 0 refresh-ims +refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +refresh_pattern . 0 20% 4320 + + +# cache_dir ufs /var/spool/squid 100 16 256 +# upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks +# cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default + +################################## Reverse Proxy To Sandbox ################################ +http_port ${REVERSE_PROXY_PORT} accel vhost +cache_peer ${SANDBOX_HOST} parent ${SANDBOX_PORT} 0 no-query originserver +acl src_all src all +http_access allow src_all diff --git a/web/.vscode/settings.example.json b/web/.vscode/settings.example.json index 6162d021d0..a2dfe7c669 100644 --- a/web/.vscode/settings.example.json +++ b/web/.vscode/settings.example.json @@ -22,4 +22,4 @@ }, "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file +} diff --git a/web/Dockerfile b/web/Dockerfile index f2fc4af2f8..56957f0927 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -9,7 +9,7 @@ RUN apk add --no-cache tzdata # install packages -FROM base as packages +FROM base AS packages WORKDIR /app/web @@ -22,7 +22,7 @@ COPY yarn.lock . RUN yarn install --frozen-lockfile # build resources -FROM base as builder +FROM base AS builder WORKDIR /app/web COPY --from=packages /app/web/ . COPY . . @@ -31,17 +31,17 @@ RUN yarn build # production stage -FROM base as production +FROM base AS production -ENV NODE_ENV production -ENV EDITION SELF_HOSTED -ENV DEPLOY_ENV PRODUCTION -ENV CONSOLE_API_URL http://127.0.0.1:5001 -ENV APP_API_URL http://127.0.0.1:5001 -ENV PORT 3000 +ENV NODE_ENV=production +ENV EDITION=SELF_HOSTED +ENV DEPLOY_ENV=PRODUCTION +ENV CONSOLE_API_URL=http://127.0.0.1:5001 +ENV APP_API_URL=http://127.0.0.1:5001 +ENV PORT=3000 # set timezone -ENV TZ UTC +ENV TZ=UTC RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \ && echo ${TZ} > /etc/timezone @@ -59,7 +59,7 @@ COPY docker/pm2.json ./pm2.json COPY docker/entrypoint.sh ./entrypoint.sh ARG COMMIT_SHA -ENV COMMIT_SHA ${COMMIT_SHA} +ENV COMMIT_SHA=${COMMIT_SHA} EXPOSE 3000 ENTRYPOINT ["/bin/sh", "./entrypoint.sh"] diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index 8f924268f2..137c2c36ee 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -1,7 +1,7 @@ import React from 'react' import ChartView from './chartView' import CardView from './cardView' -import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server' +import TracingPanel from './tracing/panel' import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel' export type IDevelopProps = { @@ -11,18 +11,10 @@ export type IDevelopProps = { const Overview = async ({ params: { appId }, }: IDevelopProps) => { - const locale = getLocaleOnServer() - /* - rename useTranslation to avoid lint error - please check: https://github.com/i18next/next-13-app-dir-i18next-example/issues/24 - */ - const { t } = await translate(locale, 'app-overview') return (
-
- {t('overview.title')} -
+
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx new file mode 100644 index 0000000000..6b65af0824 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx @@ -0,0 +1,87 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import type { PopupProps } from './config-popup' +import ConfigPopup from './config-popup' +import Button from '@/app/components/base/button' +import { Settings04 } from '@/app/components/base/icons/src/vender/line/general' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' + +const I18N_PREFIX = 'app.tracing' + +type Props = { + readOnly: boolean + className?: string + hasConfigured: boolean + controlShowPopup?: number +} & PopupProps + +const ConfigBtn: FC = ({ + className, + hasConfigured, + controlShowPopup, + ...popupProps +}) => { + const { t } = useTranslation() + 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]) + + useEffect(() => { + if (controlShowPopup) + // setOpen(!openRef.current) + setOpen(true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [controlShowPopup]) + + if (popupProps.readOnly && !hasConfigured) + return null + + const triggerContent = hasConfigured + ? ( +
+ +
+ ) + : ( + + ) + + return ( + + + {triggerContent} + + + + + + ) +} +export default React.memo(ConfigBtn) 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 new file mode 100644 index 0000000000..7aa1fca96d --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -0,0 +1,179 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import TracingIcon from './tracing-icon' +import ProviderPanel from './provider-panel' +import type { LangFuseConfig, LangSmithConfig } from './type' +import { TracingProvider } from './type' +import ProviderConfigModal from './provider-config-modal' +import Indicator from '@/app/components/header/indicator' +import Switch from '@/app/components/base/switch' +import TooltipPlus from '@/app/components/base/tooltip-plus' + +const I18N_PREFIX = 'app.tracing' + +export type PopupProps = { + appId: string + readOnly: boolean + enabled: boolean + onStatusChange: (enabled: boolean) => void + chosenProvider: TracingProvider | null + onChooseProvider: (provider: TracingProvider) => void + langSmithConfig: LangSmithConfig | null + langFuseConfig: LangFuseConfig | null + onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig) => void + onConfigRemoved: (provider: TracingProvider) => void +} + +const ConfigPopup: FC = ({ + appId, + readOnly, + enabled, + onStatusChange, + chosenProvider, + onChooseProvider, + langSmithConfig, + langFuseConfig, + onConfigUpdated, + onConfigRemoved, +}) => { + const { t } = useTranslation() + + const [currentProvider, setCurrentProvider] = useState(TracingProvider.langfuse) + const [isShowConfigModal, { + setTrue: showConfigModal, + setFalse: hideConfigModal, + }] = useBoolean(false) + const handleOnConfig = useCallback((provider: TracingProvider) => { + return () => { + setCurrentProvider(provider) + showConfigModal() + } + }, [showConfigModal]) + + const handleOnChoose = useCallback((provider: TracingProvider) => { + return () => { + onChooseProvider(provider) + } + }, [onChooseProvider]) + + const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig) => { + onConfigUpdated(currentProvider!, payload) + hideConfigModal() + }, [currentProvider, hideConfigModal, onConfigUpdated]) + + const handleConfigRemoved = useCallback(() => { + onConfigRemoved(currentProvider!) + hideConfigModal() + }, [currentProvider, hideConfigModal, onConfigRemoved]) + + const providerAllConfigured = langSmithConfig && langFuseConfig + const providerAllNotConfigured = !langSmithConfig && !langFuseConfig + + const switchContent = ( + + ) + const langSmithPanel = ( + + ) + + const langfusePanel = ( + + ) + + return ( +
+
+
+ +
{t(`${I18N_PREFIX}.tracing`)}
+
+
+ +
+ {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)} +
+ {!readOnly && ( + <> + {providerAllNotConfigured + ? ( + + {switchContent} + + + ) + : switchContent} + + )} + +
+
+ +
+ {t(`${I18N_PREFIX}.tracingDescription`)} +
+
+
+ {(providerAllConfigured || providerAllNotConfigured) + ? ( + <> +
{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}
+
+ {langSmithPanel} + {langfusePanel} +
+ + ) + : ( + <> +
{t(`${I18N_PREFIX}.configProviderTitle.configured`)}
+
+ {langSmithConfig ? langSmithPanel : langfusePanel} +
+
{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}
+
+ {!langSmithConfig ? langSmithPanel : langfusePanel} +
+ + )} + +
+ {isShowConfigModal && ( + + )} +
+ ) +} +export default React.memo(ConfigPopup) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts new file mode 100644 index 0000000000..bbc1f0f220 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts @@ -0,0 +1,6 @@ +import { TracingProvider } from './type' + +export const docURL = { + [TracingProvider.langSmith]: 'https://docs.smith.langchain.com/', + [TracingProvider.langfuse]: 'https://docs.langfuse.com', +} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx new file mode 100644 index 0000000000..6c1f25af9b --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx @@ -0,0 +1,41 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from 'classnames' + +type Props = { + className?: string + label: string + labelClassName?: string + value: string | number + onChange: (value: string) => void + isRequired?: boolean + placeholder?: string +} + +const Field: FC = ({ + className, + label, + labelClassName, + value, + onChange, + isRequired = false, + placeholder = '', +}) => { + return ( +
+
+
{label}
+ {isRequired && *} +
+ onChange(e.target.value)} + className='flex h-9 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-gray-50 placeholder:text-gray-400' + placeholder={placeholder} + /> +
+ ) +} +export default React.memo(Field) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx new file mode 100644 index 0000000000..62b98669db --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -0,0 +1,227 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import { usePathname } from 'next/navigation' +import { useBoolean } from 'ahooks' +import type { LangFuseConfig, LangSmithConfig } from './type' +import { TracingProvider } from './type' +import TracingIcon from './tracing-icon' +import ToggleExpandBtn from './toggle-fold-btn' +import ConfigButton from './config-button' +import { LangfuseIcon, LangsmithIcon } 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' +import Toast from '@/app/components/base/toast' +import { useAppContext } from '@/context/app-context' +import Loading from '@/app/components/base/loading' + +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() + const matched = pathname.match(/\/app\/([^/]+)/) + const appId = (matched?.length && matched[1]) ? matched[1] : '' + const { isCurrentWorkspaceEditor } = useAppContext() + const readOnly = !isCurrentWorkspaceEditor + + const [isLoaded, { + setTrue: setLoaded, + }] = useBoolean(false) + + const [tracingStatus, setTracingStatus] = useState(null) + const enabled = tracingStatus?.enabled || false + const handleTracingStatusChange = async (tracingStatus: TracingStatus, noToast?: boolean) => { + await updateTracingStatus({ appId, body: tracingStatus }) + setTracingStatus(tracingStatus) + if (!noToast) { + Toast.notify({ + type: 'success', + message: t('common.api.success'), + }) + } + } + + const handleTracingEnabledChange = (enabled: boolean) => { + handleTracingStatusChange({ + tracing_provider: tracingStatus?.tracing_provider || null, + enabled, + }) + } + const handleChooseProvider = (provider: TracingProvider) => { + handleTracingStatusChange({ + tracing_provider: provider, + enabled: true, + }) + } + const inUseTracingProvider: TracingProvider | null = tracingStatus?.tracing_provider || null + const InUseProviderIcon = inUseTracingProvider === TracingProvider.langSmith ? LangsmithIcon : LangfuseIcon + + const [langSmithConfig, setLangSmithConfig] = useState(null) + const [langFuseConfig, setLangFuseConfig] = useState(null) + const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig) + + const fetchTracingConfig = async () => { + const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith }) + if (!langSmithHasNotConfig) + setLangSmithConfig(langSmithConfig as LangSmithConfig) + const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse }) + if (!langFuseHasNotConfig) + setLangFuseConfig(langFuseConfig as LangFuseConfig) + } + + const handleTracingConfigUpdated = async (provider: TracingProvider) => { + // call api to hide secret key value + const { tracing_config } = await doFetchTracingConfig({ appId, provider }) + if (provider === TracingProvider.langSmith) + setLangSmithConfig(tracing_config as LangSmithConfig) + else + setLangFuseConfig(tracing_config as LangFuseConfig) + } + + const handleTracingConfigRemoved = (provider: TracingProvider) => { + if (provider === TracingProvider.langSmith) + setLangSmithConfig(null) + else + setLangFuseConfig(null) + if (provider === inUseTracingProvider) { + handleTracingStatusChange({ + enabled: false, + tracing_provider: null, + }, true) + } + } + + useEffect(() => { + (async () => { + const tracingStatus = await fetchTracingStatus({ appId }) + setTracingStatus(tracingStatus) + await fetchTracingConfig() + setLoaded() + })() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const [isFold, setFold] = useState(false) + const [controlShowPopup, setControlShowPopup] = useState(0) + const showPopup = useCallback(() => { + setControlShowPopup(Date.now()) + }, [setControlShowPopup]) + if (!isLoaded) { + return ( +
+ + <div className='w-[200px]'> + <Loading /> + </div> + </div> + ) + } + + if (!isFold && !hasConfiguredTracing) { + return ( + <div className={cn('mb-3')}> + <Title /> + <div className='mt-2 flex justify-between p-3 pr-4 items-center bg-white border-[0.5px] border-black/8 rounded-xl shadow-md'> + <div className='flex space-x-2'> + <TracingIcon size='lg' className='m-1' /> + <div> + <div className='mb-0.5 leading-6 text-base font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}</div> + <div className='flex justify-between leading-4 text-xs font-normal text-gray-500'> + <span className='mr-2'>{t(`${I18N_PREFIX}.description`)}</span> + <div className='flex space-x-3'> + <LangsmithIcon className='h-4' /> + <LangfuseIcon className='h-4' /> + </div> + </div> + </div> + </div> + + <div className='flex items-center space-x-1'> + <ConfigButton + appId={appId} + readOnly={readOnly} + hasConfigured={false} + enabled={enabled} + onStatusChange={handleTracingEnabledChange} + chosenProvider={inUseTracingProvider} + onChooseProvider={handleChooseProvider} + langSmithConfig={langSmithConfig} + langFuseConfig={langFuseConfig} + onConfigUpdated={handleTracingConfigUpdated} + onConfigRemoved={handleTracingConfigRemoved} + /> + <ToggleExpandBtn isFold={isFold} onFoldChange={setFold} /> + </div> + </div> + </div> + ) + } + + return ( + <div className={cn('mb-3 flex justify-between items-center')}> + <Title className='h-[41px]' /> + <div className='flex items-center p-2 rounded-xl border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100' onClick={showPopup}> + {!inUseTracingProvider + ? <> + <TracingIcon size='md' className='mr-2' /> + <div className='leading-5 text-sm font-semibold text-gray-700'>{t(`${I18N_PREFIX}.title`)}</div> + </> + : <InUseProviderIcon className='ml-1 h-4' />} + + {hasConfiguredTracing && ( + <div className='ml-4 mr-1 flex items-center'> + <Indicator color={enabled ? 'green' : 'gray'} /> + <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'> + {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)} + </div> + </div> + )} + + {hasConfiguredTracing && ( + <div className='ml-2 w-px h-3.5 bg-gray-200'></div> + )} + <div className='flex items-center' onClick={e => e.stopPropagation()}> + <ConfigButton + appId={appId} + readOnly={readOnly} + hasConfigured + className='ml-2' + enabled={enabled} + onStatusChange={handleTracingEnabledChange} + chosenProvider={inUseTracingProvider} + onChooseProvider={handleChooseProvider} + langSmithConfig={langSmithConfig} + langFuseConfig={langFuseConfig} + onConfigUpdated={handleTracingConfigUpdated} + onConfigRemoved={handleTracingConfigRemoved} + controlShowPopup={controlShowPopup} + /> + </div> + {!hasConfiguredTracing && ( + <div className='flex items-center' onClick={e => e.stopPropagation()}> + <div className='mx-2 w-px h-3.5 bg-gray-200'></div> + <ToggleExpandBtn isFold={isFold} onFoldChange={setFold} /> + </div> + )} + </div> + </div> + ) +} +export default React.memo(Panel) 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 new file mode 100644 index 0000000000..2411d2baa4 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -0,0 +1,292 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import Field from './field' +import type { LangFuseConfig, LangSmithConfig } from './type' +import { TracingProvider } from './type' +import { docURL } from './config' +import { + PortalToFollowElem, + PortalToFollowElemContent, +} from '@/app/components/base/portal-to-follow-elem' +import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import Button from '@/app/components/base/button' +import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' +import ConfirmUi from '@/app/components/base/confirm' +import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' +import Toast from '@/app/components/base/toast' + +type Props = { + appId: string + type: TracingProvider + payload?: LangSmithConfig | LangFuseConfig | null + onRemoved: () => void + onCancel: () => void + onSaved: (payload: LangSmithConfig | LangFuseConfig) => void + onChosen: (provider: TracingProvider) => void +} + +const I18N_PREFIX = 'app.tracing.configProvider' + +const langSmithConfigTemplate = { + api_key: '', + project: '', + endpoint: '', +} + +const langFuseConfigTemplate = { + public_key: '', + secret_key: '', + host: '', +} + +const ProviderConfigModal: FC<Props> = ({ + appId, + type, + payload, + onRemoved, + onCancel, + onSaved, + onChosen, +}) => { + const { t } = useTranslation() + const isEdit = !!payload + const isAdd = !isEdit + const [isSaving, setIsSaving] = useState(false) + const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig>((() => { + if (isEdit) + return payload + + if (type === TracingProvider.langSmith) + return langSmithConfigTemplate + + return langFuseConfigTemplate + })()) + const [isShowRemoveConfirm, { + setTrue: showRemoveConfirm, + setFalse: hideRemoveConfirm, + }] = useBoolean(false) + + const handleRemove = useCallback(async () => { + await removeTracingConfig({ + appId, + provider: type, + }) + Toast.notify({ + type: 'success', + message: t('common.api.remove'), + }) + onRemoved() + hideRemoveConfirm() + }, [hideRemoveConfirm, appId, type, t, onRemoved]) + + const handleConfigChange = useCallback((key: string) => { + return (value: string) => { + setConfig({ + ...config, + [key]: value, + }) + } + }, [config]) + + const checkValid = useCallback(() => { + let errorMessage = '' + if (type === TracingProvider.langSmith) { + const postData = config as LangSmithConfig + 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.langfuse) { + const postData = config as LangFuseConfig + if (!errorMessage && !postData.secret_key) + errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) }) + if (!errorMessage && !postData.public_key) + errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) }) + if (!errorMessage && !postData.host) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' }) + } + + return errorMessage + }, [config, t, type]) + const handleSave = useCallback(async () => { + if (isSaving) + return + const errorMessage = checkValid() + if (errorMessage) { + Toast.notify({ + type: 'error', + message: errorMessage, + }) + return + } + const action = isEdit ? updateTracingConfig : addTracingConfig + try { + await action({ + appId, + body: { + tracing_provider: type, + tracing_config: config, + }, + }) + Toast.notify({ + type: 'success', + message: t('common.api.success'), + }) + onSaved(config) + if (isAdd) + onChosen(type) + } + finally { + setIsSaving(false) + } + }, [appId, checkValid, config, isAdd, isEdit, isSaving, onChosen, onSaved, t, type]) + + return ( + <> + {!isShowRemoveConfirm + ? ( + <PortalToFollowElem open> + <PortalToFollowElemContent className='w-full h-full z-[60]'> + <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> + <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'> + <div className='px-8 pt-8'> + <div className='flex justify-between items-center mb-4'> + <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div> + </div> + + <div className='space-y-4'> + {type === TracingProvider.langSmith && ( + <> + <Field + label='API Key' + labelClassName='!text-sm' + isRequired + value={(config as LangSmithConfig).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 LangSmithConfig).project} + onChange={handleConfigChange('project')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} + /> + <Field + label='Endpoint' + labelClassName='!text-sm' + value={(config as LangSmithConfig).endpoint} + onChange={handleConfigChange('endpoint')} + placeholder={'https://api.smith.langchain.com'} + /> + </> + )} + {type === TracingProvider.langfuse && ( + <> + <Field + label={t(`${I18N_PREFIX}.secretKey`)!} + labelClassName='!text-sm' + value={(config as LangFuseConfig).secret_key} + isRequired + onChange={handleConfigChange('secret_key')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!} + /> + <Field + label={t(`${I18N_PREFIX}.publicKey`)!} + labelClassName='!text-sm' + isRequired + value={(config as LangFuseConfig).public_key} + onChange={handleConfigChange('public_key')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!} + /> + <Field + label='Host' + labelClassName='!text-sm' + isRequired + value={(config as LangFuseConfig).host} + onChange={handleConfigChange('host')} + placeholder='https://cloud.langfuse.com' + /> + </> + )} + + </div> + <div className='my-8 flex justify-between items-center h-8'> + <a + className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]' + target='_blank' + href={docURL[type]} + > + <span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span> + <LinkExternal02 className='w-3 h-3' /> + </a> + <div className='flex items-center'> + {isEdit && ( + <> + <Button + className='h-9 text-sm font-medium text-gray-700' + onClick={showRemoveConfirm} + > + <span className='text-[#D92D20]'>{t('common.operation.remove')}</span> + </Button> + <div className='mx-3 w-px h-[18px] bg-gray-200'></div> + </> + )} + <Button + className='mr-2 h-9 text-sm font-medium text-gray-700' + onClick={onCancel} + > + {t('common.operation.cancel')} + </Button> + <Button + className='h-9 text-sm font-medium' + variant='primary' + onClick={handleSave} + loading={isSaving} + > + {t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)} + </Button> + </div> + + </div> + </div> + <div className='border-t-[0.5px] border-t-black/5'> + <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'> + <Lock01 className='mr-1 w-3 h-3 text-gray-500' /> + {t('common.modelProvider.encrypted.front')} + <a + className='text-primary-600 mx-1' + target='_blank' rel='noopener noreferrer' + href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' + > + PKCS1_OAEP + </a> + {t('common.modelProvider.encrypted.back')} + </div> + </div> + </div> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) + : ( + <ConfirmUi + isShow + onClose={hideRemoveConfirm} + type='warning' + title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!} + content={t(`${I18N_PREFIX}.removeConfirmContent`)} + onConfirm={handleRemove} + onCancel={hideRemoveConfirm} + /> + )} + </> + ) +} +export default React.memo(ProviderConfigModal) 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 new file mode 100644 index 0000000000..54b211ab34 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx @@ -0,0 +1,77 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import { TracingProvider } from './type' +import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing' +import { Settings04 } from '@/app/components/base/icons/src/vender/line/general' + +const I18N_PREFIX = 'app.tracing' + +type Props = { + type: TracingProvider + readOnly: boolean + isChosen: boolean + onChoose: () => void + hasConfigured: boolean + onConfig: () => void +} + +const getIcon = (type: TracingProvider) => { + return ({ + [TracingProvider.langSmith]: LangsmithIconBig, + [TracingProvider.langfuse]: LangfuseIconBig, + })[type] +} + +const ProviderPanel: FC<Props> = ({ + type, + readOnly, + isChosen, + onChoose, + hasConfigured, + onConfig, +}) => { + const { t } = useTranslation() + const Icon = getIcon(type) + + const handleConfigBtnClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + onConfig() + }, [onConfig]) + + const handleChosen = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + if (isChosen || !hasConfigured || readOnly) + return + onChoose() + }, [hasConfigured, isChosen, onChoose, readOnly]) + return ( + <div + className={cn(isChosen ? 'border-primary-400' : 'border-transparent', !isChosen && hasConfigured && !readOnly && 'cursor-pointer', 'px-4 py-3 rounded-xl border-[1.5px] bg-gray-100')} + onClick={handleChosen} + > + <div className={'flex justify-between items-center space-x-1'}> + <div className='flex items-center'> + <Icon className='h-6' /> + {isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>} + </div> + {!readOnly && ( + <div + className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' + onClick={handleConfigBtnClick} + > + <Settings04 className='w-3 h-3' /> + <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div> + </div> + )} + + </div> + <div className='mt-2 leading-4 text-xs font-normal text-gray-500'> + {t(`${I18N_PREFIX}.${type}.description`)} + </div> + </div> + ) +} +export default React.memo(ProviderPanel) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx new file mode 100644 index 0000000000..9119deede8 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx @@ -0,0 +1,46 @@ +'use client' +import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import React, { useCallback } from 'react' +import TooltipPlus from '@/app/components/base/tooltip-plus' + +const I18N_PREFIX = 'app.tracing' + +type Props = { + isFold: boolean + onFoldChange: (isFold: boolean) => void +} + +const ToggleFoldBtn: FC<Props> = ({ + isFold, + onFoldChange, +}) => { + const { t } = useTranslation() + + const handleFoldChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => { + e.stopPropagation() + onFoldChange(!isFold) + }, [isFold, onFoldChange]) + return ( + // text-[0px] to hide spacing between tooltip elements + <div className='shrink-0 cursor-pointer text-[0px]' onClick={handleFoldChange}> + <TooltipPlus + popupContent={t(`${I18N_PREFIX}.${isFold ? 'expand' : 'collapse'}`)} + hideArrow + > + {isFold && ( + <div className='p-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5'> + <ChevronDoubleDownIcon className='w-4 h-4' /> + </div> + )} + {!isFold && ( + <div className='p-2 rounded-lg text-gray-500 border-[0.5px] border-gray-200 hover:text-gray-800 hover:bg-black/5'> + <ChevronDoubleDownIcon className='w-4 h-4 transform rotate-180' /> + </div> + )} + </TooltipPlus> + </div> + ) +} +export default React.memo(ToggleFoldBtn) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx new file mode 100644 index 0000000000..6eb324d923 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx @@ -0,0 +1,28 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from 'classnames' +import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing' + +type Props = { + className?: string + size: 'lg' | 'md' +} + +const sizeClassMap = { + lg: 'w-9 h-9 p-2 rounded-[10px]', + md: 'w-6 h-6 p-1 rounded-lg', +} + +const TracingIcon: FC<Props> = ({ + className, + size, +}) => { + const sizeClass = sizeClassMap[size] + return ( + <div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}> + <Icon className='w-full h-full' /> + </div> + ) +} +export default React.memo(TracingIcon) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts new file mode 100644 index 0000000000..e07cf37c9d --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts @@ -0,0 +1,16 @@ +export enum TracingProvider { + langSmith = 'langsmith', + langfuse = 'langfuse', +} + +export type LangSmithConfig = { + api_key: string + project: string + endpoint: string +} + +export type LangFuseConfig = { + public_key: string + secret_key: string + host: string +} diff --git a/web/app/(commonLayout)/datasets/DatasetCard.tsx b/web/app/(commonLayout)/datasets/DatasetCard.tsx index 8f13241424..df122bc298 100644 --- a/web/app/(commonLayout)/datasets/DatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/DatasetCard.tsx @@ -10,7 +10,7 @@ import { } from '@remixicon/react' import Confirm from '@/app/components/base/confirm' import { ToastContext } from '@/app/components/base/toast' -import { deleteDataset } from '@/service/datasets' +import { checkIsUsedInApp, deleteDataset } from '@/service/datasets' import type { DataSet } from '@/models/datasets' import Tooltip from '@/app/components/base/tooltip' import { Folder } from '@/app/components/base/icons/src/vender/solid/files' @@ -36,6 +36,19 @@ const DatasetCard = ({ const [showRenameModal, setShowRenameModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [confirmMessage, setConfirmMessage] = useState<string>('') + const detectIsUsedByApp = useCallback(async () => { + try { + const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id) + setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!) + } + catch (e: any) { + const res = await e.json() + notify({ type: 'error', message: res?.message || 'Unknown error' }) + } + + setShowConfirmDelete(true) + }, [dataset.id, notify, t]) const onConfirmDelete = useCallback(async () => { try { await deleteDataset(dataset.id) @@ -44,10 +57,9 @@ const DatasetCard = ({ onSuccess() } catch (e: any) { - notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` }) } setShowConfirmDelete(false) - }, [dataset.id]) + }, [dataset.id, notify, onSuccess, t]) const Operations = (props: HtmlContentProps) => { const onMouseLeave = async () => { @@ -63,7 +75,7 @@ const DatasetCard = ({ e.stopPropagation() props.onClick?.() e.preventDefault() - setShowConfirmDelete(true) + detectIsUsedByApp() } return ( <div className="relative w-full py-1" onMouseLeave={onMouseLeave}> @@ -159,7 +171,7 @@ const DatasetCard = ({ /> </div> </div> - <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/> + <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' /> <div className='!hidden group-hover:!flex shrink-0'> <CustomPopover htmlContent={<Operations />} @@ -194,7 +206,7 @@ const DatasetCard = ({ {showConfirmDelete && ( <Confirm title={t('dataset.deleteDatasetConfirmTitle')} - content={t('dataset.deleteDatasetConfirmContent')} + content={confirmMessage} isShow={showConfirmDelete} onClose={() => setShowConfirmDelete(false)} onConfirm={onConfirmDelete} diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 56b2e0da7d..640c40378f 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -1,11 +1,8 @@ 'use client' -import type { FC } from 'react' import React from 'react' - -import type { IMainProps } from '@/app/components/share/chat' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' -const Chat: FC<IMainProps> = () => { +const Chat = () => { return ( <ChatWithHistoryWrap /> ) diff --git a/web/app/(shareLayout)/chatbot/[token]/page.tsx b/web/app/(shareLayout)/chatbot/[token]/page.tsx index b78680c503..6196afecc4 100644 --- a/web/app/(shareLayout)/chatbot/[token]/page.tsx +++ b/web/app/(shareLayout)/chatbot/[token]/page.tsx @@ -1,87 +1,10 @@ 'use client' -import type { FC } from 'react' -import React, { useEffect } from 'react' -import cn from 'classnames' -import type { IMainProps } from '@/app/components/share/chat' +import React from 'react' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' -import Loading from '@/app/components/base/loading' -import { fetchSystemFeatures } from '@/service/share' -import LogoSite from '@/app/components/base/logo/logo-site' - -const Chatbot: FC<IMainProps> = () => { - const [isSSOEnforced, setIsSSOEnforced] = React.useState(true) - const [loading, setLoading] = React.useState(true) - - useEffect(() => { - fetchSystemFeatures().then((res) => { - setIsSSOEnforced(res.sso_enforced_for_web) - setLoading(false) - }) - }, []) +const Chatbot = () => { return ( - <> - { - loading - ? ( - <div className="flex items-center justify-center h-full" > - <div className={ - cn( - 'flex flex-col items-center w-full grow items-center justify-center', - 'px-6', - 'md:px-[108px]', - ) - }> - <Loading type='area' /> - </div> - </div > - ) - : ( - <> - {isSSOEnforced - ? ( - <div className={cn( - 'flex w-full min-h-screen', - 'sm:p-4 lg:p-8', - 'gap-x-20', - 'justify-center lg:justify-start', - )}> - <div className={ - cn( - 'flex w-full flex-col bg-white shadow rounded-2xl shrink-0', - 'space-between', - ) - }> - <div className='flex items-center justify-between p-6 w-full'> - <LogoSite /> - </div> - - <div className={ - cn( - 'flex flex-col items-center w-full grow items-center justify-center', - 'px-6', - 'md:px-[108px]', - ) - }> - <div className='flex flex-col md:w-[400px]'> - <div className="w-full mx-auto"> - <h2 className="text-[16px] font-bold text-gray-900"> - Warning: Chatbot is not available - </h2> - <p className="text-[16px] text-gray-600 mt-2"> - Because SSO is enforced. Please contact your administrator. - </p> - </div> - </div> - </div> - </div> - </div> - ) - : <EmbeddedChatbot /> - } - </> - )} - </> + <EmbeddedChatbot /> ) } diff --git a/web/app/(shareLayout)/completion/[token]/page.tsx b/web/app/(shareLayout)/completion/[token]/page.tsx index 28bbfa68da..e8bc9d79f5 100644 --- a/web/app/(shareLayout)/completion/[token]/page.tsx +++ b/web/app/(shareLayout)/completion/[token]/page.tsx @@ -1,13 +1,10 @@ -import type { FC } from 'react' import React from 'react' - -import type { IMainProps } from '@/app/components/share/chat' import Main from '@/app/components/share/text-generation' -const TextGeneration: FC<IMainProps> = () => { +const Completion = () => { return ( <Main /> ) } -export default React.memo(TextGeneration) +export default React.memo(Completion) diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index ebb83884d6..4394cef822 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -2,150 +2,99 @@ import cn from 'classnames' import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' +import React, { useEffect } from 'react' import Toast from '@/app/components/base/toast' -import Button from '@/app/components/base/button' import { fetchSystemFeatures, fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' -import LogoSite from '@/app/components/base/logo/logo-site' import { setAccessToken } from '@/app/components/share/utils' +import Loading from '@/app/components/base/loading' const WebSSOForm: FC = () => { const searchParams = useSearchParams() + const router = useRouter() const redirectUrl = searchParams.get('redirect_url') const tokenFromUrl = searchParams.get('web_sso_token') const message = searchParams.get('message') - const router = useRouter() - const { t } = useTranslation() + const showErrorToast = (message: string) => { + Toast.notify({ + type: 'error', + message, + }) + } - const [isLoading, setIsLoading] = useState(false) - const [protocol, setProtocol] = useState('') + const getAppCodeFromRedirectUrl = () => { + const appCode = redirectUrl?.split('/').pop() + if (!appCode) + return null - useEffect(() => { - const fetchFeaturesAndSetToken = async () => { - await fetchSystemFeatures().then((res) => { - setProtocol(res.sso_enforced_for_web_protocol) - }) + return appCode + } - // Callback from SSO, process token and redirect - if (tokenFromUrl && redirectUrl) { - const appCode = redirectUrl.split('/').pop() - if (!appCode) { - Toast.notify({ - type: 'error', - message: 'redirect url is invalid. App code is not found.', - }) - return - } + const processTokenAndRedirect = async () => { + const appCode = getAppCodeFromRedirectUrl() + if (!appCode || !tokenFromUrl || !redirectUrl) { + showErrorToast('redirect url or app code or token is invalid.') + return + } - await setAccessToken(appCode, tokenFromUrl) - router.push(redirectUrl) + await setAccessToken(appCode, tokenFromUrl) + router.push(redirectUrl) + } + + const handleSSOLogin = async (protocol: string) => { + const appCode = getAppCodeFromRedirectUrl() + if (!appCode || !redirectUrl) { + showErrorToast('redirect url or app code is invalid.') + return + } + + switch (protocol) { + case 'saml': { + const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) + router.push(samlRes.url) + break } - } - - fetchFeaturesAndSetToken() - - if (message) { - Toast.notify({ - type: 'error', - message, - }) - } - }, []) - - const handleSSOLogin = () => { - setIsLoading(true) - - if (!redirectUrl) { - Toast.notify({ - type: 'error', - message: 'redirect url is not found.', - }) - setIsLoading(false) - return - } - - const appCode = redirectUrl.split('/').pop() - if (!appCode) { - Toast.notify({ - type: 'error', - message: 'redirect url is invalid. App code is not found.', - }) - return - } - - if (protocol === 'saml') { - fetchWebSAMLSSOUrl(appCode, redirectUrl).then((res) => { - router.push(res.url) - }).finally(() => { - setIsLoading(false) - }) - } - else if (protocol === 'oidc') { - fetchWebOIDCSSOUrl(appCode, redirectUrl).then((res) => { - router.push(res.url) - }).finally(() => { - setIsLoading(false) - }) - } - else if (protocol === 'oauth2') { - fetchWebOAuth2SSOUrl(appCode, redirectUrl).then((res) => { - router.push(res.url) - }).finally(() => { - setIsLoading(false) - }) - } - else { - Toast.notify({ - type: 'error', - message: 'sso protocol is not supported.', - }) - setIsLoading(false) + case 'oidc': { + const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) + router.push(oidcRes.url) + break + } + case 'oauth2': { + const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) + router.push(oauth2Res.url) + break + } + default: + showErrorToast('SSO protocol is not supported.') } } - return ( - <div className={cn( - 'flex w-full min-h-screen', - 'sm:p-4 lg:p-8', - 'gap-x-20', - 'justify-center lg:justify-start', - )}> - <div className={ - cn( - 'flex w-full flex-col bg-white shadow rounded-2xl shrink-0', - 'space-between', - ) - }> - <div className='flex items-center justify-between p-6 w-full'> - <LogoSite /> - </div> + useEffect(() => { + const init = async () => { + const res = await fetchSystemFeatures() + const protocol = res.sso_enforced_for_web_protocol - <div className={ - cn( - 'flex flex-col items-center w-full grow items-center justify-center', - 'px-6', - 'md:px-[108px]', - ) - }> - <div className='flex flex-col md:w-[400px]'> - <div className="w-full mx-auto"> - <h2 className="text-[32px] font-bold text-gray-900">{t('login.pageTitle')}</h2> - </div> - <div className="w-full mx-auto mt-10"> - <Button - tabIndex={0} - variant='primary' - onClick={() => { handleSSOLogin() }} - disabled={isLoading} - className="w-full !text-sm" - >{t('login.sso')} - </Button> - </div> - </div> - </div> + if (message) { + showErrorToast(message) + return + } + + if (!tokenFromUrl) { + await handleSSOLogin(protocol) + return + } + + await processTokenAndRedirect() + } + + init() + }, [message, tokenFromUrl]) // Added dependencies to useEffect + + return ( + <div className="flex items-center justify-center h-full"> + <div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}> + <Loading type='area' /> </div> </div> ) diff --git a/web/app/(shareLayout)/workflow/[token]/page.tsx b/web/app/(shareLayout)/workflow/[token]/page.tsx index c1d7fa13a5..e93bc8c1af 100644 --- a/web/app/(shareLayout)/workflow/[token]/page.tsx +++ b/web/app/(shareLayout)/workflow/[token]/page.tsx @@ -1,13 +1,11 @@ -import type { FC } from 'react' import React from 'react' -import type { IMainProps } from '@/app/components/share/text-generation' import Main from '@/app/components/share/text-generation' -const TextGeneration: FC<IMainProps> = () => { +const Workflow = () => { return ( <Main isWorkflow /> ) } -export default React.memo(TextGeneration) +export default React.memo(Workflow) diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 89d5a93dbe..c2f3bfc9dd 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -27,6 +27,7 @@ import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTrave import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' +import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal' export type IAppInfoProps = { expand: boolean @@ -45,6 +46,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showSwitchTip, setShowSwitchTip] = useState<string>('') const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false) + const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false) const mutateApps = useContextSelector( AppsContext, @@ -295,9 +297,6 @@ const AppInfo = ({ expand }: IAppInfoProps) => { }}> <span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span> </div> - <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}> - <span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span> - </div> {(appDetail.mode === 'completion' || appDetail.mode === 'chat') && ( <> <Divider className="!my-1" /> @@ -315,6 +314,22 @@ const AppInfo = ({ expand }: IAppInfoProps) => { </> )} <Divider className="!my-1" /> + <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}> + <span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span> + </div> + { + (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && ( + <div + className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' + onClick={() => { + setOpen(false) + setShowImportDSLModal(true) + }}> + <span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span> + </div> + ) + } + <Divider className="!my-1" /> <div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => { setOpen(false) setShowConfirmDelete(true) @@ -388,6 +403,14 @@ const AppInfo = ({ expand }: IAppInfoProps) => { onCancel={() => setShowConfirmDelete(false)} /> )} + { + showImportDSLModal && ( + <UpdateDSLModal + onCancel={() => setShowImportDSLModal(false)} + onBackup={onExport} + /> + ) + } </div> </PortalToFollowElem> ) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index c330b4c270..d7e9856ce4 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -226,6 +226,7 @@ const AppPublisher = ({ </div> </PortalToFollowElemContent> <EmbeddedModal + siteInfo={appDetail?.site} isShow={embeddingModalOpen} onClose={() => setEmbeddingModalOpen(false)} appBaseUrl={appBaseURL} diff --git a/web/app/components/app/chat/answer/index.tsx b/web/app/components/app/chat/answer/index.tsx deleted file mode 100644 index 1ba033911a..0000000000 --- a/web/app/components/app/chat/answer/index.tsx +++ /dev/null @@ -1,428 +0,0 @@ -'use client' -import type { FC, ReactNode } from 'react' -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { UserCircleIcon } from '@heroicons/react/24/solid' -import cn from 'classnames' -import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem } from '../type' -import OperationBtn from '../operation' -import LoadingAnim from '../loading-anim' -import { RatingIcon } from '../icon-component' -import s from '../style.module.css' -import MoreInfo from '../more-info' -import CopyBtn from '../copy-btn' -import Thought from '../thought' -import Citation from '../citation' -import AudioBtn from '@/app/components/base/audio-btn' -import { randomString } from '@/utils' -import type { MessageRating } from '@/models/log' -import Tooltip from '@/app/components/base/tooltip' -import { Markdown } from '@/app/components/base/markdown' -import type { DataSet } from '@/models/datasets' -import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn' -import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' -import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' -import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' -import type { Emoji } from '@/app/components/tools/types' -import type { VisionFile } from '@/types/app' -import ImageGallery from '@/app/components/base/image-gallery' -import Log from '@/app/components/app/chat/log' - -const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => { - return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}> - {children} - </div> -} -export type IAnswerProps = { - item: IChatItem - index: number - feedbackDisabled: boolean - isHideFeedbackEdit: boolean - onQueryChange: (query: string) => void - onFeedback?: FeedbackFunc - displayScene: DisplayScene - isResponding?: boolean - answerIcon?: ReactNode - citation?: CitationItem[] - dataSets?: DataSet[] - isShowCitation?: boolean - isShowCitationHitInfo?: boolean - isShowTextToSpeech?: boolean - // Annotation props - supportAnnotation?: boolean - appId?: string - question: string - onAnnotationEdited?: (question: string, answer: string, index: number) => void - onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void - onAnnotationRemoved?: (index: number) => void - allToolIcons?: Record<string, string | Emoji> - isShowPromptLog?: boolean -} -// The component needs to maintain its own state to control whether to display input component -const Answer: FC<IAnswerProps> = ({ - item, - index, - onQueryChange, - feedbackDisabled = false, - isHideFeedbackEdit = false, - onFeedback, - displayScene = 'web', - isResponding, - answerIcon, - citation, - isShowCitation, - isShowCitationHitInfo = false, - isShowTextToSpeech, - supportAnnotation, - appId, - question, - onAnnotationEdited, - onAnnotationAdded, - onAnnotationRemoved, - allToolIcons, - isShowPromptLog, -}) => { - const { id, content, more, feedback, adminFeedback, annotation, agent_thoughts } = item - const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0 - const hasAnnotation = useMemo(() => !!annotation, [annotation]) - // const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation) - // const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '') - const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback) - // const { userProfile } = useContext(AppContext) - const { t } = useTranslation() - - const [isShowReplyModal, setIsShowReplyModal] = useState(false) - - /** - * Render feedback results (distinguish between users and administrators) - * User reviews cannot be cancelled in Console - * @param rating feedback result - * @param isUserFeedback Whether it is user's feedback - * @param isWebScene Whether it is web scene - * @returns comp - */ - const renderFeedbackRating = (rating: MessageRating | undefined, isUserFeedback = true, isWebScene = true) => { - if (!rating) - return null - - const isLike = rating === 'like' - const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200' - const UserSymbol = <UserCircleIcon className='absolute top-[-2px] left-[18px] w-3 h-3 rounded-lg text-gray-400 bg-white' /> - // The tooltip is always displayed, but the content is different for different scenarios. - return ( - <Tooltip - selector={`user-feedback-${randomString(16)}`} - content={((isWebScene || (!isUserFeedback && !isWebScene)) ? isLike ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree') : (!isWebScene && isUserFeedback) ? `${t('appDebug.operation.userAction')}${isLike ? t('appDebug.operation.agree') : t('appDebug.operation.disagree')}` : '') as string} - > - <div - className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${(!isWebScene && isUserFeedback) ? '!cursor-default' : ''}`} - style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }} - {...((isWebScene || (!isUserFeedback && !isWebScene)) - ? { - onClick: async () => { - const res = await onFeedback?.(id, { rating: null }) - if (res && !isWebScene) - setLocalAdminFeedback({ rating: null }) - }, - } - : {})} - > - <div className={`${ratingIconClassname} rounded-lg h-6 w-6 flex items-center justify-center`}> - <RatingIcon isLike={isLike} /> - </div> - {!isWebScene && isUserFeedback && UserSymbol} - </div> - </Tooltip> - ) - } - - /** - * Different scenarios have different operation items. - * @param isWebScene Whether it is web scene - * @returns comp - */ - const renderItemOperation = (isWebScene = true) => { - const userOperation = () => { - return feedback?.rating - ? null - : <div className='flex gap-1'> - <Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}> - {OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })} - </Tooltip> - <Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}> - {OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })} - </Tooltip> - </div> - } - - const adminOperation = () => { - return <div className='flex gap-1'> - {!localAdminFeedback?.rating && <> - <Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}> - {OperationBtn({ - innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, - onClick: async () => { - const res = await onFeedback?.(id, { rating: 'like' }) - if (res) - setLocalAdminFeedback({ rating: 'like' }) - }, - })} - </Tooltip> - <Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}> - {OperationBtn({ - innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, - onClick: async () => { - const res = await onFeedback?.(id, { rating: 'dislike' }) - if (res) - setLocalAdminFeedback({ rating: 'dislike' }) - }, - })} - </Tooltip> - </>} - </div> - } - - return ( - <div className={`${s.itemOperation} flex gap-2`}> - {isWebScene ? userOperation() : adminOperation()} - </div> - ) - } - - const getImgs = (list?: VisionFile[]) => { - if (!list) - return [] - return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant') - } - - const agentModeAnswer = ( - <div> - {agent_thoughts?.map((item, index) => ( - <div key={index}> - {item.thought && ( - <Markdown content={item.thought} /> - )} - {/* {item.tool} */} - {/* perhaps not use tool */} - {!!item.tool && ( - <Thought - thought={item} - allToolIcons={allToolIcons || {}} - isFinished={!!item.observation || !isResponding} - /> - )} - - {getImgs(item.message_files).length > 0 && ( - <ImageGallery srcs={getImgs(item.message_files).map(item => item.url)} /> - )} - </div> - ))} - </div> - ) - - const [containerWidth, setContainerWidth] = useState(0) - const [contentWidth, setContentWidth] = useState(0) - const containerRef = useRef<HTMLDivElement>(null) - const contentRef = useRef<HTMLDivElement>(null) - - const getContainerWidth = () => { - if (containerRef.current) - setContainerWidth(containerRef.current?.clientWidth + 24) - } - const getContentWidth = () => { - if (contentRef.current) - setContentWidth(contentRef.current?.clientWidth) - } - - useEffect(() => { - getContainerWidth() - }, []) - - useEffect(() => { - if (!isResponding) - getContentWidth() - }, [isResponding]) - - const operationWidth = useMemo(() => { - let width = 0 - if (!item.isOpeningStatement) - width += 28 - if (!item.isOpeningStatement && isShowPromptLog) - width += 102 + 8 - if (!item.isOpeningStatement && isShowTextToSpeech) - width += 33 - if (!item.isOpeningStatement && supportAnnotation) - width += 96 + 8 - if (!feedbackDisabled && !item.feedbackDisabled) - width += 60 + 8 - if (!feedbackDisabled && localAdminFeedback?.rating && !item.isOpeningStatement) - width += 60 + 8 - if (!feedbackDisabled && feedback?.rating && !item.isOpeningStatement) - width += 28 + 8 - return width - }, [item.isOpeningStatement, item.feedbackDisabled, isShowPromptLog, isShowTextToSpeech, supportAnnotation, feedbackDisabled, localAdminFeedback?.rating, feedback?.rating]) - - const positionRight = useMemo(() => operationWidth < containerWidth - contentWidth - 4, [operationWidth, containerWidth, contentWidth]) - - return ( - // data-id for debug the item message is right - <div key={id} data-id={id}> - <div className='flex items-start'> - { - answerIcon || ( - <div className={`${s.answerIcon} w-10 h-10 shrink-0`}> - {isResponding - && <div className={s.typeingIcon}> - <LoadingAnim type='avatar' /> - </div> - } - </div> - ) - } - <div ref={containerRef} className={cn(s.answerWrapWrap, 'chat-answer-container')}> - <div className={cn(s.answerWrap, 'group')}> - <div ref={contentRef} className={`${s.answer} relative text-sm text-gray-900`}> - <div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}> - {(isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) - ? ( - <div className='flex items-center justify-center w-6 h-5'> - <LoadingAnim type='text' /> - </div> - ) - : ( - <div> - {annotation?.logAnnotation && ( - <div className='mb-1'> - <div className='mb-3'> - {isAgentMode - ? (<div className='line-through !text-gray-400'>{agentModeAnswer}</div>) - : ( - <Markdown className='line-through !text-gray-400' content={content} /> - )} - </div> - <EditTitle title={t('appAnnotation.editBy', { - author: annotation?.logAnnotation.account?.name, - })} /> - </div> - )} - <div> - {annotation?.logAnnotation - ? ( - <Markdown content={annotation?.logAnnotation.content || ''} /> - ) - : (isAgentMode - ? agentModeAnswer - : ( - <Markdown content={content} /> - ))} - </div> - {(hasAnnotation && !annotation?.logAnnotation) && ( - <EditTitle className='mt-1' title={t('appAnnotation.editBy', { - author: annotation?.authorName, - })} /> - )} - {item.isOpeningStatement && item.suggestedQuestions && item.suggestedQuestions.filter(q => !!q && q.trim()).length > 0 && ( - <div className='flex flex-wrap'> - {item.suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => ( - <div - key={index} - className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer' - onClick={() => onQueryChange(question)} - > - {question} - </div>), - )} - </div> - )} - </div> - )} - { - !!citation?.length && isShowCitation && !isResponding && ( - <Citation data={citation} showHitInfo={isShowCitationHitInfo} /> - ) - } - </div> - {hasAnnotation && ( - <div - className={cn(s.hasAnnotationBtn, 'absolute -top-3.5 -right-3.5 box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7]')} - style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }} - > - <div className='p-1 rounded-lg bg-[#EEF4FF] '> - <MessageFast className='w-4 h-4' /> - </div> - </div> - )} - <div - className={cn( - 'absolute -top-3.5 flex justify-end gap-1', - positionRight ? '!top-[9px]' : '-right-3.5', - )} - style={positionRight ? { left: contentWidth + 8 } : {}} - > - {!item.isOpeningStatement && ( - <CopyBtn - value={content} - className={cn(s.copyBtn, 'mr-1')} - /> - )} - {((isShowPromptLog && !isResponding) || (!item.isOpeningStatement && isShowTextToSpeech)) && ( - <div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'> - {isShowPromptLog && !isResponding && ( - <Log logItem={item} /> - )} - {!item.isOpeningStatement && isShowTextToSpeech && ( - <> - <div className='mx-1 w-[1px] h-[14px] bg-gray-200'/> - <AudioBtn - value={content} - noCache={false} - className={cn(s.playBtn)} - /> - </> - )} - </div> - )} - {(!item.isOpeningStatement && supportAnnotation) && ( - <AnnotationCtrlBtn - appId={appId!} - messageId={id} - annotationId={annotation?.id || ''} - className={cn(s.annotationBtn, 'ml-1 shrink-0')} - cached={hasAnnotation} - query={question} - answer={content} - onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)} - onEdit={() => setIsShowReplyModal(true)} - onRemoved={() => onAnnotationRemoved!(index)} - /> - )} - - <EditReplyModal - isShow={isShowReplyModal} - onHide={() => setIsShowReplyModal(false)} - query={question} - answer={content} - onEdited={(editedQuery, editedAnswer) => onAnnotationEdited!(editedQuery, editedAnswer, index)} - onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded!(annotationId, authorName, editedQuery, editedAnswer, index)} - appId={appId!} - messageId={id} - annotationId={annotation?.id || ''} - createdAt={annotation?.created_at} - onRemove={() => { }} - /> - - {!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')} - {/* Admin feedback is displayed only in the background. */} - {!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)} - {/* User feedback must be displayed */} - {!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')} - </div> - </div> - {more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />} - </div> - </div> - </div> - </div> - ) -} -export default React.memo(Answer) diff --git a/web/app/components/app/chat/icon-component/index.tsx b/web/app/components/app/chat/icon-component/index.tsx deleted file mode 100644 index c35fb77855..0000000000 --- a/web/app/components/app/chat/icon-component/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { FC, SVGProps } from 'react' -import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' - -export const stopIcon = ( - <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path fillRule="evenodd" clipRule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" /> - </svg> -) - -export const OpeningStatementIcon = ({ className }: SVGProps<SVGElement>) => ( - <svg className={className} width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path fillRule="evenodd" clipRule="evenodd" d="M6.25002 1C3.62667 1 1.50002 3.12665 1.50002 5.75C1.50002 6.28 1.58702 6.79071 1.7479 7.26801C1.7762 7.35196 1.79285 7.40164 1.80368 7.43828L1.80722 7.45061L1.80535 7.45452C1.79249 7.48102 1.77339 7.51661 1.73766 7.58274L0.911727 9.11152C0.860537 9.20622 0.807123 9.30503 0.770392 9.39095C0.733879 9.47635 0.674738 9.63304 0.703838 9.81878C0.737949 10.0365 0.866092 10.2282 1.05423 10.343C1.21474 10.4409 1.38213 10.4461 1.475 10.4451C1.56844 10.444 1.68015 10.4324 1.78723 10.4213L4.36472 10.1549C4.406 10.1506 4.42758 10.1484 4.44339 10.1472L4.44542 10.147L4.45161 10.1492C4.47103 10.1562 4.49738 10.1663 4.54285 10.1838C5.07332 10.3882 5.64921 10.5 6.25002 10.5C8.87338 10.5 11 8.37335 11 5.75C11 3.12665 8.87338 1 6.25002 1ZM4.48481 4.29111C5.04844 3.81548 5.7986 3.9552 6.24846 4.47463C6.69831 3.9552 7.43879 3.82048 8.01211 4.29111C8.58544 4.76175 8.6551 5.562 8.21247 6.12453C7.93825 6.47305 7.24997 7.10957 6.76594 7.54348C6.58814 7.70286 6.49924 7.78255 6.39255 7.81466C6.30103 7.84221 6.19589 7.84221 6.10436 7.81466C5.99767 7.78255 5.90878 7.70286 5.73098 7.54348C5.24694 7.10957 4.55867 6.47305 4.28444 6.12453C3.84182 5.562 3.92117 4.76675 4.48481 4.29111Z" fill="#667085" /> - </svg> -) - -export const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => { - return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' /> -} - -export const EditIcon = ({ className }: SVGProps<SVGElement>) => { - return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}> - <path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" /> - </svg> -} - -export const EditIconSolid = ({ className }: SVGProps<SVGElement>) => { - return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}> - <path fillRule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" /> - <path fillRule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" /> - </svg> -} - -export const TryToAskIcon = ( - <svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M5.88889 0.683718C5.827 0.522805 5.67241 0.416626 5.5 0.416626C5.3276 0.416626 5.173 0.522805 5.11111 0.683718L4.27279 2.86334C4.14762 3.18877 4.10829 3.28255 4.05449 3.35821C4.00051 3.43413 3.93418 3.50047 3.85826 3.55445C3.78259 3.60825 3.68881 3.64758 3.36338 3.77275L1.18376 4.61106C1.02285 4.67295 0.916668 4.82755 0.916668 4.99996C0.916668 5.17236 1.02285 5.32696 1.18376 5.38885L3.36338 6.22717C3.68881 6.35234 3.78259 6.39167 3.85826 6.44547C3.93418 6.49945 4.00051 6.56578 4.05449 6.6417C4.10829 6.71737 4.14762 6.81115 4.27279 7.13658L5.11111 9.3162C5.173 9.47711 5.3276 9.58329 5.5 9.58329C5.67241 9.58329 5.82701 9.47711 5.8889 9.3162L6.72721 7.13658C6.85238 6.81115 6.89171 6.71737 6.94551 6.6417C6.99949 6.56578 7.06583 6.49945 7.14175 6.44547C7.21741 6.39167 7.31119 6.35234 7.63662 6.22717L9.81624 5.38885C9.97715 5.32696 10.0833 5.17236 10.0833 4.99996C10.0833 4.82755 9.97715 4.67295 9.81624 4.61106L7.63662 3.77275C7.31119 3.64758 7.21741 3.60825 7.14175 3.55445C7.06583 3.50047 6.99949 3.43413 6.94551 3.35821C6.89171 3.28255 6.85238 3.18877 6.72721 2.86334L5.88889 0.683718Z" fill="#667085" /> - </svg> -) - -export const ReplayIcon = ({ className }: SVGProps<SVGElement>) => ( - <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}> - <path d="M1.33301 6.66667C1.33301 6.66667 2.66966 4.84548 3.75556 3.75883C4.84147 2.67218 6.34207 2 7.99967 2C11.3134 2 13.9997 4.68629 13.9997 8C13.9997 11.3137 11.3134 14 7.99967 14C5.26428 14 2.95642 12.1695 2.23419 9.66667M1.33301 6.66667V2.66667M1.33301 6.66667H5.33301" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - </svg> -) diff --git a/web/app/components/app/chat/icons/answer.svg b/web/app/components/app/chat/icons/answer.svg deleted file mode 100644 index e983039306..0000000000 --- a/web/app/components/app/chat/icons/answer.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z" fill="#F3F4F6"/> -</svg> diff --git a/web/app/components/app/chat/icons/default-avatar.jpg b/web/app/components/app/chat/icons/default-avatar.jpg deleted file mode 100644 index 396d5dd291..0000000000 Binary files a/web/app/components/app/chat/icons/default-avatar.jpg and /dev/null differ diff --git a/web/app/components/app/chat/icons/edit.svg b/web/app/components/app/chat/icons/edit.svg deleted file mode 100644 index a922970b6c..0000000000 --- a/web/app/components/app/chat/icons/edit.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" /> -</svg> \ No newline at end of file diff --git a/web/app/components/app/chat/icons/question.svg b/web/app/components/app/chat/icons/question.svg deleted file mode 100644 index 39904f52a5..0000000000 --- a/web/app/components/app/chat/icons/question.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="#E1EFFE"/> -</svg> diff --git a/web/app/components/app/chat/icons/robot.svg b/web/app/components/app/chat/icons/robot.svg deleted file mode 100644 index a50c8886d1..0000000000 --- a/web/app/components/app/chat/icons/robot.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<rect width="40" height="40" rx="20" fill="#D5F5F6"/> -<path d="M11 28.76H29V10.76H11V28.76Z" fill="url(#pattern0)"/> -<defs> -<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1"> -<use xlink:href="#image0_135_973" transform="scale(0.00625)"/> -</pattern> -<image id="image0_135_973" width="160" height="160" xlink:href="data:image/png;base64,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"/> -</defs> -</svg> diff --git a/web/app/components/app/chat/icons/send-active.svg b/web/app/components/app/chat/icons/send-active.svg deleted file mode 100644 index 03d4734bc6..0000000000 --- a/web/app/components/app/chat/icons/send-active.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#1C64F2"/> -</svg> diff --git a/web/app/components/app/chat/icons/send.svg b/web/app/components/app/chat/icons/send.svg deleted file mode 100644 index e977ef95bb..0000000000 --- a/web/app/components/app/chat/icons/send.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#D1D5DB"/> -</svg> diff --git a/web/app/components/app/chat/icons/typing.svg b/web/app/components/app/chat/icons/typing.svg deleted file mode 100644 index 7b28f0ef78..0000000000 --- a/web/app/components/app/chat/icons/typing.svg +++ /dev/null @@ -1,19 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g filter="url(#filter0_d_2358_1380)"> -<rect x="2" y="1" width="16" height="16" rx="8" fill="white"/> -<path opacity="0.7" d="M13.5 9H13.505M14 9C14 9.13261 13.9473 9.25979 13.8536 9.35355C13.7598 9.44732 13.6326 9.5 13.5 9.5C13.3674 9.5 13.2402 9.44732 13.1464 9.35355C13.0527 9.25979 13 9.13261 13 9C13 8.86739 13.0527 8.74021 13.1464 8.64645C13.2402 8.55268 13.3674 8.5 13.5 8.5C13.6326 8.5 13.7598 8.55268 13.8536 8.64645C13.9473 8.74021 14 8.86739 14 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/> -<path opacity="0.6" d="M10 9H10.005M10.5 9C10.5 9.13261 10.4473 9.25979 10.3536 9.35355C10.2598 9.44732 10.1326 9.5 10 9.5C9.86739 9.5 9.74021 9.44732 9.64645 9.35355C9.55268 9.25979 9.5 9.13261 9.5 9C9.5 8.86739 9.55268 8.74021 9.64645 8.64645C9.74021 8.55268 9.86739 8.5 10 8.5C10.1326 8.5 10.2598 8.55268 10.3536 8.64645C10.4473 8.74021 10.5 8.86739 10.5 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/> -<path opacity="0.3" d="M6.5 9H6.505M7 9C7 9.13261 6.94732 9.25979 6.85355 9.35355C6.75979 9.44732 6.63261 9.5 6.5 9.5C6.36739 9.5 6.24021 9.44732 6.14645 9.35355C6.05268 9.25979 6 9.13261 6 9C6 8.86739 6.05268 8.74021 6.14645 8.64645C6.24021 8.55268 6.36739 8.5 6.5 8.5C6.63261 8.5 6.75979 8.55268 6.85355 8.64645C6.94732 8.74021 7 8.86739 7 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<filter id="filter0_d_2358_1380" x="0" y="0" width="20" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> -<feFlood flood-opacity="0" result="BackgroundImageFix"/> -<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> -<feOffset dy="1"/> -<feGaussianBlur stdDeviation="1"/> -<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/> -<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2358_1380"/> -<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2358_1380" result="shape"/> -</filter> -</defs> -</svg> diff --git a/web/app/components/app/chat/icons/user.svg b/web/app/components/app/chat/icons/user.svg deleted file mode 100644 index 556aaf7bfb..0000000000 --- a/web/app/components/app/chat/icons/user.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<rect width="40" height="40" rx="20" fill="white"/> -<rect width="40" height="40" rx="20" fill="url(#pattern0)"/> -<defs> -<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1"> -<use xlink:href="#image0_84_1144" transform="scale(0.00238095)"/> -</pattern> -<image id="image0_84_1144" width="420" height="420" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGkCAIAAADxLsZiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMC1jMDAwIDc5LjE3MWMyN2ZhYiwgMjAyMi8wOC8xNi0yMjozNTo0MSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmNjg5NjkyMy05NDA5LTRjODgtOTE3OC0zOTBmNDAzNWQzYjIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTI2RDk1MDg3REFEMTFFRDgxQUM4MDk5QUJENjI4QzIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTI2RDk1MDc3REFEMTFFRDgxQUM4MDk5QUJENjI4QzIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI0LjEgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YzQzOGNjNS03MDEzLTRmNDMtOTg0Mi02NmNkNDI5OTVkYmIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6ZjY4OTY5MjMtOTQwOS00Yzg4LTkxNzgtMzkwZjQwMzVkM2Iy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"/> -</defs> -</svg> diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx deleted file mode 100644 index d861ddb2de..0000000000 --- a/web/app/components/app/chat/index.tsx +++ /dev/null @@ -1,455 +0,0 @@ -'use client' -import type { FC, ReactNode } from 'react' -import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' -import Textarea from 'rc-textarea' -import { useContext } from 'use-context-selector' -import cn from 'classnames' -import Recorder from 'js-audio-recorder' -import { useTranslation } from 'react-i18next' -import s from './style.module.css' -import type { DisplayScene, FeedbackFunc, IChatItem } from './type' -import { TryToAskIcon, stopIcon } from './icon-component' -import Answer from './answer' -import Question from './question' -import TooltipPlus from '@/app/components/base/tooltip-plus' -import { ToastContext } from '@/app/components/base/toast' -import Button from '@/app/components/base/button' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import VoiceInput from '@/app/components/base/voice-input' -import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' -import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' -import type { DataSet } from '@/models/datasets' -import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader' -import ImageList from '@/app/components/base/image-uploader/image-list' -import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app' -import { useClipboardUploader, useDraggableUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks' -import type { Annotation } from '@/models/log' -import type { Emoji } from '@/app/components/tools/types' - -export type IChatProps = { - appId?: string - configElem?: React.ReactNode - chatList: IChatItem[] - onChatListChange?: (chatList: IChatItem[]) => void - controlChatUpdateAllConversation?: number - /** - * Whether to display the editing area and rating status - */ - feedbackDisabled?: boolean - /** - * Whether to display the input area - */ - isHideFeedbackEdit?: boolean - isHideSendInput?: boolean - onFeedback?: FeedbackFunc - checkCanSend?: () => boolean - query?: string - onQueryChange?: (query: string) => void - onSend?: (message: string, files: VisionFile[]) => void - displayScene?: DisplayScene - useCurrentUserAvatar?: boolean - isResponding?: boolean - canStopResponding?: boolean - abortResponding?: () => void - controlClearQuery?: number - controlFocus?: number - isShowSuggestion?: boolean - suggestionList?: string[] - isShowSpeechToText?: boolean - isShowTextToSpeech?: boolean - isShowCitation?: boolean - answerIcon?: ReactNode - isShowConfigElem?: boolean - dataSets?: DataSet[] - isShowCitationHitInfo?: boolean - isShowPromptLog?: boolean - visionConfig?: VisionSettings - supportAnnotation?: boolean - allToolIcons?: Record<string, string | Emoji> - customDisclaimer?: string -} - -const Chat: FC<IChatProps> = ({ - configElem, - chatList, - query = '', - onQueryChange = () => { }, - feedbackDisabled = false, - isHideFeedbackEdit = false, - isHideSendInput = false, - onFeedback, - checkCanSend, - onSend = () => { }, - displayScene, - useCurrentUserAvatar, - isResponding, - canStopResponding, - abortResponding, - controlClearQuery, - controlFocus, - isShowSuggestion, - suggestionList, - isShowSpeechToText, - isShowTextToSpeech, - isShowCitation, - answerIcon, - isShowConfigElem, - dataSets, - isShowCitationHitInfo, - isShowPromptLog, - visionConfig, - appId, - supportAnnotation, - onChatListChange, - allToolIcons, - customDisclaimer, -}) => { - const { t } = useTranslation() - const { notify } = useContext(ToastContext) - const { - files, - onUpload, - onRemove, - onReUpload, - onImageLinkLoadError, - onImageLinkLoadSuccess, - onClear, - } = useImageFiles() - const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files }) - const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader<HTMLTextAreaElement>({ onUpload, files, visionConfig }) - const isUseInputMethod = useRef(false) - - const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - const value = e.target.value - onQueryChange(value) - } - - const logError = (message: string) => { - notify({ type: 'error', message, duration: 3000 }) - } - - const valid = (q?: string) => { - const sendQuery = q || query - if (!sendQuery || sendQuery.trim() === '') { - logError('Message cannot be empty') - return false - } - return true - } - - useEffect(() => { - if (controlClearQuery) - onQueryChange('') - }, [controlClearQuery]) - - const handleSend = (q?: string) => { - if (!valid(q) || (checkCanSend && !checkCanSend())) - return - onSend(q || query, files.filter(file => file.progress !== -1).map(fileItem => ({ - type: 'image', - transfer_method: fileItem.type, - url: fileItem.url, - upload_file_id: fileItem.fileId, - }))) - if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) { - if (files.length) - onClear() - if (!isResponding) - onQueryChange('') - } - } - - const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { - if (e.code === 'Enter') { - e.preventDefault() - // prevent send message when using input method enter - if (!e.shiftKey && !isUseInputMethod.current) - handleSend() - } - } - - const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { - isUseInputMethod.current = e.nativeEvent.isComposing - if (e.code === 'Enter' && !e.shiftKey) { - onQueryChange(query.replace(/\n$/, '')) - e.preventDefault() - } - } - - const media = useBreakpoints() - const isMobile = media === MediaType.mobile - const sendBtn = <div className={cn(!(!query || query.trim() === '') && s.sendBtnActive, `${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`)} onClick={() => handleSend()}></div> - - const suggestionListRef = useRef<HTMLDivElement>(null) - const [hasScrollbar, setHasScrollbar] = useState(false) - useLayoutEffect(() => { - if (suggestionListRef.current) { - const listDom = suggestionListRef.current - const hasScrollbar = listDom.scrollWidth > listDom.clientWidth - setHasScrollbar(hasScrollbar) - } - }, [suggestionList]) - - const [voiceInputShow, setVoiceInputShow] = useState(false) - const handleVoiceInputShow = () => { - (Recorder as any).getPermission().then(() => { - setVoiceInputShow(true) - }, () => { - logError(t('common.voiceInput.notAllow')) - }) - } - const handleQueryChangeFromAnswer = useCallback((val: string) => { - onQueryChange(val) - handleSend(val) - }, []) - const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index - 1) { - return { - ...item, - content: query, - } - } - if (i === index) { - return { - ...item, - annotation: { - ...item.annotation, - logAnnotation: { - ...item.annotation?.logAnnotation, - content: answer, - }, - } as any, - } - } - return item - })) - }, [chatList]) - const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index - 1) { - return { - ...item, - content: query, - } - } - if (i === index) { - const answerItem = { - ...item, - content: item.content, - annotation: { - id: annotationId, - authorName, - logAnnotation: { - content: answer, - account: { - id: '', - name: authorName, - email: '', - }, - }, - } as Annotation, - } - return answerItem - } - return item - })) - }, [chatList]) - const handleAnnotationRemoved = useCallback((index: number) => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index) { - return { - ...item, - content: item.content, - annotation: undefined, - } - } - return item - })) - }, [chatList]) - - return ( - <div className={cn('px-3.5', 'h-full')}> - {isShowConfigElem && (configElem || null)} - {/* Chat List */} - <div className={cn((isShowConfigElem && configElem) ? 'h-0' : 'h-full', 'space-y-[30px]')}> - {chatList.map((item, index) => { - if (item.isAnswer) { - const isLast = item.id === chatList[chatList.length - 1].id - const citation = item.citation - return <Answer - key={item.id} - item={item} - index={index} - onQueryChange={handleQueryChangeFromAnswer} - feedbackDisabled={feedbackDisabled} - isHideFeedbackEdit={isHideFeedbackEdit} - onFeedback={onFeedback} - displayScene={displayScene ?? 'web'} - isResponding={isResponding && isLast} - answerIcon={answerIcon} - citation={citation} - dataSets={dataSets} - isShowCitation={isShowCitation} - isShowCitationHitInfo={isShowCitationHitInfo} - isShowTextToSpeech={isShowTextToSpeech} - supportAnnotation={supportAnnotation} - appId={appId} - question={chatList[index - 1]?.content} - onAnnotationEdited={handleAnnotationEdited} - onAnnotationAdded={handleAnnotationAdded} - onAnnotationRemoved={handleAnnotationRemoved} - allToolIcons={allToolIcons} - isShowPromptLog={isShowPromptLog} - /> - } - return ( - <Question - key={item.id} - id={item.id} - content={item.content} - more={item.more} - useCurrentUserAvatar={useCurrentUserAvatar} - item={item} - isShowPromptLog={isShowPromptLog} - isResponding={isResponding} - /> - ) - })} - </div> - {!isHideSendInput && ( - <div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}> - {/* Thinking is sync and can not be stopped */} - {(isResponding && canStopResponding && ((!!chatList[chatList.length - 1]?.content) || (chatList[chatList.length - 1]?.agent_thoughts && chatList[chatList.length - 1].agent_thoughts!.length > 0))) && ( - <div className='flex justify-center mb-4'> - <Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponding?.()}> - {stopIcon} - <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span> - </Button> - </div> - )} - {isShowSuggestion && ( - <div className='pt-2'> - <div className='flex items-center justify-center mb-2.5'> - <div className='grow h-[1px]' - style={{ - background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)', - }}></div> - <div className='shrink-0 flex items-center px-3 space-x-1'> - {TryToAskIcon} - <span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span> - </div> - <div className='grow h-[1px]' - style={{ - background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)', - }}></div> - </div> - {/* has scrollbar would hide part of first item */} - <div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}> - {suggestionList?.map((item, index) => ( - <div key={item} className='shrink-0 flex justify-center mr-2'> - <Button - key={index} - onClick={() => onQueryChange(item)} - > - <span className='text-primary-600 text-xs font-medium'>{item}</span> - </Button> - </div> - ))} - </div> - </div> - )} - <div className='relative'> - <div className={cn('relative p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto', isDragActive && 'border-primary-600')}> - {visionConfig?.enabled && ( - <> - <div className='absolute bottom-2 left-2 flex items-center'> - <ChatImageUploader - settings={visionConfig} - onUpload={onUpload} - disabled={files.length >= visionConfig.number_limits} - /> - <div className='mx-1 w-[1px] h-4 bg-black/5' /> - </div> - <div className='pl-[52px]'> - <ImageList - list={files} - onRemove={onRemove} - onReUpload={onReUpload} - onImageLinkLoadSuccess={onImageLinkLoadSuccess} - onImageLinkLoadError={onImageLinkLoadError} - /> - </div> - </> - )} - <Textarea - className={` - block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none - ${visionConfig?.enabled && 'pl-12'} - `} - value={query} - onChange={handleContentChange} - onKeyUp={handleKeyUp} - onKeyDown={handleKeyDown} - onPaste={onPaste} - onDragEnter={onDragEnter} - onDragLeave={onDragLeave} - onDragOver={onDragOver} - onDrop={onDrop} - autoSize - /> - </div> - <div className="absolute bottom-2 right-2 flex items-center h-8"> - <div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div> - { - query - ? ( - <div className='flex justify-center items-center w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => onQueryChange('')}> - <XCircle className='w-4 h-4 text-[#98A2B3]' /> - </div> - ) - : isShowSpeechToText - ? ( - <div - className='group flex justify-center items-center w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer' - onClick={handleVoiceInputShow} - > - <Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' /> - <Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' /> - </div> - ) - : null - } - <div className='mx-2 w-[1px] h-4 bg-black opacity-5' /> - {isMobile - ? sendBtn - : ( - <TooltipPlus - popupContent={ - <div> - <div>{t('common.operation.send')} Enter</div> - <div>{t('common.operation.lineBreak')} Shift Enter</div> - </div> - } - > - {sendBtn} - </TooltipPlus> - )} - </div> - {voiceInputShow && ( - <VoiceInput - onCancel={() => setVoiceInputShow(false)} - onConverted={text => onQueryChange(text)} - /> - )} - </div> - {customDisclaimer && <div className='text-xs text-gray-500 mt-1 text-center'> - {customDisclaimer} - </div>} - </div> - )} - </div> - ) -} -export default React.memo(Chat) diff --git a/web/app/components/app/chat/more-info/index.tsx b/web/app/components/app/chat/more-info/index.tsx deleted file mode 100644 index ff4f99e517..0000000000 --- a/web/app/components/app/chat/more-info/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import { useTranslation } from 'react-i18next' -import type { MessageMore } from '../type' -import { formatNumber } from '@/utils/format' - -export type IMoreInfoProps = { - more: MessageMore - isQuestion: boolean - className?: string -} - -const MoreInfo: FC<IMoreInfoProps> = ({ more, isQuestion, className }) => { - const { t } = useTranslation() - return (<div className={`mt-1 w-full text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'pl-2 text-left float-right'} ${className}`}> - <span className='mr-2'>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span> - <span className='mr-2'>{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}</span> - <span className='mr-2'>·</span> - <span>{more.time}</span> - </div>) -} -export default React.memo(MoreInfo) diff --git a/web/app/components/app/chat/operation/index.tsx b/web/app/components/app/chat/operation/index.tsx deleted file mode 100644 index 9ff1226b0c..0000000000 --- a/web/app/components/app/chat/operation/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' -import React from 'react' - -const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => ( - <div - className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`} - style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }} - onClick={onClick && onClick} - > - {innerContent} - </div> -) - -export default OperationBtn diff --git a/web/app/components/app/chat/question/index.tsx b/web/app/components/app/chat/question/index.tsx deleted file mode 100644 index 248c7496f0..0000000000 --- a/web/app/components/app/chat/question/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useRef } from 'react' -import { useContext } from 'use-context-selector' -import s from '../style.module.css' -import type { IChatItem } from '../type' -import MoreInfo from '../more-info' -import AppContext from '@/context/app-context' -import { Markdown } from '@/app/components/base/markdown' -import ImageGallery from '@/app/components/base/image-gallery' - -type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & { - isShowPromptLog?: boolean - item: IChatItem - isResponding?: boolean -} - -const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item }) => { - const { userProfile } = useContext(AppContext) - const userName = userProfile?.name - const ref = useRef(null) - const imgSrcs = item.message_files?.map(item => item.url) - - return ( - <div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}> - <div className={s.questionWrapWrap}> - - <div className={`${s.question} group relative text-sm text-gray-900`}> - <div - className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'} - > - {imgSrcs && imgSrcs.length > 0 && ( - <ImageGallery srcs={imgSrcs} /> - )} - <Markdown content={content} /> - </div> - </div> - {more && <MoreInfo more={more} isQuestion={true} />} - </div> - {useCurrentUserAvatar - ? ( - <div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'> - {userName?.[0].toLocaleUpperCase()} - </div> - ) - : ( - <div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div> - )} - </div> - ) -} -export default React.memo(Question) diff --git a/web/app/components/app/chat/style.module.css b/web/app/components/app/chat/style.module.css deleted file mode 100644 index b0c8aba524..0000000000 --- a/web/app/components/app/chat/style.module.css +++ /dev/null @@ -1,136 +0,0 @@ -.answerIcon { - position: relative; - background: url(./icons/robot.svg) 100%/100%; -} - -.typeingIcon { - position: absolute; - top: 0px; - left: 0px; - display: flex; - justify-content: center; - align-items: center; - width: 16px; - height: 16px; - background: #FFFFFF; - box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); - border-radius: 16px; -} - - -.questionIcon { - background: url(./icons/default-avatar.jpg); - background-size: contain; - border-radius: 50%; -} - -.answer::before, -.question::before { - content: ''; - position: absolute; - top: 0; - width: 8px; - height: 12px; -} - -.answer::before { - left: 0; - background: url(./icons/answer.svg) no-repeat; -} - -.copyBtn, -.playBtn, -.annotationBtn { - display: none; -} - -pre:hover .copyBtn { - display: block; -} - -.answerWrapWrap, -.questionWrapWrap { - width: 0; - flex-grow: 1; -} - -.questionWrapWrap { - display: flex; - justify-content: flex-end; -} - -.question { - display: inline-block; - max-width: 100%; -} - -.answer { - display: inline-block; - max-width: 100%; -} - -.answerWrap:hover .copyBtn, -.answerWrap:hover .playBtn, -.answerWrap:hover .annotationBtn { - display: block; -} - -.answerWrap:hover .hasAnnotationBtn { - display: none; -} - -.answerWrap .itemOperation { - display: none; -} - -.answerWrap:hover .itemOperation { - display: flex; -} - -.question::before { - right: 0; - background: url(./icons/question.svg) no-repeat; -} - -.textArea { - padding-top: 13px; - padding-bottom: 13px; - padding-right: 130px; - border-radius: 12px; - line-height: 20px; - background-color: #fff; -} - -.textArea:hover { - background-color: #fff; -} - -/* .textArea:focus { - box-shadow: 0px 3px 15px -3px rgba(0, 0, 0, 0.1), 0px 4px 6px rgba(0, 0, 0, 0.05); -} */ - -.count { - /* display: none; */ - padding: 0 2px; -} - -.sendBtn { - background: url(./icons/send.svg) center center no-repeat; -} - -.sendBtnActive { - background-image: url(./icons/send-active.svg); -} - -.sendBtn:hover { - background-image: url(./icons/send-active.svg); - background-color: #EBF5FF; -} - -.textArea:focus+div .count { - display: block; -} - -.textArea:focus+div .sendBtn { - background-image: url(./icons/send-active.svg); -} diff --git a/web/app/components/app/chat/thought/style.module.css b/web/app/components/app/chat/thought/style.module.css deleted file mode 100644 index 2b6aa266d7..0000000000 --- a/web/app/components/app/chat/thought/style.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.wrap { - background-color: rgba(255, 255, 255, 0.92); -} - -.wrapHoverEffect:hover{ - box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.1); -} \ No newline at end of file diff --git a/web/app/components/app/configuration/dataset-config/card-item/item.tsx b/web/app/components/app/configuration/dataset-config/card-item/item.tsx index 2b3f04d2d3..b9f1ced38e 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/item.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/item.tsx @@ -10,6 +10,7 @@ import { formatNumber } from '@/utils/format' import FileIcon from '@/app/components/base/file-icon' import { Settings01 } from '@/app/components/base/icons/src/vender/line/general' import { Folder } from '@/app/components/base/icons/src/vender/solid/files' +import { Globe06 } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' import Drawer from '@/app/components/base/drawer' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -54,6 +55,13 @@ const Item: FC<ItemProps> = ({ </div> ) } + { + config.data_source_type === DataSourceType.WEB && ( + <div className='shrink-0 flex items-center justify-center mr-2 w-6 h-6 bg-[#F5FAFF] border-[0.5px] border-blue-100 rounded-md'> + <Globe06 className='w-4 h-4 text-blue-600' /> + </div> + ) + } <div className='grow'> <div className='flex items-center h-[18px]'> <div className='grow text-[13px] font-medium text-gray-800 truncate' title={config.name}>{config.name}</div> 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 266d3dce94..d87138506a 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 @@ -99,7 +99,10 @@ const SettingsModal: FC<SettingsModalProps> = ({ description, permission, indexing_technique: indexMethod, - retrieval_model: postRetrievalConfig, + retrieval_model: { + ...postRetrievalConfig, + score_threshold: postRetrievalConfig.score_threshold_enabled ? postRetrievalConfig.score_threshold : 0, + }, embedding_model: localeCurrentDataset.embedding_model, embedding_model_provider: localeCurrentDataset.embedding_model_provider, }, @@ -229,7 +232,7 @@ const SettingsModal: FC<SettingsModalProps> = ({ <div> <div>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='leading-[18px] text-xs font-normal text-gray-500'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/features/retrieval-augment' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.description')} </div> </div> diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx index 39a063182e..e79cdf4793 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx @@ -20,7 +20,7 @@ import type { OnSend } from '@/app/components/base/chat/types' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useProviderContext } from '@/context/provider-context' import { - fetchConvesationMessages, + fetchConversationMessages, fetchSuggestedQuestions, stopChatMessageResponding, } from '@/service/debug' @@ -89,7 +89,7 @@ const ChatItem: FC<ChatItemProps> = ({ `apps/${appId}/chat-messages`, data, { - onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController), + onGetConvesationMessages: (conversationId, getAbortController) => fetchConversationMessages(appId, conversationId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), }, ) @@ -128,6 +128,7 @@ const ChatItem: FC<ChatItemProps> = ({ showPromptLog questionIcon={<Avatar name={userProfile.name} size={40} />} allToolIcons={allToolIcons} + hideLogModal /> ) } diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx index 9ffc7ad0ea..ea15c1a4ce 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx @@ -15,7 +15,7 @@ import { useDebugConfigurationContext } from '@/context/debug-configuration' import type { OnSend } from '@/app/components/base/chat/types' import { useProviderContext } from '@/context/provider-context' import { - fetchConvesationMessages, + fetchConversationMessages, fetchSuggestedQuestions, stopChatMessageResponding, } from '@/service/debug' @@ -94,7 +94,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi `apps/${appId}/chat-messages`, data, { - onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController), + onGetConvesationMessages: (conversationId, getAbortController) => fetchConversationMessages(appId, conversationId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), }, ) diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index dd89cf5368..ff476246fe 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -39,6 +39,7 @@ import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useProviderContext } from '@/context/provider-context' +import AgentLogModal from '@/app/components/base/agent-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' @@ -370,11 +371,13 @@ const Debug: FC<IDebug> = ({ handleVisionConfigInMultipleModel() }, [multipleModelConfigs, mode]) - const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore(useShallow(state => ({ + const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, showPromptLogModal: state.showPromptLogModal, setShowPromptLogModal: state.setShowPromptLogModal, + showAgentLogModal: state.showAgentLogModal, + setShowAgentLogModal: state.setShowAgentLogModal, }))) const [width, setWidth] = useState(0) const ref = useRef<HTMLDivElement>(null) @@ -434,13 +437,33 @@ const Debug: FC<IDebug> = ({ </div> { debugWithMultipleModel && ( - <div className='grow mt-3 overflow-hidden'> + <div className='grow mt-3 overflow-hidden' ref={ref}> <DebugWithMultipleModel multipleModelConfigs={multipleModelConfigs} onMultipleModelConfigsChange={onMultipleModelConfigsChange} onDebugWithMultipleModelChange={handleChangeToSingleModel} checkCanSend={checkCanSend} /> + {showPromptLogModal && ( + <PromptLogModal + width={width} + currentLogItem={currentLogItem} + onCancel={() => { + setCurrentLogItem() + setShowPromptLogModal(false) + }} + /> + )} + {showAgentLogModal && ( + <AgentLogModal + width={width} + currentLogItem={currentLogItem} + onCancel={() => { + setCurrentLogItem() + setShowAgentLogModal(false) + }} + /> + )} </div> ) } @@ -474,6 +497,7 @@ const Debug: FC<IDebug> = ({ supportAnnotation appId={appId} varList={varList} + siteInfo={null} /> )} </div> diff --git a/web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx b/web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx index 6327035599..b9c2ab3629 100644 --- a/web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx +++ b/web/app/components/app/configuration/features/chat-group/opening-statement/index.tsx @@ -20,6 +20,7 @@ import { getInputKeys } from '@/app/components/base/block-input' import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var' import { getNewVar } from '@/utils/var' import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight' +import Toast from '@/app/components/base/toast' const MAX_QUESTION_NUM = 5 @@ -93,6 +94,15 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({ } const handleConfirm = () => { + if (!(tempValue || '').trim()) { + Toast.notify({ + type: 'error', + message: t('common.errorMsg.fieldRequired', { + field: t('appDebug.openingStatement.title'), + }), + }) + return + } const keys = getInputKeys(tempValue) const promptKeys = promptVariables.map(item => item.key) let notIncludeKeys: string[] = [] diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index 3aff1fd212..39c50d3ba8 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -15,11 +15,13 @@ import Button from '@/app/components/base/button' export type Props = { file: File | undefined updateFile: (file?: File) => void + className?: string } const Uploader: FC<Props> = ({ file, updateFile, + className, }) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) @@ -83,7 +85,7 @@ const Uploader: FC<Props> = ({ }, []) return ( - <div className='mt-6'> + <div className={cn('mt-6', className)}> <input ref={fileUploader} style={{ display: 'none' }} diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index c33067ad18..b1c398dc0b 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import useSWR from 'swr' import { HandThumbDownIcon, @@ -8,6 +8,7 @@ import { InformationCircleIcon, XMarkIcon, } from '@heroicons/react/24/outline' +import { RiEditFill } from '@remixicon/react' import { get } from 'lodash-es' import InfiniteScroll from 'react-infinite-scroll-component' import dayjs from 'dayjs' @@ -20,14 +21,13 @@ import cn from 'classnames' import s from './style.module.css' import VarPanel from './var-panel' import { randomString } from '@/utils' -import { EditIconSolid } from '@/app/components/app/chat/icon-component' -import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type' -import type { ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log' +import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type' +import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log' import type { App } from '@/types/app' import Loading from '@/app/components/base/loading' import Drawer from '@/app/components/base/drawer' import Popover from '@/app/components/base/popover' -import Chat from '@/app/components/app/chat' +import Chat from '@/app/components/base/chat/chat' import Tooltip from '@/app/components/base/tooltip' import { ToastContext } from '@/app/components/base/toast' import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' @@ -38,8 +38,6 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import TextGeneration from '@/app/components/app/text-generate/item' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' -import AgentLogModal from '@/app/components/base/agent-log-modal' -import PromptLogModal from '@/app/components/base/prompt-log-modal' import MessageLogModal from '@/app/components/base/message-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' import { useAppContext } from '@/context/app-context' @@ -166,13 +164,9 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo const { userProfile: { timezone } } = useAppContext() const { formatTime } = useTimestamp() const { onClose, appDetail } = useContext(DrawerContext) - const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ + const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, - showPromptLogModal: state.showPromptLogModal, - setShowPromptLogModal: state.setShowPromptLogModal, - showAgentLogModal: state.showAgentLogModal, - setShowAgentLogModal: state.setShowAgentLogModal, showMessageLogModal: state.showMessageLogModal, setShowMessageLogModal: state.setShowMessageLogModal, currentLogModalActiveTab: state.currentLogModalActiveTab, @@ -218,6 +212,72 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo } } + const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { + setItems(items.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + return { + ...item, + annotation: { + ...item.annotation, + logAnnotation: { + ...item.annotation?.logAnnotation, + content: answer, + }, + } as any, + } + } + return item + })) + }, [items]) + const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { + setItems(items.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + const answerItem = { + ...item, + content: item.content, + annotation: { + id: annotationId, + authorName, + logAnnotation: { + content: answer, + account: { + id: '', + name: authorName, + email: '', + }, + }, + } as Annotation, + } + return answerItem + } + return item + })) + }, [items]) + const handleAnnotationRemoved = useCallback((index: number) => { + setItems(items.map((item, i) => { + if (i === index) { + return { + ...item, + content: item.content, + annotation: undefined, + } + } + return item + })) + }, [items]) + useEffect(() => { if (appDetail?.id && detail.id && appDetail?.mode !== 'completion') fetchData() @@ -374,24 +434,36 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo isShowTextToSpeech appId={appDetail?.id} varList={varList} + siteInfo={null} /> </div> : items.length < 8 - ? <div className="px-2.5 pt-4 mb-4"> + ? <div className="pt-4 mb-4"> <Chat + config={{ + appId: appDetail?.id, + text_to_speech: { + enabled: true, + }, + supportAnnotation: true, + annotation_reply: { + enabled: true, + }, + supportFeedback: true, + } as any} chatList={items} - isHideSendInput={true} + onAnnotationAdded={handleAnnotationAdded} + onAnnotationEdited={handleAnnotationEdited} + onAnnotationRemoved={handleAnnotationRemoved} onFeedback={onFeedback} - displayScene='console' - isShowPromptLog - supportAnnotation - isShowTextToSpeech - appId={appDetail?.id} - onChatListChange={setItems} + noChatInput + showPromptLog + hideProcessDetail + chatContainerInnerClassName='px-6' /> </div> : <div - className="px-2.5 py-4" + className="py-4" id="scrollableDiv" style={{ height: 1000, // Specify a value @@ -422,35 +494,30 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo inverse={true} > <Chat + config={{ + app_id: appDetail?.id, + text_to_speech: { + enabled: true, + }, + supportAnnotation: true, + annotation_reply: { + enabled: true, + }, + supportFeedback: true, + } as any} chatList={items} - isHideSendInput={true} + onAnnotationAdded={handleAnnotationAdded} + onAnnotationEdited={handleAnnotationEdited} + onAnnotationRemoved={handleAnnotationRemoved} onFeedback={onFeedback} - displayScene='console' - isShowPromptLog + noChatInput + showPromptLog + hideProcessDetail + chatContainerInnerClassName='px-6' /> </InfiniteScroll> </div> } - {showPromptLogModal && ( - <PromptLogModal - width={width} - currentLogItem={currentLogItem} - onCancel={() => { - setCurrentLogItem() - setShowPromptLogModal(false) - }} - /> - )} - {showAgentLogModal && ( - <AgentLogModal - width={width} - currentLogItem={currentLogItem} - onCancel={() => { - setCurrentLogItem() - setShowAgentLogModal(false) - }} - /> - )} {showMessageLogModal && ( <MessageLogModal width={width} @@ -575,7 +642,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) <Tooltip htmlContent={ <span className='text-xs text-gray-500 inline-flex items-center'> - <EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} + <RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} </span> } className={(isHighlight && !isChatMode) ? '' : '!hidden'} diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 820c240603..4f5f93f22b 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -247,12 +247,14 @@ function AppCard({ ? ( <> <SettingsModal + isChat={appMode === 'chat'} appInfo={appInfo} isShow={showSettingsModal} onClose={() => setShowSettingsModal(false)} onSave={onSaveSiteConfig} /> <EmbeddedModal + siteInfo={appInfo.site} isShow={showEmbedded} onClose={() => setShowEmbedded(false)} appBaseUrl={app_base_url} diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index f9e9e999e6..f16fc81f16 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -4,12 +4,15 @@ import cn from 'classnames' import copy from 'copy-to-clipboard' import style from './style.module.css' import Modal from '@/app/components/base/modal' -import copyStyle from '@/app/components/app/chat/copy-btn/style.module.css' +import copyStyle from '@/app/components/base/copy-btn/style.module.css' import Tooltip from '@/app/components/base/tooltip' import { useAppContext } from '@/context/app-context' import { IS_CE_EDITION } from '@/config' +import type { SiteInfo } from '@/models/share' +import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' type Props = { + siteInfo?: SiteInfo isShow: boolean onClose: () => void accessToken: string @@ -28,7 +31,7 @@ const OPTION_MAP = { </iframe>`, }, scripts: { - getContent: (url: string, token: string, isTestEnv?: boolean) => + getContent: (url: string, token: string, primaryColor: string, isTestEnv?: boolean) => `<script> window.difyChatbotConfig = { token: '${token}'${isTestEnv @@ -44,7 +47,12 @@ const OPTION_MAP = { src="${url}/embed.min.js" id="${token}" defer> -</script>`, +</script> +<style> + #dify-chatbot-bubble-button { + background-color: ${primaryColor} !important; + } +</style>`, }, chromePlugin: { getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`, @@ -60,12 +68,14 @@ type OptionStatus = { chromePlugin: boolean } -const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props) => { +const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, className }: Props) => { const { t } = useTranslation() const [option, setOption] = useState<Option>('iframe') const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false }) const { langeniusVersionInfo } = useAppContext() + const themeBuilder = useThemeContext() + themeBuilder.buildTheme(siteInfo?.chat_color_theme ?? null, siteInfo?.chat_color_theme_inverted ?? false) const isTestEnv = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT' const onClickCopy = () => { if (option === 'chromePlugin') { @@ -74,7 +84,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props copy(splitUrl[1]) } else { - copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)) + copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)) } setIsCopied({ ...isCopied, [option]: true }) } @@ -154,7 +164,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props </div> <div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto"> <div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono"> - <pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)}</pre> + <pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre> </div> </div> </div> diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 6994d9c280..fabffcf809 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -17,6 +17,7 @@ import { useToastContext } from '@/app/components/base/toast' import { languages } from '@/i18n/language' export type ISettingsModalProps = { + isChat: boolean appInfo: AppDetailResponse isShow: boolean defaultValue?: string @@ -28,6 +29,8 @@ export type ConfigParams = { title: string description: string default_language: string + chat_color_theme: string + chat_color_theme_inverted: boolean prompt_public: boolean copyright: string privacy_policy: string @@ -40,6 +43,7 @@ export type ConfigParams = { const prefixSettings = 'appOverview.overview.appInfo.settings' const SettingsModal: FC<ISettingsModalProps> = ({ + isChat, appInfo, isShow = false, onClose, @@ -48,8 +52,27 @@ const SettingsModal: FC<ISettingsModalProps> = ({ const { notify } = useToastContext() const [isShowMore, setIsShowMore] = useState(false) const { icon, icon_background } = appInfo - const { title, description, copyright, privacy_policy, custom_disclaimer, default_language, show_workflow_steps } = appInfo.site - const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps }) + const { + title, + description, + chat_color_theme, + chat_color_theme_inverted, + copyright, + privacy_policy, + custom_disclaimer, + default_language, + show_workflow_steps, + } = appInfo.site + const [inputInfo, setInputInfo] = useState({ + title, + desc: description, + chatColorTheme: chat_color_theme, + chatColorThemeInverted: chat_color_theme_inverted, + copyright, + privacyPolicy: privacy_policy, + customDisclaimer: custom_disclaimer, + show_workflow_steps, + }) const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() @@ -58,7 +81,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({ const [emoji, setEmoji] = useState({ icon, icon_background }) useEffect(() => { - setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps }) + setInputInfo({ + title, + desc: description, + chatColorTheme: chat_color_theme, + chatColorThemeInverted: chat_color_theme_inverted, + copyright, + privacyPolicy: privacy_policy, + customDisclaimer: custom_disclaimer, + show_workflow_steps, + }) setLanguage(default_language) setEmoji({ icon, icon_background }) }, [appInfo]) @@ -75,11 +107,30 @@ const SettingsModal: FC<ISettingsModalProps> = ({ notify({ type: 'error', message: t('app.newApp.nameNotEmpty') }) return } + + const validateColorHex = (hex: string | null) => { + if (hex === null || hex.length === 0) + return true + + const regex = /#([A-Fa-f0-9]{6})/ + const check = regex.test(hex) + return check + } + + if (inputInfo !== null) { + if (!validateColorHex(inputInfo.chatColorTheme)) { + notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`) }) + return + } + } + setSaveLoading(true) const params = { title: inputInfo.title, description: inputInfo.desc, default_language: language, + chat_color_theme: inputInfo.chatColorTheme, + chat_color_theme_inverted: inputInfo.chatColorThemeInverted, prompt_public: false, copyright: inputInfo.copyright, privacy_policy: inputInfo.privacyPolicy, @@ -95,7 +146,13 @@ const SettingsModal: FC<ISettingsModalProps> = ({ const onChange = (field: string) => { return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { - setInputInfo(item => ({ ...item, [field]: e.target.value })) + let value: string | boolean + if (e.target.type === 'checkbox') + value = (e.target as HTMLInputElement).checked + else + value = e.target.value + + setInputInfo(item => ({ ...item, [field]: value })) } } @@ -144,6 +201,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({ onSelect={item => setInputInfo({ ...inputInfo, show_workflow_steps: item.value === 'true' })} /> </>} + {isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div> + <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p> + <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`} + value={inputInfo.chatColorTheme ?? ''} + onChange={onChange('chatColorTheme')} + placeholder= 'E.g #A020F0' + /> + </>} {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}> <div className='flex justify-between'> <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div> diff --git a/web/app/components/app/store.ts b/web/app/components/app/store.ts index eb7ad62f77..a89b96d65d 100644 --- a/web/app/components/app/store.ts +++ b/web/app/components/app/store.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import type { App } from '@/types/app' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' type State = { appDetail?: App diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 486ab90edb..79880630e3 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -16,7 +16,7 @@ import { Markdown } from '@/app/components/base/markdown' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' import AudioBtn from '@/app/components/base/audio-btn' -import type { Feedbacktype } from '@/app/components/app/chat/type' +import type { Feedbacktype } from '@/app/components/base/chat/chat/type' import { fetchMoreLikeThis, updateFeedback } from '@/service/share' import { File02 } from '@/app/components/base/icons/src/vender/line/files' import { Bookmark } from '@/app/components/base/icons/src/vender/line/general' diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index b9258a4092..2b4f77f5a2 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -12,7 +12,7 @@ import Loading from '@/app/components/base/loading' import { fetchAgentLogDetail } from '@/service/log' import type { AgentIteration, AgentLogDetailResponse } from '@/models/log' import { useStore as useAppStore } from '@/app/components/app/store' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' export type AgentLogDetailProps = { activeTab?: 'DETAIL' | 'TRACING' diff --git a/web/app/components/base/agent-log-modal/index.tsx b/web/app/components/base/agent-log-modal/index.tsx index b74029c722..b63266bd08 100644 --- a/web/app/components/base/agent-log-modal/index.tsx +++ b/web/app/components/base/agent-log-modal/index.tsx @@ -5,7 +5,7 @@ import { RiCloseLine } from '@remixicon/react' import { useEffect, useRef, useState } from 'react' import { useClickAway } from 'ahooks' import AgentLogDetail from './detail' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' type AgentLogModalProps = { currentLogItem?: IChatItem diff --git a/web/app/components/base/button/index.tsx b/web/app/components/base/button/index.tsx index ee73eba481..ca14f6debb 100644 --- a/web/app/components/base/button/index.tsx +++ b/web/app/components/base/button/index.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import React from 'react' import { type VariantProps, cva } from 'class-variance-authority' import classNames from 'classnames' @@ -29,15 +30,17 @@ const buttonVariants = cva( export type ButtonProps = { loading?: boolean + styleCss?: CSSProperties } & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants> const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, loading, children, ...props }, ref) => { + ({ className, variant, size, loading, styleCss, children, ...props }, ref) => { return ( <button type='button' className={classNames(buttonVariants({ variant, size, className }))} ref={ref} + style={styleCss} {...props} > {children} diff --git a/web/app/components/base/chat/chat-with-history/config-panel/index.tsx b/web/app/components/base/chat/chat-with-history/config-panel/index.tsx index 6a3fe5d7e9..6bee44b8ef 100644 --- a/web/app/components/base/chat/chat-with-history/config-panel/index.tsx +++ b/web/app/components/base/chat/chat-with-history/config-panel/index.tsx @@ -7,7 +7,7 @@ import AppIcon from '@/app/components/base/app-icon' import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication' import { Edit02 } from '@/app/components/base/icons/src/vender/line/general' import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes' -import { FootLogo } from '@/app/components/share/chat/welcome/massive-component' +import LogoSite from '@/app/components/base/logo/logo-site' const ConfigPanel = () => { const { t } = useTranslation() @@ -153,7 +153,7 @@ const ConfigPanel = () => { { customConfig?.replace_webapp_logo ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' /> - : <FootLogo /> + : <LogoSite className='!h-5' /> } </div> </div> diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index 4f7298bfb9..f589878711 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -10,7 +10,7 @@ import Button from '@/app/components/base/button' import { Edit05 } from '@/app/components/base/icons/src/vender/line/general' import type { ConversationItem } from '@/models/share' import Confirm from '@/app/components/base/confirm' -import RenameModal from '@/app/components/share/chat/sidebar/rename-modal' +import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal' const Sidebar = () => { const { t } = useTranslation() diff --git a/web/app/components/share/chat/sidebar/rename-modal/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx similarity index 100% rename from web/app/components/share/chat/sidebar/rename-modal/index.tsx rename to web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx diff --git a/web/app/components/base/chat/chat/answer/agent-content.tsx b/web/app/components/base/chat/chat/answer/agent-content.tsx index b605060615..7ac6fcf84e 100644 --- a/web/app/components/base/chat/chat/answer/agent-content.tsx +++ b/web/app/components/base/chat/chat/answer/agent-content.tsx @@ -5,7 +5,7 @@ import type { VisionFile, } from '../../types' import { Markdown } from '@/app/components/base/markdown' -import Thought from '@/app/components/app/chat/thought' +import Thought from '@/app/components/base/chat/chat/thought' import ImageGallery from '@/app/components/base/image-gallery' import type { Emoji } from '@/app/components/tools/types' diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index 213ac99e28..3e6b07083f 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -16,8 +16,8 @@ import More from './more' import WorkflowProcess from './workflow-process' import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/general' import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' -import LoadingAnim from '@/app/components/app/chat/loading-anim' -import Citation from '@/app/components/app/chat/citation' +import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' +import Citation from '@/app/components/base/chat/chat/citation' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import type { Emoji } from '@/app/components/tools/types' import type { AppData } from '@/models/share' diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index b63b7077b8..5b45557eb0 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -8,7 +8,7 @@ import cn from 'classnames' import { useTranslation } from 'react-i18next' import type { ChatItem } from '../../types' import { useChatContext } from '../context' -import CopyBtn from '@/app/components/app/chat/copy-btn' +import CopyBtn from '@/app/components/base/copy-btn' import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' import AudioBtn from '@/app/components/base/audio-btn' import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn' @@ -18,7 +18,7 @@ import { ThumbsUp, } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' import TooltipPlus from '@/app/components/base/tooltip-plus' -import Log from '@/app/components/app/chat/log' +import Log from '@/app/components/base/chat/chat/log' type OperationProps = { item: ChatItem diff --git a/web/app/components/base/chat/chat/chat-input.tsx b/web/app/components/base/chat/chat/chat-input.tsx index 17e316062d..e5a99be065 100644 --- a/web/app/components/base/chat/chat/chat-input.tsx +++ b/web/app/components/base/chat/chat/chat-input.tsx @@ -15,6 +15,8 @@ import type { } from '../types' import { TransferMethod } from '../types' import { useChatWithHistoryContext } from '../chat-with-history/context' +import type { Theme } from '../embedded-chatbot/theme/theme-context' +import { CssTransform } from '../embedded-chatbot/theme/utils' import TooltipPlus from '@/app/components/base/tooltip-plus' import { ToastContext } from '@/app/components/base/toast' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -35,11 +37,13 @@ type ChatInputProps = { visionConfig?: VisionConfig speechToTextConfig?: EnableType onSend?: OnSend + theme?: Theme | null } const ChatInput: FC<ChatInputProps> = ({ visionConfig, speechToTextConfig, onSend, + theme, }) => { const { appData } = useChatWithHistoryContext() const { t } = useTranslation() @@ -112,14 +116,25 @@ const ChatInput: FC<ChatInputProps> = ({ }) } + const [isActiveIconFocused, setActiveIconFocused] = useState(false) + const media = useBreakpoints() const isMobile = media === MediaType.mobile + const sendIconThemeStyle = theme + ? { + color: (isActiveIconFocused || query || (query.trim() !== '')) ? theme.primaryColor : '#d1d5db', + } + : {} const sendBtn = ( <div className='group flex items-center justify-center w-8 h-8 rounded-lg hover:bg-[#EBF5FF] cursor-pointer' + onMouseEnter={() => setActiveIconFocused(true)} + onMouseLeave={() => setActiveIconFocused(false)} onClick={handleSend} + style={isActiveIconFocused ? CssTransform(theme?.chatBubbleColorStyle ?? '') : {}} > <Send03 + style={sendIconThemeStyle} className={` w-5 h-5 text-gray-300 group-hover:text-primary-600 ${!!query.trim() && 'text-primary-600'} diff --git a/web/app/components/app/chat/citation/index.tsx b/web/app/components/base/chat/chat/citation/index.tsx similarity index 100% rename from web/app/components/app/chat/citation/index.tsx rename to web/app/components/base/chat/chat/citation/index.tsx diff --git a/web/app/components/app/chat/citation/popup.tsx b/web/app/components/base/chat/chat/citation/popup.tsx similarity index 100% rename from web/app/components/app/chat/citation/popup.tsx rename to web/app/components/base/chat/chat/citation/popup.tsx diff --git a/web/app/components/app/chat/citation/progress-tooltip.tsx b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx similarity index 100% rename from web/app/components/app/chat/citation/progress-tooltip.tsx rename to web/app/components/base/chat/chat/citation/progress-tooltip.tsx diff --git a/web/app/components/app/chat/citation/tooltip.tsx b/web/app/components/base/chat/chat/citation/tooltip.tsx similarity index 100% rename from web/app/components/app/chat/citation/tooltip.tsx rename to web/app/components/base/chat/chat/citation/tooltip.tsx diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index 208d752253..489cb920fb 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -19,6 +19,7 @@ import type { Feedback, OnSend, } from '../types' +import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context' import Question from './question' import Answer from './answer' import ChatInput from './chat-input' @@ -57,7 +58,10 @@ export type ChatProps = { onFeedback?: (messageId: string, feedback: Feedback) => void chatAnswerContainerInner?: string hideProcessDetail?: boolean + hideLogModal?: boolean + themeBuilder?: ThemeBuilder } + const Chat: FC<ChatProps> = ({ appData, config, @@ -83,6 +87,8 @@ const Chat: FC<ChatProps> = ({ onFeedback, chatAnswerContainerInner, hideProcessDetail, + hideLogModal, + themeBuilder, }) => { const { t } = useTranslation() const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ @@ -219,6 +225,7 @@ const Chat: FC<ChatProps> = ({ key={item.id} item={item} questionIcon={questionIcon} + theme={themeBuilder?.theme} /> ) }) @@ -260,12 +267,13 @@ const Chat: FC<ChatProps> = ({ visionConfig={config?.file_upload?.image} speechToTextConfig={config?.speech_to_text} onSend={onSend} + theme={themeBuilder?.theme} /> ) } </div> </div> - {showPromptLogModal && ( + {showPromptLogModal && !hideLogModal && ( <PromptLogModal width={width} currentLogItem={currentLogItem} @@ -275,7 +283,7 @@ const Chat: FC<ChatProps> = ({ }} /> )} - {showAgentLogModal && ( + {showAgentLogModal && !hideLogModal && ( <AgentLogModal width={width} currentLogItem={currentLogItem} diff --git a/web/app/components/app/chat/loading-anim/index.tsx b/web/app/components/base/chat/chat/loading-anim/index.tsx similarity index 100% rename from web/app/components/app/chat/loading-anim/index.tsx rename to web/app/components/base/chat/chat/loading-anim/index.tsx diff --git a/web/app/components/app/chat/loading-anim/style.module.css b/web/app/components/base/chat/chat/loading-anim/style.module.css similarity index 100% rename from web/app/components/app/chat/loading-anim/style.module.css rename to web/app/components/base/chat/chat/loading-anim/style.module.css diff --git a/web/app/components/app/chat/log/index.tsx b/web/app/components/base/chat/chat/log/index.tsx similarity index 95% rename from web/app/components/app/chat/log/index.tsx rename to web/app/components/base/chat/chat/log/index.tsx index c4111eb2e1..c2b976164e 100644 --- a/web/app/components/app/chat/log/index.tsx +++ b/web/app/components/base/chat/chat/log/index.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { File02 } from '@/app/components/base/icons/src/vender/line/files' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' import { useStore as useAppStore } from '@/app/components/app/store' type LogProps = { diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx index f8eaad1ce6..3122857628 100644 --- a/web/app/components/base/chat/chat/question.tsx +++ b/web/app/components/base/chat/chat/question.tsx @@ -6,6 +6,8 @@ import { memo, } from 'react' import type { ChatItem } from '../types' +import type { Theme } from '../embedded-chatbot/theme/theme-context' +import { CssTransform } from '../embedded-chatbot/theme/utils' import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general' import { User } from '@/app/components/base/icons/src/public/avatar' import { Markdown } from '@/app/components/base/markdown' @@ -14,10 +16,12 @@ import ImageGallery from '@/app/components/base/image-gallery' type QuestionProps = { item: ChatItem questionIcon?: ReactNode + theme: Theme | null | undefined } const Question: FC<QuestionProps> = ({ item, questionIcon, + theme, }) => { const { content, @@ -25,12 +29,17 @@ const Question: FC<QuestionProps> = ({ } = item const imgSrcs = message_files?.length ? message_files.map(item => item.url) : [] - return ( <div className='flex justify-end mb-2 last:mb-0 pl-10'> <div className='group relative mr-4'> - <QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' /> - <div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'> + <QuestionTriangle + className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' + style={theme ? { color: theme.chatBubbleColor } : {}} + /> + <div + className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900' + style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}} + > { !!imgSrcs.length && ( <ImageGallery srcs={imgSrcs} /> diff --git a/web/app/components/app/chat/thought/index.tsx b/web/app/components/base/chat/chat/thought/index.tsx similarity index 96% rename from web/app/components/app/chat/thought/index.tsx rename to web/app/components/base/chat/chat/thought/index.tsx index e87d484ed4..10bc8394e5 100644 --- a/web/app/components/app/chat/thought/index.tsx +++ b/web/app/components/base/chat/chat/thought/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import React from 'react' import { useContext } from 'use-context-selector' import type { ThoughtItem, ToolInfoInThought } from '../type' -import Tool from '@/app/components/app/chat/thought/tool' +import Tool from '@/app/components/base/chat/chat/thought/tool' import type { Emoji } from '@/app/components/tools/types' import I18n from '@/context/i18n' diff --git a/web/app/components/app/chat/thought/panel.tsx b/web/app/components/base/chat/chat/thought/panel.tsx similarity index 100% rename from web/app/components/app/chat/thought/panel.tsx rename to web/app/components/base/chat/chat/thought/panel.tsx diff --git a/web/app/components/app/chat/thought/tool.tsx b/web/app/components/base/chat/chat/thought/tool.tsx similarity index 100% rename from web/app/components/app/chat/thought/tool.tsx rename to web/app/components/base/chat/chat/thought/tool.tsx diff --git a/web/app/components/app/chat/type.ts b/web/app/components/base/chat/chat/type.ts similarity index 95% rename from web/app/components/app/chat/type.ts rename to web/app/components/base/chat/chat/type.ts index 0deae18cd9..d1c2839e09 100644 --- a/web/app/components/app/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -1,4 +1,4 @@ -import type { TypeWithI18N } from '../../header/account-setting/model-provider-page/declarations' +import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Annotation, MessageRating } from '@/models/log' import type { VisionFile } from '@/types/app' 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 41dcbb8eb1..6b895ae319 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -32,6 +32,7 @@ const ChatWrapper = () => { appMeta, handleFeedback, currentChatInstanceRef, + themeBuilder, } = useEmbeddedChatbotContext() const appConfig = useMemo(() => { const config = appParams || {} @@ -130,6 +131,7 @@ const ChatWrapper = () => { suggestedQuestions={suggestedQuestions} answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null} hideProcessDetail + themeBuilder={themeBuilder} /> ) } diff --git a/web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx b/web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx index 4cd0c7cb29..b3e6c2c532 100644 --- a/web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx @@ -2,13 +2,15 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import cn from 'classnames' import { useEmbeddedChatbotContext } from '../context' +import { useThemeContext } from '../theme/theme-context' +import { CssTransform } from '../theme/utils' import Form from './form' import Button from '@/app/components/base/button' import AppIcon from '@/app/components/base/app-icon' import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication' import { Edit02 } from '@/app/components/base/icons/src/vender/line/general' import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes' -import { FootLogo } from '@/app/components/share/chat/welcome/massive-component' +import LogoSite from '@/app/components/base/logo/logo-site' const ConfigPanel = () => { const { t } = useTranslation() @@ -22,6 +24,7 @@ const ConfigPanel = () => { const [collapsed, setCollapsed] = useState(true) const customConfig = appData?.custom_config const site = appData?.site + const themeBuilder = useThemeContext() return ( <div className='flex flex-col max-h-[80%] w-full max-w-[720px]'> @@ -34,6 +37,7 @@ const ConfigPanel = () => { )} > <div + style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')} className={` flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25 ${isMobile && '!px-4 !py-3'} @@ -68,6 +72,7 @@ const ConfigPanel = () => { {t('share.chat.configStatusDes')} </div> <Button + styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')} variant='secondary-accent' size='small' className='shrink-0' @@ -96,6 +101,7 @@ const ConfigPanel = () => { <Form /> <div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}> <Button + styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')} variant='primary' className='mr-2' onClick={() => { @@ -119,6 +125,7 @@ const ConfigPanel = () => { <div className='p-6 rounded-b-xl'> <Form /> <Button + styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')} className={cn(inputsForms.length && !isMobile && 'ml-[136px]')} variant='primary' size='large' @@ -154,7 +161,7 @@ const ConfigPanel = () => { { customConfig?.replace_webapp_logo ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' /> - : <FootLogo /> + : <LogoSite className='!h-5' /> } </div> </div> diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index 6f54dd9403..933b357fea 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -7,6 +7,7 @@ import type { ChatItem, Feedback, } from '../types' +import type { ThemeBuilder } from './theme/theme-context' import type { AppConversationData, AppData, @@ -40,6 +41,7 @@ export type EmbeddedChatbotContextValue = { appId?: string handleFeedback: (messageId: string, feedback: Feedback) => void currentChatInstanceRef: RefObject<{ handleStop: () => void }> + themeBuilder?: ThemeBuilder } export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ diff --git a/web/app/components/base/chat/embedded-chatbot/header.tsx b/web/app/components/base/chat/embedded-chatbot/header.tsx index 45d917b8d3..c35f98e3f2 100644 --- a/web/app/components/base/chat/embedded-chatbot/header.tsx +++ b/web/app/components/base/chat/embedded-chatbot/header.tsx @@ -1,24 +1,23 @@ import type { FC } from 'react' import React from 'react' +import { RiRefreshLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' -// import AppIcon from '@/app/components/base/app-icon' -import { ReplayIcon } from '@/app/components/app/chat/icon-component' +import type { Theme } from './theme/theme-context' +import { CssTransform } from './theme/utils' import Tooltip from '@/app/components/base/tooltip' export type IHeaderProps = { isMobile?: boolean customerIcon?: React.ReactNode title: string - // icon: string - // icon_background: string + theme?: Theme onCreateNewChat?: () => void } const Header: FC<IHeaderProps> = ({ isMobile, customerIcon, title, - // icon, - // icon_background, + theme, onCreateNewChat, }) => { const { t } = useTranslation() @@ -28,14 +27,15 @@ const Header: FC<IHeaderProps> = ({ return ( <div className={` - shrink-0 flex items-center justify-between h-14 px-4 bg-gray-100 - bg-gradient-to-r from-blue-600 to-sky-500 + shrink-0 flex items-center justify-between h-14 px-4 `} + style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) } > <div className="flex items-center space-x-2"> {customerIcon} <div className={'text-sm font-bold text-white'} + style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')} > {title} </div> @@ -48,7 +48,7 @@ const Header: FC<IHeaderProps> = ({ <div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => { onCreateNewChat?.() }}> - <ReplayIcon className="h-4 w-4 text-sm font-bold text-white" /> + <RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" color={theme?.colorPathOnHeader}/> </div> </Tooltip> </div> diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx index a85ed41fa2..9f3de8d589 100644 --- a/web/app/components/base/chat/embedded-chatbot/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/index.tsx @@ -10,6 +10,7 @@ import { } from './context' import { useEmbeddedChatbot } from './hooks' import { isDify } from './utils' +import { useThemeContext } from './theme/theme-context' import { checkOrSetAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -29,6 +30,7 @@ const Chatbot = () => { showConfigPanelBeforeChat, appChatListDataLoading, handleNewConversation, + themeBuilder, } = useEmbeddedChatbotContext() const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length) @@ -38,6 +40,7 @@ const Chatbot = () => { const difyIcon = <LogoHeader /> useEffect(() => { + themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted) if (site) { if (customConfig) document.title = `${site.title}` @@ -63,6 +66,7 @@ const Chatbot = () => { isMobile={isMobile} title={site?.title || ''} customerIcon={isDify() ? difyIcon : ''} + theme={themeBuilder?.theme} onCreateNewChat={handleNewConversation} /> <div className='flex bg-white overflow-hidden'> @@ -87,6 +91,7 @@ const Chatbot = () => { const EmbeddedChatbotWrapper = () => { const media = useBreakpoints() const isMobile = media === MediaType.mobile + const themeBuilder = useThemeContext() const { appInfoError, @@ -141,6 +146,7 @@ const EmbeddedChatbotWrapper = () => { appId, handleFeedback, currentChatInstanceRef, + themeBuilder, }}> <Chatbot /> </EmbeddedChatbotContext.Provider> diff --git a/web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts b/web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts new file mode 100644 index 0000000000..fd424cd046 --- /dev/null +++ b/web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts @@ -0,0 +1,72 @@ +import { createContext, useContext } from 'use-context-selector' +import { hexToRGBA } from './utils' + +export class Theme { + public chatColorTheme: string | null + public chatColorThemeInverted: boolean + + public primaryColor = '#1C64F2' + public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)' + public headerBorderBottomStyle = '' + public colorFontOnHeaderStyle = 'color: white' + public colorPathOnHeader = 'white' + public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2' + public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)' + public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)' + public chatBubbleColor = 'rgb(225 239 254)' + + constructor(chatColorTheme: string | null = null, chatColorThemeInverted = false) { + this.chatColorTheme = chatColorTheme + this.chatColorThemeInverted = chatColorThemeInverted + this.configCustomColor() + this.configInvertedColor() + } + + private configCustomColor() { + if (this.chatColorTheme !== null && this.chatColorTheme !== '') { + this.primaryColor = this.chatColorTheme ?? '#1C64F2' + this.backgroundHeaderColorStyle = `backgroundColor: ${this.primaryColor}` + this.backgroundButtonDefaultColorStyle = `backgroundColor: ${this.primaryColor}` + this.roundedBackgroundColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.05)}` + this.chatBubbleColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.15)}` + this.chatBubbleColor = `${hexToRGBA(this.primaryColor, 0.15)}` + } + } + + private configInvertedColor() { + if (this.chatColorThemeInverted) { + this.backgroundHeaderColorStyle = 'backgroundColor: #ffffff' + this.colorFontOnHeaderStyle = `color: ${this.primaryColor}` + this.headerBorderBottomStyle = 'borderBottom: 1px solid #ccc' + this.colorPathOnHeader = this.primaryColor + } + } +} + +export class ThemeBuilder { + private _theme?: Theme + private buildChecker = false + + public get theme() { + if (this._theme === undefined) + throw new Error('The theme should be built first and then accessed') + else + return this._theme + } + + public buildTheme(chatColorTheme: string | null = null, chatColorThemeInverted = false) { + if (!this.buildChecker) { + this._theme = new Theme(chatColorTheme, chatColorThemeInverted) + this.buildChecker = true + } + else { + if (this.theme?.chatColorTheme !== chatColorTheme || this.theme?.chatColorThemeInverted !== chatColorThemeInverted) { + this._theme = new Theme(chatColorTheme, chatColorThemeInverted) + this.buildChecker = true + } + } + } +} + +const ThemeContext = createContext<ThemeBuilder>(new ThemeBuilder()) +export const useThemeContext = () => useContext(ThemeContext) diff --git a/web/app/components/base/chat/embedded-chatbot/theme/utils.ts b/web/app/components/base/chat/embedded-chatbot/theme/utils.ts new file mode 100644 index 0000000000..690d7a7608 --- /dev/null +++ b/web/app/components/base/chat/embedded-chatbot/theme/utils.ts @@ -0,0 +1,29 @@ +export function hexToRGBA(hex: string, opacity: number): string { + hex = hex.replace('#', '') + + const r = parseInt(hex.slice(0, 2), 16) + const g = parseInt(hex.slice(2, 4), 16) + const b = parseInt(hex.slice(4, 6), 16) + + // Returning an RGB color object + return `rgba(${r},${g},${b},${opacity.toString()})` +} + +/** + * Since strings cannot be directly assigned to the 'style' attribute in JSX, + * this method transforms the string into an object representation of the styles. + */ +export function CssTransform(cssString: string): object { + if (cssString.length === 0) + return {} + + const style: object = {} + const propertyValuePairs = cssString.split(';') + for (const pair of propertyValuePairs) { + if (pair.trim().length > 0) { + const [property, value] = pair.split(':') + Object.assign(style, { [property.trim()]: value.trim() }) + } + } + return style +} diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index 61fa5c4fdc..baffe42843 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -3,7 +3,7 @@ import type { VisionFile, VisionSettings, } from '@/types/app' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { NodeTracing } from '@/types/workflow' import type { WorkflowRunningStatus } from '@/app/components/workflow/types' diff --git a/web/app/components/app/chat/copy-btn/index.tsx b/web/app/components/base/copy-btn/index.tsx similarity index 100% rename from web/app/components/app/chat/copy-btn/index.tsx rename to web/app/components/base/copy-btn/index.tsx diff --git a/web/app/components/app/chat/copy-btn/style.module.css b/web/app/components/base/copy-btn/style.module.css similarity index 100% rename from web/app/components/app/chat/copy-btn/style.module.css rename to web/app/components/base/copy-btn/style.module.css diff --git a/web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg b/web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg new file mode 100644 index 0000000000..6ce0c27a72 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg @@ -0,0 +1,32 @@ +<svg width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Clip path group"> +<mask id="mask0_20135_18315" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="144" height="24"> +<g id="clip0_823_291"> +<path id="Vector" d="M143.36 0H0V24H143.36V0Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask0_20135_18315)"> +<g id="Group"> +<path id="Vector_2" d="M31.9258 17.3144V5.5896H33.5612V15.7456H39.5908V17.3144H31.9258ZM41.5502 11.1713C41.8806 9.47033 43.3343 8.36391 45.1845 8.36391C47.3155 8.36391 48.7197 9.60244 48.7197 12.03V15.3492C48.7197 15.729 48.8849 15.8942 49.2483 15.8942H49.6283V17.3144H49.1657C48.1085 17.3144 47.4807 16.7529 47.3155 15.9768C46.9852 16.7859 45.9609 17.5125 44.5898 17.5125C42.8222 17.5125 41.4016 16.5878 41.4016 15.019C41.4016 13.2024 42.7396 12.6905 44.755 12.3107L47.1173 11.8483C47.1008 10.5107 46.3409 9.85015 45.168 9.85015C44.1768 9.85015 43.45 10.4446 43.2517 11.2868L41.5502 11.1713ZM43.0865 14.9859C43.1031 15.5969 43.6317 16.1089 44.7881 16.1089C46.1096 16.1089 47.1503 15.1841 47.1503 13.6153V13.2355L45.2671 13.5657C44.0447 13.7804 43.0865 13.8795 43.0865 14.9859ZM51.8189 8.56208H53.2892L53.3387 10.0318C53.8013 8.90887 54.7759 8.36391 55.9488 8.36391C57.8981 8.36391 58.8563 9.80061 58.8563 11.6832V17.3144H57.2539V12.096C57.2539 10.5437 56.7418 9.73455 55.5358 9.73455C54.2638 9.73455 53.4213 10.5437 53.4213 12.096V17.3144H51.8189V8.56208ZM64.8465 16.852C62.6824 16.852 61.1461 15.118 61.1461 12.6575C61.1461 10.1144 62.6824 8.36391 64.8465 8.36391C66.0193 8.36391 67.0436 8.99143 67.44 9.89969L67.4565 8.56208H68.9929V16.3566C68.9763 18.7511 67.4565 19.9896 65.1108 19.9896C63.1945 19.9896 61.8069 18.9823 61.3939 17.463L63.0789 17.3474C63.4258 18.107 64.1031 18.5529 65.1108 18.5529C66.5315 18.5529 67.3739 17.9089 67.3905 16.7034V15.3988C66.9444 16.241 65.8872 16.852 64.8465 16.852ZM62.8311 12.641C62.8311 14.2924 63.7066 15.4483 65.1438 15.4483C66.548 15.4483 67.407 14.2924 67.4235 12.641C67.4565 11.0061 66.581 9.85015 65.1438 9.85015C63.7066 9.85015 62.8311 11.0061 62.8311 12.641ZM73.4961 8.18226C73.4961 6.56391 74.3055 5.5896 76.0897 5.5896H78.5015V7.00978H76.0566C75.495 7.00978 75.0985 7.43914 75.0985 8.14923V9.12354H78.3859V10.5437H75.0985V17.3144H73.4961V10.5437H71.1999V9.12354H73.4961V8.18226ZM88.3571 17.3144H86.8373L86.8207 15.8942C86.3582 16.9841 85.4001 17.5125 84.2767 17.5125C82.3605 17.5125 81.4189 16.0758 81.4189 14.1933V8.56208H83.0212V13.7804C83.0212 15.3327 83.5334 16.1419 84.6897 16.1419C85.9287 16.1419 86.7547 15.3327 86.7547 13.7804V8.56208H88.3571V17.3144ZM96.7925 11.3034C96.6108 10.3786 95.7518 9.85015 94.7275 9.85015C93.885 9.85015 93.1086 10.263 93.1251 11.0722C93.1251 11.9474 94.1824 12.1456 95.1571 12.3933C96.8255 12.8061 98.494 13.4171 98.494 15.052C98.494 16.7694 96.842 17.5125 95.0249 17.5125C92.9765 17.5125 91.308 16.3566 91.1758 14.5401L92.8608 14.441C93.026 15.4153 93.9016 16.0263 95.0249 16.0263C95.9004 16.0263 96.809 15.7951 96.809 14.9694C96.809 14.1107 95.7022 13.9621 94.7275 13.7309C93.0756 13.3346 91.4402 12.7235 91.4402 11.1382C91.4402 9.37125 93.026 8.36391 94.8762 8.36391C96.7264 8.36391 98.1636 9.47033 98.4444 11.2043L96.7925 11.3034ZM100.817 12.9382C100.817 10.1474 102.419 8.36391 104.88 8.36391C106.846 8.36391 108.63 9.63547 108.763 12.7896V13.4006H102.518C102.65 15.052 103.493 16.0263 104.88 16.0263C105.756 16.0263 106.549 15.5144 106.929 14.6391L108.63 14.7878C108.135 16.4557 106.632 17.5125 104.88 17.5125C102.419 17.5125 100.817 15.729 100.817 12.9382ZM102.568 12.1125H107.011C106.78 10.4446 105.872 9.85015 104.88 9.85015C103.608 9.85015 102.799 10.6924 102.568 12.1125Z" fill="black"/> +<g id="Clip path group_2"> +<mask id="mask1_20135_18315" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="25" height="26"> +<g id="clip1_823_291"> +<path id="Vector_3" d="M24.5471 -0.0771484H0.308594V24.1529H24.5471V-0.0771484Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask1_20135_18315)"> +<g id="Group_2"> +<path id="Vector_4" d="M21.9423 16.8532C20.8746 18.2972 19.2396 19.2605 17.3946 19.4012C17.3372 19.4051 17.2797 19.4092 17.2215 19.4124C15.0582 19.5201 13.424 18.5334 12.3404 17.6174C10.4229 16.6027 8.92256 16.3471 8.04475 16.1261C7.4064 15.9654 6.84782 15.644 6.60842 15.4833C6.39617 15.3572 5.96925 15.0647 5.65086 14.5191C5.29416 13.9077 5.27342 13.3235 5.279 13.0752C5.46493 13.0504 5.70512 13.023 5.98601 13.0054C6.21582 12.9909 6.43845 12.9853 6.68823 12.9844C8.62571 12.982 10.1283 13.0768 11.2215 13.5622C11.8128 13.8241 12.361 14.1728 12.8493 14.5979C13.4774 15.1459 14.7613 16.012 16.3437 15.7164C16.5448 15.6786 16.7379 15.6287 16.9239 15.5677C17.4935 15.4857 18.4679 15.4198 19.6154 15.7243C20.7046 16.0136 21.4882 16.5134 21.9423 16.8532Z" fill="#0A60B5" stroke="black" stroke-width="2.56" stroke-miterlimit="10"/> +<path id="Vector_5" d="M21.8003 6.90944C21.319 6.28602 20.7339 5.69669 20.043 5.29456C19.3066 4.86626 18.367 4.49744 17.2306 4.3975C14.4541 4.15321 12.5557 5.69273 12.2099 5.98382C11.7796 6.38753 10.0582 7.81602 7.98609 8.60917C7.62509 8.73768 6.93016 9.0414 6.31253 9.71961C5.82401 10.2557 5.58492 10.8046 5.46777 11.1457C5.71483 11.1798 5.96985 11.2044 6.23284 11.2194C8.2419 11.3313 9.96813 11.3424 11.0974 10.7896C11.8433 10.4247 12.4976 9.90517 13.0618 9.29681C14.0787 8.19987 15.5618 7.71051 16.9118 8.04605C17.5318 8.13247 18.6117 8.19833 19.8605 7.81602C20.7228 7.55189 21.3652 7.21957 21.8003 6.90944Z" fill="#0A60B5" stroke="black" stroke-width="2.56" stroke-miterlimit="10"/> +<path id="Vector_6" d="M2.95884 7.37229C2.1886 6.97653 1.58732 6.52001 1.17721 6.16263C0.947963 5.96275 0.591797 6.12665 0.591797 6.43206V8.92893C0.591797 9.03766 0.640978 9.14079 0.725063 9.20796L1.74835 10.1922C1.80229 9.79638 1.9181 9.25834 2.17829 8.66347C2.4234 8.10227 2.71769 7.67288 2.95884 7.37229Z" fill="#0A60B5" stroke="black" stroke-width="2.56" stroke-miterlimit="10"/> +<path id="Vector_7" d="M19.4326 12.9446C19.5497 12.584 19.6146 12.2056 19.6243 11.8201C19.6323 11.5156 19.597 11.4018 19.5441 11.1069C20.0886 11.0155 20.7686 10.8664 21.3886 10.7061C22.0438 10.537 22.5282 10.3406 23.0727 10.145C23.1521 10.5257 23.2355 10.7109 23.2644 10.993C23.2916 11.2591 23.3085 11.5348 23.3132 11.8201C23.3277 12.7066 23.2194 13.5105 23.0526 14.215C22.5394 13.9488 21.9299 13.6732 21.2282 13.4311C20.5754 13.2051 19.9683 13.0512 19.4326 12.9446Z" fill="#0A60B5" stroke="black" stroke-width="2.56" stroke-miterlimit="10"/> +<path id="Vector_8" d="M1.15342 18.2166C0.918616 18.3871 0.591797 18.2191 0.591797 17.927V14.8605C0.591797 14.7676 0.627493 14.6796 0.689366 14.614C0.720303 14.5812 0.748859 14.5628 0.761552 14.5556L1.77532 14.1204C1.84988 14.5268 1.97284 15.0133 2.17829 15.5429C2.41864 16.1621 2.7042 16.6637 2.95884 17.0454C2.35676 17.4358 1.75469 17.8263 1.15342 18.2166Z" fill="#0A60B5" stroke="black" stroke-width="2.56" stroke-miterlimit="10"/> +<path id="Vector_9" fill-rule="evenodd" clip-rule="evenodd" d="M7.68233 4.81872C9.70993 4.71818 11.2261 5.56713 12.0077 6.13451C11.4161 6.65689 9.81509 7.91742 7.92157 8.64547C7.79386 8.69117 7.62441 8.7588 7.43136 8.85733C6.06118 9.53194 5.24643 10.8764 5.21006 12.3127C5.20395 12.5545 5.21629 12.7953 5.24827 13.032C5.24481 13.0325 5.24136 13.033 5.23794 13.0334C5.23236 13.2797 5.2531 13.8593 5.60958 14.4661C5.9278 15.0073 6.35446 15.2975 6.5666 15.4227C6.67187 15.4928 6.83891 15.5939 7.04732 15.6986C7.29214 15.829 7.56361 15.9383 7.86305 16.0229C7.90892 16.0362 7.95532 16.0488 8.00211 16.0605C8.11111 16.0877 8.22985 16.1159 8.35794 16.1463C9.26137 16.3607 10.6303 16.6854 12.3087 17.5673C12.4783 17.7095 12.6609 17.8529 12.8568 17.993C12.7973 18.0463 12.7472 18.0927 12.7067 18.1309C12.3606 18.4235 10.4609 19.9709 7.68233 19.7254C6.5451 19.625 5.60404 19.2542 4.86793 18.8238C3.62779 18.0991 2.69071 16.9526 2.17951 15.6109C1.85014 14.7459 1.56303 13.6274 1.5423 12.3111C1.52395 11.187 1.70419 10.1969 1.94983 9.37575C2.70188 6.86682 4.89504 5.0268 7.5093 4.82989L7.50941 4.82988C7.5668 4.82589 7.62418 4.82191 7.68233 4.81872ZM21.8835 16.7762C21.4284 16.4391 20.6476 15.947 19.5661 15.6619C18.4192 15.3597 17.4455 15.4251 16.8761 15.5064C16.6902 15.567 16.4972 15.6164 16.2963 15.6539C14.7148 15.9473 13.4316 15.0879 12.8039 14.5442C12.6819 14.4387 12.5561 14.338 12.4269 14.2422C12.8493 13.871 13.3137 13.5508 13.82 13.3021C14.9501 12.7473 16.6775 12.7584 18.6881 12.87C21.4586 13.0247 23.3726 14.3441 24.1367 14.95C24.222 15.0177 24.2707 15.1198 24.2707 15.2282V17.718C24.2707 18.0233 23.9125 18.1867 23.682 17.9867C23.2692 17.6292 22.6618 17.1721 21.8835 16.7762ZM13.0009 9.33672C12.8193 9.5334 12.6283 9.72087 12.4279 9.89693C12.8159 10.1847 13.2353 10.4283 13.6788 10.6234C14.7715 11.1049 16.2732 11.199 18.2095 11.1966C21.0495 11.1926 23.1406 10.1235 24.1 9.54153C24.206 9.47696 24.2707 9.36218 24.2707 9.23781V6.182C24.2707 5.89101 23.9421 5.72359 23.706 5.8934C23.1472 6.29546 22.3162 6.80863 21.2445 7.21229C20.8586 7.43971 20.3776 7.6719 19.8046 7.84826C18.5548 8.23249 17.4742 8.16632 16.8538 8.07944C15.5028 7.74222 14.0186 8.2341 13.0009 9.33672Z" fill="#E11312"/> +<path id="Vector_10" d="M12.0069 6.13459L12.21 6.36447L12.4968 6.11122L12.1871 5.88642L12.0069 6.13459ZM7.68154 4.8188L7.66636 4.51247L7.66557 4.51251L7.66477 4.51255L7.68154 4.8188ZM7.92078 8.64555L8.0241 8.9344L8.02755 8.93317L8.03092 8.93187L7.92078 8.64555ZM7.43056 8.85741L7.56612 9.13253L7.56811 9.13161L7.57008 9.13062L7.43056 8.85741ZM5.24747 13.0321L5.28765 13.3361L5.59271 13.2959L5.55152 12.991L5.24747 13.0321ZM5.23715 13.0335L5.1967 12.7295L4.93636 12.764L4.93041 13.0265L5.23715 13.0335ZM6.56581 15.4228L6.736 15.1676L6.729 15.1629L6.72175 15.1587L6.56581 15.4228ZM7.04653 15.6987L7.19079 15.428L7.18759 15.4263L7.18433 15.4247L7.04653 15.6987ZM7.86225 16.023L7.94762 15.7285L7.9467 15.7282L7.94571 15.7279L7.86225 16.023ZM12.3079 17.5673L12.5052 17.3324L12.4799 17.3112L12.4506 17.2958L12.3079 17.5673ZM12.856 17.9931L13.0606 18.2216L13.3458 17.9664L13.0346 17.7437L12.856 17.9931ZM12.7059 18.131L12.904 18.3652L12.9105 18.3598L12.9165 18.3541L12.7059 18.131ZM4.86713 18.8239L5.02207 18.5591L5.02197 18.559L4.86713 18.8239ZM1.5415 12.3112L1.84828 12.3064L1.84827 12.3062L1.5415 12.3112ZM1.94903 9.37583L1.65512 9.28773L1.65507 9.28796L1.94903 9.37583ZM7.5085 4.82997L7.48794 4.52395L7.48669 4.52403L7.48544 4.52413L7.5085 4.82997ZM7.50862 4.82996L7.52918 5.13598L7.52987 5.13593L7.50862 4.82996ZM19.5653 15.662L19.6435 15.3654H19.6435L19.5653 15.662ZM21.8827 16.7763L21.7001 17.0227L21.7207 17.038L21.7436 17.0496L21.8827 16.7763ZM16.8753 15.5065L16.8319 15.2028L16.8055 15.2066L16.7801 15.2149L16.8753 15.5065ZM16.2955 15.654L16.3515 15.9555H16.3517L16.2955 15.654ZM12.8031 14.5443L13.0041 14.3125L13.0038 14.3122L12.8031 14.5443ZM12.4261 14.2422L12.2236 14.0119L11.9383 14.2625L12.2434 14.4886L12.4261 14.2422ZM18.6873 12.8701L18.7044 12.5638H18.7042L18.6873 12.8701ZM24.1359 14.95L24.3267 14.7099L24.3266 14.7098L24.1359 14.95ZM23.6813 17.9868L23.8824 17.7552L23.8821 17.7549L23.6813 17.9868ZM12.4271 9.89701L12.2246 9.66667L11.9394 9.91717L12.2444 10.1434L12.4271 9.89701ZM13.678 10.6234L13.8018 10.3428L13.8016 10.3427L13.678 10.6234ZM18.2087 11.1967L18.2091 11.5034H18.2092L18.2087 11.1967ZM24.0992 9.54161L24.2584 9.80384L24.2588 9.80361L24.0992 9.54161ZM21.2437 7.21237L21.1355 6.92536L21.1107 6.9347L21.088 6.94815L21.2437 7.21237ZM16.853 8.07952L16.7786 8.37711L16.7943 8.38102L16.8104 8.38324L16.853 8.07952ZM12.1871 5.88642C11.3742 5.29623 9.7896 4.40718 7.66636 4.51247L7.69672 5.12514C9.62867 5.02934 11.0765 5.83819 11.8266 6.38275L12.1871 5.88642ZM8.03092 8.93187C9.97246 8.18534 11.605 6.89867 12.21 6.36447L11.8038 5.90471C11.2255 6.41528 9.65613 7.64973 7.81063 8.35932L8.03092 8.93187ZM7.57008 9.13062C7.74904 9.03922 7.90597 8.97657 8.0241 8.9344L7.81746 8.35679C7.68016 8.40586 7.49818 8.47855 7.29104 8.58429L7.57008 9.13062ZM5.51598 12.3205C5.54953 10.9957 6.30059 9.75569 7.56612 9.13253L7.29499 8.5823C5.82017 9.30843 4.94173 10.7573 4.90255 12.3051L5.51598 12.3205ZM5.55152 12.991C5.52184 12.7713 5.51027 12.5468 5.51598 12.3205L4.90255 12.3051C4.89604 12.5623 4.90915 12.8196 4.94341 13.0731L5.55152 12.991ZM5.27759 13.3375C5.28094 13.3371 5.28429 13.3366 5.28765 13.3361L5.20729 12.728C5.20373 12.7285 5.2002 12.729 5.1967 12.7295L5.27759 13.3375ZM5.87334 14.3108C5.55756 13.7734 5.53893 13.2588 5.54388 13.0404L4.93041 13.0265C4.92419 13.3008 4.94703 13.9455 5.34423 14.6215L5.87334 14.3108ZM6.72175 15.1587C6.53331 15.0475 6.15501 14.7899 5.87331 14.3107L5.34423 14.6215C5.69895 15.2249 6.17402 15.5477 6.40985 15.6869L6.72175 15.1587ZM7.18433 15.4247C6.98719 15.3256 6.83096 15.2309 6.736 15.1676L6.39562 15.678C6.51119 15.755 6.68904 15.8623 6.90873 15.9728L7.18433 15.4247ZM7.94571 15.7279C7.66622 15.6489 7.41526 15.5476 7.19079 15.428L6.90227 15.9694C7.16743 16.1106 7.4594 16.2279 7.7788 16.3182L7.94571 15.7279ZM8.07572 15.763C8.03277 15.7523 7.99004 15.7407 7.94762 15.7285L7.7768 16.3176C7.8262 16.3319 7.87621 16.3455 7.92691 16.3581L8.07572 15.763ZM8.42802 15.8479C8.29947 15.8174 8.18257 15.7897 8.07572 15.763L7.92691 16.3581C8.03798 16.3859 8.15864 16.4145 8.28627 16.4448L8.42802 15.8479ZM12.4506 17.2958C10.7369 16.3954 9.3372 16.0636 8.42802 15.8479L8.28627 16.4448C9.18402 16.6578 10.522 16.9755 12.1651 17.8389L12.4506 17.2958ZM13.0346 17.7437C12.8458 17.6086 12.6693 17.4702 12.5052 17.3324L12.1107 17.8023C12.2855 17.949 12.4745 18.0973 12.6774 18.2425L13.0346 17.7437ZM12.9165 18.3541C12.9555 18.3173 13.0035 18.2727 13.0606 18.2216L12.6514 17.7646C12.5895 17.8199 12.5373 17.8684 12.4953 17.908L12.9165 18.3541ZM7.65454 20.031C10.5596 20.2878 12.5414 18.6718 12.904 18.3652L12.5078 17.8968C12.1782 18.1755 10.3606 19.6543 7.70861 19.42L7.65454 20.031ZM4.7122 19.0885C5.48062 19.538 6.46494 19.9259 7.65455 20.0311L7.70861 19.42C6.62375 19.3242 5.72585 18.9707 5.02207 18.5591L4.7122 19.0885ZM1.89197 15.7201C2.42664 17.1235 3.4083 18.3266 4.71229 19.0886L5.02197 18.559C3.84569 17.8717 2.95318 16.7819 2.46543 15.5018L1.89197 15.7201ZM1.23472 12.316C1.25612 13.6744 1.55244 14.8284 1.89197 15.7201L2.46546 15.5019C2.14624 14.6635 1.86835 13.5804 1.84828 12.3064L1.23472 12.316ZM1.65507 9.28796C1.40184 10.1345 1.21579 11.1561 1.23472 12.3163L1.84827 12.3062C1.83052 11.2181 2.00495 10.2594 2.24298 9.4637L1.65507 9.28796ZM7.48544 4.52413C4.73874 4.73102 2.44195 6.66285 1.65512 9.28773L2.24293 9.46385C2.96021 7.07095 5.04976 5.32275 7.53155 5.13581L7.48544 4.52413ZM7.48805 4.52394L7.48794 4.52395L7.52906 5.13599L7.52918 5.13598L7.48805 4.52394ZM7.66477 4.51255C7.60399 4.51588 7.54439 4.52003 7.48736 4.52399L7.52987 5.13593C7.58761 5.13192 7.64276 5.1281 7.69834 5.12505L7.66477 4.51255ZM19.4871 15.9585C20.5195 16.2307 21.2651 16.7006 21.7001 17.0227L22.0654 16.5298C21.5901 16.1778 20.7741 15.6634 19.6435 15.3654L19.4871 15.9585ZM16.9186 15.8101C17.4624 15.7325 18.3923 15.6701 19.4871 15.9585L19.6435 15.3654C18.4447 15.0495 17.427 15.1179 16.8319 15.2028L16.9186 15.8101ZM16.3517 15.9555C16.5658 15.9156 16.7717 15.8629 16.9704 15.7981L16.7801 15.2149C16.6071 15.2713 16.4271 15.3174 16.2392 15.3524L16.3517 15.9555ZM12.6023 14.776C13.2517 15.3386 14.6295 16.275 16.3515 15.9555L16.2395 15.3524C14.7985 15.6197 13.6099 14.8372 13.0041 14.3125L12.6023 14.776ZM12.2434 14.4886C12.3665 14.5799 12.4863 14.6758 12.6025 14.7763L13.0038 14.3122C12.876 14.2017 12.7442 14.0962 12.6089 13.9959L12.2434 14.4886ZM13.6839 13.0269C13.1508 13.2889 12.6639 13.625 12.2236 14.0119L12.6286 14.4726C13.033 14.1173 13.4752 13.8129 13.9546 13.5774L13.6839 13.0269ZM18.7042 12.5638C17.6973 12.5079 16.7474 12.4763 15.9035 12.5301C15.0621 12.5838 14.3014 12.7237 13.6839 13.0269L13.9546 13.5774C14.4672 13.3258 15.1352 13.1938 15.9426 13.1423C16.7477 13.0909 17.6667 13.1206 18.6702 13.1763L18.7042 12.5638ZM24.3266 14.7098C23.5387 14.085 21.5647 12.7236 18.7044 12.5638L18.6702 13.1763C21.3509 13.3259 23.2049 14.6033 23.9452 15.1903L24.3266 14.7098ZM24.5767 15.2283C24.5767 15.0273 24.4861 14.8365 24.3267 14.7099L23.945 15.1902C23.9563 15.1992 23.9631 15.2124 23.9631 15.2283H24.5767ZM24.5767 17.7181V15.2283H23.9631V17.7181H24.5767ZM23.4801 18.2183C23.9096 18.5912 24.5767 18.2859 24.5767 17.7181H23.9631C23.9631 17.7326 23.9593 17.7405 23.9559 17.7456C23.9516 17.7519 23.9445 17.7584 23.9345 17.7629C23.9246 17.7675 23.915 17.7685 23.9076 17.7677C23.9016 17.7669 23.8933 17.7646 23.8824 17.7552L23.4801 18.2183ZM21.7436 17.0496C22.4948 17.4318 23.0817 17.8734 23.4804 18.2186L23.8821 17.7549C23.4551 17.3852 22.8272 16.9126 22.0218 16.5029L21.7436 17.0496ZM12.6296 10.1274C12.8386 9.94385 13.0372 9.74886 13.2256 9.54483L12.7747 9.1287C12.5998 9.31809 12.4165 9.49805 12.2246 9.66667L12.6296 10.1274ZM13.8016 10.3427C13.379 10.1569 12.9795 9.92476 12.6099 9.65072L12.2444 10.1434C12.6507 10.4448 13.0899 10.6999 13.5545 10.9042L13.8016 10.3427ZM18.2083 10.89C16.2651 10.8924 14.8268 10.7946 13.8018 10.3428L13.5543 10.9041C14.7145 11.4154 16.2797 11.5058 18.2091 11.5034L18.2083 10.89ZM23.94 9.27945C23.0063 9.84586 20.971 10.8861 18.2083 10.89L18.2092 11.5034C21.1263 11.4993 23.2733 10.4014 24.2584 9.80384L23.94 9.27945ZM23.9631 9.23789C23.9631 9.25522 23.9542 9.27078 23.9396 9.27968L24.2588 9.80361C24.4563 9.68338 24.5767 9.4693 24.5767 9.23789H23.9631ZM23.9631 6.18208V9.23789H24.5767V6.18208H23.9631ZM23.8844 6.14243C23.9185 6.11792 23.9631 6.14225 23.9631 6.18208H24.5767C24.5767 5.63993 23.9641 5.32941 23.526 5.64453L23.8844 6.14243ZM21.3519 7.49938C22.4546 7.08404 23.3093 6.55621 23.8844 6.14243L23.526 5.64453C22.9834 6.03488 22.1762 6.53338 21.1355 6.92536L21.3519 7.49938ZM19.894 8.14148C20.4934 7.95707 20.9962 7.71423 21.3995 7.4766L21.088 6.94815C20.7192 7.16536 20.2602 7.38697 19.7135 7.55519L19.894 8.14148ZM16.8104 8.38324C17.4579 8.47395 18.5872 8.54334 19.894 8.14148L19.7136 7.55517C18.5209 7.92187 17.4889 7.85892 16.8955 7.7758L16.8104 8.38324ZM13.2256 9.54483C14.1759 8.5152 15.5483 8.07001 16.7786 8.37711L16.9273 7.78194C15.4556 7.41459 13.8597 7.95323 12.7746 9.12877L13.2256 9.54483Z" fill="black"/> +</g> +</g> +</g> +</g> +</g> +</g> +</svg> diff --git a/web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg b/web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg new file mode 100644 index 0000000000..fe10082fc3 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg @@ -0,0 +1,32 @@ +<svg width="74" height="16" viewBox="0 0 74 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Clip path group"> +<mask id="mask0_20135_12984" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="96" height="16"> +<g id="clip0_823_291"> +<path id="Vector" d="M95.5733 0H0V16H95.5733V0Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask0_20135_12984)"> +<g id="Group"> +<path id="Vector_2" d="M21.2832 11.5431V3.72656H22.3735V10.4972H26.3932V11.5431H21.2832ZM27.6995 7.44766C27.9198 6.31372 28.8889 5.5761 30.1224 5.5761C31.543 5.5761 32.4791 6.40179 32.4791 8.02014V10.233C32.4791 10.4862 32.5893 10.5963 32.8316 10.5963H33.0849V11.5431H32.7765C32.0717 11.5431 31.6532 11.1688 31.543 10.6513C31.3228 11.1908 30.64 11.6752 29.7259 11.6752C28.5475 11.6752 27.6004 11.0587 27.6004 10.0128C27.6004 8.80179 28.4924 8.46051 29.836 8.2073L31.4109 7.89904C31.3999 7.0073 30.8933 6.56693 30.1114 6.56693C29.4506 6.56693 28.966 6.96326 28.8338 7.52473L27.6995 7.44766ZM28.7237 9.99078C28.7347 10.3981 29.0871 10.7394 29.8581 10.7394C30.7391 10.7394 31.4329 10.1229 31.4329 9.07702V8.82381L30.1774 9.04399C29.3625 9.18711 28.7237 9.25317 28.7237 9.99078ZM34.5453 5.70821H35.5255L35.5585 6.68803C35.8669 5.93941 36.5166 5.5761 37.2986 5.5761C38.5981 5.5761 39.2369 6.5339 39.2369 7.78895V11.5431H38.1686V8.06418C38.1686 7.02931 37.8272 6.48987 37.0232 6.48987C36.1752 6.48987 35.6136 7.02931 35.6136 8.06418V11.5431H34.5453V5.70821ZM43.2303 11.2348C41.7876 11.2348 40.7634 10.0789 40.7634 8.43849C40.7634 6.74308 41.7876 5.5761 43.2303 5.5761C44.0122 5.5761 44.6951 5.99445 44.9594 6.59996L44.9704 5.70821H45.9946V10.9045C45.9836 12.5009 44.9704 13.3266 43.4065 13.3266C42.129 13.3266 41.2039 12.655 40.9286 11.6422L42.0519 11.5651C42.2832 12.0715 42.7347 12.3688 43.4065 12.3688C44.3536 12.3688 44.9153 11.9394 44.9263 11.1357V10.266C44.629 10.8275 43.9241 11.2348 43.2303 11.2348ZM41.8867 8.42748C41.8867 9.5284 42.4704 10.299 43.4286 10.299C44.3647 10.299 44.9373 9.5284 44.9483 8.42748C44.9704 7.33757 44.3867 6.56693 43.4286 6.56693C42.4704 6.56693 41.8867 7.33757 41.8867 8.42748ZM48.9967 5.455C48.9967 4.3761 49.5364 3.72656 50.7258 3.72656H52.3337V4.67335H50.7038C50.3293 4.67335 50.065 4.95959 50.065 5.43298V6.08253H52.2566V7.02931H50.065V11.5431H48.9967V7.02931H47.4659V6.08253H48.9967V5.455ZM58.9041 11.5431H57.8909L57.8798 10.5963C57.5715 11.3229 56.9327 11.6752 56.1838 11.6752C54.9063 11.6752 54.2786 10.7174 54.2786 9.46234V5.70821H55.3468V9.18711C55.3468 10.222 55.6883 10.7614 56.4592 10.7614C57.2851 10.7614 57.8358 10.222 57.8358 9.18711V5.70821H58.9041V11.5431ZM64.5277 7.53574C64.4065 6.91922 63.8338 6.56693 63.151 6.56693C62.5894 6.56693 62.0718 6.84216 62.0828 7.38161C62.0828 7.9651 62.7876 8.09721 63.4374 8.26234C64.5497 8.53757 65.662 8.94491 65.662 10.0348C65.662 11.1798 64.5607 11.6752 63.3493 11.6752C61.9837 11.6752 60.8713 10.9045 60.7832 9.69354L61.9066 9.62748C62.0167 10.277 62.6004 10.6844 63.3493 10.6844C63.933 10.6844 64.5387 10.5302 64.5387 9.97977C64.5387 9.4073 63.8008 9.30821 63.151 9.15409C62.0497 8.88987 60.9594 8.48253 60.9594 7.42565C60.9594 6.24766 62.0167 5.5761 63.2502 5.5761C64.4836 5.5761 65.4417 6.31372 65.629 7.46968L64.5277 7.53574ZM67.2104 8.62565C67.2104 6.76509 68.2787 5.5761 69.9196 5.5761C71.2302 5.5761 72.4196 6.42381 72.5077 8.52656V8.9339H68.3448C68.4329 10.0348 68.9945 10.6844 69.9196 10.6844C70.5033 10.6844 71.032 10.3431 71.2853 9.75959L72.4196 9.85867C72.0892 10.9706 71.087 11.6752 69.9196 11.6752C68.2787 11.6752 67.2104 10.4862 67.2104 8.62565ZM68.3778 8.07519H71.3403C71.1861 6.96326 70.5804 6.56693 69.9196 6.56693C69.0716 6.56693 68.532 7.1284 68.3778 8.07519Z" fill="black"/> +<g id="Clip path group_2"> +<mask id="mask1_20135_12984" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="17" height="18"> +<g id="clip1_823_291"> +<path id="Vector_3" d="M16.3621 -0.0512695H0.203125V16.1021H16.3621V-0.0512695Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask1_20135_12984)"> +<g id="Group_2"> +<path id="Vector_4" d="M14.6259 11.2357C13.9141 12.1984 12.8241 12.8406 11.5941 12.9344C11.5558 12.937 11.5175 12.9397 11.4787 12.9419C10.0365 13.0136 8.94706 12.3558 8.22466 11.7452C6.94631 11.0687 5.94609 10.8983 5.36089 10.751C4.93532 10.6438 4.56293 10.4296 4.40334 10.3225C4.26183 10.2384 3.97722 10.0434 3.76496 9.67965C3.52716 9.27204 3.51333 8.88257 3.51706 8.71705C3.641 8.70048 3.80113 8.68224 3.98839 8.67048C4.1416 8.66082 4.29002 8.65709 4.45654 8.65652C5.74819 8.65494 6.7499 8.71812 7.47874 9.0417C7.87295 9.21632 8.23842 9.4488 8.56395 9.73215C8.98265 10.0975 9.83862 10.6749 10.8935 10.4778C11.0276 10.4526 11.1563 10.4194 11.2803 10.3787C11.6601 10.3241 12.3097 10.2801 13.0747 10.4831C13.8008 10.676 14.3232 11.0092 14.6259 11.2357Z" fill="#0A60B5" stroke="black" stroke-width="1.70667" stroke-miterlimit="10"/> +<path id="Vector_5" d="M14.53 4.60662C14.2091 4.19101 13.819 3.79812 13.3584 3.53003C12.8675 3.2445 12.2411 2.99862 11.4835 2.93199C9.63248 2.76913 8.36691 3.79548 8.13634 3.98954C7.84947 4.25868 6.70187 5.21101 5.32048 5.73977C5.07981 5.82545 4.61653 6.02793 4.20477 6.48007C3.87909 6.83749 3.7197 7.20339 3.6416 7.43076C3.80631 7.45351 3.97632 7.46992 4.15164 7.47994C5.49102 7.55452 6.64184 7.56193 7.39466 7.19337C7.89196 6.95015 8.32815 6.60377 8.70431 6.1982C9.38222 5.4669 10.3709 5.14067 11.271 5.36436C11.6843 5.42197 12.4042 5.46588 13.2368 5.21101C13.8116 5.03492 14.2399 4.81337 14.53 4.60662Z" fill="#0A60B5" stroke="black" stroke-width="1.70667" stroke-miterlimit="10"/> +<path id="Vector_6" d="M1.96963 4.91518C1.45614 4.65135 1.05528 4.347 0.781876 4.10874C0.629046 3.97549 0.391602 4.08476 0.391602 4.28837V5.95295C0.391602 6.02543 0.424389 6.09419 0.480445 6.13896L1.16264 6.79512C1.19859 6.53125 1.2758 6.17255 1.44926 5.77597C1.61267 5.40184 1.80886 5.11558 1.96963 4.91518Z" fill="#0A60B5" stroke="black" stroke-width="1.70667" stroke-miterlimit="10"/> +<path id="Vector_7" d="M12.9521 8.63005C13.0302 8.38964 13.0735 8.13742 13.0799 7.8804C13.0853 7.67736 13.0617 7.6015 13.0264 7.4049C13.3895 7.34397 13.8428 7.24459 14.2561 7.1377C14.6929 7.02499 15.0158 6.89407 15.3789 6.76367C15.4318 7.01747 15.4874 7.14092 15.5067 7.32899C15.5248 7.50642 15.5361 7.69019 15.5392 7.8804C15.5489 8.47138 15.4767 9.0073 15.3655 9.47698C15.0233 9.29954 14.617 9.11577 14.1492 8.95439C13.714 8.8037 13.3093 8.70115 12.9521 8.63005Z" fill="#0A60B5" stroke="black" stroke-width="1.70667" stroke-miterlimit="10"/> +<path id="Vector_8" d="M0.766014 12.1447C0.609481 12.2583 0.391602 12.1463 0.391602 11.9516V9.90721C0.391602 9.84531 0.415399 9.78667 0.456648 9.74292C0.477272 9.72104 0.49631 9.70877 0.504771 9.70397L1.18061 9.41382C1.23032 9.6848 1.3123 10.0091 1.44926 10.3622C1.6095 10.775 1.79987 11.1094 1.96963 11.3638C1.56825 11.6241 1.16686 11.8844 0.766014 12.1447Z" fill="#0A60B5" stroke="black" stroke-width="1.70667" stroke-miterlimit="10"/> +<path id="Vector_9" fill-rule="evenodd" clip-rule="evenodd" d="M5.11863 3.21273C6.47036 3.1457 7.48116 3.71166 8.00219 4.08992C7.60778 4.43817 6.54047 5.27853 5.27812 5.76389C5.19298 5.79436 5.08001 5.83945 4.95131 5.90513C4.03786 6.35487 3.49469 7.25118 3.47044 8.20872C3.46637 8.3699 3.4746 8.53046 3.49592 8.68826C3.49361 8.68857 3.49131 8.68888 3.48903 8.68918C3.48531 8.85338 3.49914 9.23978 3.73679 9.64428C3.94894 10.0051 4.23338 10.1986 4.37481 10.282C4.44499 10.3288 4.55634 10.3962 4.69529 10.466C4.8585 10.5529 5.03948 10.6258 5.2391 10.6822C5.26968 10.6911 5.30062 10.6995 5.33181 10.7072C5.40448 10.7254 5.48364 10.7442 5.56903 10.7644C6.17131 10.9074 7.08394 11.1238 8.20285 11.7118C8.31591 11.8066 8.43766 11.9022 8.56827 11.9956C8.52858 12.0311 8.49519 12.0621 8.46819 12.0875C8.23747 12.2826 6.97098 13.3142 5.11863 13.1505C4.36047 13.0836 3.73309 12.8364 3.24236 12.5494C2.4156 12.0663 1.79088 11.302 1.45008 10.4075C1.2305 9.83086 1.03909 9.08515 1.02527 8.20765C1.01304 7.45826 1.1332 6.79817 1.29696 6.25074C1.79833 4.57812 3.26043 3.35145 5.00327 3.22017L5.00335 3.22016C5.0416 3.21751 5.07986 3.21485 5.11863 3.21273ZM14.5861 11.1844C14.2827 10.9597 13.7622 10.6316 13.0411 10.4415C12.2766 10.2401 11.6274 10.2837 11.2478 10.3378C11.1239 10.3782 10.9952 10.4112 10.8613 10.4362C9.80694 10.6318 8.95148 10.0588 8.53303 9.69637C8.45168 9.62603 8.36781 9.55891 8.28165 9.49501C8.56326 9.2476 8.87288 9.03413 9.21043 8.8683C9.96382 8.49841 11.1154 8.50582 12.4558 8.58025C14.3028 8.68336 15.5788 9.56295 16.0882 9.96688C16.145 10.0121 16.1775 10.0801 16.1775 10.1524V11.8123C16.1775 12.0158 15.9388 12.1247 15.7851 11.9914C15.5098 11.7531 15.1049 11.4483 14.5861 11.1844ZM8.66435 6.22472C8.54326 6.35584 8.41593 6.48083 8.28237 6.59819C8.54101 6.79004 8.82057 6.95244 9.11629 7.08249C9.84473 7.40351 10.8459 7.46623 12.1367 7.46465C14.0301 7.46199 15.4241 6.74925 16.0637 6.36126C16.1344 6.31822 16.1775 6.2417 16.1775 6.15878V4.12158C16.1775 3.92758 15.9585 3.81597 15.8011 3.92917C15.4285 4.19722 14.8745 4.53933 14.1601 4.80844C13.9028 4.96005 13.5822 5.11485 13.2001 5.23242C12.367 5.48857 11.6466 5.44446 11.2329 5.38654C10.3323 5.16172 9.34277 5.48964 8.66435 6.22472Z" fill="#E11312"/> +<path id="Vector_10" d="M8.00166 4.09005L8.13707 4.2433L8.32826 4.07447L8.12183 3.92461L8.00166 4.09005ZM5.11809 3.21286L5.10798 3.00864L5.10745 3.00866L5.10692 3.0087L5.11809 3.21286ZM5.27759 5.76403L5.34647 5.95659L5.34877 5.95577L5.35102 5.9549L5.27759 5.76403ZM4.95078 5.90527L5.04115 6.08868L5.04247 6.08807L5.04379 6.0874L4.95078 5.90527ZM3.49538 8.6884L3.52217 8.89108L3.72555 8.86425L3.69809 8.661L3.49538 8.6884ZM3.4885 8.68932L3.46154 8.48664L3.28798 8.50969L3.28401 8.68467L3.4885 8.68932ZM4.37427 10.2822L4.48774 10.112L4.48307 10.1089L4.47824 10.1061L4.37427 10.2822ZM4.69475 10.4661L4.79093 10.2857L4.78879 10.2845L4.78663 10.2834L4.69475 10.4661ZM5.23857 10.6823L5.29549 10.486L5.29487 10.4858L5.29421 10.4856L5.23857 10.6823ZM8.20232 11.7119L8.33384 11.5553L8.31701 11.5412L8.29748 11.5309L8.20232 11.7119ZM8.56773 11.9957L8.70411 12.1481L8.89429 11.978L8.68678 11.8295L8.56773 11.9957ZM8.46766 12.0877L8.59975 12.2438L8.60404 12.2402L8.60808 12.2364L8.46766 12.0877ZM3.24183 12.5496L3.34511 12.3731L3.34505 12.373L3.24183 12.5496ZM1.02474 8.20779L1.22926 8.20456L1.22925 8.20446L1.02474 8.20779ZM1.29642 6.25088L1.10049 6.19214L1.10045 6.1923L1.29642 6.25088ZM5.00274 3.2203L4.98903 3.01629L4.9882 3.01635L4.98737 3.01641L5.00274 3.2203ZM5.00281 3.2203L5.01652 3.42431L5.01698 3.42428L5.00281 3.2203ZM13.0406 10.4417L13.0928 10.2439H13.0927L13.0406 10.4417ZM14.5855 11.1845L14.4638 11.3488L14.4775 11.359L14.4928 11.3667L14.5855 11.1845ZM11.2473 10.338L11.2183 10.1356L11.2007 10.1381L11.1838 10.1436L11.2473 10.338ZM10.8607 10.4363L10.8981 10.6373H10.8982L10.8607 10.4363ZM8.5325 9.6965L8.66648 9.54197L8.66627 9.54177L8.5325 9.6965ZM8.28112 9.49515L8.14612 9.34159L7.95594 9.50864L8.15931 9.65939L8.28112 9.49515ZM12.4553 8.58039L12.4667 8.37622H12.4666L12.4553 8.58039ZM16.0877 9.96702L16.2149 9.80692L16.2148 9.80687L16.0877 9.96702ZM15.7846 11.9915L15.9187 11.8371L15.9185 11.8369L15.7846 11.9915ZM8.28183 6.59833L8.14678 6.44477L7.95666 6.61177L8.15998 6.76257L8.28183 6.59833ZM9.11576 7.08262L9.19829 6.89553L9.19814 6.89548L9.11576 7.08262ZM12.1362 7.46478L12.1365 7.66925H12.1365L12.1362 7.46478ZM16.0632 6.3614L16.1693 6.53622L16.1696 6.53607L16.0632 6.3614ZM14.1596 4.80857L14.0874 4.61723L14.0709 4.62346L14.0557 4.63242L14.1596 4.80857ZM11.2324 5.38667L11.1828 5.58506L11.1933 5.58767L11.204 5.58915L11.2324 5.38667ZM8.12183 3.92461C7.57989 3.53114 6.52347 2.93845 5.10798 3.00864L5.12822 3.41708C6.41618 3.35322 7.38138 3.89245 7.88144 4.25549L8.12183 3.92461ZM5.35102 5.9549C6.64538 5.45722 7.73371 4.59944 8.13707 4.2433L7.86625 3.9368C7.48074 4.27718 6.43449 5.10015 5.20416 5.5732L5.35102 5.9549ZM5.04379 6.0874C5.16309 6.02647 5.26772 5.98471 5.34647 5.95659L5.20871 5.57152C5.11717 5.60423 4.99585 5.65269 4.85776 5.72318L5.04379 6.0874ZM3.67439 8.21402C3.69676 7.3308 4.19746 6.50412 5.04115 6.08868L4.8604 5.72186C3.87719 6.20595 3.29156 7.17188 3.26543 8.2037L3.67439 8.21402ZM3.69809 8.661C3.6783 8.51455 3.67058 8.36487 3.67439 8.21402L3.26543 8.2037C3.2611 8.3752 3.26984 8.5467 3.29268 8.71575L3.69809 8.661ZM3.51546 8.892C3.5177 8.8917 3.51993 8.89139 3.52217 8.89108L3.4686 8.48566C3.46623 8.48597 3.46387 8.48633 3.46154 8.48664L3.51546 8.892ZM3.91263 9.54085C3.70211 9.18256 3.68969 8.83956 3.69299 8.69392L3.28401 8.68467C3.27987 8.86752 3.29509 9.29732 3.55989 9.74798L3.91263 9.54085ZM4.47824 10.1061C4.35261 10.032 4.10041 9.86028 3.91261 9.54079L3.55989 9.74798C3.79637 10.1503 4.11309 10.3655 4.2703 10.4583L4.47824 10.1061ZM4.78663 10.2834C4.6552 10.2174 4.55104 10.1543 4.48774 10.112L4.26081 10.4523C4.33787 10.5037 4.45643 10.5752 4.60289 10.6488L4.78663 10.2834ZM5.29421 10.4856C5.10788 10.4329 4.94058 10.3654 4.79093 10.2857L4.59858 10.6466C4.77536 10.7407 4.97 10.819 5.18294 10.8791L5.29421 10.4856ZM5.38088 10.509C5.35225 10.5019 5.32376 10.4941 5.29549 10.486L5.18161 10.8787C5.21454 10.8883 5.24788 10.8973 5.28168 10.9058L5.38088 10.509ZM5.61575 10.5656C5.53005 10.5453 5.45212 10.5268 5.38088 10.509L5.28168 10.9058C5.35572 10.9243 5.43616 10.9433 5.52125 10.9635L5.61575 10.5656ZM8.29748 11.5309C7.155 10.9306 6.22187 10.7094 5.61575 10.5656L5.52125 10.9635C6.11975 11.1055 7.01177 11.3174 8.10715 11.8929L8.29748 11.5309ZM8.68678 11.8295C8.56093 11.7394 8.44327 11.6471 8.33384 11.5553L8.07085 11.8685C8.18744 11.9664 8.31338 12.0652 8.44864 12.162L8.68678 11.8295ZM8.60808 12.2364C8.63406 12.2119 8.66607 12.1821 8.70411 12.1481L8.4313 11.8434C8.39004 11.8803 8.35526 11.9126 8.32724 11.939L8.60808 12.2364ZM5.10009 13.3543C7.03682 13.5255 8.35798 12.4482 8.59975 12.2438L8.33558 11.9315C8.11585 12.1173 6.90412 13.1032 5.13615 12.947L5.10009 13.3543ZM3.13854 12.726C3.65082 13.0256 4.30703 13.2843 5.10011 13.3544L5.13615 12.947C4.4129 12.8831 3.8143 12.6475 3.34511 12.3731L3.13854 12.726ZM1.25838 10.4804C1.61483 11.416 2.26927 12.2181 3.1386 12.7261L3.34505 12.373C2.56087 11.9148 1.96586 11.1883 1.64069 10.3349L1.25838 10.4804ZM0.820219 8.21101C0.834481 9.11662 1.03203 9.88594 1.25838 10.4804L1.64071 10.3349C1.4279 9.77599 1.24263 9.05395 1.22926 8.20456L0.820219 8.21101ZM1.10045 6.1923C0.93163 6.75664 0.807599 7.43774 0.820219 8.21116L1.22925 8.20446C1.21742 7.47904 1.3337 6.83991 1.49239 6.30946L1.10045 6.1923ZM4.98737 3.01641C3.15623 3.15434 1.62504 4.44222 1.10049 6.19214L1.49236 6.30956C1.97055 4.7143 3.36357 3.54883 5.0181 3.4242L4.98737 3.01641ZM4.9891 3.01629L4.98903 3.01629L5.01644 3.42432L5.01652 3.42431L4.9891 3.01629ZM5.10692 3.0087C5.0664 3.01091 5.02666 3.01368 4.98864 3.01632L5.01698 3.42428C5.05547 3.42161 5.09225 3.41906 5.12929 3.41703L5.10692 3.0087ZM12.9885 10.6393C13.6767 10.8208 14.1738 11.134 14.4638 11.3488L14.7073 11.0202C14.3904 10.7855 13.8465 10.4426 13.0928 10.2439L12.9885 10.6393ZM11.2762 10.5404C11.6387 10.4886 12.2586 10.4471 12.9885 10.6393L13.0927 10.2439C12.2935 10.0333 11.6151 10.0789 11.2183 10.1356L11.2762 10.5404ZM10.8982 10.6373C11.0409 10.6107 11.1782 10.5756 11.3107 10.5324L11.1838 10.1436C11.0685 10.1812 10.9485 10.2119 10.8232 10.2353L10.8982 10.6373ZM8.39858 9.85098C8.83155 10.2261 9.75005 10.8503 10.8981 10.6373L10.8234 10.2353C9.86276 10.4135 9.07035 9.89182 8.66648 9.54197L8.39858 9.85098ZM8.15931 9.65939C8.24138 9.72027 8.32126 9.78422 8.39873 9.85118L8.66627 9.54177C8.58108 9.46816 8.49323 9.39782 8.40297 9.3309L8.15931 9.65939ZM9.1197 8.68492C8.76425 8.85959 8.43969 9.08364 8.14612 9.34159L8.41617 9.64876C8.68576 9.41187 8.98056 9.20894 9.30011 9.05195L9.1197 8.68492ZM12.4666 8.37622C11.7952 8.33895 11.162 8.31784 10.5994 8.35373C10.0385 8.38951 9.53134 8.4828 9.1197 8.68492L9.30011 9.05195C9.64185 8.88418 10.0872 8.79621 10.6255 8.76186C11.1622 8.72761 11.7749 8.74739 12.4439 8.78455L12.4666 8.37622ZM16.2148 9.80687C15.6896 9.39035 14.3735 8.4827 12.4667 8.37622L12.4438 8.78455C14.231 8.88428 15.467 9.73586 15.9605 10.1272L16.2148 9.80687ZM16.3815 10.1525C16.3815 10.0185 16.3211 9.89131 16.2149 9.80692L15.9604 10.1271C15.9679 10.1331 15.9724 10.1419 15.9724 10.1525H16.3815ZM16.3815 11.8124V10.1525H15.9724V11.8124H16.3815ZM15.6504 12.1459C15.9368 12.3945 16.3815 12.1909 16.3815 11.8124H15.9724C15.9724 11.822 15.9699 11.8273 15.9676 11.8307C15.9648 11.8349 15.9601 11.8393 15.9534 11.8423C15.9468 11.8453 15.9404 11.846 15.9355 11.8455C15.9315 11.8449 15.926 11.8434 15.9187 11.8371L15.6504 12.1459ZM14.4928 11.3667C14.9936 11.6215 15.3848 11.916 15.6507 12.1461L15.9185 11.8369C15.6338 11.5905 15.2152 11.2754 14.6783 11.0023L14.4928 11.3667ZM8.41683 6.75194C8.55613 6.62956 8.68852 6.49957 8.81416 6.36354L8.51353 6.08612C8.39694 6.21239 8.27472 6.33236 8.14678 6.44477L8.41683 6.75194ZM9.19814 6.89548C8.91638 6.77157 8.65006 6.61683 8.40369 6.43414L8.15998 6.76257C8.43089 6.96352 8.7237 7.13359 9.03343 7.26982L9.19814 6.89548ZM12.136 7.26031C10.8405 7.26189 9.88163 7.19672 9.19829 6.89553L9.03328 7.26972C9.80676 7.61062 10.8502 7.67084 12.1365 7.66925L12.136 7.26031ZM15.9571 6.18662C15.3346 6.56423 13.9777 7.2577 12.136 7.26031L12.1365 7.66925C14.0813 7.66655 15.5126 6.93458 16.1693 6.53622L15.9571 6.18662ZM15.9724 6.15892C15.9724 6.17047 15.9666 6.18085 15.9568 6.18678L16.1696 6.53607C16.3012 6.45591 16.3815 6.31319 16.3815 6.15892H15.9724ZM15.9724 4.12171V6.15892H16.3815V4.12171H15.9724ZM15.92 4.09528C15.9427 4.07894 15.9724 4.09516 15.9724 4.12171H16.3815C16.3815 3.76028 15.9731 3.55327 15.6811 3.76334L15.92 4.09528ZM14.2317 4.99991C14.9668 4.72302 15.5366 4.37113 15.92 4.09528L15.6811 3.76334C15.3193 4.02358 14.7812 4.35591 14.0874 4.61723L14.2317 4.99991ZM13.2597 5.42798C13.6594 5.30504 13.9946 5.14315 14.2634 4.98473L14.0557 4.63242C13.8099 4.77723 13.5039 4.92497 13.1394 5.03712L13.2597 5.42798ZM11.204 5.58915C11.6356 5.64963 12.3885 5.69589 13.2597 5.42798L13.1395 5.03711C12.3443 5.28157 11.6564 5.23961 11.2608 5.18419L11.204 5.58915ZM8.81416 6.36354C9.44768 5.67713 10.3626 5.38033 11.1828 5.58506L11.2819 5.18828C10.3008 4.94339 9.23685 5.30248 8.51348 6.08617L8.81416 6.36354Z" fill="black"/> +</g> +</g> +</g> +</g> +</g> +</g> +</svg> diff --git a/web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg b/web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg new file mode 100644 index 0000000000..95e1ff423c --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg @@ -0,0 +1,24 @@ +<svg width="124" height="20" viewBox="0 0 124 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Clip path group"> +<mask id="mask0_20135_18175" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="124" height="20"> +<g id="a"> +<path id="Vector" d="M123.825 0.399902H0.200195V19.5999H123.825V0.399902Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask0_20135_18175)"> +<g id="Group"> +<path id="Vector_2" d="M45.54 4.18408V15.827H53.561V14.069H47.361V4.18408H45.54Z" fill="#1C3C3C"/> +<path id="Vector_3" d="M57.8358 6.94629C56.0878 6.94629 54.7807 7.76575 54.25 9.19423C54.2162 9.28562 54.1141 9.56133 54.1141 9.56133L55.6124 10.5305L55.8159 9.99986C56.1631 9.09515 56.8051 8.67352 57.8358 8.67352C58.8664 8.67352 59.4563 9.17349 59.4455 10.1581C59.4455 10.198 59.4424 10.3186 59.4424 10.3186C59.4424 10.3186 58.0785 10.5398 57.5163 10.6588C55.1178 11.1657 54.1133 12.0811 54.1133 13.5787C54.1133 14.3767 54.5564 15.2407 55.3651 15.7253C55.8505 16.0156 56.4841 16.1254 57.1837 16.1254C57.6438 16.1254 58.0908 16.0571 58.5047 15.9311C59.4455 15.6185 59.7082 15.0041 59.7082 15.0041V15.8075H61.2664V10.0644C61.2664 8.11211 59.9839 6.94629 57.8358 6.94629ZM59.4517 13.0749C59.4517 13.6786 58.7942 14.5288 57.2629 14.5288C56.8305 14.5288 56.524 14.4143 56.3197 14.2438C56.0463 14.0157 55.9565 13.6878 55.9941 13.3983C56.0102 13.2723 56.0863 13.0012 56.3681 12.7662C56.6561 12.5258 57.1653 12.3538 57.9517 12.1825C58.5984 12.042 59.4524 11.8868 59.4524 11.8868V13.0757L59.4517 13.0749Z" fill="#1C3C3C"/> +<path id="Vector_4" d="M67.0275 6.94657C66.8109 6.94657 66.5997 6.96193 66.3946 6.99034C64.9992 7.20001 64.5906 7.90887 64.5906 7.90887L64.5921 7.20001H62.8457V15.8093H64.6666V11.0339C64.6666 9.41108 65.8501 8.67226 66.9499 8.67226C68.1388 8.67226 68.7163 9.31124 68.7163 10.6268V15.8093H70.5372V10.3765C70.5372 8.25985 69.1925 6.9458 67.0282 6.9458L67.0275 6.94657Z" fill="#1C3C3C"/> +<path id="Vector_5" d="M78.1373 7.19359V8.08063C78.1373 8.08063 77.6911 6.94629 75.6611 6.94629C73.139 6.94629 71.5723 8.68658 71.5723 11.489C71.5723 13.0703 72.0776 14.3152 72.9693 15.1017C73.6628 15.713 74.589 16.0264 75.6918 16.0479C76.4591 16.0624 76.9559 15.8536 77.2664 15.6562C77.8623 15.2768 78.0835 14.9166 78.0835 14.9166C78.0835 14.9166 78.0582 15.1984 78.0121 15.5801C77.9791 15.8566 77.9169 16.0509 77.9169 16.0509C77.6396 17.0378 76.8285 17.6084 75.6457 17.6084C74.463 17.6084 73.7465 17.2191 73.6044 16.4518L71.8342 16.9802C72.1398 18.4548 73.5238 19.3349 75.5359 19.3349C76.9037 19.3349 77.976 18.9632 78.7233 18.229C79.4767 17.4886 79.8591 16.4219 79.8591 15.0579V7.19282H78.1373V7.19359ZM78.0229 11.5666C78.0229 13.29 77.1811 14.3191 75.7709 14.3191C74.2603 14.3191 73.394 13.2869 73.394 11.4882C73.394 9.68959 74.2603 8.67275 75.7709 8.67275C77.1473 8.67275 78.0098 9.69726 78.0229 11.3469V11.5666Z" fill="#1C3C3C"/> +<path id="Vector_6" d="M90.532 14.0495C90.7777 13.6033 90.9022 13.0772 90.9022 12.4851C90.9022 11.893 90.7969 11.4383 90.5888 11.0704C90.3807 10.701 90.1119 10.3992 89.7909 10.1727C89.4675 9.94455 89.1258 9.76484 88.7771 9.63735C88.4269 9.50987 88.1051 9.40695 87.8217 9.33246L85.7427 8.75262C85.4801 8.68273 85.2174 8.59441 84.9609 8.48919C84.7021 8.38321 84.4817 8.23652 84.3073 8.05298C84.1292 7.86635 84.0385 7.62367 84.0385 7.32952C84.0385 7.02079 84.1437 6.74661 84.3503 6.51467C84.5554 6.28504 84.8288 6.10687 85.1637 5.98475C85.4962 5.86264 85.8625 5.80351 86.2527 5.80888C86.6536 5.81963 87.0368 5.90181 87.3909 6.05387C87.7464 6.20594 88.0521 6.43019 88.2994 6.7205C88.5398 7.00312 88.7064 7.34719 88.7955 7.74424L90.8054 7.3948C90.6341 6.70667 90.3423 6.10994 89.9368 5.62149C89.5236 5.12383 89.0029 4.73829 88.3885 4.4764C87.7733 4.21375 87.0629 4.07781 86.2765 4.07243C85.5023 4.06706 84.7858 4.19071 84.1522 4.44031C83.5201 4.68914 83.011 5.06853 82.6385 5.56773C82.2668 6.06616 82.0778 6.69131 82.0778 7.42552C82.0778 7.92779 82.1615 8.35403 82.3274 8.69349C82.4933 9.03371 82.7099 9.31634 82.9702 9.53522C83.2321 9.75487 83.5132 9.92843 83.8066 10.0529C84.1023 10.178 84.3811 10.2794 84.636 10.3531L87.6328 11.2394C87.8493 11.3047 88.0429 11.383 88.208 11.4721C88.3747 11.562 88.5129 11.6633 88.6197 11.7732C88.7272 11.8838 88.8101 12.0113 88.8654 12.1526C88.9207 12.2939 88.9484 12.449 88.9484 12.6134C88.9484 12.9812 88.8301 13.2969 88.5958 13.5496C88.3647 13.7999 88.0598 13.9935 87.6904 14.1232C87.3225 14.253 86.9254 14.3191 86.5092 14.3191C85.8065 14.3191 85.1767 14.1271 84.6383 13.7485C84.1077 13.3752 83.749 12.8422 83.5724 12.1648L81.6309 12.4598C81.7507 13.1909 82.0264 13.8322 82.4503 14.3652C82.8819 14.9081 83.441 15.3313 84.1107 15.6224C84.782 15.9142 85.5484 16.0624 86.3871 16.0624C86.9769 16.0624 87.5491 15.9872 88.089 15.8382C88.6273 15.69 89.1127 15.4642 89.5313 15.167C89.9491 14.8713 90.2855 14.4942 90.5304 14.048L90.532 14.0495Z" fill="#1C3C3C"/> +<path id="Vector_7" d="M100.071 8.84108C100.322 8.69747 100.611 8.62451 100.928 8.62451C101.441 8.62451 101.855 8.79654 102.156 9.13676C102.457 9.47545 102.61 9.94931 102.61 10.5476V15.7462H104.474V10.0607C104.474 9.14368 104.218 8.39334 103.715 7.83116C103.212 7.27052 102.477 6.9856 101.532 6.9856C100.961 6.9856 100.436 7.11308 99.9714 7.36422C99.536 7.6 99.1789 7.9287 98.9116 8.34035L98.8763 8.39488L98.8455 8.33804C98.6343 7.9479 98.3348 7.62918 97.9547 7.3911C97.5253 7.1223 96.9831 6.9856 96.3442 6.9856C95.7628 6.9856 95.2306 7.11462 94.7636 7.36806C94.405 7.56236 94.0985 7.81657 93.8528 8.12224L93.7844 8.20748V7.2014H92.1455V15.7462H94.0263V10.4762C94.0263 9.93164 94.1799 9.48236 94.4833 9.14137C94.7874 8.79884 95.1968 8.62528 95.7006 8.62528C96.2044 8.62528 96.636 8.79884 96.9378 9.14137C97.2381 9.48236 97.3902 9.9639 97.3902 10.5714V15.7462H99.2464V10.4762C99.2464 10.0937 99.3209 9.75884 99.4684 9.48006C99.6166 9.20051 99.8194 8.98624 100.071 8.84185L100.071 8.84108Z" fill="#1C3C3C"/> +<path id="Vector_8" d="M110.408 13.5589C110.418 13.9429 110.522 14.3254 110.717 14.694C110.938 15.0972 111.265 15.3967 111.689 15.5834C112.118 15.7707 112.61 15.8729 113.153 15.8859C113.689 15.899 114.243 15.8537 114.801 15.7515V14.201C114.276 14.2762 113.8 14.2962 113.387 14.2593C112.951 14.2209 112.63 14.0328 112.431 13.701C112.325 13.5305 112.269 13.307 112.26 13.0382C112.252 12.7748 112.248 12.466 112.248 12.1189V8.56844H114.801V7.12307H112.248V4.19775H110.392V7.12307H108.812V8.56844H110.392V12.2318C110.392 12.7249 110.397 13.1718 110.408 13.5597V13.5589Z" fill="#1C3C3C"/> +<path id="Vector_9" d="M120.316 6.93339C120.116 6.93339 119.922 6.94645 119.733 6.97103C118.359 7.1853 117.955 7.88495 117.955 7.88495V7.67989H117.955V4.1709H116.134V15.7977H117.955V11.0222C117.955 9.38869 119.138 8.64527 120.238 8.64527C121.427 8.64527 122.004 9.28424 122.004 10.5998V15.7977H123.825V10.3495C123.825 8.27509 122.448 6.93416 120.316 6.93416L120.316 6.93339Z" fill="#1C3C3C"/> +<path id="Vector_10" d="M107.589 7.19922H105.777V15.8162H107.589V7.19922Z" fill="#1C3C3C"/> +<path id="Vector_11" d="M106.682 6.55761C107.334 6.55761 107.863 6.02913 107.863 5.37719C107.863 4.72527 107.334 4.19678 106.682 4.19678C106.03 4.19678 105.502 4.72527 105.502 5.37719C105.502 6.02913 106.03 6.55761 106.682 6.55761Z" fill="#1C3C3C"/> +<path id="Vector_12" d="M22.3912 15.1309C22.286 15.306 21.9696 15.3175 21.7 15.1555C21.5618 15.0725 21.455 14.9581 21.3997 14.8337C21.349 14.7208 21.3483 14.614 21.3966 14.5334C21.4519 14.4412 21.5664 14.3944 21.7015 14.3944C21.8229 14.3944 21.9611 14.432 22.0886 14.5088C22.3582 14.6709 22.4972 14.9558 22.392 15.1309H22.3912ZM37.9908 9.9999C37.9908 15.293 33.6839 19.5999 28.3908 19.5999H9.81289C4.51983 19.5999 0.212891 15.2937 0.212891 9.9999C0.212891 4.70608 4.51983 0.399902 9.81289 0.399902H28.3908C33.6846 0.399902 37.9908 4.70685 37.9908 9.9999ZM18.714 14.8145C18.8653 14.6309 18.1664 14.1141 18.0236 13.9244C17.7333 13.6095 17.7317 13.1564 17.5359 12.7885C17.0567 11.678 16.506 10.5759 15.7357 9.63587C14.9216 8.60752 13.9171 7.75657 13.0347 6.7912C12.3795 6.11766 12.2044 5.15843 11.6261 4.43421C10.829 3.25686 8.30838 2.93584 7.93897 4.59856C7.94051 4.65078 7.92438 4.68381 7.87906 4.71683C7.67477 4.86505 7.49276 5.03478 7.33992 5.23984C6.96591 5.76054 6.90831 6.64374 7.37525 7.11145C7.39061 6.86493 7.39906 6.63222 7.59413 6.45558C7.9551 6.76585 8.50037 6.87491 8.91893 6.64374C9.8436 7.96393 9.6132 9.79024 10.3474 11.2126C10.5502 11.549 10.7545 11.8923 11.0148 12.1872C11.226 12.5159 11.9556 12.9037 11.9986 13.2078C12.0063 13.7301 11.9449 14.3007 12.2874 14.7377C12.4487 15.0649 12.0524 15.3936 11.7329 15.3529C11.3182 15.4097 10.8121 15.0741 10.4488 15.2807C10.3205 15.4197 10.0694 15.2661 9.9588 15.4588C9.9204 15.5587 9.71304 15.6992 9.83669 15.7952C9.97416 15.6908 10.1017 15.5817 10.2867 15.6439C10.2591 15.7945 10.3781 15.816 10.4726 15.8597C10.4695 15.9619 10.4096 16.0663 10.488 16.1531C10.5793 16.061 10.6339 15.9304 10.779 15.892C11.2613 16.5348 11.7521 15.2415 12.7958 15.8236C12.5838 15.8137 12.3957 15.8398 12.2528 16.0141C12.2175 16.0533 12.1875 16.0994 12.2497 16.15C12.8127 15.7868 12.8096 16.2745 13.1752 16.1247C13.4563 15.978 13.7358 15.7945 14.0699 15.8467C13.745 15.9404 13.732 16.2015 13.5415 16.4219C13.5093 16.4557 13.4939 16.4941 13.5315 16.5502C14.2058 16.4933 14.2611 16.2691 14.8057 15.9941C15.2119 15.7461 15.6167 16.3474 15.9684 16.0049C16.046 15.9304 16.152 15.9557 16.248 15.9458C16.1251 15.2907 14.7742 16.0656 14.7957 15.187C15.2304 14.8913 15.1305 14.3253 15.1597 13.8683C15.6597 14.1456 16.2157 14.3068 16.7057 14.5718C16.953 14.9712 17.3408 15.4988 17.8577 15.4642C17.8715 15.4243 17.8838 15.389 17.8984 15.3483C18.0551 15.3751 18.2563 15.4788 18.3423 15.2807C18.5765 15.5257 18.9206 15.5134 19.227 15.4504C19.4536 15.2661 18.8008 15.0034 18.7132 14.8137L18.714 14.8145ZM25.722 11.7187L24.6944 10.3317C23.7974 11.3577 23.1984 11.8577 23.1876 11.8669C23.1822 11.8723 22.6101 12.4291 22.0886 12.9068C21.5771 13.3753 21.1731 13.7462 20.9673 14.1517C20.9105 14.2638 20.7868 14.677 20.9604 15.0902C21.094 15.4097 21.3667 15.637 21.7714 15.766C21.8928 15.8044 22.0087 15.8213 22.1193 15.8213C22.8482 15.8213 23.3266 15.0902 23.3297 15.0841C23.3358 15.0756 23.9556 14.1901 24.7106 13.0719C24.9617 12.7002 25.2489 12.307 25.722 11.7179V11.7187ZM30.5535 14.9128C30.5535 14.7085 30.479 14.5111 30.3438 14.3583L30.2163 14.2139C29.446 13.3407 27.4684 11.0989 26.6989 10.228C25.7328 9.13437 24.6522 7.74045 24.5623 7.62371L24.4325 7.35491V6.88182C24.4325 6.70825 24.398 6.53853 24.3312 6.37878L24.0562 5.72598C24.0524 5.71677 24.0508 5.70601 24.0524 5.69603L24.0631 5.60541C24.0647 5.59081 24.0716 5.57776 24.0831 5.56777C24.2974 5.37885 25.0946 4.76598 26.3287 4.81744C26.49 4.82435 26.5184 4.73603 26.523 4.6984C26.5453 4.51715 26.1314 4.30365 25.7458 4.22454C25.2159 4.11625 23.8074 3.82902 22.6807 4.56861L22.6723 4.57475C21.9442 5.18301 21.359 5.64765 21.3529 5.65225L21.3398 5.66531C21.3314 5.67529 21.1255 5.92182 21.1755 6.23593C21.2077 6.44022 21.1017 6.51318 21.0956 6.51702C21.0894 6.52086 20.9451 6.61149 20.7961 6.50934C20.6156 6.37417 20.3022 6.60611 20.2385 6.6568L19.7654 7.06384L19.7562 7.07305C19.7477 7.08304 19.545 7.32035 19.8161 7.70128C20.0503 8.03075 20.1333 8.14057 20.3368 8.39401C20.5434 8.65053 20.9159 8.97539 20.9358 8.99229C20.9451 8.99997 21.1724 9.172 21.4857 8.93238C21.743 8.73501 21.9496 8.55683 21.9496 8.55683C21.9665 8.54301 22.1155 8.42013 22.1224 8.23888C22.1247 8.18665 22.1224 8.14134 22.1224 8.09987C22.1186 7.97238 22.1178 7.93475 22.2138 7.87331C22.2599 7.87331 22.4012 7.92477 22.5225 7.98621C22.5356 7.99389 22.8389 8.16438 23.1147 8.15209C23.2882 8.17513 23.4802 8.37251 23.5463 8.45315C23.5524 8.45929 24.1392 9.07523 24.9655 10.1558C25.123 10.3609 25.7013 11.1312 25.8595 11.3462C26.1237 11.7056 26.5238 12.2494 26.9539 12.8354C27.6988 13.8499 28.5344 14.9873 28.9138 15.4988C29.0398 15.6685 29.2233 15.7837 29.4307 15.8236L29.5712 15.8505C29.625 15.8605 29.6787 15.8659 29.7325 15.8659C29.9813 15.8659 30.2171 15.7568 30.373 15.5633L30.3815 15.5525C30.4936 15.4105 30.555 15.2284 30.555 15.0411V14.9128H30.5535ZM31.2147 5.80355L31.0512 5.63997C31.0035 5.59235 30.9367 5.56393 30.8691 5.56931C30.8016 5.57238 30.7378 5.6031 30.694 5.65533L29.6649 6.87261C29.6357 6.90717 29.5943 6.92867 29.5497 6.93328L29.1834 6.97014C29.1365 6.97475 29.0897 6.96016 29.0536 6.93021L28.4684 6.43485C28.4307 6.40259 28.4085 6.35651 28.4069 6.30736L28.3985 6.01398C28.397 5.97174 28.4115 5.93027 28.4384 5.89801L29.4391 4.69225C29.5144 4.60163 29.5136 4.4703 29.4376 4.37968L29.337 4.26064C29.2709 4.18307 29.1619 4.15465 29.0667 4.19075C28.8301 4.28061 28.2341 4.51331 27.8033 4.74678C27.1943 5.07549 26.7711 5.59696 26.6981 6.10768C26.6444 6.48169 26.6666 7.0984 26.6851 7.43248C26.692 7.56381 26.6628 7.69513 26.5991 7.81264C26.5207 7.95856 26.3825 8.19203 26.1721 8.47312C26.0645 8.62134 25.997 8.67587 25.904 8.78569L27.0361 10.1166C27.3087 9.79869 27.5475 9.55753 27.7557 9.32483C28.1358 8.90166 28.2541 8.89782 28.5705 8.88707C28.7656 8.88016 29.0329 8.87171 29.456 8.76573C30.6111 8.47696 30.9767 7.22665 30.992 7.17136L31.2793 6.03549C31.3 5.95331 31.2754 5.86422 31.2155 5.80432L31.2147 5.80355ZM13.4524 13.7324C13.328 14.2178 13.2873 15.0449 12.656 15.0687C12.6038 15.349 12.8503 15.4542 13.0738 15.3644C13.2958 15.2622 13.401 15.445 13.4755 15.627C13.818 15.677 14.3249 15.5126 14.3441 15.1071C13.8326 14.8122 13.6744 14.2516 13.4517 13.7324H13.4524Z" fill="#1C3C3C"/> +</g> +</g> +</g> +</svg> diff --git a/web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg b/web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg new file mode 100644 index 0000000000..8efa791d54 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg @@ -0,0 +1,24 @@ +<svg width="84" height="14" viewBox="0 0 84 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Clip path group"> +<mask id="mask0_20135_16592" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="84" height="14"> +<g id="a"> +<path id="Vector" d="M83.2164 0.600098H0.799805V13.4001H83.2164V0.600098Z" fill="white"/> +</g> +</mask> +<g mask="url(#mask0_20135_16592)"> +<g id="Group"> +<path id="Vector_2" d="M31.0264 3.12256V10.8845H36.3737V9.71251H32.2403V3.12256H31.0264Z" fill="#1C3C3C"/> +<path id="Vector_3" d="M39.2238 4.96436C38.0585 4.96436 37.1871 5.51066 36.8333 6.46298C36.8108 6.52391 36.7427 6.70772 36.7427 6.70772L37.7416 7.35386L37.8773 7.00007C38.1087 6.39693 38.5367 6.11584 39.2238 6.11584C39.911 6.11584 40.3042 6.44916 40.297 7.10554C40.297 7.13216 40.295 7.21255 40.295 7.21255C40.295 7.21255 39.3856 7.36 39.0109 7.43936C37.4119 7.77728 36.7422 8.38759 36.7422 9.38599C36.7422 9.91796 37.0376 10.494 37.5767 10.817C37.9003 11.0106 38.3227 11.0838 38.7892 11.0838C39.0959 11.0838 39.3938 11.0382 39.6698 10.9542C40.297 10.7459 40.4721 10.3363 40.4721 10.3363V10.8718H41.511V7.04308C41.511 5.74157 40.6559 4.96436 39.2238 4.96436ZM40.3011 9.05012C40.3011 9.45255 39.8628 10.0193 38.8419 10.0193C38.5536 10.0193 38.3494 9.94304 38.2132 9.82938C38.0309 9.67732 37.971 9.45869 37.9961 9.26567C38.0068 9.1817 38.0575 9.00096 38.2454 8.84429C38.4374 8.68404 38.7769 8.56935 39.3012 8.45517C39.7323 8.36148 40.3016 8.25805 40.3016 8.25805V9.05063L40.3011 9.05012Z" fill="#1C3C3C"/> +<path id="Vector_4" d="M45.3523 4.96438C45.2079 4.96438 45.0671 4.97462 44.9304 4.99356C44.0001 5.13334 43.7277 5.60591 43.7277 5.60591L43.7287 5.13334H42.5645V10.8729H43.7784V7.68924C43.7784 6.60739 44.5674 6.11484 45.3006 6.11484C46.0932 6.11484 46.4782 6.54083 46.4782 7.41788V10.8729H47.6921V7.25097C47.6921 5.8399 46.7956 4.96387 45.3528 4.96387L45.3523 4.96438Z" fill="#1C3C3C"/> +<path id="Vector_5" d="M52.7575 5.12922V5.72058C52.7575 5.72058 52.4601 4.96436 51.1067 4.96436C49.4253 4.96436 48.3809 6.12455 48.3809 7.99284C48.3809 9.04704 48.7178 9.877 49.3122 10.4013C49.7745 10.8088 50.392 11.0177 51.1272 11.0321C51.6387 11.0418 51.97 10.9025 52.1769 10.7709C52.5742 10.518 52.7217 10.2779 52.7217 10.2779C52.7217 10.2779 52.7048 10.4658 52.6741 10.7203C52.6521 10.9046 52.6106 11.0341 52.6106 11.0341C52.4258 11.692 51.885 12.0725 51.0965 12.0725C50.308 12.0725 49.8303 11.8129 49.7356 11.3014L48.5555 11.6536C48.7592 12.6367 49.6819 13.2234 51.0233 13.2234C51.9352 13.2234 52.65 12.9756 53.1482 12.4861C53.6505 11.9926 53.9054 11.2814 53.9054 10.3721V5.12871H52.7575V5.12922ZM52.6813 8.04455C52.6813 9.19348 52.1201 9.87956 51.18 9.87956C50.1729 9.87956 49.5953 9.19143 49.5953 7.99232C49.5953 6.79322 50.1729 6.11533 51.18 6.11533C52.0976 6.11533 52.6725 6.79834 52.6813 7.89812V8.04455Z" fill="#1C3C3C"/> +<path id="Vector_6" d="M61.022 9.69984C61.1858 9.40237 61.2688 9.05165 61.2688 8.65689C61.2688 8.26214 61.1986 7.95904 61.0599 7.71379C60.9211 7.46752 60.7419 7.2663 60.5279 7.11526C60.3123 6.9632 60.0845 6.84339 59.852 6.7584C59.6186 6.67341 59.404 6.6048 59.2151 6.55513L57.8291 6.16857C57.654 6.12198 57.4789 6.0631 57.3079 5.99296C57.1354 5.9223 56.9884 5.82451 56.8722 5.70215C56.7534 5.57773 56.693 5.41594 56.693 5.21984C56.693 5.01402 56.7632 4.83124 56.9009 4.67661C57.0376 4.52352 57.2199 4.40474 57.4431 4.32333C57.6648 4.24192 57.909 4.2025 58.1691 4.20608C58.4364 4.21325 58.6919 4.26804 58.9279 4.36941C59.1649 4.47079 59.3687 4.62029 59.5336 4.81383C59.6938 5.00224 59.8049 5.23162 59.8643 5.49632L61.2042 5.26336C61.0901 4.80461 60.8955 4.40679 60.6252 4.08116C60.3497 3.74938 60.0026 3.49236 59.593 3.31776C59.1829 3.14266 58.7093 3.05204 58.185 3.04845C57.6689 3.04487 57.1912 3.1273 56.7688 3.2937C56.3474 3.45959 56.008 3.71252 55.7596 4.04532C55.5118 4.3776 55.3859 4.79437 55.3859 5.28384C55.3859 5.61869 55.4417 5.90285 55.5523 6.12916C55.6629 6.35597 55.8072 6.54439 55.9808 6.69031C56.1554 6.83674 56.3428 6.95245 56.5384 7.0354C56.7355 7.11885 56.9214 7.18644 57.0913 7.23559L59.0892 7.82644C59.2335 7.86996 59.3626 7.92218 59.4727 7.98157C59.5838 8.04148 59.6759 8.10906 59.7471 8.18228C59.8188 8.256 59.8741 8.341 59.9109 8.4352C59.9478 8.52941 59.9662 8.63284 59.9662 8.7424C59.9662 8.98765 59.8874 9.19808 59.7312 9.36653C59.5771 9.53344 59.3738 9.66247 59.1276 9.749C58.8823 9.83552 58.6176 9.87956 58.3401 9.87956C57.8716 9.87956 57.4518 9.75156 57.0929 9.49914C56.7391 9.25031 56.5 8.89498 56.3822 8.4434L55.0879 8.64C55.1678 9.12743 55.3516 9.55495 55.6342 9.91028C55.9219 10.2723 56.2947 10.5544 56.7411 10.7484C57.1886 10.943 57.6996 11.0418 58.2587 11.0418C58.6519 11.0418 59.0334 10.9916 59.3933 10.8923C59.7522 10.7935 60.0758 10.6429 60.3548 10.4448C60.6334 10.2477 60.8576 9.99629 61.0209 9.69882L61.022 9.69984Z" fill="#1C3C3C"/> +<path id="Vector_7" d="M67.38 6.22747C67.5479 6.13173 67.7405 6.08309 67.9514 6.08309C68.2939 6.08309 68.5699 6.19777 68.7706 6.42459C68.9708 6.65038 69.0727 6.96629 69.0727 7.36513V10.8309H70.3158V7.04053C70.3158 6.4292 70.1453 5.92897 69.8094 5.55419C69.4741 5.18043 68.9846 4.99048 68.3543 4.99048C67.9734 4.99048 67.6237 5.07547 67.314 5.24289C67.0237 5.40008 66.7856 5.61921 66.6074 5.89365L66.5838 5.93L66.5634 5.89211C66.4226 5.63201 66.2229 5.41953 65.9694 5.26081C65.6832 5.08161 65.3218 4.99048 64.8958 4.99048C64.5082 4.99048 64.1534 5.07649 63.8421 5.24545C63.603 5.37499 63.3987 5.54446 63.2349 5.74824L63.1893 5.80507V5.13435H62.0967V10.8309H63.3506V7.31752C63.3506 6.95451 63.453 6.65499 63.6552 6.42766C63.858 6.19931 64.1309 6.0836 64.4667 6.0836C64.8026 6.0836 65.0903 6.19931 65.2916 6.42766C65.4918 6.65499 65.5931 6.97601 65.5931 7.38101V10.8309H66.8306V7.31752C66.8306 7.06254 66.8803 6.83931 66.9786 6.65345C67.0774 6.46709 67.2126 6.32424 67.3805 6.22798L67.38 6.22747Z" fill="#1C3C3C"/> +<path id="Vector_8" d="M74.2724 9.3726C74.2796 9.6286 74.3487 9.88358 74.4787 10.1293C74.6257 10.3981 74.8438 10.5978 75.1269 10.7222C75.4126 10.8472 75.7408 10.9153 76.1028 10.924C76.4597 10.9327 76.8293 10.9025 77.2016 10.8344V9.80064C76.8514 9.85081 76.5339 9.86412 76.2585 9.83955C75.9682 9.81395 75.7541 9.68851 75.621 9.46732C75.5509 9.35366 75.513 9.20467 75.5074 9.02547C75.5022 8.84985 75.4992 8.64403 75.4992 8.4126V6.04563H77.2016V5.08204H75.4992V3.13184H74.2617V5.08204H73.209V6.04563H74.2617V8.48787C74.2617 8.81657 74.2652 9.11456 74.2724 9.37312V9.3726Z" fill="#1C3C3C"/> +<path id="Vector_9" d="M80.8767 4.95543C80.7436 4.95543 80.6141 4.96414 80.4881 4.98052C79.5726 5.12337 79.3033 5.5898 79.3033 5.5898V5.4531H79.3028V3.11377H78.0889V10.8649H79.3028V7.68132C79.3028 6.5923 80.0918 6.09668 80.825 6.09668C81.6176 6.09668 82.0026 6.52267 82.0026 7.39972V10.8649H83.2165V7.23281C83.2165 5.8499 82.298 4.95595 80.8772 4.95595L80.8767 4.95543Z" fill="#1C3C3C"/> +<path id="Vector_10" d="M72.3934 5.13281H71.1855V10.8775H72.3934V5.13281Z" fill="#1C3C3C"/> +<path id="Vector_11" d="M71.7889 4.70524C72.2236 4.70524 72.5758 4.35291 72.5758 3.91829C72.5758 3.48368 72.2236 3.13135 71.7889 3.13135C71.3542 3.13135 71.002 3.48368 71.002 3.91829C71.002 4.35291 71.3542 4.70524 71.7889 4.70524Z" fill="#1C3C3C"/> +<path id="Vector_12" d="M15.5941 10.4208C15.524 10.5375 15.313 10.5452 15.1333 10.4372C15.0412 10.3819 14.97 10.3056 14.9331 10.2226C14.8993 10.1474 14.8988 10.0762 14.9311 10.0224C14.968 9.96099 15.0442 9.92976 15.1344 9.92976C15.2152 9.92976 15.3074 9.95485 15.3924 10.0061C15.5721 10.1141 15.6648 10.304 15.5946 10.4208H15.5941ZM25.9939 7.0001C25.9939 10.5288 23.1226 13.4001 19.5939 13.4001H7.20859C3.67989 13.4001 0.808594 10.5293 0.808594 7.0001C0.808594 3.47088 3.67989 0.600098 7.20859 0.600098H19.5939C23.1231 0.600098 25.9939 3.47139 25.9939 7.0001ZM13.1427 10.2098C13.2435 10.0875 12.7776 9.74288 12.6824 9.61642C12.4888 9.4065 12.4878 9.10442 12.3573 8.85917C12.0378 8.11882 11.6707 7.3841 11.1571 6.75741C10.6144 6.07184 9.94472 5.50455 9.35643 4.86096C8.9197 4.41194 8.80296 3.77245 8.41743 3.28963C7.88597 2.50474 6.20559 2.29072 5.95931 3.3992C5.96034 3.43402 5.94959 3.45603 5.91937 3.47805C5.78318 3.57687 5.66184 3.69002 5.55995 3.82672C5.3106 4.17386 5.2722 4.76266 5.5835 5.07447C5.59374 4.91011 5.59937 4.75498 5.72942 4.63722C5.97007 4.84407 6.33358 4.91677 6.61262 4.76266C7.22907 5.64279 7.07547 6.86032 7.56494 7.80855C7.70011 8.0328 7.8363 8.26167 8.00987 8.45827C8.15067 8.67741 8.63707 8.93597 8.66574 9.13872C8.67086 9.48688 8.6299 9.8673 8.85825 10.1586C8.96577 10.3767 8.70158 10.5959 8.48859 10.5687C8.21211 10.6066 7.8747 10.3829 7.63252 10.5206C7.54702 10.6133 7.3796 10.5109 7.30587 10.6394C7.28027 10.706 7.14203 10.7997 7.22446 10.8637C7.31611 10.794 7.4011 10.7213 7.52449 10.7628C7.50606 10.8631 7.58542 10.8775 7.6484 10.9067C7.64635 10.9748 7.60641 11.0444 7.65864 11.1022C7.71956 11.0408 7.75592 10.9538 7.85268 10.9282C8.17422 11.3567 8.50139 10.4945 9.1972 10.8826C9.05588 10.8759 8.93044 10.8933 8.83521 11.0096C8.81166 11.0357 8.79169 11.0664 8.83316 11.1002C9.20846 10.858 9.20641 11.1831 9.45012 11.0833C9.63752 10.9855 9.82388 10.8631 10.0466 10.898C9.83003 10.9604 9.82132 11.1345 9.69435 11.2814C9.67284 11.304 9.6626 11.3296 9.68769 11.3669C10.1372 11.3291 10.1741 11.1796 10.5371 10.9963C10.8079 10.8309 11.0778 11.2318 11.3123 11.0034C11.364 10.9538 11.4346 10.9707 11.4986 10.964C11.4167 10.5273 10.5161 11.0439 10.5304 10.4581C10.8202 10.261 10.7537 9.88368 10.7731 9.57904C11.1064 9.76387 11.4771 9.87139 11.8038 10.048C11.9687 10.3143 12.2272 10.666 12.5718 10.643C12.581 10.6164 12.5892 10.5928 12.5989 10.5657C12.7034 10.5836 12.8375 10.6527 12.8949 10.5206C13.051 10.6839 13.2804 10.6757 13.4847 10.6338C13.6357 10.5109 13.2005 10.3358 13.1422 10.2093L13.1427 10.2098ZM17.8147 8.14595L17.1296 7.22128C16.5316 7.90531 16.1322 8.23863 16.1251 8.24477C16.1215 8.24835 15.74 8.61955 15.3924 8.93802C15.0514 9.25034 14.7821 9.49763 14.6449 9.76797C14.607 9.84272 14.5246 10.1182 14.6403 10.3936C14.7294 10.6066 14.9111 10.7582 15.1809 10.8442C15.2618 10.8698 15.3392 10.8811 15.4129 10.8811C15.8988 10.8811 16.2177 10.3936 16.2198 10.3895C16.2239 10.3839 16.6371 9.79357 17.1404 9.0481C17.3078 8.80029 17.4993 8.53815 17.8147 8.14544V8.14595ZM21.0357 10.2754C21.0357 10.1392 20.986 10.0076 20.8959 9.9057L20.8109 9.80944C20.2974 9.2273 18.979 7.73277 18.4659 7.15216C17.8218 6.42307 17.1015 5.49379 17.0416 5.41597L16.955 5.23677V4.92138C16.955 4.80567 16.932 4.69251 16.8874 4.58602L16.7041 4.15082C16.7016 4.14467 16.7006 4.13751 16.7016 4.13085L16.7088 4.07043C16.7098 4.06071 16.7144 4.052 16.7221 4.04535C16.8649 3.91939 17.3964 3.51082 18.2192 3.54512C18.3267 3.54973 18.3456 3.49085 18.3487 3.46576C18.3635 3.34493 18.0876 3.20259 17.8305 3.14986C17.4773 3.07767 16.5383 2.88618 15.7872 3.37923L15.7815 3.38333C15.2961 3.78883 14.906 4.09859 14.9019 4.10167L14.8932 4.11037C14.8876 4.11703 14.7504 4.28138 14.7836 4.49079C14.8051 4.62698 14.7345 4.67562 14.7304 4.67818C14.7263 4.68074 14.63 4.74115 14.5307 4.67306C14.4104 4.58295 14.2015 4.73757 14.159 4.77136L13.8436 5.04272L13.8375 5.04887C13.8318 5.05552 13.6967 5.21373 13.8774 5.46768C14.0336 5.68733 14.0888 5.76055 14.2245 5.92951C14.3623 6.10051 14.6106 6.31709 14.6239 6.32835C14.63 6.33347 14.7816 6.44816 14.9905 6.28842C15.162 6.15683 15.2997 6.03805 15.2997 6.03805C15.311 6.02883 15.4103 5.94691 15.4149 5.82608C15.4165 5.79127 15.4149 5.76106 15.4149 5.73341C15.4124 5.64842 15.4119 5.62333 15.4759 5.58237C15.5066 5.58237 15.6008 5.61667 15.6817 5.65763C15.6904 5.66275 15.8926 5.77642 16.0764 5.76823C16.1921 5.78359 16.3201 5.91517 16.3642 5.96893C16.3683 5.97303 16.7594 6.38365 17.3104 7.10403C17.4153 7.24074 17.8008 7.75427 17.9063 7.89763C18.0824 8.13725 18.3492 8.49975 18.6359 8.8904C19.1326 9.56675 19.6896 10.325 19.9425 10.666C20.0265 10.7792 20.1489 10.856 20.2871 10.8826L20.3808 10.9005C20.4167 10.9072 20.4525 10.9108 20.4883 10.9108C20.6542 10.9108 20.8114 10.8381 20.9153 10.709L20.921 10.7019C20.9957 10.6071 21.0367 10.4858 21.0367 10.3609V10.2754H21.0357ZM21.4765 4.20253L21.3674 4.09347C21.3357 4.06173 21.2912 4.04279 21.2461 4.04637C21.201 4.04842 21.1585 4.0689 21.1294 4.10371L20.4433 4.91523C20.4238 4.93827 20.3962 4.95261 20.3665 4.95568L20.1223 4.98026C20.091 4.98333 20.0598 4.9736 20.0357 4.95363L19.6456 4.62339C19.6205 4.60189 19.6056 4.57117 19.6046 4.5384L19.599 4.34282C19.598 4.31466 19.6077 4.28701 19.6256 4.26551L20.2928 3.46167C20.3429 3.40125 20.3424 3.3137 20.2917 3.25328L20.2247 3.17392C20.1806 3.12221 20.1079 3.10327 20.0444 3.12733C19.8867 3.18723 19.4894 3.34237 19.2022 3.49802C18.7962 3.71715 18.5141 4.0648 18.4654 4.40528C18.4296 4.65463 18.4444 5.06576 18.4567 5.28848C18.4613 5.37603 18.4419 5.46359 18.3994 5.54192C18.3472 5.6392 18.255 5.79485 18.1147 5.98224C18.043 6.08106 17.998 6.11741 17.936 6.19063L18.6907 7.07792C18.8725 6.86595 19.0317 6.70519 19.1704 6.55005C19.4239 6.26794 19.5027 6.26538 19.7137 6.25821C19.8437 6.2536 20.0219 6.24797 20.304 6.17731C21.0741 5.9848 21.3178 5.15127 21.328 5.1144L21.5195 4.35715C21.5333 4.30237 21.5169 4.24298 21.477 4.20304L21.4765 4.20253ZM9.63496 9.48842C9.55202 9.812 9.52488 10.3634 9.10402 10.3793C9.0692 10.5662 9.23355 10.6363 9.38255 10.5764C9.53051 10.5083 9.60066 10.6302 9.65032 10.7515C9.87867 10.7848 10.2166 10.6752 10.2294 10.4049C9.8884 10.2083 9.78293 9.83453 9.63445 9.48842H9.63496Z" fill="#1C3C3C"/> +</g> +</g> +</g> +</svg> diff --git a/web/app/components/base/icons/assets/public/tracing/tracing-icon.svg b/web/app/components/base/icons/assets/public/tracing/tracing-icon.svg new file mode 100644 index 0000000000..b58357f3e9 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/tracing-icon.svg @@ -0,0 +1,6 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="analytics-fill"> +<path id="Vector" opacity="0.6" d="M5 2.5C3.61929 2.5 2.5 3.61929 2.5 5V9.16667H6.15164C6.78293 9.16667 7.36003 9.52333 7.64235 10.088L8.33333 11.4699L10.9213 6.29399C11.0625 6.01167 11.351 5.83333 11.6667 5.83333C11.9823 5.83333 12.2708 6.01167 12.412 6.29399L13.8483 9.16667H17.5V5C17.5 3.61929 16.3807 2.5 15 2.5H5Z" fill="white"/> +<path id="Vector_2" d="M2.5 14.9999C2.5 16.3807 3.61929 17.4999 5 17.4999H15C16.3807 17.4999 17.5 16.3807 17.5 14.9999V10.8333H13.8483C13.2171 10.8333 12.64 10.4766 12.3577 9.91195L11.6667 8.53003L9.07867 13.7059C8.9375 13.9883 8.649 14.1666 8.33333 14.1666C8.01769 14.1666 7.72913 13.9883 7.58798 13.7059L6.15164 10.8333H2.5V14.9999Z" fill="white"/> +</g> +</svg> diff --git a/web/app/components/base/icons/assets/vender/line/arrows/flip-backward.svg b/web/app/components/base/icons/assets/vender/line/arrows/flip-backward.svg deleted file mode 100644 index f5d8da0c79..0000000000 --- a/web/app/components/base/icons/assets/vender/line/arrows/flip-backward.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/web/app/components/base/icons/assets/vender/line/arrows/flip-forward.svg b/web/app/components/base/icons/assets/vender/line/arrows/flip-forward.svg deleted file mode 100644 index 996245b3b4..0000000000 --- a/web/app/components/base/icons/assets/vender/line/arrows/flip-forward.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g id="Icon"> -<path id="Icon_2" d="M14 6.00016H5C3.34315 6.00016 2 7.34331 2 9.00016C2 10.657 3.34315 12.0002 5 12.0002H8M14 6.00016L11.3333 3.3335M14 6.00016L11.3333 8.66683" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/web/app/components/base/icons/assets/vender/solid/mapsAndTravel/globe-06.svg b/web/app/components/base/icons/assets/vender/solid/mapsAndTravel/globe-06.svg new file mode 100644 index 0000000000..45f3778f0b --- /dev/null +++ b/web/app/components/base/icons/assets/vender/solid/mapsAndTravel/globe-06.svg @@ -0,0 +1,8 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Icon"> +<g id="Solid"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M6.39498 2.71706C6.90587 2.57557 7.44415 2.49996 8.00008 2.49996C9.30806 2.49996 10.5183 2.91849 11.5041 3.62893C10.9796 3.97562 10.5883 4.35208 10.3171 4.75458C9.90275 5.36959 9.79654 6.00558 9.88236 6.58587C9.96571 7.1494 10.2245 7.63066 10.4965 7.98669C10.7602 8.33189 11.0838 8.6206 11.3688 8.76305C12.0863 9.12177 12.9143 9.30141 13.5334 9.39399C14.0933 9.47774 15.2805 9.75802 15.3244 8.86608C15.3304 8.74474 15.3334 8.62267 15.3334 8.49996C15.3334 4.44987 12.0502 1.16663 8.00008 1.16663C3.94999 1.16663 0.666748 4.44987 0.666748 8.49996C0.666748 12.55 3.94999 15.8333 8.00008 15.8333C8.1228 15.8333 8.24486 15.8303 8.3662 15.8243C8.73395 15.8062 9.01738 15.4934 8.99927 15.1256C8.98117 14.7579 8.66837 14.4745 8.30063 14.4926C8.20111 14.4975 8.10091 14.5 8.00008 14.5C5.6605 14.5 3.63367 13.1609 2.6442 11.2074L3.28991 10.8346L5.67171 11.2804C6.28881 11.3959 6.85846 10.9208 6.85566 10.293L6.84632 8.19093L8.06357 6.10697C8.26079 5.76932 8.24312 5.3477 8.01833 5.02774L6.39498 2.71706Z" fill="#1570EF"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29718 8.93736C9.05189 8.84432 8.77484 8.90379 8.58934 9.08929C8.40383 9.27479 8.34437 9.55184 8.43741 9.79713L10.5486 15.363C10.6461 15.6199 10.8912 15.7908 11.166 15.7932C11.4408 15.7956 11.689 15.6292 11.791 15.374L12.6714 13.1714L14.874 12.2909C15.1292 12.1889 15.2957 11.9408 15.2932 11.666C15.2908 11.3912 15.12 11.146 14.863 11.0486L9.29718 8.93736Z" fill="#1570EF"/> +</g> +</g> +</svg> diff --git a/web/app/components/base/icons/src/public/tracing/LangfuseIcon.json b/web/app/components/base/icons/src/public/tracing/LangfuseIcon.json new file mode 100644 index 0000000000..ab0b8fbc1c --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangfuseIcon.json @@ -0,0 +1,236 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "74", + "height": "16", + "viewBox": "0 0 74 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask0_20135_12984", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "0", + "width": "96", + "height": "16" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "clip0_823_291" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M95.5733 0H0V16H95.5733V0Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask0_20135_12984)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M21.2832 11.5431V3.72656H22.3735V10.4972H26.3932V11.5431H21.2832ZM27.6995 7.44766C27.9198 6.31372 28.8889 5.5761 30.1224 5.5761C31.543 5.5761 32.4791 6.40179 32.4791 8.02014V10.233C32.4791 10.4862 32.5893 10.5963 32.8316 10.5963H33.0849V11.5431H32.7765C32.0717 11.5431 31.6532 11.1688 31.543 10.6513C31.3228 11.1908 30.64 11.6752 29.7259 11.6752C28.5475 11.6752 27.6004 11.0587 27.6004 10.0128C27.6004 8.80179 28.4924 8.46051 29.836 8.2073L31.4109 7.89904C31.3999 7.0073 30.8933 6.56693 30.1114 6.56693C29.4506 6.56693 28.966 6.96326 28.8338 7.52473L27.6995 7.44766ZM28.7237 9.99078C28.7347 10.3981 29.0871 10.7394 29.8581 10.7394C30.7391 10.7394 31.4329 10.1229 31.4329 9.07702V8.82381L30.1774 9.04399C29.3625 9.18711 28.7237 9.25317 28.7237 9.99078ZM34.5453 5.70821H35.5255L35.5585 6.68803C35.8669 5.93941 36.5166 5.5761 37.2986 5.5761C38.5981 5.5761 39.2369 6.5339 39.2369 7.78895V11.5431H38.1686V8.06418C38.1686 7.02931 37.8272 6.48987 37.0232 6.48987C36.1752 6.48987 35.6136 7.02931 35.6136 8.06418V11.5431H34.5453V5.70821ZM43.2303 11.2348C41.7876 11.2348 40.7634 10.0789 40.7634 8.43849C40.7634 6.74308 41.7876 5.5761 43.2303 5.5761C44.0122 5.5761 44.6951 5.99445 44.9594 6.59996L44.9704 5.70821H45.9946V10.9045C45.9836 12.5009 44.9704 13.3266 43.4065 13.3266C42.129 13.3266 41.2039 12.655 40.9286 11.6422L42.0519 11.5651C42.2832 12.0715 42.7347 12.3688 43.4065 12.3688C44.3536 12.3688 44.9153 11.9394 44.9263 11.1357V10.266C44.629 10.8275 43.9241 11.2348 43.2303 11.2348ZM41.8867 8.42748C41.8867 9.5284 42.4704 10.299 43.4286 10.299C44.3647 10.299 44.9373 9.5284 44.9483 8.42748C44.9704 7.33757 44.3867 6.56693 43.4286 6.56693C42.4704 6.56693 41.8867 7.33757 41.8867 8.42748ZM48.9967 5.455C48.9967 4.3761 49.5364 3.72656 50.7258 3.72656H52.3337V4.67335H50.7038C50.3293 4.67335 50.065 4.95959 50.065 5.43298V6.08253H52.2566V7.02931H50.065V11.5431H48.9967V7.02931H47.4659V6.08253H48.9967V5.455ZM58.9041 11.5431H57.8909L57.8798 10.5963C57.5715 11.3229 56.9327 11.6752 56.1838 11.6752C54.9063 11.6752 54.2786 10.7174 54.2786 9.46234V5.70821H55.3468V9.18711C55.3468 10.222 55.6883 10.7614 56.4592 10.7614C57.2851 10.7614 57.8358 10.222 57.8358 9.18711V5.70821H58.9041V11.5431ZM64.5277 7.53574C64.4065 6.91922 63.8338 6.56693 63.151 6.56693C62.5894 6.56693 62.0718 6.84216 62.0828 7.38161C62.0828 7.9651 62.7876 8.09721 63.4374 8.26234C64.5497 8.53757 65.662 8.94491 65.662 10.0348C65.662 11.1798 64.5607 11.6752 63.3493 11.6752C61.9837 11.6752 60.8713 10.9045 60.7832 9.69354L61.9066 9.62748C62.0167 10.277 62.6004 10.6844 63.3493 10.6844C63.933 10.6844 64.5387 10.5302 64.5387 9.97977C64.5387 9.4073 63.8008 9.30821 63.151 9.15409C62.0497 8.88987 60.9594 8.48253 60.9594 7.42565C60.9594 6.24766 62.0167 5.5761 63.2502 5.5761C64.4836 5.5761 65.4417 6.31372 65.629 7.46968L64.5277 7.53574ZM67.2104 8.62565C67.2104 6.76509 68.2787 5.5761 69.9196 5.5761C71.2302 5.5761 72.4196 6.42381 72.5077 8.52656V8.9339H68.3448C68.4329 10.0348 68.9945 10.6844 69.9196 10.6844C70.5033 10.6844 71.032 10.3431 71.2853 9.75959L72.4196 9.85867C72.0892 10.9706 71.087 11.6752 69.9196 11.6752C68.2787 11.6752 67.2104 10.4862 67.2104 8.62565ZM68.3778 8.07519H71.3403C71.1861 6.96326 70.5804 6.56693 69.9196 6.56693C69.0716 6.56693 68.532 7.1284 68.3778 8.07519Z", + "fill": "black" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group_2" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask1_20135_12984", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "-1", + "width": "17", + "height": "18" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "clip1_823_291" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M16.3621 -0.0512695H0.203125V16.1021H16.3621V-0.0512695Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask1_20135_12984)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group_2" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M14.6259 11.2357C13.9141 12.1984 12.8241 12.8406 11.5941 12.9344C11.5558 12.937 11.5175 12.9397 11.4787 12.9419C10.0365 13.0136 8.94706 12.3558 8.22466 11.7452C6.94631 11.0687 5.94609 10.8983 5.36089 10.751C4.93532 10.6438 4.56293 10.4296 4.40334 10.3225C4.26183 10.2384 3.97722 10.0434 3.76496 9.67965C3.52716 9.27204 3.51333 8.88257 3.51706 8.71705C3.641 8.70048 3.80113 8.68224 3.98839 8.67048C4.1416 8.66082 4.29002 8.65709 4.45654 8.65652C5.74819 8.65494 6.7499 8.71812 7.47874 9.0417C7.87295 9.21632 8.23842 9.4488 8.56395 9.73215C8.98265 10.0975 9.83862 10.6749 10.8935 10.4778C11.0276 10.4526 11.1563 10.4194 11.2803 10.3787C11.6601 10.3241 12.3097 10.2801 13.0747 10.4831C13.8008 10.676 14.3232 11.0092 14.6259 11.2357Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "1.70667", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M14.53 4.60662C14.2091 4.19101 13.819 3.79812 13.3584 3.53003C12.8675 3.2445 12.2411 2.99862 11.4835 2.93199C9.63248 2.76913 8.36691 3.79548 8.13634 3.98954C7.84947 4.25868 6.70187 5.21101 5.32048 5.73977C5.07981 5.82545 4.61653 6.02793 4.20477 6.48007C3.87909 6.83749 3.7197 7.20339 3.6416 7.43076C3.80631 7.45351 3.97632 7.46992 4.15164 7.47994C5.49102 7.55452 6.64184 7.56193 7.39466 7.19337C7.89196 6.95015 8.32815 6.60377 8.70431 6.1982C9.38222 5.4669 10.3709 5.14067 11.271 5.36436C11.6843 5.42197 12.4042 5.46588 13.2368 5.21101C13.8116 5.03492 14.2399 4.81337 14.53 4.60662Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "1.70667", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M1.96963 4.91518C1.45614 4.65135 1.05528 4.347 0.781876 4.10874C0.629046 3.97549 0.391602 4.08476 0.391602 4.28837V5.95295C0.391602 6.02543 0.424389 6.09419 0.480445 6.13896L1.16264 6.79512C1.19859 6.53125 1.2758 6.17255 1.44926 5.77597C1.61267 5.40184 1.80886 5.11558 1.96963 4.91518Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "1.70667", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M12.9521 8.63005C13.0302 8.38964 13.0735 8.13742 13.0799 7.8804C13.0853 7.67736 13.0617 7.6015 13.0264 7.4049C13.3895 7.34397 13.8428 7.24459 14.2561 7.1377C14.6929 7.02499 15.0158 6.89407 15.3789 6.76367C15.4318 7.01747 15.4874 7.14092 15.5067 7.32899C15.5248 7.50642 15.5361 7.69019 15.5392 7.8804C15.5489 8.47138 15.4767 9.0073 15.3655 9.47698C15.0233 9.29954 14.617 9.11577 14.1492 8.95439C13.714 8.8037 13.3093 8.70115 12.9521 8.63005Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "1.70667", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M0.766014 12.1447C0.609481 12.2583 0.391602 12.1463 0.391602 11.9516V9.90721C0.391602 9.84531 0.415399 9.78667 0.456648 9.74292C0.477272 9.72104 0.49631 9.70877 0.504771 9.70397L1.18061 9.41382C1.23032 9.6848 1.3123 10.0091 1.44926 10.3622C1.6095 10.775 1.79987 11.1094 1.96963 11.3638C1.56825 11.6241 1.16686 11.8844 0.766014 12.1447Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "1.70667", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M5.11863 3.21273C6.47036 3.1457 7.48116 3.71166 8.00219 4.08992C7.60778 4.43817 6.54047 5.27853 5.27812 5.76389C5.19298 5.79436 5.08001 5.83945 4.95131 5.90513C4.03786 6.35487 3.49469 7.25118 3.47044 8.20872C3.46637 8.3699 3.4746 8.53046 3.49592 8.68826C3.49361 8.68857 3.49131 8.68888 3.48903 8.68918C3.48531 8.85338 3.49914 9.23978 3.73679 9.64428C3.94894 10.0051 4.23338 10.1986 4.37481 10.282C4.44499 10.3288 4.55634 10.3962 4.69529 10.466C4.8585 10.5529 5.03948 10.6258 5.2391 10.6822C5.26968 10.6911 5.30062 10.6995 5.33181 10.7072C5.40448 10.7254 5.48364 10.7442 5.56903 10.7644C6.17131 10.9074 7.08394 11.1238 8.20285 11.7118C8.31591 11.8066 8.43766 11.9022 8.56827 11.9956C8.52858 12.0311 8.49519 12.0621 8.46819 12.0875C8.23747 12.2826 6.97098 13.3142 5.11863 13.1505C4.36047 13.0836 3.73309 12.8364 3.24236 12.5494C2.4156 12.0663 1.79088 11.302 1.45008 10.4075C1.2305 9.83086 1.03909 9.08515 1.02527 8.20765C1.01304 7.45826 1.1332 6.79817 1.29696 6.25074C1.79833 4.57812 3.26043 3.35145 5.00327 3.22017L5.00335 3.22016C5.0416 3.21751 5.07986 3.21485 5.11863 3.21273ZM14.5861 11.1844C14.2827 10.9597 13.7622 10.6316 13.0411 10.4415C12.2766 10.2401 11.6274 10.2837 11.2478 10.3378C11.1239 10.3782 10.9952 10.4112 10.8613 10.4362C9.80694 10.6318 8.95148 10.0588 8.53303 9.69637C8.45168 9.62603 8.36781 9.55891 8.28165 9.49501C8.56326 9.2476 8.87288 9.03413 9.21043 8.8683C9.96382 8.49841 11.1154 8.50582 12.4558 8.58025C14.3028 8.68336 15.5788 9.56295 16.0882 9.96688C16.145 10.0121 16.1775 10.0801 16.1775 10.1524V11.8123C16.1775 12.0158 15.9388 12.1247 15.7851 11.9914C15.5098 11.7531 15.1049 11.4483 14.5861 11.1844ZM8.66435 6.22472C8.54326 6.35584 8.41593 6.48083 8.28237 6.59819C8.54101 6.79004 8.82057 6.95244 9.11629 7.08249C9.84473 7.40351 10.8459 7.46623 12.1367 7.46465C14.0301 7.46199 15.4241 6.74925 16.0637 6.36126C16.1344 6.31822 16.1775 6.2417 16.1775 6.15878V4.12158C16.1775 3.92758 15.9585 3.81597 15.8011 3.92917C15.4285 4.19722 14.8745 4.53933 14.1601 4.80844C13.9028 4.96005 13.5822 5.11485 13.2001 5.23242C12.367 5.48857 11.6466 5.44446 11.2329 5.38654C10.3323 5.16172 9.34277 5.48964 8.66435 6.22472Z", + "fill": "#E11312" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_10", + "d": "M8.00166 4.09005L8.13707 4.2433L8.32826 4.07447L8.12183 3.92461L8.00166 4.09005ZM5.11809 3.21286L5.10798 3.00864L5.10745 3.00866L5.10692 3.0087L5.11809 3.21286ZM5.27759 5.76403L5.34647 5.95659L5.34877 5.95577L5.35102 5.9549L5.27759 5.76403ZM4.95078 5.90527L5.04115 6.08868L5.04247 6.08807L5.04379 6.0874L4.95078 5.90527ZM3.49538 8.6884L3.52217 8.89108L3.72555 8.86425L3.69809 8.661L3.49538 8.6884ZM3.4885 8.68932L3.46154 8.48664L3.28798 8.50969L3.28401 8.68467L3.4885 8.68932ZM4.37427 10.2822L4.48774 10.112L4.48307 10.1089L4.47824 10.1061L4.37427 10.2822ZM4.69475 10.4661L4.79093 10.2857L4.78879 10.2845L4.78663 10.2834L4.69475 10.4661ZM5.23857 10.6823L5.29549 10.486L5.29487 10.4858L5.29421 10.4856L5.23857 10.6823ZM8.20232 11.7119L8.33384 11.5553L8.31701 11.5412L8.29748 11.5309L8.20232 11.7119ZM8.56773 11.9957L8.70411 12.1481L8.89429 11.978L8.68678 11.8295L8.56773 11.9957ZM8.46766 12.0877L8.59975 12.2438L8.60404 12.2402L8.60808 12.2364L8.46766 12.0877ZM3.24183 12.5496L3.34511 12.3731L3.34505 12.373L3.24183 12.5496ZM1.02474 8.20779L1.22926 8.20456L1.22925 8.20446L1.02474 8.20779ZM1.29642 6.25088L1.10049 6.19214L1.10045 6.1923L1.29642 6.25088ZM5.00274 3.2203L4.98903 3.01629L4.9882 3.01635L4.98737 3.01641L5.00274 3.2203ZM5.00281 3.2203L5.01652 3.42431L5.01698 3.42428L5.00281 3.2203ZM13.0406 10.4417L13.0928 10.2439H13.0927L13.0406 10.4417ZM14.5855 11.1845L14.4638 11.3488L14.4775 11.359L14.4928 11.3667L14.5855 11.1845ZM11.2473 10.338L11.2183 10.1356L11.2007 10.1381L11.1838 10.1436L11.2473 10.338ZM10.8607 10.4363L10.8981 10.6373H10.8982L10.8607 10.4363ZM8.5325 9.6965L8.66648 9.54197L8.66627 9.54177L8.5325 9.6965ZM8.28112 9.49515L8.14612 9.34159L7.95594 9.50864L8.15931 9.65939L8.28112 9.49515ZM12.4553 8.58039L12.4667 8.37622H12.4666L12.4553 8.58039ZM16.0877 9.96702L16.2149 9.80692L16.2148 9.80687L16.0877 9.96702ZM15.7846 11.9915L15.9187 11.8371L15.9185 11.8369L15.7846 11.9915ZM8.28183 6.59833L8.14678 6.44477L7.95666 6.61177L8.15998 6.76257L8.28183 6.59833ZM9.11576 7.08262L9.19829 6.89553L9.19814 6.89548L9.11576 7.08262ZM12.1362 7.46478L12.1365 7.66925H12.1365L12.1362 7.46478ZM16.0632 6.3614L16.1693 6.53622L16.1696 6.53607L16.0632 6.3614ZM14.1596 4.80857L14.0874 4.61723L14.0709 4.62346L14.0557 4.63242L14.1596 4.80857ZM11.2324 5.38667L11.1828 5.58506L11.1933 5.58767L11.204 5.58915L11.2324 5.38667ZM8.12183 3.92461C7.57989 3.53114 6.52347 2.93845 5.10798 3.00864L5.12822 3.41708C6.41618 3.35322 7.38138 3.89245 7.88144 4.25549L8.12183 3.92461ZM5.35102 5.9549C6.64538 5.45722 7.73371 4.59944 8.13707 4.2433L7.86625 3.9368C7.48074 4.27718 6.43449 5.10015 5.20416 5.5732L5.35102 5.9549ZM5.04379 6.0874C5.16309 6.02647 5.26772 5.98471 5.34647 5.95659L5.20871 5.57152C5.11717 5.60423 4.99585 5.65269 4.85776 5.72318L5.04379 6.0874ZM3.67439 8.21402C3.69676 7.3308 4.19746 6.50412 5.04115 6.08868L4.8604 5.72186C3.87719 6.20595 3.29156 7.17188 3.26543 8.2037L3.67439 8.21402ZM3.69809 8.661C3.6783 8.51455 3.67058 8.36487 3.67439 8.21402L3.26543 8.2037C3.2611 8.3752 3.26984 8.5467 3.29268 8.71575L3.69809 8.661ZM3.51546 8.892C3.5177 8.8917 3.51993 8.89139 3.52217 8.89108L3.4686 8.48566C3.46623 8.48597 3.46387 8.48633 3.46154 8.48664L3.51546 8.892ZM3.91263 9.54085C3.70211 9.18256 3.68969 8.83956 3.69299 8.69392L3.28401 8.68467C3.27987 8.86752 3.29509 9.29732 3.55989 9.74798L3.91263 9.54085ZM4.47824 10.1061C4.35261 10.032 4.10041 9.86028 3.91261 9.54079L3.55989 9.74798C3.79637 10.1503 4.11309 10.3655 4.2703 10.4583L4.47824 10.1061ZM4.78663 10.2834C4.6552 10.2174 4.55104 10.1543 4.48774 10.112L4.26081 10.4523C4.33787 10.5037 4.45643 10.5752 4.60289 10.6488L4.78663 10.2834ZM5.29421 10.4856C5.10788 10.4329 4.94058 10.3654 4.79093 10.2857L4.59858 10.6466C4.77536 10.7407 4.97 10.819 5.18294 10.8791L5.29421 10.4856ZM5.38088 10.509C5.35225 10.5019 5.32376 10.4941 5.29549 10.486L5.18161 10.8787C5.21454 10.8883 5.24788 10.8973 5.28168 10.9058L5.38088 10.509ZM5.61575 10.5656C5.53005 10.5453 5.45212 10.5268 5.38088 10.509L5.28168 10.9058C5.35572 10.9243 5.43616 10.9433 5.52125 10.9635L5.61575 10.5656ZM8.29748 11.5309C7.155 10.9306 6.22187 10.7094 5.61575 10.5656L5.52125 10.9635C6.11975 11.1055 7.01177 11.3174 8.10715 11.8929L8.29748 11.5309ZM8.68678 11.8295C8.56093 11.7394 8.44327 11.6471 8.33384 11.5553L8.07085 11.8685C8.18744 11.9664 8.31338 12.0652 8.44864 12.162L8.68678 11.8295ZM8.60808 12.2364C8.63406 12.2119 8.66607 12.1821 8.70411 12.1481L8.4313 11.8434C8.39004 11.8803 8.35526 11.9126 8.32724 11.939L8.60808 12.2364ZM5.10009 13.3543C7.03682 13.5255 8.35798 12.4482 8.59975 12.2438L8.33558 11.9315C8.11585 12.1173 6.90412 13.1032 5.13615 12.947L5.10009 13.3543ZM3.13854 12.726C3.65082 13.0256 4.30703 13.2843 5.10011 13.3544L5.13615 12.947C4.4129 12.8831 3.8143 12.6475 3.34511 12.3731L3.13854 12.726ZM1.25838 10.4804C1.61483 11.416 2.26927 12.2181 3.1386 12.7261L3.34505 12.373C2.56087 11.9148 1.96586 11.1883 1.64069 10.3349L1.25838 10.4804ZM0.820219 8.21101C0.834481 9.11662 1.03203 9.88594 1.25838 10.4804L1.64071 10.3349C1.4279 9.77599 1.24263 9.05395 1.22926 8.20456L0.820219 8.21101ZM1.10045 6.1923C0.93163 6.75664 0.807599 7.43774 0.820219 8.21116L1.22925 8.20446C1.21742 7.47904 1.3337 6.83991 1.49239 6.30946L1.10045 6.1923ZM4.98737 3.01641C3.15623 3.15434 1.62504 4.44222 1.10049 6.19214L1.49236 6.30956C1.97055 4.7143 3.36357 3.54883 5.0181 3.4242L4.98737 3.01641ZM4.9891 3.01629L4.98903 3.01629L5.01644 3.42432L5.01652 3.42431L4.9891 3.01629ZM5.10692 3.0087C5.0664 3.01091 5.02666 3.01368 4.98864 3.01632L5.01698 3.42428C5.05547 3.42161 5.09225 3.41906 5.12929 3.41703L5.10692 3.0087ZM12.9885 10.6393C13.6767 10.8208 14.1738 11.134 14.4638 11.3488L14.7073 11.0202C14.3904 10.7855 13.8465 10.4426 13.0928 10.2439L12.9885 10.6393ZM11.2762 10.5404C11.6387 10.4886 12.2586 10.4471 12.9885 10.6393L13.0927 10.2439C12.2935 10.0333 11.6151 10.0789 11.2183 10.1356L11.2762 10.5404ZM10.8982 10.6373C11.0409 10.6107 11.1782 10.5756 11.3107 10.5324L11.1838 10.1436C11.0685 10.1812 10.9485 10.2119 10.8232 10.2353L10.8982 10.6373ZM8.39858 9.85098C8.83155 10.2261 9.75005 10.8503 10.8981 10.6373L10.8234 10.2353C9.86276 10.4135 9.07035 9.89182 8.66648 9.54197L8.39858 9.85098ZM8.15931 9.65939C8.24138 9.72027 8.32126 9.78422 8.39873 9.85118L8.66627 9.54177C8.58108 9.46816 8.49323 9.39782 8.40297 9.3309L8.15931 9.65939ZM9.1197 8.68492C8.76425 8.85959 8.43969 9.08364 8.14612 9.34159L8.41617 9.64876C8.68576 9.41187 8.98056 9.20894 9.30011 9.05195L9.1197 8.68492ZM12.4666 8.37622C11.7952 8.33895 11.162 8.31784 10.5994 8.35373C10.0385 8.38951 9.53134 8.4828 9.1197 8.68492L9.30011 9.05195C9.64185 8.88418 10.0872 8.79621 10.6255 8.76186C11.1622 8.72761 11.7749 8.74739 12.4439 8.78455L12.4666 8.37622ZM16.2148 9.80687C15.6896 9.39035 14.3735 8.4827 12.4667 8.37622L12.4438 8.78455C14.231 8.88428 15.467 9.73586 15.9605 10.1272L16.2148 9.80687ZM16.3815 10.1525C16.3815 10.0185 16.3211 9.89131 16.2149 9.80692L15.9604 10.1271C15.9679 10.1331 15.9724 10.1419 15.9724 10.1525H16.3815ZM16.3815 11.8124V10.1525H15.9724V11.8124H16.3815ZM15.6504 12.1459C15.9368 12.3945 16.3815 12.1909 16.3815 11.8124H15.9724C15.9724 11.822 15.9699 11.8273 15.9676 11.8307C15.9648 11.8349 15.9601 11.8393 15.9534 11.8423C15.9468 11.8453 15.9404 11.846 15.9355 11.8455C15.9315 11.8449 15.926 11.8434 15.9187 11.8371L15.6504 12.1459ZM14.4928 11.3667C14.9936 11.6215 15.3848 11.916 15.6507 12.1461L15.9185 11.8369C15.6338 11.5905 15.2152 11.2754 14.6783 11.0023L14.4928 11.3667ZM8.41683 6.75194C8.55613 6.62956 8.68852 6.49957 8.81416 6.36354L8.51353 6.08612C8.39694 6.21239 8.27472 6.33236 8.14678 6.44477L8.41683 6.75194ZM9.19814 6.89548C8.91638 6.77157 8.65006 6.61683 8.40369 6.43414L8.15998 6.76257C8.43089 6.96352 8.7237 7.13359 9.03343 7.26982L9.19814 6.89548ZM12.136 7.26031C10.8405 7.26189 9.88163 7.19672 9.19829 6.89553L9.03328 7.26972C9.80676 7.61062 10.8502 7.67084 12.1365 7.66925L12.136 7.26031ZM15.9571 6.18662C15.3346 6.56423 13.9777 7.2577 12.136 7.26031L12.1365 7.66925C14.0813 7.66655 15.5126 6.93458 16.1693 6.53622L15.9571 6.18662ZM15.9724 6.15892C15.9724 6.17047 15.9666 6.18085 15.9568 6.18678L16.1696 6.53607C16.3012 6.45591 16.3815 6.31319 16.3815 6.15892H15.9724ZM15.9724 4.12171V6.15892H16.3815V4.12171H15.9724ZM15.92 4.09528C15.9427 4.07894 15.9724 4.09516 15.9724 4.12171H16.3815C16.3815 3.76028 15.9731 3.55327 15.6811 3.76334L15.92 4.09528ZM14.2317 4.99991C14.9668 4.72302 15.5366 4.37113 15.92 4.09528L15.6811 3.76334C15.3193 4.02358 14.7812 4.35591 14.0874 4.61723L14.2317 4.99991ZM13.2597 5.42798C13.6594 5.30504 13.9946 5.14315 14.2634 4.98473L14.0557 4.63242C13.8099 4.77723 13.5039 4.92497 13.1394 5.03712L13.2597 5.42798ZM11.204 5.58915C11.6356 5.64963 12.3885 5.69589 13.2597 5.42798L13.1395 5.03711C12.3443 5.28157 11.6564 5.23961 11.2608 5.18419L11.204 5.58915ZM8.81416 6.36354C9.44768 5.67713 10.3626 5.38033 11.1828 5.58506L11.2819 5.18828C10.3008 4.94339 9.23685 5.30248 8.51348 6.08617L8.81416 6.36354Z", + "fill": "black" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + "name": "LangfuseIcon" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/arrows/FlipBackward.tsx b/web/app/components/base/icons/src/public/tracing/LangfuseIcon.tsx similarity index 85% rename from web/app/components/base/icons/src/vender/line/arrows/FlipBackward.tsx rename to web/app/components/base/icons/src/public/tracing/LangfuseIcon.tsx index e2c0ba6e8c..38e763eb61 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/FlipBackward.tsx +++ b/web/app/components/base/icons/src/public/tracing/LangfuseIcon.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './FlipBackward.json' +import data from './LangfuseIcon.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' @@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP ref, ) => <IconBase {...props} ref={ref} data={data as IconData} />) -Icon.displayName = 'FlipBackward' +Icon.displayName = 'LangfuseIcon' export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.json b/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.json new file mode 100644 index 0000000000..0fee622bd8 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.json @@ -0,0 +1,236 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "111", + "height": "24", + "viewBox": "0 0 111 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask0_20135_18315", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "0", + "width": "144", + "height": "24" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "clip0_823_291" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M143.36 0H0V24H143.36V0Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask0_20135_18315)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M31.9258 17.3144V5.5896H33.5612V15.7456H39.5908V17.3144H31.9258ZM41.5502 11.1713C41.8806 9.47033 43.3343 8.36391 45.1845 8.36391C47.3155 8.36391 48.7197 9.60244 48.7197 12.03V15.3492C48.7197 15.729 48.8849 15.8942 49.2483 15.8942H49.6283V17.3144H49.1657C48.1085 17.3144 47.4807 16.7529 47.3155 15.9768C46.9852 16.7859 45.9609 17.5125 44.5898 17.5125C42.8222 17.5125 41.4016 16.5878 41.4016 15.019C41.4016 13.2024 42.7396 12.6905 44.755 12.3107L47.1173 11.8483C47.1008 10.5107 46.3409 9.85015 45.168 9.85015C44.1768 9.85015 43.45 10.4446 43.2517 11.2868L41.5502 11.1713ZM43.0865 14.9859C43.1031 15.5969 43.6317 16.1089 44.7881 16.1089C46.1096 16.1089 47.1503 15.1841 47.1503 13.6153V13.2355L45.2671 13.5657C44.0447 13.7804 43.0865 13.8795 43.0865 14.9859ZM51.8189 8.56208H53.2892L53.3387 10.0318C53.8013 8.90887 54.7759 8.36391 55.9488 8.36391C57.8981 8.36391 58.8563 9.80061 58.8563 11.6832V17.3144H57.2539V12.096C57.2539 10.5437 56.7418 9.73455 55.5358 9.73455C54.2638 9.73455 53.4213 10.5437 53.4213 12.096V17.3144H51.8189V8.56208ZM64.8465 16.852C62.6824 16.852 61.1461 15.118 61.1461 12.6575C61.1461 10.1144 62.6824 8.36391 64.8465 8.36391C66.0193 8.36391 67.0436 8.99143 67.44 9.89969L67.4565 8.56208H68.9929V16.3566C68.9763 18.7511 67.4565 19.9896 65.1108 19.9896C63.1945 19.9896 61.8069 18.9823 61.3939 17.463L63.0789 17.3474C63.4258 18.107 64.1031 18.5529 65.1108 18.5529C66.5315 18.5529 67.3739 17.9089 67.3905 16.7034V15.3988C66.9444 16.241 65.8872 16.852 64.8465 16.852ZM62.8311 12.641C62.8311 14.2924 63.7066 15.4483 65.1438 15.4483C66.548 15.4483 67.407 14.2924 67.4235 12.641C67.4565 11.0061 66.581 9.85015 65.1438 9.85015C63.7066 9.85015 62.8311 11.0061 62.8311 12.641ZM73.4961 8.18226C73.4961 6.56391 74.3055 5.5896 76.0897 5.5896H78.5015V7.00978H76.0566C75.495 7.00978 75.0985 7.43914 75.0985 8.14923V9.12354H78.3859V10.5437H75.0985V17.3144H73.4961V10.5437H71.1999V9.12354H73.4961V8.18226ZM88.3571 17.3144H86.8373L86.8207 15.8942C86.3582 16.9841 85.4001 17.5125 84.2767 17.5125C82.3605 17.5125 81.4189 16.0758 81.4189 14.1933V8.56208H83.0212V13.7804C83.0212 15.3327 83.5334 16.1419 84.6897 16.1419C85.9287 16.1419 86.7547 15.3327 86.7547 13.7804V8.56208H88.3571V17.3144ZM96.7925 11.3034C96.6108 10.3786 95.7518 9.85015 94.7275 9.85015C93.885 9.85015 93.1086 10.263 93.1251 11.0722C93.1251 11.9474 94.1824 12.1456 95.1571 12.3933C96.8255 12.8061 98.494 13.4171 98.494 15.052C98.494 16.7694 96.842 17.5125 95.0249 17.5125C92.9765 17.5125 91.308 16.3566 91.1758 14.5401L92.8608 14.441C93.026 15.4153 93.9016 16.0263 95.0249 16.0263C95.9004 16.0263 96.809 15.7951 96.809 14.9694C96.809 14.1107 95.7022 13.9621 94.7275 13.7309C93.0756 13.3346 91.4402 12.7235 91.4402 11.1382C91.4402 9.37125 93.026 8.36391 94.8762 8.36391C96.7264 8.36391 98.1636 9.47033 98.4444 11.2043L96.7925 11.3034ZM100.817 12.9382C100.817 10.1474 102.419 8.36391 104.88 8.36391C106.846 8.36391 108.63 9.63547 108.763 12.7896V13.4006H102.518C102.65 15.052 103.493 16.0263 104.88 16.0263C105.756 16.0263 106.549 15.5144 106.929 14.6391L108.63 14.7878C108.135 16.4557 106.632 17.5125 104.88 17.5125C102.419 17.5125 100.817 15.729 100.817 12.9382ZM102.568 12.1125H107.011C106.78 10.4446 105.872 9.85015 104.88 9.85015C103.608 9.85015 102.799 10.6924 102.568 12.1125Z", + "fill": "black" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group_2" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask1_20135_18315", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "-1", + "width": "25", + "height": "26" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "clip1_823_291" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M24.5471 -0.0771484H0.308594V24.1529H24.5471V-0.0771484Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask1_20135_18315)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group_2" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M21.9423 16.8532C20.8746 18.2972 19.2396 19.2605 17.3946 19.4012C17.3372 19.4051 17.2797 19.4092 17.2215 19.4124C15.0582 19.5201 13.424 18.5334 12.3404 17.6174C10.4229 16.6027 8.92256 16.3471 8.04475 16.1261C7.4064 15.9654 6.84782 15.644 6.60842 15.4833C6.39617 15.3572 5.96925 15.0647 5.65086 14.5191C5.29416 13.9077 5.27342 13.3235 5.279 13.0752C5.46493 13.0504 5.70512 13.023 5.98601 13.0054C6.21582 12.9909 6.43845 12.9853 6.68823 12.9844C8.62571 12.982 10.1283 13.0768 11.2215 13.5622C11.8128 13.8241 12.361 14.1728 12.8493 14.5979C13.4774 15.1459 14.7613 16.012 16.3437 15.7164C16.5448 15.6786 16.7379 15.6287 16.9239 15.5677C17.4935 15.4857 18.4679 15.4198 19.6154 15.7243C20.7046 16.0136 21.4882 16.5134 21.9423 16.8532Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "2.56", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M21.8003 6.90944C21.319 6.28602 20.7339 5.69669 20.043 5.29456C19.3066 4.86626 18.367 4.49744 17.2306 4.3975C14.4541 4.15321 12.5557 5.69273 12.2099 5.98382C11.7796 6.38753 10.0582 7.81602 7.98609 8.60917C7.62509 8.73768 6.93016 9.0414 6.31253 9.71961C5.82401 10.2557 5.58492 10.8046 5.46777 11.1457C5.71483 11.1798 5.96985 11.2044 6.23284 11.2194C8.2419 11.3313 9.96813 11.3424 11.0974 10.7896C11.8433 10.4247 12.4976 9.90517 13.0618 9.29681C14.0787 8.19987 15.5618 7.71051 16.9118 8.04605C17.5318 8.13247 18.6117 8.19833 19.8605 7.81602C20.7228 7.55189 21.3652 7.21957 21.8003 6.90944Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "2.56", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M2.95884 7.37229C2.1886 6.97653 1.58732 6.52001 1.17721 6.16263C0.947963 5.96275 0.591797 6.12665 0.591797 6.43206V8.92893C0.591797 9.03766 0.640978 9.14079 0.725063 9.20796L1.74835 10.1922C1.80229 9.79638 1.9181 9.25834 2.17829 8.66347C2.4234 8.10227 2.71769 7.67288 2.95884 7.37229Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "2.56", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M19.4326 12.9446C19.5497 12.584 19.6146 12.2056 19.6243 11.8201C19.6323 11.5156 19.597 11.4018 19.5441 11.1069C20.0886 11.0155 20.7686 10.8664 21.3886 10.7061C22.0438 10.537 22.5282 10.3406 23.0727 10.145C23.1521 10.5257 23.2355 10.7109 23.2644 10.993C23.2916 11.2591 23.3085 11.5348 23.3132 11.8201C23.3277 12.7066 23.2194 13.5105 23.0526 14.215C22.5394 13.9488 21.9299 13.6732 21.2282 13.4311C20.5754 13.2051 19.9683 13.0512 19.4326 12.9446Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "2.56", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M1.15342 18.2166C0.918616 18.3871 0.591797 18.2191 0.591797 17.927V14.8605C0.591797 14.7676 0.627493 14.6796 0.689366 14.614C0.720303 14.5812 0.748859 14.5628 0.761552 14.5556L1.77532 14.1204C1.84988 14.5268 1.97284 15.0133 2.17829 15.5429C2.41864 16.1621 2.7042 16.6637 2.95884 17.0454C2.35676 17.4358 1.75469 17.8263 1.15342 18.2166Z", + "fill": "#0A60B5", + "stroke": "black", + "stroke-width": "2.56", + "stroke-miterlimit": "10" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M7.68233 4.81872C9.70993 4.71818 11.2261 5.56713 12.0077 6.13451C11.4161 6.65689 9.81509 7.91742 7.92157 8.64547C7.79386 8.69117 7.62441 8.7588 7.43136 8.85733C6.06118 9.53194 5.24643 10.8764 5.21006 12.3127C5.20395 12.5545 5.21629 12.7953 5.24827 13.032C5.24481 13.0325 5.24136 13.033 5.23794 13.0334C5.23236 13.2797 5.2531 13.8593 5.60958 14.4661C5.9278 15.0073 6.35446 15.2975 6.5666 15.4227C6.67187 15.4928 6.83891 15.5939 7.04732 15.6986C7.29214 15.829 7.56361 15.9383 7.86305 16.0229C7.90892 16.0362 7.95532 16.0488 8.00211 16.0605C8.11111 16.0877 8.22985 16.1159 8.35794 16.1463C9.26137 16.3607 10.6303 16.6854 12.3087 17.5673C12.4783 17.7095 12.6609 17.8529 12.8568 17.993C12.7973 18.0463 12.7472 18.0927 12.7067 18.1309C12.3606 18.4235 10.4609 19.9709 7.68233 19.7254C6.5451 19.625 5.60404 19.2542 4.86793 18.8238C3.62779 18.0991 2.69071 16.9526 2.17951 15.6109C1.85014 14.7459 1.56303 13.6274 1.5423 12.3111C1.52395 11.187 1.70419 10.1969 1.94983 9.37575C2.70188 6.86682 4.89504 5.0268 7.5093 4.82989L7.50941 4.82988C7.5668 4.82589 7.62418 4.82191 7.68233 4.81872ZM21.8835 16.7762C21.4284 16.4391 20.6476 15.947 19.5661 15.6619C18.4192 15.3597 17.4455 15.4251 16.8761 15.5064C16.6902 15.567 16.4972 15.6164 16.2963 15.6539C14.7148 15.9473 13.4316 15.0879 12.8039 14.5442C12.6819 14.4387 12.5561 14.338 12.4269 14.2422C12.8493 13.871 13.3137 13.5508 13.82 13.3021C14.9501 12.7473 16.6775 12.7584 18.6881 12.87C21.4586 13.0247 23.3726 14.3441 24.1367 14.95C24.222 15.0177 24.2707 15.1198 24.2707 15.2282V17.718C24.2707 18.0233 23.9125 18.1867 23.682 17.9867C23.2692 17.6292 22.6618 17.1721 21.8835 16.7762ZM13.0009 9.33672C12.8193 9.5334 12.6283 9.72087 12.4279 9.89693C12.8159 10.1847 13.2353 10.4283 13.6788 10.6234C14.7715 11.1049 16.2732 11.199 18.2095 11.1966C21.0495 11.1926 23.1406 10.1235 24.1 9.54153C24.206 9.47696 24.2707 9.36218 24.2707 9.23781V6.182C24.2707 5.89101 23.9421 5.72359 23.706 5.8934C23.1472 6.29546 22.3162 6.80863 21.2445 7.21229C20.8586 7.43971 20.3776 7.6719 19.8046 7.84826C18.5548 8.23249 17.4742 8.16632 16.8538 8.07944C15.5028 7.74222 14.0186 8.2341 13.0009 9.33672Z", + "fill": "#E11312" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_10", + "d": "M12.0069 6.13459L12.21 6.36447L12.4968 6.11122L12.1871 5.88642L12.0069 6.13459ZM7.68154 4.8188L7.66636 4.51247L7.66557 4.51251L7.66477 4.51255L7.68154 4.8188ZM7.92078 8.64555L8.0241 8.9344L8.02755 8.93317L8.03092 8.93187L7.92078 8.64555ZM7.43056 8.85741L7.56612 9.13253L7.56811 9.13161L7.57008 9.13062L7.43056 8.85741ZM5.24747 13.0321L5.28765 13.3361L5.59271 13.2959L5.55152 12.991L5.24747 13.0321ZM5.23715 13.0335L5.1967 12.7295L4.93636 12.764L4.93041 13.0265L5.23715 13.0335ZM6.56581 15.4228L6.736 15.1676L6.729 15.1629L6.72175 15.1587L6.56581 15.4228ZM7.04653 15.6987L7.19079 15.428L7.18759 15.4263L7.18433 15.4247L7.04653 15.6987ZM7.86225 16.023L7.94762 15.7285L7.9467 15.7282L7.94571 15.7279L7.86225 16.023ZM12.3079 17.5673L12.5052 17.3324L12.4799 17.3112L12.4506 17.2958L12.3079 17.5673ZM12.856 17.9931L13.0606 18.2216L13.3458 17.9664L13.0346 17.7437L12.856 17.9931ZM12.7059 18.131L12.904 18.3652L12.9105 18.3598L12.9165 18.3541L12.7059 18.131ZM4.86713 18.8239L5.02207 18.5591L5.02197 18.559L4.86713 18.8239ZM1.5415 12.3112L1.84828 12.3064L1.84827 12.3062L1.5415 12.3112ZM1.94903 9.37583L1.65512 9.28773L1.65507 9.28796L1.94903 9.37583ZM7.5085 4.82997L7.48794 4.52395L7.48669 4.52403L7.48544 4.52413L7.5085 4.82997ZM7.50862 4.82996L7.52918 5.13598L7.52987 5.13593L7.50862 4.82996ZM19.5653 15.662L19.6435 15.3654H19.6435L19.5653 15.662ZM21.8827 16.7763L21.7001 17.0227L21.7207 17.038L21.7436 17.0496L21.8827 16.7763ZM16.8753 15.5065L16.8319 15.2028L16.8055 15.2066L16.7801 15.2149L16.8753 15.5065ZM16.2955 15.654L16.3515 15.9555H16.3517L16.2955 15.654ZM12.8031 14.5443L13.0041 14.3125L13.0038 14.3122L12.8031 14.5443ZM12.4261 14.2422L12.2236 14.0119L11.9383 14.2625L12.2434 14.4886L12.4261 14.2422ZM18.6873 12.8701L18.7044 12.5638H18.7042L18.6873 12.8701ZM24.1359 14.95L24.3267 14.7099L24.3266 14.7098L24.1359 14.95ZM23.6813 17.9868L23.8824 17.7552L23.8821 17.7549L23.6813 17.9868ZM12.4271 9.89701L12.2246 9.66667L11.9394 9.91717L12.2444 10.1434L12.4271 9.89701ZM13.678 10.6234L13.8018 10.3428L13.8016 10.3427L13.678 10.6234ZM18.2087 11.1967L18.2091 11.5034H18.2092L18.2087 11.1967ZM24.0992 9.54161L24.2584 9.80384L24.2588 9.80361L24.0992 9.54161ZM21.2437 7.21237L21.1355 6.92536L21.1107 6.9347L21.088 6.94815L21.2437 7.21237ZM16.853 8.07952L16.7786 8.37711L16.7943 8.38102L16.8104 8.38324L16.853 8.07952ZM12.1871 5.88642C11.3742 5.29623 9.7896 4.40718 7.66636 4.51247L7.69672 5.12514C9.62867 5.02934 11.0765 5.83819 11.8266 6.38275L12.1871 5.88642ZM8.03092 8.93187C9.97246 8.18534 11.605 6.89867 12.21 6.36447L11.8038 5.90471C11.2255 6.41528 9.65613 7.64973 7.81063 8.35932L8.03092 8.93187ZM7.57008 9.13062C7.74904 9.03922 7.90597 8.97657 8.0241 8.9344L7.81746 8.35679C7.68016 8.40586 7.49818 8.47855 7.29104 8.58429L7.57008 9.13062ZM5.51598 12.3205C5.54953 10.9957 6.30059 9.75569 7.56612 9.13253L7.29499 8.5823C5.82017 9.30843 4.94173 10.7573 4.90255 12.3051L5.51598 12.3205ZM5.55152 12.991C5.52184 12.7713 5.51027 12.5468 5.51598 12.3205L4.90255 12.3051C4.89604 12.5623 4.90915 12.8196 4.94341 13.0731L5.55152 12.991ZM5.27759 13.3375C5.28094 13.3371 5.28429 13.3366 5.28765 13.3361L5.20729 12.728C5.20373 12.7285 5.2002 12.729 5.1967 12.7295L5.27759 13.3375ZM5.87334 14.3108C5.55756 13.7734 5.53893 13.2588 5.54388 13.0404L4.93041 13.0265C4.92419 13.3008 4.94703 13.9455 5.34423 14.6215L5.87334 14.3108ZM6.72175 15.1587C6.53331 15.0475 6.15501 14.7899 5.87331 14.3107L5.34423 14.6215C5.69895 15.2249 6.17402 15.5477 6.40985 15.6869L6.72175 15.1587ZM7.18433 15.4247C6.98719 15.3256 6.83096 15.2309 6.736 15.1676L6.39562 15.678C6.51119 15.755 6.68904 15.8623 6.90873 15.9728L7.18433 15.4247ZM7.94571 15.7279C7.66622 15.6489 7.41526 15.5476 7.19079 15.428L6.90227 15.9694C7.16743 16.1106 7.4594 16.2279 7.7788 16.3182L7.94571 15.7279ZM8.07572 15.763C8.03277 15.7523 7.99004 15.7407 7.94762 15.7285L7.7768 16.3176C7.8262 16.3319 7.87621 16.3455 7.92691 16.3581L8.07572 15.763ZM8.42802 15.8479C8.29947 15.8174 8.18257 15.7897 8.07572 15.763L7.92691 16.3581C8.03798 16.3859 8.15864 16.4145 8.28627 16.4448L8.42802 15.8479ZM12.4506 17.2958C10.7369 16.3954 9.3372 16.0636 8.42802 15.8479L8.28627 16.4448C9.18402 16.6578 10.522 16.9755 12.1651 17.8389L12.4506 17.2958ZM13.0346 17.7437C12.8458 17.6086 12.6693 17.4702 12.5052 17.3324L12.1107 17.8023C12.2855 17.949 12.4745 18.0973 12.6774 18.2425L13.0346 17.7437ZM12.9165 18.3541C12.9555 18.3173 13.0035 18.2727 13.0606 18.2216L12.6514 17.7646C12.5895 17.8199 12.5373 17.8684 12.4953 17.908L12.9165 18.3541ZM7.65454 20.031C10.5596 20.2878 12.5414 18.6718 12.904 18.3652L12.5078 17.8968C12.1782 18.1755 10.3606 19.6543 7.70861 19.42L7.65454 20.031ZM4.7122 19.0885C5.48062 19.538 6.46494 19.9259 7.65455 20.0311L7.70861 19.42C6.62375 19.3242 5.72585 18.9707 5.02207 18.5591L4.7122 19.0885ZM1.89197 15.7201C2.42664 17.1235 3.4083 18.3266 4.71229 19.0886L5.02197 18.559C3.84569 17.8717 2.95318 16.7819 2.46543 15.5018L1.89197 15.7201ZM1.23472 12.316C1.25612 13.6744 1.55244 14.8284 1.89197 15.7201L2.46546 15.5019C2.14624 14.6635 1.86835 13.5804 1.84828 12.3064L1.23472 12.316ZM1.65507 9.28796C1.40184 10.1345 1.21579 11.1561 1.23472 12.3163L1.84827 12.3062C1.83052 11.2181 2.00495 10.2594 2.24298 9.4637L1.65507 9.28796ZM7.48544 4.52413C4.73874 4.73102 2.44195 6.66285 1.65512 9.28773L2.24293 9.46385C2.96021 7.07095 5.04976 5.32275 7.53155 5.13581L7.48544 4.52413ZM7.48805 4.52394L7.48794 4.52395L7.52906 5.13599L7.52918 5.13598L7.48805 4.52394ZM7.66477 4.51255C7.60399 4.51588 7.54439 4.52003 7.48736 4.52399L7.52987 5.13593C7.58761 5.13192 7.64276 5.1281 7.69834 5.12505L7.66477 4.51255ZM19.4871 15.9585C20.5195 16.2307 21.2651 16.7006 21.7001 17.0227L22.0654 16.5298C21.5901 16.1778 20.7741 15.6634 19.6435 15.3654L19.4871 15.9585ZM16.9186 15.8101C17.4624 15.7325 18.3923 15.6701 19.4871 15.9585L19.6435 15.3654C18.4447 15.0495 17.427 15.1179 16.8319 15.2028L16.9186 15.8101ZM16.3517 15.9555C16.5658 15.9156 16.7717 15.8629 16.9704 15.7981L16.7801 15.2149C16.6071 15.2713 16.4271 15.3174 16.2392 15.3524L16.3517 15.9555ZM12.6023 14.776C13.2517 15.3386 14.6295 16.275 16.3515 15.9555L16.2395 15.3524C14.7985 15.6197 13.6099 14.8372 13.0041 14.3125L12.6023 14.776ZM12.2434 14.4886C12.3665 14.5799 12.4863 14.6758 12.6025 14.7763L13.0038 14.3122C12.876 14.2017 12.7442 14.0962 12.6089 13.9959L12.2434 14.4886ZM13.6839 13.0269C13.1508 13.2889 12.6639 13.625 12.2236 14.0119L12.6286 14.4726C13.033 14.1173 13.4752 13.8129 13.9546 13.5774L13.6839 13.0269ZM18.7042 12.5638C17.6973 12.5079 16.7474 12.4763 15.9035 12.5301C15.0621 12.5838 14.3014 12.7237 13.6839 13.0269L13.9546 13.5774C14.4672 13.3258 15.1352 13.1938 15.9426 13.1423C16.7477 13.0909 17.6667 13.1206 18.6702 13.1763L18.7042 12.5638ZM24.3266 14.7098C23.5387 14.085 21.5647 12.7236 18.7044 12.5638L18.6702 13.1763C21.3509 13.3259 23.2049 14.6033 23.9452 15.1903L24.3266 14.7098ZM24.5767 15.2283C24.5767 15.0273 24.4861 14.8365 24.3267 14.7099L23.945 15.1902C23.9563 15.1992 23.9631 15.2124 23.9631 15.2283H24.5767ZM24.5767 17.7181V15.2283H23.9631V17.7181H24.5767ZM23.4801 18.2183C23.9096 18.5912 24.5767 18.2859 24.5767 17.7181H23.9631C23.9631 17.7326 23.9593 17.7405 23.9559 17.7456C23.9516 17.7519 23.9445 17.7584 23.9345 17.7629C23.9246 17.7675 23.915 17.7685 23.9076 17.7677C23.9016 17.7669 23.8933 17.7646 23.8824 17.7552L23.4801 18.2183ZM21.7436 17.0496C22.4948 17.4318 23.0817 17.8734 23.4804 18.2186L23.8821 17.7549C23.4551 17.3852 22.8272 16.9126 22.0218 16.5029L21.7436 17.0496ZM12.6296 10.1274C12.8386 9.94385 13.0372 9.74886 13.2256 9.54483L12.7747 9.1287C12.5998 9.31809 12.4165 9.49805 12.2246 9.66667L12.6296 10.1274ZM13.8016 10.3427C13.379 10.1569 12.9795 9.92476 12.6099 9.65072L12.2444 10.1434C12.6507 10.4448 13.0899 10.6999 13.5545 10.9042L13.8016 10.3427ZM18.2083 10.89C16.2651 10.8924 14.8268 10.7946 13.8018 10.3428L13.5543 10.9041C14.7145 11.4154 16.2797 11.5058 18.2091 11.5034L18.2083 10.89ZM23.94 9.27945C23.0063 9.84586 20.971 10.8861 18.2083 10.89L18.2092 11.5034C21.1263 11.4993 23.2733 10.4014 24.2584 9.80384L23.94 9.27945ZM23.9631 9.23789C23.9631 9.25522 23.9542 9.27078 23.9396 9.27968L24.2588 9.80361C24.4563 9.68338 24.5767 9.4693 24.5767 9.23789H23.9631ZM23.9631 6.18208V9.23789H24.5767V6.18208H23.9631ZM23.8844 6.14243C23.9185 6.11792 23.9631 6.14225 23.9631 6.18208H24.5767C24.5767 5.63993 23.9641 5.32941 23.526 5.64453L23.8844 6.14243ZM21.3519 7.49938C22.4546 7.08404 23.3093 6.55621 23.8844 6.14243L23.526 5.64453C22.9834 6.03488 22.1762 6.53338 21.1355 6.92536L21.3519 7.49938ZM19.894 8.14148C20.4934 7.95707 20.9962 7.71423 21.3995 7.4766L21.088 6.94815C20.7192 7.16536 20.2602 7.38697 19.7135 7.55519L19.894 8.14148ZM16.8104 8.38324C17.4579 8.47395 18.5872 8.54334 19.894 8.14148L19.7136 7.55517C18.5209 7.92187 17.4889 7.85892 16.8955 7.7758L16.8104 8.38324ZM13.2256 9.54483C14.1759 8.5152 15.5483 8.07001 16.7786 8.37711L16.9273 7.78194C15.4556 7.41459 13.8597 7.95323 12.7746 9.12877L13.2256 9.54483Z", + "fill": "black" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + "name": "LangfuseIconBig" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.tsx b/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.tsx new file mode 100644 index 0000000000..d1d3d001b9 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangfuseIconBig.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LangfuseIconBig.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 = 'LangfuseIconBig' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/LangsmithIcon.json b/web/app/components/base/icons/src/public/tracing/LangsmithIcon.json new file mode 100644 index 0000000000..04d480bd20 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangsmithIcon.json @@ -0,0 +1,188 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "84", + "height": "14", + "viewBox": "0 0 84 14", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask0_20135_16592", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "0", + "width": "84", + "height": "14" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "a" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M83.2164 0.600098H0.799805V13.4001H83.2164V0.600098Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask0_20135_16592)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M31.0264 3.12256V10.8845H36.3737V9.71251H32.2403V3.12256H31.0264Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M39.2238 4.96436C38.0585 4.96436 37.1871 5.51066 36.8333 6.46298C36.8108 6.52391 36.7427 6.70772 36.7427 6.70772L37.7416 7.35386L37.8773 7.00007C38.1087 6.39693 38.5367 6.11584 39.2238 6.11584C39.911 6.11584 40.3042 6.44916 40.297 7.10554C40.297 7.13216 40.295 7.21255 40.295 7.21255C40.295 7.21255 39.3856 7.36 39.0109 7.43936C37.4119 7.77728 36.7422 8.38759 36.7422 9.38599C36.7422 9.91796 37.0376 10.494 37.5767 10.817C37.9003 11.0106 38.3227 11.0838 38.7892 11.0838C39.0959 11.0838 39.3938 11.0382 39.6698 10.9542C40.297 10.7459 40.4721 10.3363 40.4721 10.3363V10.8718H41.511V7.04308C41.511 5.74157 40.6559 4.96436 39.2238 4.96436ZM40.3011 9.05012C40.3011 9.45255 39.8628 10.0193 38.8419 10.0193C38.5536 10.0193 38.3494 9.94304 38.2132 9.82938C38.0309 9.67732 37.971 9.45869 37.9961 9.26567C38.0068 9.1817 38.0575 9.00096 38.2454 8.84429C38.4374 8.68404 38.7769 8.56935 39.3012 8.45517C39.7323 8.36148 40.3016 8.25805 40.3016 8.25805V9.05063L40.3011 9.05012Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M45.3523 4.96438C45.2079 4.96438 45.0671 4.97462 44.9304 4.99356C44.0001 5.13334 43.7277 5.60591 43.7277 5.60591L43.7287 5.13334H42.5645V10.8729H43.7784V7.68924C43.7784 6.60739 44.5674 6.11484 45.3006 6.11484C46.0932 6.11484 46.4782 6.54083 46.4782 7.41788V10.8729H47.6921V7.25097C47.6921 5.8399 46.7956 4.96387 45.3528 4.96387L45.3523 4.96438Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M52.7575 5.12922V5.72058C52.7575 5.72058 52.4601 4.96436 51.1067 4.96436C49.4253 4.96436 48.3809 6.12455 48.3809 7.99284C48.3809 9.04704 48.7178 9.877 49.3122 10.4013C49.7745 10.8088 50.392 11.0177 51.1272 11.0321C51.6387 11.0418 51.97 10.9025 52.1769 10.7709C52.5742 10.518 52.7217 10.2779 52.7217 10.2779C52.7217 10.2779 52.7048 10.4658 52.6741 10.7203C52.6521 10.9046 52.6106 11.0341 52.6106 11.0341C52.4258 11.692 51.885 12.0725 51.0965 12.0725C50.308 12.0725 49.8303 11.8129 49.7356 11.3014L48.5555 11.6536C48.7592 12.6367 49.6819 13.2234 51.0233 13.2234C51.9352 13.2234 52.65 12.9756 53.1482 12.4861C53.6505 11.9926 53.9054 11.2814 53.9054 10.3721V5.12871H52.7575V5.12922ZM52.6813 8.04455C52.6813 9.19348 52.1201 9.87956 51.18 9.87956C50.1729 9.87956 49.5953 9.19143 49.5953 7.99232C49.5953 6.79322 50.1729 6.11533 51.18 6.11533C52.0976 6.11533 52.6725 6.79834 52.6813 7.89812V8.04455Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M61.022 9.69984C61.1858 9.40237 61.2688 9.05165 61.2688 8.65689C61.2688 8.26214 61.1986 7.95904 61.0599 7.71379C60.9211 7.46752 60.7419 7.2663 60.5279 7.11526C60.3123 6.9632 60.0845 6.84339 59.852 6.7584C59.6186 6.67341 59.404 6.6048 59.2151 6.55513L57.8291 6.16857C57.654 6.12198 57.4789 6.0631 57.3079 5.99296C57.1354 5.9223 56.9884 5.82451 56.8722 5.70215C56.7534 5.57773 56.693 5.41594 56.693 5.21984C56.693 5.01402 56.7632 4.83124 56.9009 4.67661C57.0376 4.52352 57.2199 4.40474 57.4431 4.32333C57.6648 4.24192 57.909 4.2025 58.1691 4.20608C58.4364 4.21325 58.6919 4.26804 58.9279 4.36941C59.1649 4.47079 59.3687 4.62029 59.5336 4.81383C59.6938 5.00224 59.8049 5.23162 59.8643 5.49632L61.2042 5.26336C61.0901 4.80461 60.8955 4.40679 60.6252 4.08116C60.3497 3.74938 60.0026 3.49236 59.593 3.31776C59.1829 3.14266 58.7093 3.05204 58.185 3.04845C57.6689 3.04487 57.1912 3.1273 56.7688 3.2937C56.3474 3.45959 56.008 3.71252 55.7596 4.04532C55.5118 4.3776 55.3859 4.79437 55.3859 5.28384C55.3859 5.61869 55.4417 5.90285 55.5523 6.12916C55.6629 6.35597 55.8072 6.54439 55.9808 6.69031C56.1554 6.83674 56.3428 6.95245 56.5384 7.0354C56.7355 7.11885 56.9214 7.18644 57.0913 7.23559L59.0892 7.82644C59.2335 7.86996 59.3626 7.92218 59.4727 7.98157C59.5838 8.04148 59.6759 8.10906 59.7471 8.18228C59.8188 8.256 59.8741 8.341 59.9109 8.4352C59.9478 8.52941 59.9662 8.63284 59.9662 8.7424C59.9662 8.98765 59.8874 9.19808 59.7312 9.36653C59.5771 9.53344 59.3738 9.66247 59.1276 9.749C58.8823 9.83552 58.6176 9.87956 58.3401 9.87956C57.8716 9.87956 57.4518 9.75156 57.0929 9.49914C56.7391 9.25031 56.5 8.89498 56.3822 8.4434L55.0879 8.64C55.1678 9.12743 55.3516 9.55495 55.6342 9.91028C55.9219 10.2723 56.2947 10.5544 56.7411 10.7484C57.1886 10.943 57.6996 11.0418 58.2587 11.0418C58.6519 11.0418 59.0334 10.9916 59.3933 10.8923C59.7522 10.7935 60.0758 10.6429 60.3548 10.4448C60.6334 10.2477 60.8576 9.99629 61.0209 9.69882L61.022 9.69984Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M67.38 6.22747C67.5479 6.13173 67.7405 6.08309 67.9514 6.08309C68.2939 6.08309 68.5699 6.19777 68.7706 6.42459C68.9708 6.65038 69.0727 6.96629 69.0727 7.36513V10.8309H70.3158V7.04053C70.3158 6.4292 70.1453 5.92897 69.8094 5.55419C69.4741 5.18043 68.9846 4.99048 68.3543 4.99048C67.9734 4.99048 67.6237 5.07547 67.314 5.24289C67.0237 5.40008 66.7856 5.61921 66.6074 5.89365L66.5838 5.93L66.5634 5.89211C66.4226 5.63201 66.2229 5.41953 65.9694 5.26081C65.6832 5.08161 65.3218 4.99048 64.8958 4.99048C64.5082 4.99048 64.1534 5.07649 63.8421 5.24545C63.603 5.37499 63.3987 5.54446 63.2349 5.74824L63.1893 5.80507V5.13435H62.0967V10.8309H63.3506V7.31752C63.3506 6.95451 63.453 6.65499 63.6552 6.42766C63.858 6.19931 64.1309 6.0836 64.4667 6.0836C64.8026 6.0836 65.0903 6.19931 65.2916 6.42766C65.4918 6.65499 65.5931 6.97601 65.5931 7.38101V10.8309H66.8306V7.31752C66.8306 7.06254 66.8803 6.83931 66.9786 6.65345C67.0774 6.46709 67.2126 6.32424 67.3805 6.22798L67.38 6.22747Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M74.2724 9.3726C74.2796 9.6286 74.3487 9.88358 74.4787 10.1293C74.6257 10.3981 74.8438 10.5978 75.1269 10.7222C75.4126 10.8472 75.7408 10.9153 76.1028 10.924C76.4597 10.9327 76.8293 10.9025 77.2016 10.8344V9.80064C76.8514 9.85081 76.5339 9.86412 76.2585 9.83955C75.9682 9.81395 75.7541 9.68851 75.621 9.46732C75.5509 9.35366 75.513 9.20467 75.5074 9.02547C75.5022 8.84985 75.4992 8.64403 75.4992 8.4126V6.04563H77.2016V5.08204H75.4992V3.13184H74.2617V5.08204H73.209V6.04563H74.2617V8.48787C74.2617 8.81657 74.2652 9.11456 74.2724 9.37312V9.3726Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "d": "M80.8767 4.95543C80.7436 4.95543 80.6141 4.96414 80.4881 4.98052C79.5726 5.12337 79.3033 5.5898 79.3033 5.5898V5.4531H79.3028V3.11377H78.0889V10.8649H79.3028V7.68132C79.3028 6.5923 80.0918 6.09668 80.825 6.09668C81.6176 6.09668 82.0026 6.52267 82.0026 7.39972V10.8649H83.2165V7.23281C83.2165 5.8499 82.298 4.95595 80.8772 4.95595L80.8767 4.95543Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_10", + "d": "M72.3934 5.13281H71.1855V10.8775H72.3934V5.13281Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_11", + "d": "M71.7889 4.70524C72.2236 4.70524 72.5758 4.35291 72.5758 3.91829C72.5758 3.48368 72.2236 3.13135 71.7889 3.13135C71.3542 3.13135 71.002 3.48368 71.002 3.91829C71.002 4.35291 71.3542 4.70524 71.7889 4.70524Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_12", + "d": "M15.5941 10.4208C15.524 10.5375 15.313 10.5452 15.1333 10.4372C15.0412 10.3819 14.97 10.3056 14.9331 10.2226C14.8993 10.1474 14.8988 10.0762 14.9311 10.0224C14.968 9.96099 15.0442 9.92976 15.1344 9.92976C15.2152 9.92976 15.3074 9.95485 15.3924 10.0061C15.5721 10.1141 15.6648 10.304 15.5946 10.4208H15.5941ZM25.9939 7.0001C25.9939 10.5288 23.1226 13.4001 19.5939 13.4001H7.20859C3.67989 13.4001 0.808594 10.5293 0.808594 7.0001C0.808594 3.47088 3.67989 0.600098 7.20859 0.600098H19.5939C23.1231 0.600098 25.9939 3.47139 25.9939 7.0001ZM13.1427 10.2098C13.2435 10.0875 12.7776 9.74288 12.6824 9.61642C12.4888 9.4065 12.4878 9.10442 12.3573 8.85917C12.0378 8.11882 11.6707 7.3841 11.1571 6.75741C10.6144 6.07184 9.94472 5.50455 9.35643 4.86096C8.9197 4.41194 8.80296 3.77245 8.41743 3.28963C7.88597 2.50474 6.20559 2.29072 5.95931 3.3992C5.96034 3.43402 5.94959 3.45603 5.91937 3.47805C5.78318 3.57687 5.66184 3.69002 5.55995 3.82672C5.3106 4.17386 5.2722 4.76266 5.5835 5.07447C5.59374 4.91011 5.59937 4.75498 5.72942 4.63722C5.97007 4.84407 6.33358 4.91677 6.61262 4.76266C7.22907 5.64279 7.07547 6.86032 7.56494 7.80855C7.70011 8.0328 7.8363 8.26167 8.00987 8.45827C8.15067 8.67741 8.63707 8.93597 8.66574 9.13872C8.67086 9.48688 8.6299 9.8673 8.85825 10.1586C8.96577 10.3767 8.70158 10.5959 8.48859 10.5687C8.21211 10.6066 7.8747 10.3829 7.63252 10.5206C7.54702 10.6133 7.3796 10.5109 7.30587 10.6394C7.28027 10.706 7.14203 10.7997 7.22446 10.8637C7.31611 10.794 7.4011 10.7213 7.52449 10.7628C7.50606 10.8631 7.58542 10.8775 7.6484 10.9067C7.64635 10.9748 7.60641 11.0444 7.65864 11.1022C7.71956 11.0408 7.75592 10.9538 7.85268 10.9282C8.17422 11.3567 8.50139 10.4945 9.1972 10.8826C9.05588 10.8759 8.93044 10.8933 8.83521 11.0096C8.81166 11.0357 8.79169 11.0664 8.83316 11.1002C9.20846 10.858 9.20641 11.1831 9.45012 11.0833C9.63752 10.9855 9.82388 10.8631 10.0466 10.898C9.83003 10.9604 9.82132 11.1345 9.69435 11.2814C9.67284 11.304 9.6626 11.3296 9.68769 11.3669C10.1372 11.3291 10.1741 11.1796 10.5371 10.9963C10.8079 10.8309 11.0778 11.2318 11.3123 11.0034C11.364 10.9538 11.4346 10.9707 11.4986 10.964C11.4167 10.5273 10.5161 11.0439 10.5304 10.4581C10.8202 10.261 10.7537 9.88368 10.7731 9.57904C11.1064 9.76387 11.4771 9.87139 11.8038 10.048C11.9687 10.3143 12.2272 10.666 12.5718 10.643C12.581 10.6164 12.5892 10.5928 12.5989 10.5657C12.7034 10.5836 12.8375 10.6527 12.8949 10.5206C13.051 10.6839 13.2804 10.6757 13.4847 10.6338C13.6357 10.5109 13.2005 10.3358 13.1422 10.2093L13.1427 10.2098ZM17.8147 8.14595L17.1296 7.22128C16.5316 7.90531 16.1322 8.23863 16.1251 8.24477C16.1215 8.24835 15.74 8.61955 15.3924 8.93802C15.0514 9.25034 14.7821 9.49763 14.6449 9.76797C14.607 9.84272 14.5246 10.1182 14.6403 10.3936C14.7294 10.6066 14.9111 10.7582 15.1809 10.8442C15.2618 10.8698 15.3392 10.8811 15.4129 10.8811C15.8988 10.8811 16.2177 10.3936 16.2198 10.3895C16.2239 10.3839 16.6371 9.79357 17.1404 9.0481C17.3078 8.80029 17.4993 8.53815 17.8147 8.14544V8.14595ZM21.0357 10.2754C21.0357 10.1392 20.986 10.0076 20.8959 9.9057L20.8109 9.80944C20.2974 9.2273 18.979 7.73277 18.4659 7.15216C17.8218 6.42307 17.1015 5.49379 17.0416 5.41597L16.955 5.23677V4.92138C16.955 4.80567 16.932 4.69251 16.8874 4.58602L16.7041 4.15082C16.7016 4.14467 16.7006 4.13751 16.7016 4.13085L16.7088 4.07043C16.7098 4.06071 16.7144 4.052 16.7221 4.04535C16.8649 3.91939 17.3964 3.51082 18.2192 3.54512C18.3267 3.54973 18.3456 3.49085 18.3487 3.46576C18.3635 3.34493 18.0876 3.20259 17.8305 3.14986C17.4773 3.07767 16.5383 2.88618 15.7872 3.37923L15.7815 3.38333C15.2961 3.78883 14.906 4.09859 14.9019 4.10167L14.8932 4.11037C14.8876 4.11703 14.7504 4.28138 14.7836 4.49079C14.8051 4.62698 14.7345 4.67562 14.7304 4.67818C14.7263 4.68074 14.63 4.74115 14.5307 4.67306C14.4104 4.58295 14.2015 4.73757 14.159 4.77136L13.8436 5.04272L13.8375 5.04887C13.8318 5.05552 13.6967 5.21373 13.8774 5.46768C14.0336 5.68733 14.0888 5.76055 14.2245 5.92951C14.3623 6.10051 14.6106 6.31709 14.6239 6.32835C14.63 6.33347 14.7816 6.44816 14.9905 6.28842C15.162 6.15683 15.2997 6.03805 15.2997 6.03805C15.311 6.02883 15.4103 5.94691 15.4149 5.82608C15.4165 5.79127 15.4149 5.76106 15.4149 5.73341C15.4124 5.64842 15.4119 5.62333 15.4759 5.58237C15.5066 5.58237 15.6008 5.61667 15.6817 5.65763C15.6904 5.66275 15.8926 5.77642 16.0764 5.76823C16.1921 5.78359 16.3201 5.91517 16.3642 5.96893C16.3683 5.97303 16.7594 6.38365 17.3104 7.10403C17.4153 7.24074 17.8008 7.75427 17.9063 7.89763C18.0824 8.13725 18.3492 8.49975 18.6359 8.8904C19.1326 9.56675 19.6896 10.325 19.9425 10.666C20.0265 10.7792 20.1489 10.856 20.2871 10.8826L20.3808 10.9005C20.4167 10.9072 20.4525 10.9108 20.4883 10.9108C20.6542 10.9108 20.8114 10.8381 20.9153 10.709L20.921 10.7019C20.9957 10.6071 21.0367 10.4858 21.0367 10.3609V10.2754H21.0357ZM21.4765 4.20253L21.3674 4.09347C21.3357 4.06173 21.2912 4.04279 21.2461 4.04637C21.201 4.04842 21.1585 4.0689 21.1294 4.10371L20.4433 4.91523C20.4238 4.93827 20.3962 4.95261 20.3665 4.95568L20.1223 4.98026C20.091 4.98333 20.0598 4.9736 20.0357 4.95363L19.6456 4.62339C19.6205 4.60189 19.6056 4.57117 19.6046 4.5384L19.599 4.34282C19.598 4.31466 19.6077 4.28701 19.6256 4.26551L20.2928 3.46167C20.3429 3.40125 20.3424 3.3137 20.2917 3.25328L20.2247 3.17392C20.1806 3.12221 20.1079 3.10327 20.0444 3.12733C19.8867 3.18723 19.4894 3.34237 19.2022 3.49802C18.7962 3.71715 18.5141 4.0648 18.4654 4.40528C18.4296 4.65463 18.4444 5.06576 18.4567 5.28848C18.4613 5.37603 18.4419 5.46359 18.3994 5.54192C18.3472 5.6392 18.255 5.79485 18.1147 5.98224C18.043 6.08106 17.998 6.11741 17.936 6.19063L18.6907 7.07792C18.8725 6.86595 19.0317 6.70519 19.1704 6.55005C19.4239 6.26794 19.5027 6.26538 19.7137 6.25821C19.8437 6.2536 20.0219 6.24797 20.304 6.17731C21.0741 5.9848 21.3178 5.15127 21.328 5.1144L21.5195 4.35715C21.5333 4.30237 21.5169 4.24298 21.477 4.20304L21.4765 4.20253ZM9.63496 9.48842C9.55202 9.812 9.52488 10.3634 9.10402 10.3793C9.0692 10.5662 9.23355 10.6363 9.38255 10.5764C9.53051 10.5083 9.60066 10.6302 9.65032 10.7515C9.87867 10.7848 10.2166 10.6752 10.2294 10.4049C9.8884 10.2083 9.78293 9.83453 9.63445 9.48842H9.63496Z", + "fill": "#1C3C3C" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "LangsmithIcon" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/tracing/LangsmithIcon.tsx b/web/app/components/base/icons/src/public/tracing/LangsmithIcon.tsx new file mode 100644 index 0000000000..5565f24f51 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangsmithIcon.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LangsmithIcon.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 = 'LangsmithIcon' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.json b/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.json new file mode 100644 index 0000000000..4aa76acc8d --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.json @@ -0,0 +1,188 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "124", + "height": "20", + "viewBox": "0 0 124 20", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Clip path group" + }, + "children": [ + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask0_20135_18175", + "style": "mask-type:luminance", + "maskUnits": "userSpaceOnUse", + "x": "0", + "y": "0", + "width": "124", + "height": "20" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "a" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M123.825 0.399902H0.200195V19.5999H123.825V0.399902Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask0_20135_18175)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M45.54 4.18408V15.827H53.561V14.069H47.361V4.18408H45.54Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M57.8358 6.94629C56.0878 6.94629 54.7807 7.76575 54.25 9.19423C54.2162 9.28562 54.1141 9.56133 54.1141 9.56133L55.6124 10.5305L55.8159 9.99986C56.1631 9.09515 56.8051 8.67352 57.8358 8.67352C58.8664 8.67352 59.4563 9.17349 59.4455 10.1581C59.4455 10.198 59.4424 10.3186 59.4424 10.3186C59.4424 10.3186 58.0785 10.5398 57.5163 10.6588C55.1178 11.1657 54.1133 12.0811 54.1133 13.5787C54.1133 14.3767 54.5564 15.2407 55.3651 15.7253C55.8505 16.0156 56.4841 16.1254 57.1837 16.1254C57.6438 16.1254 58.0908 16.0571 58.5047 15.9311C59.4455 15.6185 59.7082 15.0041 59.7082 15.0041V15.8075H61.2664V10.0644C61.2664 8.11211 59.9839 6.94629 57.8358 6.94629ZM59.4517 13.0749C59.4517 13.6786 58.7942 14.5288 57.2629 14.5288C56.8305 14.5288 56.524 14.4143 56.3197 14.2438C56.0463 14.0157 55.9565 13.6878 55.9941 13.3983C56.0102 13.2723 56.0863 13.0012 56.3681 12.7662C56.6561 12.5258 57.1653 12.3538 57.9517 12.1825C58.5984 12.042 59.4524 11.8868 59.4524 11.8868V13.0757L59.4517 13.0749Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M67.0275 6.94657C66.8109 6.94657 66.5997 6.96193 66.3946 6.99034C64.9992 7.20001 64.5906 7.90887 64.5906 7.90887L64.5921 7.20001H62.8457V15.8093H64.6666V11.0339C64.6666 9.41108 65.8501 8.67226 66.9499 8.67226C68.1388 8.67226 68.7163 9.31124 68.7163 10.6268V15.8093H70.5372V10.3765C70.5372 8.25985 69.1925 6.9458 67.0282 6.9458L67.0275 6.94657Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M78.1373 7.19359V8.08063C78.1373 8.08063 77.6911 6.94629 75.6611 6.94629C73.139 6.94629 71.5723 8.68658 71.5723 11.489C71.5723 13.0703 72.0776 14.3152 72.9693 15.1017C73.6628 15.713 74.589 16.0264 75.6918 16.0479C76.4591 16.0624 76.9559 15.8536 77.2664 15.6562C77.8623 15.2768 78.0835 14.9166 78.0835 14.9166C78.0835 14.9166 78.0582 15.1984 78.0121 15.5801C77.9791 15.8566 77.9169 16.0509 77.9169 16.0509C77.6396 17.0378 76.8285 17.6084 75.6457 17.6084C74.463 17.6084 73.7465 17.2191 73.6044 16.4518L71.8342 16.9802C72.1398 18.4548 73.5238 19.3349 75.5359 19.3349C76.9037 19.3349 77.976 18.9632 78.7233 18.229C79.4767 17.4886 79.8591 16.4219 79.8591 15.0579V7.19282H78.1373V7.19359ZM78.0229 11.5666C78.0229 13.29 77.1811 14.3191 75.7709 14.3191C74.2603 14.3191 73.394 13.2869 73.394 11.4882C73.394 9.68959 74.2603 8.67275 75.7709 8.67275C77.1473 8.67275 78.0098 9.69726 78.0229 11.3469V11.5666Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M90.532 14.0495C90.7777 13.6033 90.9022 13.0772 90.9022 12.4851C90.9022 11.893 90.7969 11.4383 90.5888 11.0704C90.3807 10.701 90.1119 10.3992 89.7909 10.1727C89.4675 9.94455 89.1258 9.76484 88.7771 9.63735C88.4269 9.50987 88.1051 9.40695 87.8217 9.33246L85.7427 8.75262C85.4801 8.68273 85.2174 8.59441 84.9609 8.48919C84.7021 8.38321 84.4817 8.23652 84.3073 8.05298C84.1292 7.86635 84.0385 7.62367 84.0385 7.32952C84.0385 7.02079 84.1437 6.74661 84.3503 6.51467C84.5554 6.28504 84.8288 6.10687 85.1637 5.98475C85.4962 5.86264 85.8625 5.80351 86.2527 5.80888C86.6536 5.81963 87.0368 5.90181 87.3909 6.05387C87.7464 6.20594 88.0521 6.43019 88.2994 6.7205C88.5398 7.00312 88.7064 7.34719 88.7955 7.74424L90.8054 7.3948C90.6341 6.70667 90.3423 6.10994 89.9368 5.62149C89.5236 5.12383 89.0029 4.73829 88.3885 4.4764C87.7733 4.21375 87.0629 4.07781 86.2765 4.07243C85.5023 4.06706 84.7858 4.19071 84.1522 4.44031C83.5201 4.68914 83.011 5.06853 82.6385 5.56773C82.2668 6.06616 82.0778 6.69131 82.0778 7.42552C82.0778 7.92779 82.1615 8.35403 82.3274 8.69349C82.4933 9.03371 82.7099 9.31634 82.9702 9.53522C83.2321 9.75487 83.5132 9.92843 83.8066 10.0529C84.1023 10.178 84.3811 10.2794 84.636 10.3531L87.6328 11.2394C87.8493 11.3047 88.0429 11.383 88.208 11.4721C88.3747 11.562 88.5129 11.6633 88.6197 11.7732C88.7272 11.8838 88.8101 12.0113 88.8654 12.1526C88.9207 12.2939 88.9484 12.449 88.9484 12.6134C88.9484 12.9812 88.8301 13.2969 88.5958 13.5496C88.3647 13.7999 88.0598 13.9935 87.6904 14.1232C87.3225 14.253 86.9254 14.3191 86.5092 14.3191C85.8065 14.3191 85.1767 14.1271 84.6383 13.7485C84.1077 13.3752 83.749 12.8422 83.5724 12.1648L81.6309 12.4598C81.7507 13.1909 82.0264 13.8322 82.4503 14.3652C82.8819 14.9081 83.441 15.3313 84.1107 15.6224C84.782 15.9142 85.5484 16.0624 86.3871 16.0624C86.9769 16.0624 87.5491 15.9872 88.089 15.8382C88.6273 15.69 89.1127 15.4642 89.5313 15.167C89.9491 14.8713 90.2855 14.4942 90.5304 14.048L90.532 14.0495Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M100.071 8.84108C100.322 8.69747 100.611 8.62451 100.928 8.62451C101.441 8.62451 101.855 8.79654 102.156 9.13676C102.457 9.47545 102.61 9.94931 102.61 10.5476V15.7462H104.474V10.0607C104.474 9.14368 104.218 8.39334 103.715 7.83116C103.212 7.27052 102.477 6.9856 101.532 6.9856C100.961 6.9856 100.436 7.11308 99.9714 7.36422C99.536 7.6 99.1789 7.9287 98.9116 8.34035L98.8763 8.39488L98.8455 8.33804C98.6343 7.9479 98.3348 7.62918 97.9547 7.3911C97.5253 7.1223 96.9831 6.9856 96.3442 6.9856C95.7628 6.9856 95.2306 7.11462 94.7636 7.36806C94.405 7.56236 94.0985 7.81657 93.8528 8.12224L93.7844 8.20748V7.2014H92.1455V15.7462H94.0263V10.4762C94.0263 9.93164 94.1799 9.48236 94.4833 9.14137C94.7874 8.79884 95.1968 8.62528 95.7006 8.62528C96.2044 8.62528 96.636 8.79884 96.9378 9.14137C97.2381 9.48236 97.3902 9.9639 97.3902 10.5714V15.7462H99.2464V10.4762C99.2464 10.0937 99.3209 9.75884 99.4684 9.48006C99.6166 9.20051 99.8194 8.98624 100.071 8.84185L100.071 8.84108Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M110.408 13.5589C110.418 13.9429 110.522 14.3254 110.717 14.694C110.938 15.0972 111.265 15.3967 111.689 15.5834C112.118 15.7707 112.61 15.8729 113.153 15.8859C113.689 15.899 114.243 15.8537 114.801 15.7515V14.201C114.276 14.2762 113.8 14.2962 113.387 14.2593C112.951 14.2209 112.63 14.0328 112.431 13.701C112.325 13.5305 112.269 13.307 112.26 13.0382C112.252 12.7748 112.248 12.466 112.248 12.1189V8.56844H114.801V7.12307H112.248V4.19775H110.392V7.12307H108.812V8.56844H110.392V12.2318C110.392 12.7249 110.397 13.1718 110.408 13.5597V13.5589Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "d": "M120.316 6.93339C120.116 6.93339 119.922 6.94645 119.733 6.97103C118.359 7.1853 117.955 7.88495 117.955 7.88495V7.67989H117.955V4.1709H116.134V15.7977H117.955V11.0222C117.955 9.38869 119.138 8.64527 120.238 8.64527C121.427 8.64527 122.004 9.28424 122.004 10.5998V15.7977H123.825V10.3495C123.825 8.27509 122.448 6.93416 120.316 6.93416L120.316 6.93339Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_10", + "d": "M107.589 7.19922H105.777V15.8162H107.589V7.19922Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_11", + "d": "M106.682 6.55761C107.334 6.55761 107.863 6.02913 107.863 5.37719C107.863 4.72527 107.334 4.19678 106.682 4.19678C106.03 4.19678 105.502 4.72527 105.502 5.37719C105.502 6.02913 106.03 6.55761 106.682 6.55761Z", + "fill": "#1C3C3C" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_12", + "d": "M22.3912 15.1309C22.286 15.306 21.9696 15.3175 21.7 15.1555C21.5618 15.0725 21.455 14.9581 21.3997 14.8337C21.349 14.7208 21.3483 14.614 21.3966 14.5334C21.4519 14.4412 21.5664 14.3944 21.7015 14.3944C21.8229 14.3944 21.9611 14.432 22.0886 14.5088C22.3582 14.6709 22.4972 14.9558 22.392 15.1309H22.3912ZM37.9908 9.9999C37.9908 15.293 33.6839 19.5999 28.3908 19.5999H9.81289C4.51983 19.5999 0.212891 15.2937 0.212891 9.9999C0.212891 4.70608 4.51983 0.399902 9.81289 0.399902H28.3908C33.6846 0.399902 37.9908 4.70685 37.9908 9.9999ZM18.714 14.8145C18.8653 14.6309 18.1664 14.1141 18.0236 13.9244C17.7333 13.6095 17.7317 13.1564 17.5359 12.7885C17.0567 11.678 16.506 10.5759 15.7357 9.63587C14.9216 8.60752 13.9171 7.75657 13.0347 6.7912C12.3795 6.11766 12.2044 5.15843 11.6261 4.43421C10.829 3.25686 8.30838 2.93584 7.93897 4.59856C7.94051 4.65078 7.92438 4.68381 7.87906 4.71683C7.67477 4.86505 7.49276 5.03478 7.33992 5.23984C6.96591 5.76054 6.90831 6.64374 7.37525 7.11145C7.39061 6.86493 7.39906 6.63222 7.59413 6.45558C7.9551 6.76585 8.50037 6.87491 8.91893 6.64374C9.8436 7.96393 9.6132 9.79024 10.3474 11.2126C10.5502 11.549 10.7545 11.8923 11.0148 12.1872C11.226 12.5159 11.9556 12.9037 11.9986 13.2078C12.0063 13.7301 11.9449 14.3007 12.2874 14.7377C12.4487 15.0649 12.0524 15.3936 11.7329 15.3529C11.3182 15.4097 10.8121 15.0741 10.4488 15.2807C10.3205 15.4197 10.0694 15.2661 9.9588 15.4588C9.9204 15.5587 9.71304 15.6992 9.83669 15.7952C9.97416 15.6908 10.1017 15.5817 10.2867 15.6439C10.2591 15.7945 10.3781 15.816 10.4726 15.8597C10.4695 15.9619 10.4096 16.0663 10.488 16.1531C10.5793 16.061 10.6339 15.9304 10.779 15.892C11.2613 16.5348 11.7521 15.2415 12.7958 15.8236C12.5838 15.8137 12.3957 15.8398 12.2528 16.0141C12.2175 16.0533 12.1875 16.0994 12.2497 16.15C12.8127 15.7868 12.8096 16.2745 13.1752 16.1247C13.4563 15.978 13.7358 15.7945 14.0699 15.8467C13.745 15.9404 13.732 16.2015 13.5415 16.4219C13.5093 16.4557 13.4939 16.4941 13.5315 16.5502C14.2058 16.4933 14.2611 16.2691 14.8057 15.9941C15.2119 15.7461 15.6167 16.3474 15.9684 16.0049C16.046 15.9304 16.152 15.9557 16.248 15.9458C16.1251 15.2907 14.7742 16.0656 14.7957 15.187C15.2304 14.8913 15.1305 14.3253 15.1597 13.8683C15.6597 14.1456 16.2157 14.3068 16.7057 14.5718C16.953 14.9712 17.3408 15.4988 17.8577 15.4642C17.8715 15.4243 17.8838 15.389 17.8984 15.3483C18.0551 15.3751 18.2563 15.4788 18.3423 15.2807C18.5765 15.5257 18.9206 15.5134 19.227 15.4504C19.4536 15.2661 18.8008 15.0034 18.7132 14.8137L18.714 14.8145ZM25.722 11.7187L24.6944 10.3317C23.7974 11.3577 23.1984 11.8577 23.1876 11.8669C23.1822 11.8723 22.6101 12.4291 22.0886 12.9068C21.5771 13.3753 21.1731 13.7462 20.9673 14.1517C20.9105 14.2638 20.7868 14.677 20.9604 15.0902C21.094 15.4097 21.3667 15.637 21.7714 15.766C21.8928 15.8044 22.0087 15.8213 22.1193 15.8213C22.8482 15.8213 23.3266 15.0902 23.3297 15.0841C23.3358 15.0756 23.9556 14.1901 24.7106 13.0719C24.9617 12.7002 25.2489 12.307 25.722 11.7179V11.7187ZM30.5535 14.9128C30.5535 14.7085 30.479 14.5111 30.3438 14.3583L30.2163 14.2139C29.446 13.3407 27.4684 11.0989 26.6989 10.228C25.7328 9.13437 24.6522 7.74045 24.5623 7.62371L24.4325 7.35491V6.88182C24.4325 6.70825 24.398 6.53853 24.3312 6.37878L24.0562 5.72598C24.0524 5.71677 24.0508 5.70601 24.0524 5.69603L24.0631 5.60541C24.0647 5.59081 24.0716 5.57776 24.0831 5.56777C24.2974 5.37885 25.0946 4.76598 26.3287 4.81744C26.49 4.82435 26.5184 4.73603 26.523 4.6984C26.5453 4.51715 26.1314 4.30365 25.7458 4.22454C25.2159 4.11625 23.8074 3.82902 22.6807 4.56861L22.6723 4.57475C21.9442 5.18301 21.359 5.64765 21.3529 5.65225L21.3398 5.66531C21.3314 5.67529 21.1255 5.92182 21.1755 6.23593C21.2077 6.44022 21.1017 6.51318 21.0956 6.51702C21.0894 6.52086 20.9451 6.61149 20.7961 6.50934C20.6156 6.37417 20.3022 6.60611 20.2385 6.6568L19.7654 7.06384L19.7562 7.07305C19.7477 7.08304 19.545 7.32035 19.8161 7.70128C20.0503 8.03075 20.1333 8.14057 20.3368 8.39401C20.5434 8.65053 20.9159 8.97539 20.9358 8.99229C20.9451 8.99997 21.1724 9.172 21.4857 8.93238C21.743 8.73501 21.9496 8.55683 21.9496 8.55683C21.9665 8.54301 22.1155 8.42013 22.1224 8.23888C22.1247 8.18665 22.1224 8.14134 22.1224 8.09987C22.1186 7.97238 22.1178 7.93475 22.2138 7.87331C22.2599 7.87331 22.4012 7.92477 22.5225 7.98621C22.5356 7.99389 22.8389 8.16438 23.1147 8.15209C23.2882 8.17513 23.4802 8.37251 23.5463 8.45315C23.5524 8.45929 24.1392 9.07523 24.9655 10.1558C25.123 10.3609 25.7013 11.1312 25.8595 11.3462C26.1237 11.7056 26.5238 12.2494 26.9539 12.8354C27.6988 13.8499 28.5344 14.9873 28.9138 15.4988C29.0398 15.6685 29.2233 15.7837 29.4307 15.8236L29.5712 15.8505C29.625 15.8605 29.6787 15.8659 29.7325 15.8659C29.9813 15.8659 30.2171 15.7568 30.373 15.5633L30.3815 15.5525C30.4936 15.4105 30.555 15.2284 30.555 15.0411V14.9128H30.5535ZM31.2147 5.80355L31.0512 5.63997C31.0035 5.59235 30.9367 5.56393 30.8691 5.56931C30.8016 5.57238 30.7378 5.6031 30.694 5.65533L29.6649 6.87261C29.6357 6.90717 29.5943 6.92867 29.5497 6.93328L29.1834 6.97014C29.1365 6.97475 29.0897 6.96016 29.0536 6.93021L28.4684 6.43485C28.4307 6.40259 28.4085 6.35651 28.4069 6.30736L28.3985 6.01398C28.397 5.97174 28.4115 5.93027 28.4384 5.89801L29.4391 4.69225C29.5144 4.60163 29.5136 4.4703 29.4376 4.37968L29.337 4.26064C29.2709 4.18307 29.1619 4.15465 29.0667 4.19075C28.8301 4.28061 28.2341 4.51331 27.8033 4.74678C27.1943 5.07549 26.7711 5.59696 26.6981 6.10768C26.6444 6.48169 26.6666 7.0984 26.6851 7.43248C26.692 7.56381 26.6628 7.69513 26.5991 7.81264C26.5207 7.95856 26.3825 8.19203 26.1721 8.47312C26.0645 8.62134 25.997 8.67587 25.904 8.78569L27.0361 10.1166C27.3087 9.79869 27.5475 9.55753 27.7557 9.32483C28.1358 8.90166 28.2541 8.89782 28.5705 8.88707C28.7656 8.88016 29.0329 8.87171 29.456 8.76573C30.6111 8.47696 30.9767 7.22665 30.992 7.17136L31.2793 6.03549C31.3 5.95331 31.2754 5.86422 31.2155 5.80432L31.2147 5.80355ZM13.4524 13.7324C13.328 14.2178 13.2873 15.0449 12.656 15.0687C12.6038 15.349 12.8503 15.4542 13.0738 15.3644C13.2958 15.2622 13.401 15.445 13.4755 15.627C13.818 15.677 14.3249 15.5126 14.3441 15.1071C13.8326 14.8122 13.6744 14.2516 13.4517 13.7324H13.4524Z", + "fill": "#1C3C3C" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "LangsmithIconBig" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.tsx b/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.tsx new file mode 100644 index 0000000000..0a0f2e0d05 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/LangsmithIconBig.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LangsmithIconBig.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 = 'LangsmithIconBig' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/TracingIcon.json b/web/app/components/base/icons/src/public/tracing/TracingIcon.json new file mode 100644 index 0000000000..508b555b0f --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/TracingIcon.json @@ -0,0 +1,47 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "20", + "height": "20", + "viewBox": "0 0 20 20", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "analytics-fill" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "opacity": "0.6", + "d": "M5 2.5C3.61929 2.5 2.5 3.61929 2.5 5V9.16667H6.15164C6.78293 9.16667 7.36003 9.52333 7.64235 10.088L8.33333 11.4699L10.9213 6.29399C11.0625 6.01167 11.351 5.83333 11.6667 5.83333C11.9823 5.83333 12.2708 6.01167 12.412 6.29399L13.8483 9.16667H17.5V5C17.5 3.61929 16.3807 2.5 15 2.5H5Z", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M2.5 14.9999C2.5 16.3807 3.61929 17.4999 5 17.4999H15C16.3807 17.4999 17.5 16.3807 17.5 14.9999V10.8333H13.8483C13.2171 10.8333 12.64 10.4766 12.3577 9.91195L11.6667 8.53003L9.07867 13.7059C8.9375 13.9883 8.649 14.1666 8.33333 14.1666C8.01769 14.1666 7.72913 13.9883 7.58798 13.7059L6.15164 10.8333H2.5V14.9999Z", + "fill": "white" + }, + "children": [] + } + ] + } + ] + }, + "name": "TracingIcon" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/arrows/FlipForward.tsx b/web/app/components/base/icons/src/public/tracing/TracingIcon.tsx similarity index 85% rename from web/app/components/base/icons/src/vender/line/arrows/FlipForward.tsx rename to web/app/components/base/icons/src/public/tracing/TracingIcon.tsx index 1a091772a5..e24b2d05f6 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/FlipForward.tsx +++ b/web/app/components/base/icons/src/public/tracing/TracingIcon.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './FlipForward.json' +import data from './TracingIcon.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' @@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP ref, ) => <IconBase {...props} ref={ref} data={data as IconData} />) -Icon.displayName = 'FlipForward' +Icon.displayName = 'TracingIcon' 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 new file mode 100644 index 0000000000..9cedf9cec3 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/index.ts @@ -0,0 +1,5 @@ +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 TracingIcon } from './TracingIcon' diff --git a/web/app/components/base/icons/src/vender/line/arrows/FlipBackward.json b/web/app/components/base/icons/src/vender/line/arrows/FlipBackward.json deleted file mode 100644 index f72101e3f1..0000000000 --- a/web/app/components/base/icons/src/vender/line/arrows/FlipBackward.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "24", - "height": "24", - "viewBox": "0 0 24 24", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13", - "stroke": "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round" - }, - "children": [] - } - ] - }, - "name": "FlipBackward" -} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/arrows/FlipForward.json b/web/app/components/base/icons/src/vender/line/arrows/FlipForward.json deleted file mode 100644 index 2e0b517c38..0000000000 --- a/web/app/components/base/icons/src/vender/line/arrows/FlipForward.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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": "g", - "attributes": { - "id": "Icon" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "id": "Icon_2", - "d": "M14 6.00016H5C3.34315 6.00016 2 7.34331 2 9.00016C2 10.657 3.34315 12.0002 5 12.0002H8M14 6.00016L11.3333 3.3335M14 6.00016L11.3333 8.66683", - "stroke": "currentColor", - "stroke-width": "1.5", - "stroke-linecap": "round", - "stroke-linejoin": "round" - }, - "children": [] - } - ] - } - ] - }, - "name": "FlipForward" -} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/arrows/index.ts b/web/app/components/base/icons/src/vender/line/arrows/index.ts index c1a1410482..c329b3636e 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/index.ts +++ b/web/app/components/base/icons/src/vender/line/arrows/index.ts @@ -3,8 +3,6 @@ export { default as ArrowUpRight } from './ArrowUpRight' export { default as ChevronDownDouble } from './ChevronDownDouble' export { default as ChevronRight } from './ChevronRight' export { default as ChevronSelectorVertical } from './ChevronSelectorVertical' -export { default as FlipBackward } from './FlipBackward' -export { default as FlipForward } from './FlipForward' export { default as RefreshCcw01 } from './RefreshCcw01' export { default as RefreshCw05 } from './RefreshCw05' export { default as ReverseLeft } from './ReverseLeft' diff --git a/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json b/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json index 8830ee5837..68e9a637cd 100644 --- a/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json +++ b/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json @@ -33,7 +33,7 @@ "attributes": { "d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z", "fill": "currentColor", - "fill-opacity": "0.5" + "fill-opacity": "0" }, "children": [] } diff --git a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json new file mode 100644 index 0000000000..6cc565ffdf --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json @@ -0,0 +1,57 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "17", + "viewBox": "0 0 16 17", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Solid" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M6.39498 2.71706C6.90587 2.57557 7.44415 2.49996 8.00008 2.49996C9.30806 2.49996 10.5183 2.91849 11.5041 3.62893C10.9796 3.97562 10.5883 4.35208 10.3171 4.75458C9.90275 5.36959 9.79654 6.00558 9.88236 6.58587C9.96571 7.1494 10.2245 7.63066 10.4965 7.98669C10.7602 8.33189 11.0838 8.6206 11.3688 8.76305C12.0863 9.12177 12.9143 9.30141 13.5334 9.39399C14.0933 9.47774 15.2805 9.75802 15.3244 8.86608C15.3304 8.74474 15.3334 8.62267 15.3334 8.49996C15.3334 4.44987 12.0502 1.16663 8.00008 1.16663C3.94999 1.16663 0.666748 4.44987 0.666748 8.49996C0.666748 12.55 3.94999 15.8333 8.00008 15.8333C8.1228 15.8333 8.24486 15.8303 8.3662 15.8243C8.73395 15.8062 9.01738 15.4934 8.99927 15.1256C8.98117 14.7579 8.66837 14.4745 8.30063 14.4926C8.20111 14.4975 8.10091 14.5 8.00008 14.5C5.6605 14.5 3.63367 13.1609 2.6442 11.2074L3.28991 10.8346L5.67171 11.2804C6.28881 11.3959 6.85846 10.9208 6.85566 10.293L6.84632 8.19093L8.06357 6.10697C8.26079 5.76932 8.24312 5.3477 8.01833 5.02774L6.39498 2.71706Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M9.29718 8.93736C9.05189 8.84432 8.77484 8.90379 8.58934 9.08929C8.40383 9.27479 8.34437 9.55184 8.43741 9.79713L10.5486 15.363C10.6461 15.6199 10.8912 15.7908 11.166 15.7932C11.4408 15.7956 11.689 15.6292 11.791 15.374L12.6714 13.1714L14.874 12.2909C15.1292 12.1889 15.2957 11.9408 15.2932 11.666C15.2908 11.3912 15.12 11.146 14.863 11.0486L9.29718 8.93736Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "Globe06" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.tsx b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.tsx new file mode 100644 index 0000000000..01a05fc91c --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Globe06.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 = 'Globe06' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/index.ts b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/index.ts index 82cb320ff2..0a0abda63c 100644 --- a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/index.ts +++ b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/index.ts @@ -1 +1,2 @@ +export { default as Globe06 } from './Globe06' export { default as Route } from './Route' diff --git a/web/app/components/base/image-gallery/index.tsx b/web/app/components/base/image-gallery/index.tsx index 4566868153..fd85bb1551 100644 --- a/web/app/components/base/image-gallery/index.tsx +++ b/web/app/components/base/image-gallery/index.tsx @@ -46,6 +46,7 @@ const ImageGallery: FC<Props> = ({ src={src} alt='' onClick={() => setImagePreviewUrl(src)} + onError={e => e.currentTarget.remove()} /> ))} { diff --git a/web/app/components/base/markdown.tsx b/web/app/components/base/markdown.tsx index fe55f6c8f1..11db2f727c 100644 --- a/web/app/components/base/markdown.tsx +++ b/web/app/components/base/markdown.tsx @@ -7,11 +7,12 @@ import RemarkGfm from 'remark-gfm' import SyntaxHighlighter from 'react-syntax-highlighter' import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' import type { RefObject } from 'react' -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useMemo, useRef, useState } from 'react' import cn from 'classnames' -import CopyBtn from '@/app/components/app/chat/copy-btn' -import SVGBtn from '@/app/components/app/chat/svg' -import Flowchart from '@/app/components/app/chat/mermaid' +import type { CodeComponent } from 'react-markdown/lib/ast-to-react' +import CopyBtn from '@/app/components/base/copy-btn' +import SVGBtn from '@/app/components/base/svg' +import Flowchart from '@/app/components/base/mermaid' // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD const capitalizationLanguageNameMap: Record<string, string> = { @@ -44,7 +45,8 @@ const preprocessLaTeX = (content: string) => { if (typeof content !== 'string') return content return content.replace(/\\\[(.*?)\\\]/gs, (_, equation) => `$$${equation}$$`) - .replace(/\\\((.*?)\\\)/gs, (_, equation) => `$${equation}$`) + .replace(/\\\((.*?)\\\)/gs, (_, equation) => `$$${equation}$$`) + .replace(/(^|[^\\])\$(.+?)\$/gs, (_, prefix, equation) => `${prefix}$${equation}$`) } export function PreCode(props: { children: any }) { @@ -88,8 +90,78 @@ const useLazyLoad = (ref: RefObject<Element>): boolean => { return isIntersecting } +// **Add code block +// Avoid error #185 (Maximum update depth exceeded. +// This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. +// React limits the number of nested updates to prevent infinite loops.) +// Reference A: https://reactjs.org/docs/error-decoder.html?invariant=185 +// Reference B1: https://react.dev/reference/react/memo +// Reference B2: https://react.dev/reference/react/useMemo +// **** +// The original error that occurred in the streaming response during the conversation: +// Error: Minified React error 185; +// 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. +const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }) => { + const [isSVG, setIsSVG] = useState(true) + const match = /language-(\w+)/.exec(className || '') + const language = match?.[1] + const languageShowName = getCorrectCapitalizationLanguageName(language || '') + + // Use `useMemo` to ensure that `SyntaxHighlighter` only re-renders when necessary + return useMemo(() => { + return (!inline && match) + ? ( + <div> + <div + className='flex justify-between h-8 items-center p-1 pl-3 border-b' + style={{ + borderColor: 'rgba(0, 0, 0, 0.05)', + }} + > + <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div> + <div style={{ display: 'flex' }}> + {language === 'mermaid' + && <SVGBtn + isSVG={isSVG} + setIsSVG={setIsSVG} + /> + } + <CopyBtn + className='mr-1' + value={String(children).replace(/\n$/, '')} + isPlain + /> + </div> + </div> + {(language === 'mermaid' && isSVG) + ? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />) + : (<SyntaxHighlighter + {...props} + style={atelierHeathLight} + customStyle={{ + paddingLeft: 12, + backgroundColor: '#fff', + }} + language={match[1]} + showLineNumbers + PreTag="div" + > + {String(children).replace(/\n$/, '')} + </SyntaxHighlighter>)} + </div> + ) + : ( + <code {...props} className={className}> + {children} + </code> + ) + }, [children, className, inline, isSVG, language, languageShowName, match, props]) +}) + +CodeBlock.displayName = 'CodeBlock' + export function Markdown(props: { content: string; className?: string }) { - const [isSVG, setIsSVG] = useState(false) const latexContent = preprocessLaTeX(props.content) return ( <div className={cn(props.className, 'markdown-body')}> @@ -99,57 +171,7 @@ export function Markdown(props: { content: string; className?: string }) { RehypeKatex as any, ]} components={{ - code({ inline, className, children, ...props }) { - const match = /language-(\w+)/.exec(className || '') - const language = match?.[1] - const languageShowName = getCorrectCapitalizationLanguageName(language || '') - return (!inline && match) - ? ( - <div> - <div - className='flex justify-between h-8 items-center p-1 pl-3 border-b' - style={{ - borderColor: 'rgba(0, 0, 0, 0.05)', - }} - > - <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div> - <div style={{ display: 'flex' }}> - {language === 'mermaid' - && <SVGBtn - isSVG={isSVG} - setIsSVG={setIsSVG} - /> - } - <CopyBtn - className='mr-1' - value={String(children).replace(/\n$/, '')} - isPlain - /> - </div> - </div> - {(language === 'mermaid' && isSVG) - ? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />) - : (<SyntaxHighlighter - {...props} - style={atelierHeathLight} - customStyle={{ - paddingLeft: 12, - backgroundColor: '#fff', - }} - language={match[1]} - showLineNumbers - PreTag="div" - > - {String(children).replace(/\n$/, '')} - </SyntaxHighlighter>)} - </div> - ) - : ( - <code {...props} className={className}> - {children} - </code> - ) - }, + code: CodeBlock, img({ src, alt, ...props }) { return ( // eslint-disable-next-line @next/next/no-img-element diff --git a/web/app/components/app/chat/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx similarity index 71% rename from web/app/components/app/chat/mermaid/index.tsx rename to web/app/components/base/mermaid/index.tsx index 86f472c06e..bef26b7a36 100644 --- a/web/app/components/app/chat/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react' import mermaid from 'mermaid' import CryptoJS from 'crypto-js' +import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' let mermaidAPI: any mermaidAPI = null @@ -23,12 +24,24 @@ const style = { overflow: 'auto', } +const svgToBase64 = (svgGraph: string) => { + const svgBytes = new TextEncoder().encode(svgGraph) + const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' }) + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result) + reader.onerror = reject + reader.readAsDataURL(blob) + }) +} + const Flowchart = React.forwardRef((props: { PrimitiveCode: string }, ref) => { const [svgCode, setSvgCode] = useState(null) const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`) - const [isRender, setIsRender] = useState(true) + const [isRender, setIsRender] = useState(false) + const [isLoading, setIsLoading] = useState(true) const clearFlowchartCache = () => { for (let i = localStorage.length - 1; i >= 0; --i) { @@ -43,14 +56,19 @@ const Flowchart = React.forwardRef((props: { const cachedSvg: any = localStorage.getItem(chartId.current) if (cachedSvg) { setSvgCode(cachedSvg) + setIsLoading(false) return } if (typeof window !== 'undefined' && mermaidAPI) { const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode) - // eslint-disable-next-line @typescript-eslint/no-use-before-define + const dom = new DOMParser().parseFromString(svgGraph.svg, 'text/xml') + if (!dom.querySelector('g.main')) + throw new Error('empty svg') + const base64Svg: any = await svgToBase64(svgGraph.svg) setSvgCode(base64Svg) + setIsLoading(false) if (chartId.current && base64Svg) localStorage.setItem(chartId.current, base64Svg) } @@ -62,17 +80,6 @@ const Flowchart = React.forwardRef((props: { } } - const svgToBase64 = (svgGraph: string) => { - const svgBytes = new TextEncoder().encode(svgGraph) - const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' }) - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result) - reader.onerror = reject - reader.readAsDataURL(blob) - }) - } - const handleReRender = () => { setIsRender(false) setSvgCode(null) @@ -99,10 +106,15 @@ const Flowchart = React.forwardRef((props: { <div ref={ref}> { isRender - && <div id={chartId.current} className="mermaid" style={style}> + && <div className="mermaid" style={style}> {svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="Mermaid chart" />} </div> } + {isLoading + && <div className='py-4 px-[26px]'> + <LoadingAnim type='text' /> + </div> + } </div> ) }) diff --git a/web/app/components/base/message-log-modal/index.tsx b/web/app/components/base/message-log-modal/index.tsx index f31e080b42..ef5a494579 100644 --- a/web/app/components/base/message-log-modal/index.tsx +++ b/web/app/components/base/message-log-modal/index.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useBoolean, useClickAway } from 'ahooks' import { RiCloseLine } from '@remixicon/react' import IterationResultPanel from '../../workflow/run/iteration-result-panel' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' import Run from '@/app/components/workflow/run' import type { NodeTracing } from '@/types/workflow' diff --git a/web/app/components/base/prompt-log-modal/index.tsx b/web/app/components/base/prompt-log-modal/index.tsx index aa5b8a1faf..3cf47d8d1a 100644 --- a/web/app/components/base/prompt-log-modal/index.tsx +++ b/web/app/components/base/prompt-log-modal/index.tsx @@ -4,7 +4,7 @@ import { useClickAway } from 'ahooks' import { RiCloseLine } from '@remixicon/react' import Card from './card' import { CopyFeedbackNew } from '@/app/components/base/copy-feedback' -import type { IChatItem } from '@/app/components/app/chat/type' +import type { IChatItem } from '@/app/components/base/chat/chat/type' type PromptLogModalProps = { currentLogItem?: IChatItem @@ -33,7 +33,7 @@ const PromptLogModal: FC<PromptLogModalProps> = ({ return ( <div - className='fixed top-16 left-2 bottom-2 flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10' + className='relative flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10' style={{ width: 480, position: 'fixed', diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index 51e371ac09..b342ef29bb 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -191,6 +191,7 @@ const SimpleSelect: FC<ISelectProps> = ({ onClick={(e) => { e.stopPropagation() setSelectedItem(null) + onSelect({ value: null }) }} className="h-5 w-5 text-gray-400 cursor-pointer" aria-hidden="false" diff --git a/web/app/components/app/chat/svg/index.tsx b/web/app/components/base/svg/index.tsx similarity index 100% rename from web/app/components/app/chat/svg/index.tsx rename to web/app/components/base/svg/index.tsx diff --git a/web/app/components/app/chat/svg/style.module.css b/web/app/components/base/svg/style.module.css similarity index 100% rename from web/app/components/app/chat/svg/style.module.css rename to web/app/components/base/svg/style.module.css diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index f3bd8eccda..8630cc2b9c 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -775,7 +775,7 @@ const StepTwo = ({ <div className={s.label}> {t('datasetSettings.form.retrievalSetting.title')} <div className='leading-[18px] text-xs font-normal text-gray-500'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/features/retrieval-augment' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.longDescription')} </div> </div> diff --git a/web/app/components/datasets/create/website/firecrawl/index.tsx b/web/app/components/datasets/create/website/firecrawl/index.tsx index bdd99b6dcd..55a9f6b7ef 100644 --- a/web/app/components/datasets/create/website/firecrawl/index.tsx +++ b/web/app/components/datasets/create/website/firecrawl/index.tsx @@ -118,6 +118,7 @@ const FireCrawl: FC<Props> = ({ ...res, total: Math.min(res.total, parseFloat(crawlOptions.limit as string)), }) + onCheckedCrawlResultChange(res.data || []) // default select the crawl result await sleep(2500) return await waitForCrawlFinished(jobId) } @@ -162,6 +163,7 @@ const FireCrawl: FC<Props> = ({ } else { setCrawlResult(data) + onCheckedCrawlResultChange(data.data || []) // default select the crawl result setCrawlErrorMessage('') } } diff --git a/web/app/components/datasets/hit-testing/hit-detail.tsx b/web/app/components/datasets/hit-testing/hit-detail.tsx index 806662aeb5..5af022202b 100644 --- a/web/app/components/datasets/hit-testing/hit-detail.tsx +++ b/web/app/components/datasets/hit-testing/hit-detail.tsx @@ -2,51 +2,16 @@ import type { FC } from 'react' import React from 'react' import cn from 'classnames' import { useTranslation } from 'react-i18next' -import ReactECharts from 'echarts-for-react' import { SegmentIndexTag } from '../documents/detail/completed' import s from '../documents/detail/completed/style.module.css' import type { SegmentDetailModel } from '@/models/datasets' import Divider from '@/app/components/base/divider' -type IScatterChartProps = { - data: Array<number[]> - curr: Array<number[]> -} - -const ScatterChart: FC<IScatterChartProps> = ({ data, curr }) => { - const option = { - xAxis: {}, - yAxis: {}, - tooltip: { - trigger: 'item', - axisPointer: { - type: 'cross', - }, - }, - series: [ - { - type: 'effectScatter', - symbolSize: 5, - data: curr, - }, - { - type: 'scatter', - symbolSize: 5, - data, - }, - ], - } - return ( - <ReactECharts option={option} style={{ height: 380, width: 430 }} /> - ) -} - type IHitDetailProps = { segInfo?: Partial<SegmentDetailModel> & { id: string } - vectorInfo?: { curr: Array<number[]>; points: Array<number[]> } } -const HitDetail: FC<IHitDetailProps> = ({ segInfo, vectorInfo }) => { +const HitDetail: FC<IHitDetailProps> = ({ segInfo }) => { const { t } = useTranslation() const renderContent = () => { @@ -65,8 +30,8 @@ const HitDetail: FC<IHitDetailProps> = ({ segInfo, vectorInfo }) => { } return ( - <div className='flex flex-row overflow-x-auto'> - <div className="flex-1 bg-gray-25 p-6 min-w-[300px]"> + <div className='overflow-x-auto'> + <div className="bg-gray-25 p-6"> <div className="flex items-center"> <SegmentIndexTag positionId={segInfo?.position || ''} @@ -94,20 +59,6 @@ const HitDetail: FC<IHitDetailProps> = ({ segInfo, vectorInfo }) => { })} </div> </div> - <div className="flex-1 bg-white p-6"> - <div className="flex items-center"> - <div className={cn(s.commonIcon, s.bezierCurveIcon)} /> - <span className={s.numberInfo}> - {t('datasetDocuments.segment.vectorHash')} - </span> - </div> - <div - className={cn(s.numberInfo, 'w-[400px] truncate text-gray-700 mt-1')} - > - {segInfo?.index_node_hash} - </div> - <ScatterChart data={vectorInfo?.points || []} curr={vectorInfo?.curr || []} /> - </div> </div> ) } diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index bff13314b0..8c665b1889 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import useSWR from 'swr' import { omit } from 'lodash-es' @@ -62,8 +62,6 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => { const total = recordsRes?.total || 0 - const points = useMemo(() => (hitResult?.records.map(v => [v.tsne_position.x, v.tsne_position.y]) || []), [hitResult?.records]) - const onClickCard = (detail: HitTestingType) => { setCurrParagraph({ paraInfo: detail, showModal: true }) } @@ -194,17 +192,13 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => { </div> </FloatRightContainer> <Modal - className='!max-w-[960px] !p-0' + className='w-[520px] p-0' closable onClose={() => setCurrParagraph({ showModal: false })} isShow={currParagraph.showModal} > {currParagraph.showModal && <HitDetail segInfo={currParagraph.paraInfo?.segment} - vectorInfo={{ - curr: [[currParagraph.paraInfo?.tsne_position?.x || 0, currParagraph.paraInfo?.tsne_position.y || 0]], - points, - }} />} </Modal> <Drawer isOpen={isShowModifyRetrievalModal} onClose={() => setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'> 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 87bd555350..be5c1be2e7 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -77,7 +77,7 @@ const ModifyRetrievalModal: FC<Props> = ({ <div className='text-base font-semibold text-gray-900'> <div>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='leading-[18px] text-xs font-normal text-gray-500'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/features/retrieval-augment' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.description')} </div> </div> diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 64911e901c..77910c1a61 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -25,6 +25,7 @@ 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 type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' const rowClass = ` flex justify-between py-4 flex-wrap gap-y-2 @@ -110,7 +111,10 @@ const Form = () => { description, permission, indexing_technique: indexMethod, - retrieval_model: postRetrievalConfig, + retrieval_model: { + ...postRetrievalConfig, + score_threshold: postRetrievalConfig.score_threshold_enabled ? postRetrievalConfig.score_threshold : 0, + }, embedding_model: embeddingModel.model, embedding_model_provider: embeddingModel.provider, }, @@ -194,7 +198,7 @@ const Form = () => { </div> </> )} - {currentDataset && currentDataset.indexing_technique === 'high_quality' && ( + {indexMethod === 'high_quality' && ( <div className={rowClass}> <div className={labelClass}> <div>{t('datasetSettings.form.embeddingModel')}</div> @@ -217,7 +221,7 @@ const Form = () => { <div> <div>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='leading-[18px] text-xs font-normal text-gray-500'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/features/retrieval-augment' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.description')} </div> </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 778a815bc9..ca93e9e19d 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -69,7 +69,7 @@ const WorkplaceSelector = () => { <Menu.Items className={cn( ` - absolute top-[1px] min-w-[200px] z-10 bg-white border-[0.5px] border-gray-200 + absolute top-[1px] min-w-[200px] max-h-[70vh] overflow-y-scroll z-10 bg-white border-[0.5px] border-gray-200 divide-y divide-gray-100 origin-top-right rounded-xl `, s.popup, diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx index 63ad8df0d8..19ec75c6c6 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx @@ -77,7 +77,7 @@ const DataSourceWebsite: FC<Props> = () => { logo: ({ className }: { className: string }) => ( <div className={cn(className, 'flex items-center justify-center w-5 h-5 bg-white border border-gray-100 text-xs font-medium text-gray-500 rounded ml-3')}>🔥</div> ), - name: 'FireCrawl', + name: 'Firecrawl', isActive: true, }))} onRemove={handleRemove(DataSourceProvider.fireCrawl)} diff --git a/web/app/components/header/account-setting/data-source-page/panel/index.tsx b/web/app/components/header/account-setting/data-source-page/panel/index.tsx index 2c27005d1d..95475059e8 100644 --- a/web/app/components/header/account-setting/data-source-page/panel/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/panel/index.tsx @@ -46,7 +46,7 @@ const Panel: FC<Props> = ({ <div className='text-sm font-medium text-gray-800'>{t(`common.dataSource.${type}.title`)}</div> {isWebsite && ( <div className='ml-1 leading-[18px] px-1.5 rounded-md bg-white border border-gray-100 text-xs font-medium text-gray-700'> - <span className='text-gray-500'>{t('common.dataSource.website.with')}</span> 🔥 FireCrawl + <span className='text-gray-500'>{t('common.dataSource.website.with')}</span> 🔥 Firecrawl </div> )} </div> 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 ca9f3cbd38..cc8aa92fec 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 @@ -114,7 +114,7 @@ const Form: FC<FormProps> = ({ validated={validatedSuccess} placeholder={placeholder?.[language] || placeholder?.en_US} disabled={disabed} - type={formSchema.type === FormTypeEnum.textNumber ? 'number' : 'text'} + type={formSchema.type === FormTypeEnum.textNumber ? 'number' : formSchema.type === FormTypeEnum.secretInput ? 'password' : 'text'} {...(formSchema.type === FormTypeEnum.textNumber ? { min: (formSchema as CredentialFormSchemaNumberInput).min, max: (formSchema as CredentialFormSchemaNumberInput).max } : {})} /> {fieldMoreInfo?.(formSchema)} @@ -229,6 +229,7 @@ const Form: FC<FormProps> = ({ variable, label, show_on, + required, } = formSchema as CredentialFormSchemaRadio if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)) @@ -239,11 +240,16 @@ const Form: FC<FormProps> = ({ <div className='flex items-center justify-between py-2 text-sm text-gray-900'> <div className='flex items-center space-x-2'> <span className={cn(fieldLabelClassName, 'py-2 text-sm text-gray-900')}>{label[language] || label.en_US}</span> + { + required && ( + <span className='ml-1 text-red-500'>*</span> + ) + } {tooltipContent} </div> <Radio.Group className='flex items-center' - value={value[variable] ? 1 : 0} + value={value[variable] === null ? undefined : (value[variable] ? 1 : 0)} onChange={val => handleFormChange(variable, val === 1)} > <Radio value={1} className='!mr-1'>True</Radio> diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx index 12244a5cef..86d52619e6 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx @@ -53,7 +53,7 @@ const Input: FC<InputProps> = ({ onChange={e => onChange(e.target.value)} onBlur={e => toLimit(e.target.value)} onFocus={onFocus} - value={value || ''} + value={value} disabled={disabled} type={type} min={min} diff --git a/web/app/components/share/chat/config-scence/index.tsx b/web/app/components/share/chat/config-scence/index.tsx deleted file mode 100644 index 152d5e58a4..0000000000 --- a/web/app/components/share/chat/config-scence/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { FC } from 'react' -import React from 'react' -import type { IWelcomeProps } from '../welcome' -import Welcome from '../welcome' - -const ConfigSence: FC<IWelcomeProps> = (props) => { - return ( - <div className='mb-5 antialiased font-sans shrink-0'> - <Welcome {...props} /> - </div> - ) -} -export default React.memo(ConfigSence) diff --git a/web/app/components/share/chat/hooks/use-conversation.ts b/web/app/components/share/chat/hooks/use-conversation.ts deleted file mode 100644 index 00503500ec..0000000000 --- a/web/app/components/share/chat/hooks/use-conversation.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useCallback, useState } from 'react' -import produce from 'immer' -import { useGetState } from 'ahooks' -import type { ConversationItem } from '@/models/share' - -const storageConversationIdKey = 'conversationIdInfo' - -type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'> -function useConversation() { - const [conversationList, setConversationList] = useState<ConversationItem[]>([]) - const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([]) - const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1') - // when set conversation id, we do not have set appId - const setCurrConversationId = useCallback((id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { - doSetCurrConversationId(id) - if (isSetToLocalStroge && id !== '-1') { - // conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2} - const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} - conversationIdInfo[appId] = id - globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo)) - } - }, [doSetCurrConversationId]) - - const getConversationIdFromStorage = (appId: string) => { - const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} - const id = conversationIdInfo[appId] - return id - } - - const isNewConversation = currConversationId === '-1' - // input can be updated by user - const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null) - const resetNewConversationInputs = () => { - if (!newConversationInputs) - return - setNewConversationInputs(produce(newConversationInputs, (draft) => { - Object.keys(draft).forEach((key) => { - draft[key] = '' - }) - })) - } - const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null) - const currInputs = isNewConversation ? newConversationInputs : existConversationInputs - const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs - - // info is muted - const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null) - const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null) - const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo - - return { - conversationList, - setConversationList, - pinnedConversationList, - setPinnedConversationList, - currConversationId, - getCurrConversationId, - setCurrConversationId, - getConversationIdFromStorage, - isNewConversation, - currInputs, - newConversationInputs, - existConversationInputs, - resetNewConversationInputs, - setCurrInputs, - currConversationInfo, - setNewConversationInfo, - existConversationInfo, - setExistConversationInfo, - } -} - -export default useConversation diff --git a/web/app/components/share/chat/index.tsx b/web/app/components/share/chat/index.tsx deleted file mode 100644 index f2efaa58b7..0000000000 --- a/web/app/components/share/chat/index.tsx +++ /dev/null @@ -1,953 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -'use client' -import type { FC } from 'react' -import React, { useCallback, useEffect, useRef, useState } from 'react' -import cn from 'classnames' -import useSWR from 'swr' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import produce, { setAutoFreeze } from 'immer' -import { useBoolean, useGetState } from 'ahooks' -import AppUnavailable from '../../base/app-unavailable' -import { checkOrSetAccessToken } from '../utils' -import { addFileInfos, sortAgentSorts } from '../../tools/utils' -import useConversation from './hooks/use-conversation' -import { ToastContext } from '@/app/components/base/toast' -import Sidebar from '@/app/components/share/chat/sidebar' -import ConfigSence from '@/app/components/share/chat/config-scence' -import Header from '@/app/components/share/header' -import { - delConversation, - fetchAppInfo, - fetchAppMeta, - fetchAppParams, - fetchChatList, - fetchConversations, - fetchSuggestedQuestions, - generationConversationName, - pinConversation, - sendChatMessage, - stopChatMessageResponding, - unpinConversation, - updateFeedback, -} from '@/service/share' -import type { AppMeta, ConversationItem, SiteInfo } from '@/models/share' - -import type { - CitationConfig, - PromptConfig, - SpeechToTextConfig, - SuggestedQuestionsAfterAnswerConfig, - TextToSpeechConfig, -} from '@/models/debug' -import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type' -import Chat from '@/app/components/app/chat' -import { changeLanguage } from '@/i18n/i18next-config' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import Loading from '@/app/components/base/loading' -import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' -import { userInputsFormToPromptVariables } from '@/utils/model-config' -import type { InstalledApp } from '@/models/explore' -import Confirm from '@/app/components/base/confirm' -import type { VisionFile, VisionSettings } from '@/types/app' -import { Resolution, TransferMethod } from '@/types/app' -import { fetchFileUploadConfig } from '@/service/common' -import type { Annotation as AnnotationType } from '@/models/log' - -export type IMainProps = { - isInstalledApp?: boolean - installedAppInfo?: InstalledApp - isSupportPlugin?: boolean -} - -const Main: FC<IMainProps> = ({ - isInstalledApp = false, - installedAppInfo, -}) => { - const { t } = useTranslation() - const { notify } = useContext(ToastContext) - const media = useBreakpoints() - const isMobile = media === MediaType.mobile - - /* - * app info - */ - const [appUnavailable, setAppUnavailable] = useState<boolean>(false) - const [isUnknownReason, setIsUnknwonReason] = useState<boolean>(false) - const [appId, setAppId] = useState<string>('') - const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) - const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() - const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null) - const [inited, setInited] = useState<boolean>(false) - const [plan, setPlan] = useState<string>('basic') // basic/plus/pro - const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false) - const [customConfig, setCustomConfig] = useState<any>(null) - const [appMeta, setAppMeta] = useState<AppMeta | null>(null) - // in mobile, show sidebar by click button - const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) - // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. - useEffect(() => { - if (siteInfo?.title) { - if (canReplaceLogo) - document.title = `${siteInfo.title}` - else - document.title = `${siteInfo.title} - Powered by Dify` - } - }, [siteInfo?.title, canReplaceLogo]) - - /* - * conversation info - */ - const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([]) - const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false) - const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false) - const { - conversationList, - setConversationList, - pinnedConversationList, - setPinnedConversationList, - currConversationId, - getCurrConversationId, - setCurrConversationId, - getConversationIdFromStorage, - isNewConversation, - currConversationInfo, - currInputs, - newConversationInputs, - // existConversationInputs, - resetNewConversationInputs, - setCurrInputs, - setNewConversationInfo, - existConversationInfo, - setExistConversationInfo, - } = useConversation() - const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) - const [hasMore, setHasMore] = useState<boolean>(true) - const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true) - const [isShowSuggestion, setIsShowSuggestion] = useState(false) - const onMoreLoaded = useCallback(({ data: conversations, has_more }: any) => { - setHasMore(has_more) - if (isClearConversationList) { - setConversationList(conversations) - clearConversationListFalse() - } - else { - setConversationList([...conversationList, ...conversations]) - } - }, [conversationList, setConversationList, isClearConversationList, clearConversationListFalse]) - const onPinnedMoreLoaded = useCallback(({ data: conversations, has_more }: any) => { - setHasPinnedMore(has_more) - if (isClearPinnedConversationList) { - setPinnedConversationList(conversations) - clearPinnedConversationListFalse() - } - else { - setPinnedConversationList([...pinnedConversationList, ...conversations]) - } - }, [pinnedConversationList, setPinnedConversationList, isClearPinnedConversationList, clearPinnedConversationListFalse]) - const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0) - const noticeUpdateList = useCallback(() => { - setHasMore(true) - clearConversationListTrue() - - setHasPinnedMore(true) - clearPinnedConversationListTrue() - - setControlUpdateConversationList(Date.now()) - }, [clearConversationListTrue, clearPinnedConversationListTrue]) - const handlePin = useCallback(async (id: string) => { - await pinConversation(isInstalledApp, installedAppInfo?.id, id) - notify({ type: 'success', message: t('common.api.success') }) - noticeUpdateList() - }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList]) - - const handleUnpin = useCallback(async (id: string) => { - await unpinConversation(isInstalledApp, installedAppInfo?.id, id) - notify({ type: 'success', message: t('common.api.success') }) - noticeUpdateList() - }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList]) - const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false) - const [toDeleteConversationId, setToDeleteConversationId] = useState('') - const handleDelete = useCallback((id: string) => { - setToDeleteConversationId(id) - hideSidebar() // mobile - showConfirm() - }, [hideSidebar, showConfirm]) - - const didDelete = async () => { - await delConversation(isInstalledApp, installedAppInfo?.id, toDeleteConversationId) - notify({ type: 'success', message: t('common.api.success') }) - hideConfirm() - if (currConversationId === toDeleteConversationId) - handleConversationIdChange('-1') - - noticeUpdateList() - } - - const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) - const [speechToTextConfig, setSpeechToTextConfig] = useState<SpeechToTextConfig | null>(null) - const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) - const [citationConfig, setCitationConfig] = useState<CitationConfig | null>(null) - const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) - const chatListDomRef = useRef<HTMLDivElement>(null) - const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false) - const [abortController, setAbortController] = useState<AbortController | null>(null) - const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) - const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false) - const conversationIntroduction = currConversationInfo?.introduction || '' - const createNewChat = useCallback(async () => { - // if new chat is already exist, do not create new chat - abortController?.abort() - setRespondingFalse() - if (conversationList.some(item => item.id === '-1')) - return - - setConversationList(produce(conversationList, (draft) => { - draft.unshift({ - id: '-1', - name: t('share.chat.newChatDefaultName'), - inputs: newConversationInputs, - introduction: conversationIntroduction, - }) - })) - }, [ - abortController, - setRespondingFalse, - setConversationList, - conversationList, - newConversationInputs, - conversationIntroduction, - t, - ]) - const handleStartChat = useCallback((inputs: Record<string, any>) => { - createNewChat() - setConversationIdChangeBecauseOfNew(true) - setCurrInputs(inputs) - setChatStarted() - // parse variables in introduction - setChatList(generateNewChatListWithOpenstatement('', inputs)) - }, [ - createNewChat, - setConversationIdChangeBecauseOfNew, - setCurrInputs, - setChatStarted, - setChatList, - ]) - const hasSetInputs = (() => { - if (!isNewConversation) - return true - - return isChatStarted - })() - - const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string - const [controlChatUpdateAllConversation, setControlChatUpdateAllConversation] = useState(0) - - // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 - useEffect(() => { - setAutoFreeze(false) - return () => { - setAutoFreeze(true) - } - }, []) - - useEffect(() => { - (async () => { - if (controlChatUpdateAllConversation && !isNewConversation) { - const { data: allConversations } = await fetchAllConversations() as { data: ConversationItem[]; has_more: boolean } - const item = allConversations.find(item => item.id === currConversationId) - setAllConversationList(allConversations) - if (item) { - setExistConversationInfo({ - ...existConversationInfo, - name: item?.name || '', - } as any) - } - } - })() - }, [controlChatUpdateAllConversation]) - - const handleConversationSwitch = () => { - if (!inited) - return - if (!appId) { - // wait for appId - setTimeout(handleConversationSwitch, 100) - return - } - - // update inputs of current conversation - let notSyncToStateIntroduction = '' - let notSyncToStateInputs: Record<string, any> | undefined | null = {} - if (!isNewConversation) { - const item = allConversationList.find(item => item.id === currConversationId) - notSyncToStateInputs = item?.inputs || {} - setCurrInputs(notSyncToStateInputs) - notSyncToStateIntroduction = item?.introduction || '' - setExistConversationInfo({ - name: item?.name || '', - introduction: notSyncToStateIntroduction, - }) - } - else { - notSyncToStateInputs = newConversationInputs - setCurrInputs(notSyncToStateInputs) - } - - // update chat list of current conversation - if (!isNewConversation && !conversationIdChangeBecauseOfNew) { - fetchChatList(currConversationId, isInstalledApp, installedAppInfo?.id).then((res: any) => { - const { data } = res - const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs) - - data.forEach((item: any) => { - newChatList.push({ - id: `question-${item.id}`, - content: item.query, - isAnswer: false, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], - }) - newChatList.push({ - id: item.id, - content: item.answer, - agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), - feedback: item.feedback, - isAnswer: true, - citation: item.retriever_resources, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], - }) - }) - setChatList(newChatList) - }) - } - - if (isNewConversation && isChatStarted) - setChatList(generateNewChatListWithOpenstatement()) - - setControlFocus(Date.now()) - } - useEffect(handleConversationSwitch, [currConversationId, inited]) - - /* - * chat info. chat is under conversation. - */ - useEffect(() => { - // scroll to bottom - if (chatListDomRef.current) - chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight - }, [chatList, currConversationId]) - // user can not edit inputs if user had send message - const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation - - const handleConversationIdChange = useCallback((id: string) => { - if (id === '-1') { - createNewChat() - setConversationIdChangeBecauseOfNew(true) - } - else { - setConversationIdChangeBecauseOfNew(false) - } - // trigger handleConversationSwitch - setCurrConversationId(id, appId) - setIsShowSuggestion(false) - hideSidebar() - }, [ - appId, - createNewChat, - hideSidebar, - setCurrConversationId, - setIsShowSuggestion, - setConversationIdChangeBecauseOfNew, - ]) - - // sometime introduction is not applied to state - const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => { - let caculatedIntroduction = introduction || conversationIntroduction || '' - const caculatedPromptVariables = inputs || currInputs || null - if (caculatedIntroduction && caculatedPromptVariables) - caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables) - - const openstatement = { - id: `${Date.now()}`, - content: caculatedIntroduction, - isAnswer: true, - feedbackDisabled: true, - isOpeningStatement: true, - suggestedQuestions: openingSuggestedQuestions, - } - if (caculatedIntroduction) - return [openstatement] - - return [] - } - - const fetchAllConversations = () => { - return fetchConversations(isInstalledApp, installedAppInfo?.id, undefined, undefined, 100) - } - - const fetchInitData = async () => { - if (!isInstalledApp) - await checkOrSetAccessToken() - - return Promise.all([isInstalledApp - ? { - app_id: installedAppInfo?.id, - site: { - title: installedAppInfo?.app.name, - icon: installedAppInfo?.app.icon, - icon_background: installedAppInfo?.app.icon_background, - prompt_public: false, - copyright: '', - }, - plan: 'basic', - } - : fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id), fetchAppMeta(isInstalledApp, installedAppInfo?.id)]) - } - - const { data: fileUploadConfigResponse } = useSWR(isInstalledApp ? { url: '/files/upload' } : null, fetchFileUploadConfig) - - // init - useEffect(() => { - (async () => { - try { - const [appData, conversationData, appParams, appMeta]: any = await fetchInitData() - setAppMeta(appMeta) - const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData - setAppId(appId) - setPlan(plan) - setCanReplaceLogo(can_replace_logo) - setCustomConfig(custom_config) - const tempIsPublicVersion = siteInfo.prompt_public - setIsPublicVersion(tempIsPublicVersion) - const prompt_template = '' - // handle current conversation id - const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean } - const _conversationId = getConversationIdFromStorage(appId) - const isNotNewConversation = allConversations.some(item => item.id === _conversationId) - setAllConversationList(allConversations) - // fetch new conversation info - const { user_input_form, opening_statement: introduction, suggested_questions, suggested_questions_after_answer, speech_to_text, text_to_speech, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams - setVisionConfig({ - ...file_upload.image, - image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, - }) - const prompt_variables = userInputsFormToPromptVariables(user_input_form) - if (siteInfo.default_language) - changeLanguage(siteInfo.default_language) - - setNewConversationInfo({ - name: t('share.chat.newChatDefaultName'), - introduction, - }) - setOpeningSuggestedQuestions(suggested_questions || []) - - setSiteInfo(siteInfo as SiteInfo) - setPromptConfig({ - prompt_template, - prompt_variables, - } as PromptConfig) - setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer) - setSpeechToTextConfig(speech_to_text) - setTextToSpeechConfig(text_to_speech) - setCitationConfig(retriever_resource) - - // setConversationList(conversations as ConversationItem[]) - - if (isNotNewConversation) - setCurrConversationId(_conversationId, appId, false) - - setInited(true) - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknwonReason(true) - setAppUnavailable(true) - } - } - })() - }, []) - - const logError = useCallback((message: string) => { - notify({ type: 'error', message }) - }, [notify]) - - const checkCanSend = useCallback(() => { - if (currConversationId !== '-1') - return true - - const prompt_variables = promptConfig?.prompt_variables - const inputs = currInputs - if (!inputs || !prompt_variables || prompt_variables?.length === 0) - return true - - let hasEmptyInput = '' - const requiredVars = prompt_variables?.filter(({ key, name, required }) => { - const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) - return res - }) || [] // compatible with old version - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs?.[key]) - hasEmptyInput = name - }) - - if (hasEmptyInput) { - logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput })) - return false - } - return !hasEmptyInput - }, [currConversationId, currInputs, promptConfig, t, logError]) - - const [controlFocus, setControlFocus] = useState(0) - const doShowSuggestion = isShowSuggestion && !isResponding - const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState<string[]>([]) - const [messageTaskId, setMessageTaskId] = useState('') - const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) - const [isRespondingConIsCurrCon, setIsRespondingConCurrCon, getIsRespondingConIsCurrCon] = useGetState(true) - const [userQuery, setUserQuery] = useState('') - const [visionConfig, setVisionConfig] = useState<VisionSettings>({ - enabled: false, - number_limits: 2, - detail: Resolution.low, - transfer_methods: [TransferMethod.local_file], - }) - - const updateCurrentQA = ({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }: { - responseItem: IChatItem - questionId: string - placeholderAnswerId: string - questionItem: IChatItem - }) => { - // closesure new list is outdated. - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ ...responseItem }) - }) - setChatList(newListWithAnswer) - } - - const handleSend = async (message: string, files?: VisionFile[]) => { - if (isResponding) { - notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) - return - } - - if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { - notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) - return false - } - - const data: Record<string, any> = { - inputs: currInputs, - query: message, - conversation_id: isNewConversation ? null : currConversationId, - } - - if (visionConfig?.enabled && files && files?.length > 0) { - data.files = files.map((item) => { - if (item.transfer_method === TransferMethod.local_file) { - return { - ...item, - url: '', - } - } - return item - }) - } - - // qustion - const questionId = `question-${Date.now()}` - const questionItem = { - id: questionId, - content: message, - isAnswer: false, - message_files: files, - - } - - const placeholderAnswerId = `answer-placeholder-${Date.now()}` - const placeholderAnswerItem = { - id: placeholderAnswerId, - content: '', - isAnswer: true, - } - - const newList = [...getChatList(), questionItem, placeholderAnswerItem] - setChatList(newList) - - let isAgentMode = false - - // answer - const responseItem: IChatItem = { - id: `${Date.now()}`, - content: '', - agent_thoughts: [], - message_files: [], - isAnswer: true, - } - let hasSetResponseId = false - - const prevTempNewConversationId = getCurrConversationId() || '-1' - let tempNewConversationId = prevTempNewConversationId - - setHasStopResponded(false) - setRespondingTrue() - setIsShowSuggestion(false) - setIsRespondingConCurrCon(true) - sendChatMessage(data, { - getAbortController: (abortController) => { - setAbortController(abortController) - }, - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { - if (!isAgentMode) { - responseItem.content = responseItem.content + message - } - else { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] - if (lastThought) - lastThought.thought = lastThought.thought + message // need immer setAutoFreeze - } - if (messageId && !hasSetResponseId) { - responseItem.id = messageId - hasSetResponseId = true - } - - if (isFirstMessage && newConversationId) - tempNewConversationId = newConversationId - - setMessageTaskId(taskId) - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false) - return - } - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - async onCompleted(hasError?: boolean) { - if (hasError) - return - - if (getConversationIdChangeBecauseOfNew()) { - const { data: allConversations }: any = await fetchAllConversations() - const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id) - - const newAllConversations = produce(allConversations, (draft: any) => { - draft[0].name = newItem.name - }) - setAllConversationList(newAllConversations as any) - noticeUpdateList() - } - setConversationIdChangeBecauseOfNew(false) - resetNewConversationInputs() - setChatNotStarted() - setCurrConversationId(tempNewConversationId, appId, true) - if (getIsRespondingConIsCurrCon() && suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) { - const { data }: any = await fetchSuggestedQuestions(responseItem.id, isInstalledApp, installedAppInfo?.id) - setSuggestQuestions(data) - setIsShowSuggestion(true) - } - setRespondingFalse() - }, - onFile(file) { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] - if (lastThought) - lastThought.message_files = [...(lastThought as any).message_files, { ...file }] - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - onThought(thought) { - isAgentMode = true - const response = responseItem as any - if (thought.message_id && !hasSetResponseId) { - response.id = thought.message_id - hasSetResponseId = true - } - // responseItem.id = thought.message_id; - if (response.agent_thoughts.length === 0) { - response.agent_thoughts.push(thought) - } - else { - const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1] - // thought changed but still the same thought, so update. - if (lastThought.id === thought.id) { - thought.thought = lastThought.thought - thought.message_files = lastThought.message_files - responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought - } - else { - responseItem.agent_thoughts!.push(thought) - } - } - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false) - return false - } - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - onMessageEnd: (messageEnd) => { - if (messageEnd.metadata?.annotation_reply) { - responseItem.id = messageEnd.id - responseItem.annotation = ({ - id: messageEnd.metadata.annotation_reply.id, - authorName: messageEnd.metadata.annotation_reply.account.name, - } as AnnotationType) - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ - ...responseItem, - }) - }) - setChatList(newListWithAnswer) - return - } - // not support show citation - // responseItem.citation = messageEnd.retriever_resources - if (!isInstalledApp) - return - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ ...responseItem }) - }) - setChatList(newListWithAnswer) - }, - onMessageReplace: (messageReplace) => { - if (isInstalledApp) { - responseItem.content = messageReplace.answer - } - else { - setChatList(produce( - getChatList(), - (draft) => { - const current = draft.find(item => item.id === messageReplace.id) - - if (current) - current.content = messageReplace.answer - }, - )) - } - }, - onError() { - setRespondingFalse() - // role back placeholder answer - setChatList(produce(getChatList(), (draft) => { - draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) - })) - }, - }, isInstalledApp, installedAppInfo?.id) - } - - const handleFeedback = useCallback(async (messageId: string, feedback: Feedbacktype) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id) - const newChatList = chatList.map((item) => { - if (item.id === messageId) { - return { - ...item, - feedback, - } - } - return item - }) - setChatList(newChatList) - notify({ type: 'success', message: t('common.api.success') }) - }, [isInstalledApp, installedAppInfo?.id, chatList, t, notify, setChatList]) - - const handleListChanged = useCallback((list: ConversationItem[]) => { - setConversationList(list) - setControlChatUpdateAllConversation(Date.now()) - }, [setConversationList, setControlChatUpdateAllConversation]) - const handlePinnedListChanged = useCallback((list: ConversationItem[]) => { - setPinnedConversationList(list) - setControlChatUpdateAllConversation(Date.now()) - }, [setPinnedConversationList, setControlChatUpdateAllConversation]) - const handleStartChatOnSidebar = useCallback(() => { - handleConversationIdChange('-1') - }, [handleConversationIdChange]) - - const renderSidebar = () => { - if (!appId || !siteInfo || !promptConfig) - return null - return ( - <Sidebar - list={conversationList} - onListChanged={handleListChanged} - isClearConversationList={isClearConversationList} - pinnedList={pinnedConversationList} - onPinnedListChanged={handlePinnedListChanged} - isClearPinnedConversationList={isClearPinnedConversationList} - onMoreLoaded={onMoreLoaded} - onPinnedMoreLoaded={onPinnedMoreLoaded} - isNoMore={!hasMore} - isPinnedNoMore={!hasPinnedMore} - onCurrentIdChange={handleConversationIdChange} - currentId={currConversationId} - copyRight={siteInfo.copyright || siteInfo.title} - isInstalledApp={isInstalledApp} - installedAppId={installedAppInfo?.id} - siteInfo={siteInfo} - onPin={handlePin} - onUnpin={handleUnpin} - controlUpdateList={controlUpdateConversationList} - onDelete={handleDelete} - onStartChat={handleStartChatOnSidebar} - /> - ) - } - - const handleAbortResponding = useCallback(async () => { - await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id) - setHasStopResponded(true) - setRespondingFalse() - }, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id]) - - if (appUnavailable) - return <AppUnavailable isUnknownReason={isUnknownReason} /> - - if (!appId || !siteInfo || !promptConfig) { - return <div className='flex h-screen w-full'> - <Loading type='app' /> - </div> - } - - return ( - <div className='bg-gray-100 h-full flex flex-col'> - {!isInstalledApp && ( - <Header - title={siteInfo.title} - icon={siteInfo.icon || ''} - icon_background={siteInfo.icon_background || ''} - isMobile={isMobile} - onShowSideBar={showSidebar} - onCreateNewChat={handleStartChatOnSidebar} - /> - )} - - <div - className={cn( - 'flex rounded-t-2xl bg-white overflow-hidden h-full w-full', - isInstalledApp && 'rounded-b-2xl', - )} - style={isInstalledApp - ? { - boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)', - } - : {}} - > - {/* sidebar */} - {!isMobile && renderSidebar()} - {isMobile && isShowSidebar && ( - <div className='fixed inset-0 z-50' - style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }} - onClick={hideSidebar} - > - <div className='inline-block' onClick={e => e.stopPropagation()}> - {renderSidebar()} - </div> - </div> - )} - {/* main */} - <div className={cn( - 'h-full flex-grow flex flex-col overflow-y-auto', - ) - }> - <ConfigSence - conversationName={conversationName} - hasSetInputs={hasSetInputs} - isPublicVersion={isPublicVersion} - siteInfo={siteInfo} - promptConfig={promptConfig} - onStartChat={handleStartChat} - canEidtInpus={canEditInpus} - savedInputs={currInputs as Record<string, any>} - onInputsChange={setCurrInputs} - plan={plan} - canReplaceLogo={canReplaceLogo} - customConfig={customConfig} - ></ConfigSence> - - { - hasSetInputs && ( - <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponding ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}> - <div className='h-full overflow-y-auto' ref={chatListDomRef}> - <Chat - chatList={chatList} - query={userQuery} - onQueryChange={setUserQuery} - onSend={handleSend} - isHideFeedbackEdit - onFeedback={handleFeedback} - isResponding={isResponding} - canStopResponding={!!messageTaskId && isRespondingConIsCurrCon} - abortResponding={handleAbortResponding} - checkCanSend={checkCanSend} - controlFocus={controlFocus} - isShowSuggestion={doShowSuggestion} - suggestionList={suggestedQuestions} - isShowSpeechToText={speechToTextConfig?.enabled} - isShowTextToSpeech={textToSpeechConfig?.enabled} - isShowCitation={citationConfig?.enabled} - visionConfig={{ - ...visionConfig, - image_file_size_limit: fileUploadConfigResponse ? fileUploadConfigResponse.image_file_size_limit : visionConfig.image_file_size_limit, - }} - allToolIcons={appMeta?.tool_icons || {}} - customDisclaimer={siteInfo.custom_disclaimer} - /> - </div> - </div>) - } - - {isShowConfirm && ( - <Confirm - title={t('share.chat.deleteConversation.title')} - content={t('share.chat.deleteConversation.content')} - isShow={isShowConfirm} - onClose={hideConfirm} - onConfirm={didDelete} - onCancel={hideConfirm} - /> - )} - </div> - </div> - </div> - ) -} -export default React.memo(Main) diff --git a/web/app/components/share/chat/sidebar/app-info/index.tsx b/web/app/components/share/chat/sidebar/app-info/index.tsx deleted file mode 100644 index a7f5052721..0000000000 --- a/web/app/components/share/chat/sidebar/app-info/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import cn from 'classnames' -import { appDefaultIconBackground } from '@/config/index' -import AppIcon from '@/app/components/base/app-icon' - -export type IAppInfoProps = { - className?: string - icon: string - icon_background?: string - name: string -} - -const AppInfo: FC<IAppInfoProps> = ({ - className, - icon, - icon_background, - name, -}) => { - return ( - <div className={cn(className, 'flex items-center space-x-3')}> - <AppIcon size="small" icon={icon} background={icon_background || appDefaultIconBackground} /> - <div className='w-0 grow text-sm font-semibold text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div> - </div> - ) -} -export default React.memo(AppInfo) diff --git a/web/app/components/share/chat/sidebar/card.module.css b/web/app/components/share/chat/sidebar/card.module.css deleted file mode 100644 index c917cb4db6..0000000000 --- a/web/app/components/share/chat/sidebar/card.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.card:hover { - background: linear-gradient(0deg, rgba(235, 245, 255, 0.4), rgba(235, 245, 255, 0.4)), #FFFFFF; -} \ No newline at end of file diff --git a/web/app/components/share/chat/sidebar/card.tsx b/web/app/components/share/chat/sidebar/card.tsx deleted file mode 100644 index 7457315a0d..0000000000 --- a/web/app/components/share/chat/sidebar/card.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { useTranslation } from 'react-i18next' -import s from './card.module.css' - -type PropType = { - children: React.ReactNode - text?: string -} -function Card({ children, text }: PropType) { - const { t } = useTranslation() - return ( - <div className={`${s.card} box-border w-full flex flex-col items-start px-4 py-3 rounded-lg border-solid border border-gray-200 cursor-pointer hover:border-primary-300`}> - <div className='text-gray-400 font-medium text-xs mb-2'>{text ?? t('share.chat.powerBy')}</div> - {children} - </div> - ) -} - -export default Card diff --git a/web/app/components/share/chat/sidebar/index.tsx b/web/app/components/share/chat/sidebar/index.tsx deleted file mode 100644 index 023bb2253e..0000000000 --- a/web/app/components/share/chat/sidebar/index.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react' -import type { FC } from 'react' -import { useTranslation } from 'react-i18next' -import { - PencilSquareIcon, -} from '@heroicons/react/24/outline' -import cn from 'classnames' -import Button from '../../../base/button' -import List from './list' -import AppInfo from '@/app/components/share/chat/sidebar/app-info' -// import Card from './card' -import type { ConversationItem, SiteInfo } from '@/models/share' -import { fetchConversations } from '@/service/share' - -export type ISidebarProps = { - copyRight: string - currentId: string - onCurrentIdChange: (id: string) => void - list: ConversationItem[] - onListChanged: (newList: ConversationItem[]) => void - isClearConversationList: boolean - pinnedList: ConversationItem[] - onPinnedListChanged: (newList: ConversationItem[]) => void - isClearPinnedConversationList: boolean - isInstalledApp: boolean - installedAppId?: string - siteInfo: SiteInfo - onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - isNoMore: boolean - isPinnedNoMore: boolean - onPin: (id: string) => void - onUnpin: (id: string) => void - controlUpdateList: number - onDelete: (id: string) => void - onStartChat: (inputs: Record<string, any>) => void -} - -const Sidebar: FC<ISidebarProps> = ({ - copyRight, - currentId, - onCurrentIdChange, - list, - onListChanged, - isClearConversationList, - pinnedList, - onPinnedListChanged, - isClearPinnedConversationList, - isInstalledApp, - installedAppId, - siteInfo, - onMoreLoaded, - onPinnedMoreLoaded, - isNoMore, - isPinnedNoMore, - onPin, - onUnpin, - controlUpdateList, - onDelete, - onStartChat, -}) => { - const { t } = useTranslation() - const [hasPinned, setHasPinned] = useState(false) - - const checkHasPinned = async () => { - const res = await fetchConversations(isInstalledApp, installedAppId, undefined, true) as any - setHasPinned(res.data.length > 0) - } - - useEffect(() => { - checkHasPinned() - }, []) - - useEffect(() => { - if (controlUpdateList !== 0) - checkHasPinned() - }, [controlUpdateList]) - - const handleUnpin = useCallback((id: string) => { - onUnpin(id) - }, [onUnpin]) - const handlePin = useCallback((id: string) => { - onPin(id) - }, [onPin]) - - const maxListHeight = (isInstalledApp) ? 'max-h-[30vh]' : 'max-h-[40vh]' - - return ( - <div - className={ - cn( - (isInstalledApp) ? 'tablet:h-[calc(100vh_-_74px)]' : '', - 'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen', - ) - } - > - {isInstalledApp && ( - <AppInfo - className='my-4 px-4' - name={siteInfo.title || ''} - icon={siteInfo.icon || ''} - icon_background={siteInfo.icon_background} - /> - )} - <div className="flex flex-shrink-0 p-4 !pb-0"> - <Button - onClick={() => onStartChat({})} - variant='secondary-accent' - className="group w-full flex-shrink-0 justify-start"> - <PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')} - </Button> - </div> - <div className={'flex-grow flex flex-col h-0 overflow-y-auto overflow-x-hidden'}> - {/* pinned list */} - {hasPinned && ( - <div className={cn('mt-4 px-4', list.length === 0 && 'flex flex-col flex-grow')}> - <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.pinnedTitle')}</div> - <List - className={cn(list.length > 0 ? maxListHeight : 'flex-grow')} - currentId={currentId} - onCurrentIdChange={onCurrentIdChange} - list={pinnedList} - onListChanged={onPinnedListChanged} - isClearConversationList={isClearPinnedConversationList} - isInstalledApp={isInstalledApp} - installedAppId={installedAppId} - onMoreLoaded={onPinnedMoreLoaded} - isNoMore={isPinnedNoMore} - isPinned={true} - onPinChanged={handleUnpin} - controlUpdate={controlUpdateList + 1} - onDelete={onDelete} - /> - </div> - )} - {/* unpinned list */} - <div className={cn('grow flex flex-col mt-4 px-4', !hasPinned && 'flex flex-col flex-grow')}> - {(hasPinned && list.length > 0) && ( - <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.unpinnedTitle')}</div> - )} - <List - className={cn('flex-grow h-0')} - currentId={currentId} - onCurrentIdChange={onCurrentIdChange} - list={list} - onListChanged={onListChanged} - isClearConversationList={isClearConversationList} - isInstalledApp={isInstalledApp} - installedAppId={installedAppId} - onMoreLoaded={onMoreLoaded} - isNoMore={isNoMore} - isPinned={false} - onPinChanged={handlePin} - controlUpdate={controlUpdateList + 1} - onDelete={onDelete} - /> - </div> - - </div> - <div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> - <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div> - </div> - </div> - ) -} - -export default React.memo(Sidebar) diff --git a/web/app/components/share/chat/sidebar/list/index.tsx b/web/app/components/share/chat/sidebar/list/index.tsx deleted file mode 100644 index 23b5c67e03..0000000000 --- a/web/app/components/share/chat/sidebar/list/index.tsx +++ /dev/null @@ -1,143 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useRef, useState } from 'react' - -import { useBoolean, useInfiniteScroll } from 'ahooks' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import RenameModal from '../rename-modal' -import Item from './item' -import type { ConversationItem } from '@/models/share' -import { fetchConversations, renameConversation } from '@/service/share' -import Toast from '@/app/components/base/toast' - -export type IListProps = { - className: string - currentId: string - onCurrentIdChange: (id: string) => void - list: ConversationItem[] - onListChanged?: (newList: ConversationItem[]) => void - isClearConversationList: boolean - isInstalledApp: boolean - installedAppId?: string - onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - isNoMore: boolean - isPinned: boolean - onPinChanged: (id: string) => void - controlUpdate: number - onDelete: (id: string) => void -} - -const List: FC<IListProps> = ({ - className, - currentId, - onCurrentIdChange, - list, - onListChanged, - isClearConversationList, - isInstalledApp, - installedAppId, - onMoreLoaded, - isNoMore, - isPinned, - onPinChanged, - controlUpdate, - onDelete, -}) => { - const { t } = useTranslation() - const listRef = useRef<HTMLDivElement>(null) - - useInfiniteScroll( - async () => { - if (!isNoMore) { - let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined - if (lastId === '-1') - lastId = undefined - const res = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned) as any - const { data: conversations, has_more }: any = res - onMoreLoaded({ data: conversations, has_more }) - } - return { list: [] } - }, - { - target: listRef, - isNoMore: () => { - return isNoMore - }, - reloadDeps: [isNoMore, controlUpdate], - }, - ) - const [isShowRename, { setTrue: setShowRename, setFalse: setHideRename }] = useBoolean(false) - const [isSaving, { setTrue: setIsSaving, setFalse: setNotSaving }] = useBoolean(false) - const [currentConversation, setCurrentConversation] = useState<ConversationItem | null>(null) - const showRename = (item: ConversationItem) => { - setCurrentConversation(item) - setShowRename() - } - const handleRename = async (newName: string) => { - if (!newName.trim() || !currentConversation) { - Toast.notify({ - type: 'error', - message: t('common.chat.conversationNameCanNotEmpty'), - }) - return - } - - setIsSaving() - const currId = currentConversation.id - try { - await renameConversation(isInstalledApp, installedAppId, currId, newName) - - Toast.notify({ - type: 'success', - message: t('common.actionMsg.modifiedSuccessfully'), - }) - onListChanged?.(list.map((item) => { - if (item.id === currId) { - return { - ...item, - name: newName, - } - } - return item - })) - setHideRename() - } - finally { - setNotSaving() - } - } - return ( - <nav - ref={listRef} - className={cn(className, 'shrink-0 space-y-1 bg-white overflow-y-auto overflow-x-hidden')} - > - {list.map((item) => { - const isCurrent = item.id === currentId - return ( - <Item - key={item.id} - item={item} - isCurrent={isCurrent} - onClick={onCurrentIdChange} - isPinned={isPinned} - togglePin={onPinChanged} - onDelete={onDelete} - onRenameConversation={showRename} - /> - ) - })} - {isShowRename && ( - <RenameModal - isShow={isShowRename} - onClose={setHideRename} - saveLoading={isSaving} - name={currentConversation?.name || ''} - onSave={handleRename} - /> - )} - </nav> - ) -} - -export default React.memo(List) diff --git a/web/app/components/share/chat/sidebar/list/item.tsx b/web/app/components/share/chat/sidebar/list/item.tsx deleted file mode 100644 index 1871787f5c..0000000000 --- a/web/app/components/share/chat/sidebar/list/item.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useRef } from 'react' -import cn from 'classnames' -import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid' -import { - ChatBubbleOvalLeftEllipsisIcon, -} from '@heroicons/react/24/outline' -import { useHover } from 'ahooks' -import ItemOperation from '@/app/components/explore/item-operation' -import type { ConversationItem } from '@/models/share' - -export type IItemProps = { - onClick: (id: string) => void - item: ConversationItem - isCurrent: boolean - isPinned: boolean - togglePin: (id: string) => void - onDelete: (id: string) => void - onRenameConversation: (item: ConversationItem) => void -} - -const Item: FC<IItemProps> = ({ - isCurrent, - item, - onClick, - isPinned, - togglePin, - onDelete, - onRenameConversation, -}) => { - const ItemIcon = isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon - const ref = useRef(null) - const isHovering = useHover(ref) - - return ( - <div - ref={ref} - onClick={() => onClick(item.id)} - key={item.id} - className={cn( - isCurrent - ? 'bg-primary-50 text-primary-600' - : 'text-gray-700 hover:bg-gray-200 hover:text-gray-700', - 'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer', - )} - > - <div className='flex items-center w-0 grow'> - <ItemIcon - className={cn( - isCurrent - ? 'text-primary-600' - : 'text-gray-400 group-hover:text-gray-500', - 'mr-3 h-5 w-5 flex-shrink-0', - )} - aria-hidden="true" - /> - <span>{item.name}</span> - </div> - - {item.id !== '-1' && ( - <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}> - <ItemOperation - isPinned={isPinned} - isItemHovering={isHovering} - togglePin={() => togglePin(item.id)} - isShowDelete - isShowRenameConversation - onRenameConversation={() => onRenameConversation(item)} - onDelete={() => onDelete(item.id)} - /> - </div> - )} - </div> - ) -} -export default React.memo(Item) diff --git a/web/app/components/share/chat/value-panel/index.tsx b/web/app/components/share/chat/value-panel/index.tsx deleted file mode 100644 index 8a35685faa..0000000000 --- a/web/app/components/share/chat/value-panel/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' -import type { FC, ReactNode } from 'react' -import React from 'react' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import s from './style.module.css' -import { StarIcon } from '@/app/components/share/chat/welcome/massive-component' -import Button from '@/app/components/base/button' - -export type ITemplateVarPanelProps = { - className?: string - header: ReactNode - children?: ReactNode | null - isFold: boolean -} - -const TemplateVarPanel: FC<ITemplateVarPanelProps> = ({ - className, - header, - children, - isFold, -}) => { - return ( - <div className={cn(isFold ? 'border border-indigo-100' : s.boxShodow, className, 'rounded-xl ')}> - {/* header */} - <div - className={cn(isFold && 'rounded-b-xl', 'rounded-t-xl px-6 py-4 bg-indigo-25 text-xs')} - > - {header} - </div> - {/* body */} - {!isFold && children && ( - <div className='rounded-b-xl p-6'> - {children} - </div> - )} - </div> - ) -} - -export const PanelTitle: FC<{ title: string; className?: string }> = ({ - title, - className, -}) => { - return ( - <div className={cn(className, 'flex items-center space-x-1 text-indigo-600')}> - <StarIcon /> - <span className='text-xs'>{title}</span> - </div> - ) -} - -export const VarOpBtnGroup: FC<{ className?: string; onConfirm: () => void; onCancel: () => void }> = ({ - className, - onConfirm, - onCancel, -}) => { - const { t } = useTranslation() - - return ( - <div className={cn(className, 'flex mt-3 space-x-2 mobile:ml-0 tablet:ml-[128px] text-sm')}> - <Button - variant='primary' - onClick={onConfirm} - > - {t('common.operation.save')} - </Button> - <Button - onClick={onCancel} - > - {t('common.operation.cancel')} - </Button> - </div > - ) -} - -export default React.memo(TemplateVarPanel) diff --git a/web/app/components/share/chat/value-panel/style.module.css b/web/app/components/share/chat/value-panel/style.module.css deleted file mode 100644 index c7613c44a4..0000000000 --- a/web/app/components/share/chat/value-panel/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.boxShodow { - box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} \ No newline at end of file diff --git a/web/app/components/share/chat/welcome/index.tsx b/web/app/components/share/chat/welcome/index.tsx deleted file mode 100644 index adc58d8a63..0000000000 --- a/web/app/components/share/chat/welcome/index.tsx +++ /dev/null @@ -1,388 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel' -import s from './style.module.css' -import { AppInfo, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component' -import type { SiteInfo } from '@/models/share' -import type { PromptConfig } from '@/models/debug' -import { ToastContext } from '@/app/components/base/toast' -import Select from '@/app/components/base/select' -import { DEFAULT_VALUE_MAX_LEN } from '@/config' - -// regex to match the {{}} and replace it with a span -const regex = /\{\{([^}]+)\}\}/g - -export type IWelcomeProps = { - conversationName: string - hasSetInputs: boolean - isPublicVersion: boolean - siteInfo: SiteInfo - promptConfig: PromptConfig - onStartChat: (inputs: Record<string, any>) => void - canEidtInpus: boolean - savedInputs: Record<string, any> - onInputsChange: (inputs: Record<string, any>) => void - plan?: string - canReplaceLogo?: boolean - customConfig?: { - remove_webapp_brand?: boolean - replace_webapp_logo?: string - } -} - -const Welcome: FC<IWelcomeProps> = ({ - conversationName, - hasSetInputs, - isPublicVersion, - siteInfo, - promptConfig, - onStartChat, - canEidtInpus, - savedInputs, - onInputsChange, - customConfig, -}) => { - const { t } = useTranslation() - const hasVar = promptConfig.prompt_variables.length > 0 - const [isFold, setIsFold] = useState<boolean>(true) - const [inputs, setInputs] = useState<Record<string, any>>((() => { - if (hasSetInputs) - return savedInputs - - const res: Record<string, any> = {} - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = '' - }) - } - // debugger - return res - })()) - useEffect(() => { - if (!savedInputs) { - const res: Record<string, any> = {} - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = '' - }) - } - setInputs(res) - } - else { - setInputs(savedInputs) - } - }, [savedInputs]) - - const highLightPromoptTemplate = (() => { - if (!promptConfig) - return '' - const res = promptConfig.prompt_template.replace(regex, (match, p1) => { - return `<span class='text-gray-800 font-bold'>${inputs?.[p1] ? inputs?.[p1] : match}</span>` - }) - return res - })() - - const { notify } = useContext(ToastContext) - const logError = (message: string) => { - notify({ type: 'error', message, duration: 3000 }) - } - - const renderHeader = () => { - return ( - <div className='absolute top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'> - <div className='text-gray-900'>{conversationName}</div> - </div> - ) - } - - const renderInputs = () => { - return ( - <div className='space-y-3'> - {promptConfig.prompt_variables.map(item => ( - <div className='tablet:flex items-start mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}> - <label className={`flex-shrink-0 flex items-center tablet:leading-9 mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label> - {item.type === 'select' - && ( - <Select - className='w-full' - defaultValue={inputs?.[item.key]} - onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }} - items={(item.options || []).map(i => ({ name: i, value: i }))} - allowSearch={false} - bgClassName='bg-gray-50' - /> - )} - {item.type === 'string' && ( - <input - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'} - maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN} - /> - )} - {item.type === 'paragraph' && ( - <textarea - className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50" - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - /> - )} - {item.type === 'number' && ( - <input - type='number' - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'} - /> - )} - </div> - ))} - </div> - ) - } - - const canChat = () => { - const prompt_variables = promptConfig?.prompt_variables - if (!inputs || !prompt_variables || prompt_variables?.length === 0) - return true - - let hasEmptyInput = '' - const requiredVars = prompt_variables?.filter(({ key, name, required }) => { - const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) - return res - }) || [] // compatible with old version - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs?.[key]) - hasEmptyInput = name - }) - - if (hasEmptyInput) { - logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput })) - return false - } - return !hasEmptyInput - } - - const handleChat = () => { - if (!canChat()) - return - - onStartChat(inputs) - } - - const renderNoVarPanel = () => { - if (isPublicVersion) { - return ( - <div> - <AppInfo siteInfo={siteInfo} /> - <TemplateVarPanel - isFold={false} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - </> - } - > - <ChatBtn onClick={handleChat} /> - </TemplateVarPanel> - </div> - ) - } - // private version - return ( - <TemplateVarPanel - isFold={false} - header={ - <AppInfo siteInfo={siteInfo} /> - } - > - <ChatBtn onClick={handleChat} /> - </TemplateVarPanel> - ) - } - - const renderVarPanel = () => { - return ( - <TemplateVarPanel - isFold={false} - header={ - <AppInfo siteInfo={siteInfo} /> - } - > - {renderInputs()} - <ChatBtn - className='mt-3 mobile:ml-0 tablet:ml-[128px]' - onClick={handleChat} - /> - </TemplateVarPanel> - ) - } - - const renderVarOpBtnGroup = () => { - return ( - <VarOpBtnGroup - onConfirm={() => { - if (!canChat()) - return - - onInputsChange(inputs) - setIsFold(true) - }} - onCancel={() => { - setInputs(savedInputs) - setIsFold(true) - }} - /> - ) - } - - const renderHasSetInputsPublic = () => { - if (!canEidtInpus) { - return ( - <TemplateVarPanel - isFold={false} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - </> - } - /> - ) - } - - return ( - <TemplateVarPanel - isFold={isFold} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - {isFold && ( - <div className='flex items-center justify-between mt-3 border-t border-indigo-100 pt-4 text-xs text-indigo-600'> - <span className='text-gray-700'>{t('share.chat.configStatusDes')}</span> - <EditBtn onClick={() => setIsFold(false)} /> - </div> - )} - </> - } - > - {renderInputs()} - {renderVarOpBtnGroup()} - </TemplateVarPanel> - ) - } - - const renderHasSetInputsPrivate = () => { - if (!canEidtInpus || !hasVar) - return null - - return ( - <TemplateVarPanel - isFold={isFold} - header={ - <div className='flex items-center justify-between text-indigo-600'> - <PanelTitle - title={!isFold ? t('share.chat.privatePromptConfigTitle') : t('share.chat.configStatusDes')} - /> - {isFold && ( - <EditBtn onClick={() => setIsFold(false)} /> - )} - </div> - } - > - {renderInputs()} - {renderVarOpBtnGroup()} - </TemplateVarPanel> - ) - } - - const renderHasSetInputs = () => { - if ((!isPublicVersion && !canEidtInpus) || !hasVar) - return null - - return ( - <div - className='pt-[88px] mb-5' - > - {isPublicVersion ? renderHasSetInputsPublic() : renderHasSetInputsPrivate()} - </div>) - } - - return ( - <div className='relative mobile:min-h-[48px] tablet:min-h-[64px]'> - {hasSetInputs && renderHeader()} - <div className='mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'> - {/* Has't set inputs */} - { - !hasSetInputs && ( - <div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'> - {hasVar - ? ( - renderVarPanel() - ) - : ( - renderNoVarPanel() - )} - </div> - ) - } - - {/* Has set inputs */} - {hasSetInputs && renderHasSetInputs()} - - {/* foot */} - {!hasSetInputs && ( - <div className='mt-4 flex justify-between items-center h-8 text-xs text-gray-400'> - - {siteInfo.privacy_policy - ? <div>{t('share.chat.privacyPolicyLeft')} - <a - className='text-gray-500 px-1' - href={siteInfo.privacy_policy} - target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a> - {t('share.chat.privacyPolicyRight')} - </div> - : <div> - </div>} - { - customConfig?.remove_webapp_brand - ? null - : ( - <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank"> - <span className='uppercase'>{t('share.chat.powerBy')}</span> - { - customConfig?.replace_webapp_logo - ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' /> - : <FootLogo /> - } - </a> - ) - } - </div> - )} - </div> - </div > - ) -} - -export default React.memo(Welcome) diff --git a/web/app/components/share/chat/welcome/massive-component.tsx b/web/app/components/share/chat/welcome/massive-component.tsx deleted file mode 100644 index e3ff47e580..0000000000 --- a/web/app/components/share/chat/welcome/massive-component.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import { - PencilIcon, -} from '@heroicons/react/24/solid' -import s from './style.module.css' -import type { SiteInfo } from '@/models/share' -import Button from '@/app/components/base/button' -import LogoSite from '@/app/components/base/logo/logo-site' - -export const AppInfo: FC<{ siteInfo: SiteInfo }> = ({ siteInfo }) => { - return ( - <div> - <div className='flex items-center py-2 text-xl font-medium text-gray-700 rounded-md'>{siteInfo.icon && <span className='mr-2'><em-emoji id={siteInfo.icon} /></span>}{siteInfo.title}</div> - <p className='text-sm text-gray-500'>{siteInfo.description}</p> - </div> - ) -} - -export const PromptTemplate: FC<{ html: string }> = ({ html }) => { - return ( - <div - className={'box-border text-sm text-gray-700'} - dangerouslySetInnerHTML={{ __html: html }} - /> - ) -} - -export const StarIcon = () => ( - <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z" fill="#444CE7" /> - <path d="M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z" fill="#444CE7" /> - <path d="M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z" fill="#444CE7" /> - </svg> -) - -export const ChatBtn: FC<{ onClick: () => void; className?: string }> = ({ - className, - onClick, -}) => { - const { t } = useTranslation() - return ( - <Button - variant='primary' - className={cn(className, `px-4 ${s.customBtn} gap-2`)} - onClick={onClick}> - <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="M18 10.5C18 14.366 14.418 17.5 10 17.5C8.58005 17.506 7.17955 17.1698 5.917 16.52L2 17.5L3.338 14.377C2.493 13.267 2 11.934 2 10.5C2 6.634 5.582 3.5 10 3.5C14.418 3.5 18 6.634 18 10.5ZM7 9.5H5V11.5H7V9.5ZM15 9.5H13V11.5H15V9.5ZM9 9.5H11V11.5H9V9.5Z" fill="white" /> - </svg> - {t('share.chat.startChat')} - </Button> - ) -} - -export const EditBtn = ({ className, onClick }: { className?: string; onClick: () => void }) => { - const { t } = useTranslation() - - return ( - <div - className={cn('px-2 flex space-x-1 items-center rounded-md cursor-pointer', className)} - onClick={onClick} - > - <PencilIcon className='w-3 h-3' /> - <span>{t('common.operation.edit')}</span> - </div> - ) -} - -export const FootLogo = () => ( - <LogoSite className='!h-5' /> -) diff --git a/web/app/components/share/chat/welcome/style.module.css b/web/app/components/share/chat/welcome/style.module.css deleted file mode 100644 index 367cf199d6..0000000000 --- a/web/app/components/share/chat/welcome/style.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.boxShodow { - box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} - -.bgGrayColor { - background-color: #F9FAFB; -} - -.headerBg { - height: 3.5rem; - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.formLabel { - width: 120px; - margin-right: 8px; -} - -.customBtn { - width: auto; -} diff --git a/web/app/components/share/chatbot/config-scence/index.tsx b/web/app/components/share/chatbot/config-scence/index.tsx deleted file mode 100644 index 102c7133e5..0000000000 --- a/web/app/components/share/chatbot/config-scence/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { FC } from 'react' -import React from 'react' -import type { IWelcomeProps } from '../welcome' -import Welcome from '../welcome' - -const ConfigScene: FC<IWelcomeProps> = (props) => { - return ( - <div className='mb-5 antialiased font-sans shrink-0'> - <Welcome {...props} /> - </div> - ) -} -export default React.memo(ConfigScene) diff --git a/web/app/components/share/chatbot/hooks/use-conversation.ts b/web/app/components/share/chatbot/hooks/use-conversation.ts deleted file mode 100644 index 4e7f27cf50..0000000000 --- a/web/app/components/share/chatbot/hooks/use-conversation.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useState } from 'react' -import produce from 'immer' -import { useGetState } from 'ahooks' -import type { ConversationItem } from '@/models/share' - -const storageConversationIdKey = 'conversationIdInfo' - -type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'> -function useConversation() { - const [conversationList, setConversationList] = useState<ConversationItem[]>([]) - const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([]) - const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1') - // when set conversation id, we do not have set appId - const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { - doSetCurrConversationId(id) - if (isSetToLocalStroge && id !== '-1') { - // conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2} - const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} - conversationIdInfo[appId] = id - globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo)) - } - } - - const getConversationIdFromStorage = (appId: string) => { - const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} - const id = conversationIdInfo[appId] - return id - } - - const isNewConversation = currConversationId === '-1' - // input can be updated by user - const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null) - const resetNewConversationInputs = () => { - if (!newConversationInputs) - return - setNewConversationInputs(produce(newConversationInputs, (draft) => { - Object.keys(draft).forEach((key) => { - draft[key] = '' - }) - })) - } - const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null) - const currInputs = isNewConversation ? newConversationInputs : existConversationInputs - const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs - - // info is muted - const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null) - const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null) - const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo - - return { - conversationList, - setConversationList, - pinnedConversationList, - setPinnedConversationList, - currConversationId, - getCurrConversationId, - setCurrConversationId, - getConversationIdFromStorage, - isNewConversation, - currInputs, - newConversationInputs, - existConversationInputs, - resetNewConversationInputs, - setCurrInputs, - currConversationInfo, - setNewConversationInfo, - setExistConversationInfo, - } -} - -export default useConversation diff --git a/web/app/components/share/chatbot/index.tsx b/web/app/components/share/chatbot/index.tsx deleted file mode 100644 index 4bff6bbcb1..0000000000 --- a/web/app/components/share/chatbot/index.tsx +++ /dev/null @@ -1,824 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -'use client' -import type { FC } from 'react' -import React, { useEffect, useRef, useState } from 'react' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import produce, { setAutoFreeze } from 'immer' -import { useBoolean, useGetState } from 'ahooks' -import { checkOrSetAccessToken } from '../utils' -import AppUnavailable from '../../base/app-unavailable' -import useConversation from './hooks/use-conversation' -import { ToastContext } from '@/app/components/base/toast' -import ConfigScene from '@/app/components/share/chatbot/config-scence' -import Header from '@/app/components/share/header' -import { - fetchAppInfo, - fetchAppMeta, - fetchAppParams, - fetchChatList, - fetchConversations, - fetchSuggestedQuestions, - generationConversationName, - sendChatMessage, - stopChatMessageResponding, - updateFeedback, -} from '@/service/share' -import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' -import type { AppMeta, ConversationItem, SiteInfo } from '@/models/share' -import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' -import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type' -import Chat from '@/app/components/app/chat' -import { changeLanguage } from '@/i18n/i18next-config' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import Loading from '@/app/components/base/loading' -import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' -import { userInputsFormToPromptVariables } from '@/utils/model-config' -import type { InstalledApp } from '@/models/explore' -import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' -import LogoHeader from '@/app/components/base/logo/logo-embeded-chat-header' -import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar' -import type { VisionFile, VisionSettings } from '@/types/app' -import { Resolution, TransferMethod } from '@/types/app' -import type { Annotation as AnnotationType } from '@/models/log' - -export type IMainProps = { - isInstalledApp?: boolean - installedAppInfo?: InstalledApp -} - -const Main: FC<IMainProps> = ({ - isInstalledApp = false, - installedAppInfo, -}) => { - const { t } = useTranslation() - const media = useBreakpoints() - const isMobile = media === MediaType.mobile - - /* - * app info - */ - const [appUnavailable, setAppUnavailable] = useState<boolean>(false) - const [isUnknownReason, setIsUnknwonReason] = useState<boolean>(false) - const [appId, setAppId] = useState<string>('') - const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) - const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() - const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null) - const [inited, setInited] = useState<boolean>(false) - const [plan, setPlan] = useState<string>('basic') // basic/plus/pro - const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false) - const [customConfig, setCustomConfig] = useState<any>(null) - const [appMeta, setAppMeta] = useState<AppMeta | null>(null) - - // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. - useEffect(() => { - if (siteInfo?.title) { - if (canReplaceLogo) - document.title = `${siteInfo.title}` - else - document.title = `${siteInfo.title} - Powered by Dify` - } - }, [siteInfo?.title, canReplaceLogo]) - - // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 - useEffect(() => { - setAutoFreeze(false) - return () => { - setAutoFreeze(true) - } - }, []) - - /* - * conversation info - */ - const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([]) - const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false) - const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false) - const { - conversationList, - setConversationList, - pinnedConversationList, - setPinnedConversationList, - currConversationId, - getCurrConversationId, - setCurrConversationId, - getConversationIdFromStorage, - isNewConversation, - currConversationInfo, - currInputs, - newConversationInputs, - // existConversationInputs, - resetNewConversationInputs, - setCurrInputs, - setNewConversationInfo, - setExistConversationInfo, - } = useConversation() - const [hasMore, setHasMore] = useState<boolean>(true) - const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true) - - const onMoreLoaded = ({ data: conversations, has_more }: any) => { - setHasMore(has_more) - if (isClearConversationList) { - setConversationList(conversations) - clearConversationListFalse() - } - else { - setConversationList([...conversationList, ...conversations]) - } - } - - const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => { - setHasPinnedMore(has_more) - if (isClearPinnedConversationList) { - setPinnedConversationList(conversations) - clearPinnedConversationListFalse() - } - else { - setPinnedConversationList([...pinnedConversationList, ...conversations]) - } - } - - const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0) - - const noticeUpdateList = () => { - setHasMore(true) - clearConversationListTrue() - - setHasPinnedMore(true) - clearPinnedConversationListTrue() - - setControlUpdateConversationList(Date.now()) - } - const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) - const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) - const [textToSpeechConfig, setTextToSpeechConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) - const [citationConfig, setCitationConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null) - - const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) - const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false) - const handleStartChat = (inputs: Record<string, any>) => { - createNewChat() - setConversationIdChangeBecauseOfNew(true) - setCurrInputs(inputs) - setChatStarted() - // parse variables in introduction - setChatList(generateNewChatListWithOpenstatement('', inputs)) - } - const hasSetInputs = (() => { - if (!isNewConversation) - return true - - return isChatStarted - })() - - // const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string - const conversationIntroduction = currConversationInfo?.introduction || '' - - const handleConversationSwitch = () => { - if (!inited) - return - if (!appId) { - // wait for appId - setTimeout(handleConversationSwitch, 100) - return - } - - // update inputs of current conversation - let notSyncToStateIntroduction = '' - let notSyncToStateInputs: Record<string, any> | undefined | null = {} - if (!isNewConversation) { - const item = allConversationList.find(item => item.id === currConversationId) - notSyncToStateInputs = item?.inputs || {} - setCurrInputs(notSyncToStateInputs) - notSyncToStateIntroduction = item?.introduction || '' - setExistConversationInfo({ - name: item?.name || '', - introduction: notSyncToStateIntroduction, - }) - } - else { - notSyncToStateInputs = newConversationInputs - setCurrInputs(notSyncToStateInputs) - } - - // update chat list of current conversation - if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponding) { - fetchChatList(currConversationId, isInstalledApp, installedAppInfo?.id).then((res: any) => { - const { data } = res - const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs) - - data.forEach((item: any) => { - newChatList.push({ - id: `question-${item.id}`, - content: item.query, - isAnswer: false, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], - }) - newChatList.push({ - id: item.id, - content: item.answer, - agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), - feedback: item.feedback, - isAnswer: true, - citation: item.retriever_resources, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], - }) - }) - setChatList(newChatList) - }) - } - - if (isNewConversation && isChatStarted) - setChatList(generateNewChatListWithOpenstatement()) - - setControlFocus(Date.now()) - } - useEffect(handleConversationSwitch, [currConversationId, inited]) - - /* - * chat info. chat is under conversation. - */ - const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) - const chatListDomRef = useRef<HTMLDivElement>(null) - - useEffect(() => { - // scroll to bottom - if (chatListDomRef.current) - chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight - }, [chatList, currConversationId]) - // user can not edit inputs if user had send message - const canEditInputs = !chatList.some(item => item.isAnswer === false) && isNewConversation - const createNewChat = async () => { - // if new chat is already exist, do not create new chat - abortController?.abort() - setRespondingFalse() - if (conversationList.some(item => item.id === '-1')) - return - - setConversationList(produce(conversationList, (draft) => { - draft.unshift({ - id: '-1', - name: t('share.chat.newChatDefaultName'), - inputs: newConversationInputs, - introduction: conversationIntroduction, - }) - })) - } - - // sometime introduction is not applied to state - const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => { - let caculatedIntroduction = introduction || conversationIntroduction || '' - const caculatedPromptVariables = inputs || currInputs || null - if (caculatedIntroduction && caculatedPromptVariables) - caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables) - - const openstatement = { - id: `${Date.now()}`, - content: caculatedIntroduction, - isAnswer: true, - feedbackDisabled: true, - isOpeningStatement: isPublicVersion, - } - if (caculatedIntroduction) - return [openstatement] - - return [] - } - - const fetchAllConversations = () => { - return fetchConversations(isInstalledApp, installedAppInfo?.id, undefined, undefined, 100) - } - - const fetchInitData = async () => { - if (!isInstalledApp) - await checkOrSetAccessToken() - - return Promise.all([isInstalledApp - ? { - app_id: installedAppInfo?.id, - site: { - title: installedAppInfo?.app.name, - prompt_public: false, - copyright: '', - }, - plan: 'basic', - } - : fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id), fetchAppMeta(isInstalledApp, installedAppInfo?.id)]) - } - - // init - useEffect(() => { - (async () => { - try { - const [appData, conversationData, appParams, appMeta]: any = await fetchInitData() - setAppMeta(appMeta) - const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData - setAppId(appId) - setPlan(plan) - setCanReplaceLogo(can_replace_logo) - setCustomConfig(custom_config) - const tempIsPublicVersion = siteInfo.prompt_public - setIsPublicVersion(tempIsPublicVersion) - const prompt_template = '' - // handle current conversation id - const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean } - const _conversationId = getConversationIdFromStorage(appId) - const isNotNewConversation = allConversations.some(item => item.id === _conversationId) - setAllConversationList(allConversations) - // fetch new conversation info - const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, text_to_speech, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams - setVisionConfig({ - ...file_upload.image, - image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, - }) - const prompt_variables = userInputsFormToPromptVariables(user_input_form) - if (siteInfo.default_language) - changeLanguage(siteInfo.default_language) - - setNewConversationInfo({ - name: t('share.chat.newChatDefaultName'), - introduction, - }) - setSiteInfo(siteInfo as SiteInfo) - setPromptConfig({ - prompt_template, - prompt_variables, - } as PromptConfig) - setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer) - setSpeechToTextConfig(speech_to_text) - setTextToSpeechConfig(text_to_speech) - setCitationConfig(retriever_resource) - - // setConversationList(conversations as ConversationItem[]) - - if (isNotNewConversation) - setCurrConversationId(_conversationId, appId, false) - - setInited(true) - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknwonReason(true) - setAppUnavailable(true) - } - } - })() - }, []) - - const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false) - const [abortController, setAbortController] = useState<AbortController | null>(null) - const { notify } = useContext(ToastContext) - const logError = (message: string) => { - notify({ type: 'error', message }) - } - - const checkCanSend = () => { - if (currConversationId !== '-1') - return true - - const prompt_variables = promptConfig?.prompt_variables - const inputs = currInputs - if (!inputs || !prompt_variables || prompt_variables?.length === 0) - return true - - let hasEmptyInput = '' - const requiredVars = prompt_variables?.filter(({ key, name, required }) => { - const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) - return res - }) || [] // compatible with old version - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs?.[key]) - hasEmptyInput = name - }) - - if (hasEmptyInput) { - logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput })) - return false - } - return !hasEmptyInput - } - - const [controlFocus, setControlFocus] = useState(0) - const [isShowSuggestion, setIsShowSuggestion] = useState(false) - const doShowSuggestion = isShowSuggestion && !isResponding - const [suggestQuestions, setSuggestQuestions] = useState<string[]>([]) - const [messageTaskId, setMessageTaskId] = useState('') - const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) - const [isRespondingConIsCurrCon, setIsRespondingConCurrCon, getIsRespondingConIsCurrCon] = useGetState(true) - const [shouldReload, setShouldReload] = useState(false) - const [userQuery, setUserQuery] = useState('') - const [visionConfig, setVisionConfig] = useState<VisionSettings>({ - enabled: false, - number_limits: 2, - detail: Resolution.low, - transfer_methods: [TransferMethod.local_file], - }) - - const updateCurrentQA = ({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }: { - responseItem: IChatItem - questionId: string - placeholderAnswerId: string - questionItem: IChatItem - }) => { - // closesure new list is outdated. - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ ...responseItem }) - }) - setChatList(newListWithAnswer) - } - - const handleSend = async (message: string, files?: VisionFile[]) => { - if (isResponding) { - notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) - return - } - - if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { - notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) - return false - } - const data: Record<string, any> = { - inputs: currInputs, - query: message, - conversation_id: isNewConversation ? null : currConversationId, - } - - if (visionConfig.enabled && files && files?.length > 0) { - data.files = files.map((item) => { - if (item.transfer_method === TransferMethod.local_file) { - return { - ...item, - url: '', - } - } - return item - }) - } - - // qustion - const questionId = `question-${Date.now()}` - const questionItem = { - id: questionId, - content: message, - isAnswer: false, - message_files: files, - } - - const placeholderAnswerId = `answer-placeholder-${Date.now()}` - const placeholderAnswerItem = { - id: placeholderAnswerId, - content: '', - isAnswer: true, - } - - const newList = [...getChatList(), questionItem, placeholderAnswerItem] - setChatList(newList) - - let isAgentMode = false - - // answer - const responseItem: IChatItem = { - id: `${Date.now()}`, - content: '', - agent_thoughts: [], - message_files: [], - isAnswer: true, - } - let hasSetResponseId = false - - const prevTempNewConversationId = getCurrConversationId() || '-1' - let tempNewConversationId = prevTempNewConversationId - - setHasStopResponded(false) - setRespondingTrue() - setIsShowSuggestion(false) - sendChatMessage(data, { - getAbortController: (abortController) => { - setAbortController(abortController) - }, - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { - if (!isAgentMode) { - responseItem.content = responseItem.content + message - } - else { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] - if (lastThought) - lastThought.thought = lastThought.thought + message // need immer setAutoFreeze - } - if (messageId && !hasSetResponseId) { - responseItem.id = messageId - hasSetResponseId = true - } - - if (isFirstMessage && newConversationId) - tempNewConversationId = newConversationId - - setMessageTaskId(taskId) - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false) - return - } - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - async onCompleted(hasError?: boolean) { - if (hasError) - return - - if (getConversationIdChangeBecauseOfNew()) { - const { data: allConversations }: any = await fetchAllConversations() - const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id) - const newAllConversations = produce(allConversations, (draft: any) => { - draft[0].name = newItem.name - }) - setAllConversationList(newAllConversations as any) - noticeUpdateList() - } - setConversationIdChangeBecauseOfNew(false) - resetNewConversationInputs() - setChatNotStarted() - setCurrConversationId(tempNewConversationId, appId, true) - if (suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) { - const { data }: any = await fetchSuggestedQuestions(responseItem.id, isInstalledApp, installedAppInfo?.id) - setSuggestQuestions(data) - setIsShowSuggestion(true) - } - setRespondingFalse() - }, - onFile(file) { - const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] - if (lastThought) - lastThought.message_files = [...(lastThought as any).message_files, { ...file }] - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - onThought(thought) { - isAgentMode = true - const response = responseItem as any - if (thought.message_id && !hasSetResponseId) { - response.id = thought.message_id - hasSetResponseId = true - } - // responseItem.id = thought.message_id; - if (response.agent_thoughts.length === 0) { - response.agent_thoughts.push(thought) - } - else { - const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1] - // thought changed but still the same thought, so update. - if (lastThought.id === thought.id) { - thought.thought = lastThought.thought - thought.message_files = lastThought.message_files - responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought - } - else { - responseItem.agent_thoughts!.push(thought) - } - } - // has switched to other conversation - if (prevTempNewConversationId !== getCurrConversationId()) { - setIsRespondingConCurrCon(false) - return false - } - - updateCurrentQA({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }) - }, - onMessageEnd: (messageEnd) => { - if (messageEnd.metadata?.annotation_reply) { - responseItem.id = messageEnd.id - responseItem.annotation = ({ - id: messageEnd.metadata.annotation_reply.id, - authorName: messageEnd.metadata.annotation_reply.account.name, - } as AnnotationType) - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ - ...responseItem, - }) - }) - setChatList(newListWithAnswer) - return - } - // not support show citation - // responseItem.citation = messageEnd.retriever_resources - if (!isInstalledApp) - return - const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), - (draft) => { - if (!draft.find(item => item.id === questionId)) - draft.push({ ...questionItem }) - - draft.push({ ...responseItem }) - }) - setChatList(newListWithAnswer) - }, - onMessageReplace: (messageReplace) => { - if (isInstalledApp) { - responseItem.content = messageReplace.answer - } - else { - setChatList(produce( - getChatList(), - (draft) => { - const current = draft.find(item => item.id === messageReplace.id) - - if (current) - current.content = messageReplace.answer - }, - )) - } - }, - onError() { - setRespondingFalse() - // role back placeholder answer - setChatList(produce(getChatList(), (draft) => { - draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) - })) - }, - }, isInstalledApp, installedAppInfo?.id) - } - - const handleFeedback = async (messageId: string, feedback: Feedbacktype) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id) - const newChatList = chatList.map((item) => { - if (item.id === messageId) { - return { - ...item, - feedback, - } - } - return item - }) - setChatList(newChatList) - notify({ type: 'success', message: t('common.api.success') }) - } - - const handleReload = () => { - setCurrConversationId('-1', appId, false) - setChatNotStarted() - setShouldReload(false) - createNewChat() - } - - const handleConversationIdChange = (id: string) => { - if (id === '-1') { - createNewChat() - setConversationIdChangeBecauseOfNew(true) - } - else { - setConversationIdChangeBecauseOfNew(false) - } - // trigger handleConversationSwitch - setCurrConversationId(id, appId) - setIsShowSuggestion(false) - } - - const difyIcon = ( - <LogoHeader /> - ) - - if (appUnavailable) - return <AppUnavailable isUnknownReason={isUnknownReason} /> - - if (!appId || !siteInfo || !promptConfig) { - return <div className='flex h-screen w-full'> - <Loading type='app' /> - </div> - } - - return ( - <div> - <Header - title={siteInfo.title} - icon='' - customerIcon={difyIcon} - icon_background={siteInfo.icon_background || ''} - isEmbedScene={true} - isMobile={isMobile} - onCreateNewChat={() => handleConversationIdChange('-1')} - /> - - <div className={'flex bg-white overflow-hidden'}> - <div className={cn( - isInstalledApp ? 'h-full' : 'h-[calc(100vh_-_3rem)]', - 'flex-grow flex flex-col overflow-y-auto', - ) - }> - <ConfigScene - // conversationName={conversationName} - hasSetInputs={hasSetInputs} - isPublicVersion={isPublicVersion} - siteInfo={siteInfo} - promptConfig={promptConfig} - onStartChat={handleStartChat} - canEditInputs={canEditInputs} - savedInputs={currInputs as Record<string, any>} - onInputsChange={setCurrInputs} - plan={plan} - canReplaceLogo={canReplaceLogo} - customConfig={customConfig} - ></ConfigScene> - { - shouldReload && ( - <div className='flex items-center justify-between mb-5 px-4 py-2 bg-[#FEF0C7]'> - <div className='flex items-center text-xs font-medium text-[#DC6803]'> - <AlertTriangle className='mr-2 w-4 h-4' /> - {t('share.chat.temporarySystemIssue')} - </div> - <div - className='flex items-center px-3 h-7 bg-white shadow-xs rounded-md text-xs font-medium text-gray-700 cursor-pointer' - onClick={handleReload} - > - {t('share.chat.tryToSolve')} - </div> - </div> - ) - } - { - hasSetInputs && ( - <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponding ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}> - <div className='h-full overflow-y-auto' ref={chatListDomRef}> - <Chat - chatList={chatList} - query={userQuery} - onQueryChange={setUserQuery} - onSend={handleSend} - isHideFeedbackEdit - onFeedback={handleFeedback} - isResponding={isResponding} - canStopResponding={!!messageTaskId && isRespondingConIsCurrCon} - abortResponding={async () => { - await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id) - setHasStopResponded(true) - setRespondingFalse() - }} - checkCanSend={checkCanSend} - controlFocus={controlFocus} - isShowSuggestion={doShowSuggestion} - suggestionList={suggestQuestions} - displayScene='web' - isShowSpeechToText={speechToTextConfig?.enabled} - isShowTextToSpeech={textToSpeechConfig?.enabled} - isShowCitation={citationConfig?.enabled && isInstalledApp} - answerIcon={<LogoAvatar className='relative shrink-0' />} - visionConfig={visionConfig} - allToolIcons={appMeta?.tool_icons || {}} - customDisclaimer={siteInfo.custom_disclaimer} - /> - </div> - </div>) - } - - {/* {isShowConfirm && ( - <Confirm - title={t('share.chat.deleteConversation.title')} - content={t('share.chat.deleteConversation.content')} - isShow={isShowConfirm} - onClose={hideConfirm} - onConfirm={didDelete} - onCancel={hideConfirm} - /> - )} */} - </div> - </div> - </div> - ) -} -export default React.memo(Main) diff --git a/web/app/components/share/chatbot/sidebar/app-info/index.tsx b/web/app/components/share/chatbot/sidebar/app-info/index.tsx deleted file mode 100644 index a7f5052721..0000000000 --- a/web/app/components/share/chatbot/sidebar/app-info/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import cn from 'classnames' -import { appDefaultIconBackground } from '@/config/index' -import AppIcon from '@/app/components/base/app-icon' - -export type IAppInfoProps = { - className?: string - icon: string - icon_background?: string - name: string -} - -const AppInfo: FC<IAppInfoProps> = ({ - className, - icon, - icon_background, - name, -}) => { - return ( - <div className={cn(className, 'flex items-center space-x-3')}> - <AppIcon size="small" icon={icon} background={icon_background || appDefaultIconBackground} /> - <div className='w-0 grow text-sm font-semibold text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{name}</div> - </div> - ) -} -export default React.memo(AppInfo) diff --git a/web/app/components/share/chatbot/sidebar/card.module.css b/web/app/components/share/chatbot/sidebar/card.module.css deleted file mode 100644 index c917cb4db6..0000000000 --- a/web/app/components/share/chatbot/sidebar/card.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.card:hover { - background: linear-gradient(0deg, rgba(235, 245, 255, 0.4), rgba(235, 245, 255, 0.4)), #FFFFFF; -} \ No newline at end of file diff --git a/web/app/components/share/chatbot/sidebar/card.tsx b/web/app/components/share/chatbot/sidebar/card.tsx deleted file mode 100644 index 7457315a0d..0000000000 --- a/web/app/components/share/chatbot/sidebar/card.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { useTranslation } from 'react-i18next' -import s from './card.module.css' - -type PropType = { - children: React.ReactNode - text?: string -} -function Card({ children, text }: PropType) { - const { t } = useTranslation() - return ( - <div className={`${s.card} box-border w-full flex flex-col items-start px-4 py-3 rounded-lg border-solid border border-gray-200 cursor-pointer hover:border-primary-300`}> - <div className='text-gray-400 font-medium text-xs mb-2'>{text ?? t('share.chat.powerBy')}</div> - {children} - </div> - ) -} - -export default Card diff --git a/web/app/components/share/chatbot/sidebar/index.tsx b/web/app/components/share/chatbot/sidebar/index.tsx deleted file mode 100644 index a9db1cb5b4..0000000000 --- a/web/app/components/share/chatbot/sidebar/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { useEffect, useState } from 'react' -import type { FC } from 'react' -import { useTranslation } from 'react-i18next' -import { - PencilSquareIcon, -} from '@heroicons/react/24/outline' -import cn from 'classnames' -import Button from '../../../base/button' -import List from './list' -import AppInfo from '@/app/components/share/chat/sidebar/app-info' -// import Card from './card' -import type { ConversationItem, SiteInfo } from '@/models/share' -import { fetchConversations } from '@/service/share' - -export type ISidebarProps = { - copyRight: string - currentId: string - onCurrentIdChange: (id: string) => void - list: ConversationItem[] - isClearConversationList: boolean - pinnedList: ConversationItem[] - isClearPinnedConversationList: boolean - isInstalledApp: boolean - installedAppId?: string - siteInfo: SiteInfo - onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - isNoMore: boolean - isPinnedNoMore: boolean - onPin: (id: string) => void - onUnpin: (id: string) => void - controlUpdateList: number - onDelete: (id: string) => void -} - -const Sidebar: FC<ISidebarProps> = ({ - copyRight, - currentId, - onCurrentIdChange, - list, - isClearConversationList, - pinnedList, - isClearPinnedConversationList, - isInstalledApp, - installedAppId, - siteInfo, - onMoreLoaded, - onPinnedMoreLoaded, - isNoMore, - isPinnedNoMore, - onPin, - onUnpin, - controlUpdateList, - onDelete, -}) => { - const { t } = useTranslation() - const [hasPinned, setHasPinned] = useState(false) - - const checkHasPinned = async () => { - const { data }: any = await fetchConversations(isInstalledApp, installedAppId, undefined, true) - setHasPinned(data.length > 0) - } - - useEffect(() => { - checkHasPinned() - }, []) - - useEffect(() => { - if (controlUpdateList !== 0) - checkHasPinned() - }, [controlUpdateList]) - - const maxListHeight = isInstalledApp ? 'max-h-[30vh]' : 'max-h-[40vh]' - - return ( - <div - className={ - cn( - isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]', - 'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen', - ) - } - > - {isInstalledApp && ( - <AppInfo - className='my-4 px-4' - name={siteInfo.title || ''} - icon={siteInfo.icon || ''} - icon_background={siteInfo.icon_background} - /> - )} - <div className="flex flex-shrink-0 p-4 !pb-0"> - <Button - onClick={() => { onCurrentIdChange('-1') }} - variant='secondary-accent' - className="group w-full flex-shrink-0"> - <PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')} - </Button> - </div> - <div className={'flex-grow flex flex-col h-0 overflow-y-auto overflow-x-hidden'}> - {/* pinned list */} - {hasPinned && ( - <div className={cn('mt-4 px-4', list.length === 0 && 'flex flex-col flex-grow')}> - <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.pinnedTitle')}</div> - <List - className={cn(list.length > 0 ? maxListHeight : 'flex-grow')} - currentId={currentId} - onCurrentIdChange={onCurrentIdChange} - list={pinnedList} - isClearConversationList={isClearPinnedConversationList} - isInstalledApp={isInstalledApp} - installedAppId={installedAppId} - onMoreLoaded={onPinnedMoreLoaded} - isNoMore={isPinnedNoMore} - isPinned={true} - onPinChanged={id => onUnpin(id)} - controlUpdate={controlUpdateList + 1} - onDelete={onDelete} - /> - </div> - )} - {/* unpinned list */} - <div className={cn('mt-4 px-4', !hasPinned && 'flex flex-col flex-grow')}> - {(hasPinned && list.length > 0) && ( - <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.unpinnedTitle')}</div> - )} - <List - className={cn(hasPinned ? maxListHeight : 'flex-grow')} - currentId={currentId} - onCurrentIdChange={onCurrentIdChange} - list={list} - isClearConversationList={isClearConversationList} - isInstalledApp={isInstalledApp} - installedAppId={installedAppId} - onMoreLoaded={onMoreLoaded} - isNoMore={isNoMore} - isPinned={false} - onPinChanged={id => onPin(id)} - controlUpdate={controlUpdateList + 1} - onDelete={onDelete} - /> - </div> - - </div> - <div className="flex flex-shrink-0 pr-4 pb-4 pl-4"> - <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div> - </div> - </div> - ) -} - -export default React.memo(Sidebar) diff --git a/web/app/components/share/chatbot/sidebar/list/index.tsx b/web/app/components/share/chatbot/sidebar/list/index.tsx deleted file mode 100644 index 4d22d3fea8..0000000000 --- a/web/app/components/share/chatbot/sidebar/list/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useRef } from 'react' -import { - ChatBubbleOvalLeftEllipsisIcon, -} from '@heroicons/react/24/outline' -import { useInfiniteScroll } from 'ahooks' -import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid' -import cn from 'classnames' -import s from './style.module.css' -import type { ConversationItem } from '@/models/share' -import { fetchConversations } from '@/service/share' -import ItemOperation from '@/app/components/explore/item-operation' - -export type IListProps = { - className: string - currentId: string - onCurrentIdChange: (id: string) => void - list: ConversationItem[] - isClearConversationList: boolean - isInstalledApp: boolean - installedAppId?: string - onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void - isNoMore: boolean - isPinned: boolean - onPinChanged: (id: string) => void - controlUpdate: number - onDelete: (id: string) => void -} - -const List: FC<IListProps> = ({ - className, - currentId, - onCurrentIdChange, - list, - isClearConversationList, - isInstalledApp, - installedAppId, - onMoreLoaded, - isNoMore, - isPinned, - onPinChanged, - controlUpdate, - onDelete, -}) => { - const listRef = useRef<HTMLDivElement>(null) - - useInfiniteScroll( - async () => { - if (!isNoMore) { - const lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined - const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned) - onMoreLoaded({ data: conversations, has_more }) - } - return { list: [] } - }, - { - target: listRef, - isNoMore: () => { - return isNoMore - }, - reloadDeps: [isNoMore, controlUpdate], - }, - ) - return ( - <nav - ref={listRef} - className={cn(className, 'shrink-0 space-y-1 bg-white overflow-y-auto')} - > - {list.map((item) => { - const isCurrent = item.id === currentId - const ItemIcon - = isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon - return ( - <div - onClick={() => onCurrentIdChange(item.id)} - key={item.id} - className={cn(s.item, - isCurrent - ? 'bg-primary-50 text-primary-600' - : 'text-gray-700 hover:bg-gray-200 hover:text-gray-700', - 'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer', - )} - > - <div className='flex items-center w-0 grow'> - <ItemIcon - className={cn( - isCurrent - ? 'text-primary-600' - : 'text-gray-400 group-hover:text-gray-500', - 'mr-3 h-5 w-5 flex-shrink-0', - )} - aria-hidden="true" - /> - <span>{item.name}</span> - </div> - - {item.id !== '-1' && ( - <div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}> - <ItemOperation - isPinned={isPinned} - togglePin={() => onPinChanged(item.id)} - isShowDelete - onDelete={() => onDelete(item.id)} - /> - </div> - )} - </div> - ) - })} - </nav> - ) -} - -export default React.memo(List) diff --git a/web/app/components/share/chatbot/sidebar/list/style.module.css b/web/app/components/share/chatbot/sidebar/list/style.module.css deleted file mode 100644 index 50384da747..0000000000 --- a/web/app/components/share/chatbot/sidebar/list/style.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.opBtn { - visibility: hidden; -} - -.item:hover .opBtn { - visibility: visible; -} \ No newline at end of file diff --git a/web/app/components/share/chatbot/value-panel/index.tsx b/web/app/components/share/chatbot/value-panel/index.tsx deleted file mode 100644 index 694cc0ef3e..0000000000 --- a/web/app/components/share/chatbot/value-panel/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' -import type { FC, ReactNode } from 'react' -import React from 'react' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import s from './style.module.css' -import { StarIcon } from '@/app/components/share/chatbot/welcome/massive-component' -import Button from '@/app/components/base/button' - -export type ITemplateVarPanelProps = { - className?: string - header: ReactNode - children?: ReactNode | null - isFold: boolean -} - -const TemplateVarPanel: FC<ITemplateVarPanelProps> = ({ - className, - header, - children, - isFold, -}) => { - return ( - <div className={cn(isFold ? 'border border-indigo-100' : s.boxShodow, className, 'rounded-xl ')}> - {/* header */} - <div - className={cn(isFold && 'rounded-b-xl', 'rounded-t-xl px-6 py-4 bg-indigo-25 text-xs')} - > - {header} - </div> - {/* body */} - {!isFold && children && ( - <div className='rounded-b-xl p-6'> - {children} - </div> - )} - </div> - ) -} - -export const PanelTitle: FC<{ title: string; className?: string }> = ({ - title, - className, -}) => { - return ( - <div className={cn(className, 'flex items-center space-x-1 text-indigo-600')}> - <StarIcon /> - <span className='text-xs'>{title}</span> - </div> - ) -} - -export const VarOpBtnGroup: FC<{ className?: string; onConfirm: () => void; onCancel: () => void }> = ({ - className, - onConfirm, - onCancel, -}) => { - const { t } = useTranslation() - - return ( - <div className={cn(className, 'flex mt-3 space-x-2 mobile:ml-0 tablet:ml-[128px] text-sm')}> - <Button - variant='primary' - onClick={onConfirm} - > - {t('common.operation.save')} - </Button> - <Button - onClick={onCancel} - > - {t('common.operation.cancel')} - </Button> - </div > - ) -} - -export default React.memo(TemplateVarPanel) diff --git a/web/app/components/share/chatbot/value-panel/style.module.css b/web/app/components/share/chatbot/value-panel/style.module.css deleted file mode 100644 index c7613c44a4..0000000000 --- a/web/app/components/share/chatbot/value-panel/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.boxShodow { - box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} \ No newline at end of file diff --git a/web/app/components/share/chatbot/welcome/index.tsx b/web/app/components/share/chatbot/welcome/index.tsx deleted file mode 100644 index 240271d3b5..0000000000 --- a/web/app/components/share/chatbot/welcome/index.tsx +++ /dev/null @@ -1,389 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel' -import s from './style.module.css' -import { AppInfo, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component' -import type { SiteInfo } from '@/models/share' -import type { PromptConfig } from '@/models/debug' -import { ToastContext } from '@/app/components/base/toast' -import Select from '@/app/components/base/select' -import { DEFAULT_VALUE_MAX_LEN } from '@/config' - -// regex to match the {{}} and replace it with a span -const regex = /\{\{([^}]+)\}\}/g - -export type IWelcomeProps = { - // conversationName: string - hasSetInputs: boolean - isPublicVersion: boolean - siteInfo: SiteInfo - promptConfig: PromptConfig - onStartChat: (inputs: Record<string, any>) => void - canEditInputs: boolean - savedInputs: Record<string, any> - onInputsChange: (inputs: Record<string, any>) => void - plan: string - canReplaceLogo?: boolean - customConfig?: { - remove_webapp_brand?: boolean - replace_webapp_logo?: string - } -} - -const Welcome: FC<IWelcomeProps> = ({ - // conversationName, - hasSetInputs, - isPublicVersion, - siteInfo, - promptConfig, - onStartChat, - canEditInputs, - savedInputs, - onInputsChange, - customConfig, -}) => { - const { t } = useTranslation() - const hasVar = promptConfig.prompt_variables.length > 0 - const [isFold, setIsFold] = useState<boolean>(true) - const [inputs, setInputs] = useState<Record<string, any>>((() => { - if (hasSetInputs) - return savedInputs - - const res: Record<string, any> = {} - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = '' - }) - } - // debugger - return res - })()) - useEffect(() => { - if (!savedInputs) { - const res: Record<string, any> = {} - if (promptConfig) { - promptConfig.prompt_variables.forEach((item) => { - res[item.key] = '' - }) - } - setInputs(res) - } - else { - setInputs(savedInputs) - } - }, [savedInputs]) - - const highLightPromoptTemplate = (() => { - if (!promptConfig) - return '' - const res = promptConfig.prompt_template.replace(regex, (match, p1) => { - return `<span class='text-gray-800 font-bold'>${inputs?.[p1] ? inputs?.[p1] : match}</span>` - }) - return res - })() - - const { notify } = useContext(ToastContext) - const logError = (message: string) => { - notify({ type: 'error', message, duration: 3000 }) - } - - // const renderHeader = () => { - // return ( - // <div className='absolute top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'> - // <div className='text-gray-900'>{conversationName}</div> - // </div> - // ) - // } - - const renderInputs = () => { - return ( - <div className='space-y-3'> - {promptConfig.prompt_variables.map(item => ( - <div className='tablet:flex items-start mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}> - <label className={`flex-shrink-0 flex items-center tablet:leading-9 mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label> - {item.type === 'select' - && ( - <Select - className='w-full' - defaultValue={inputs?.[item.key]} - onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }} - items={(item.options || []).map(i => ({ name: i, value: i }))} - allowSearch={false} - bgClassName='bg-gray-50' - /> - ) - } - {item.type === 'string' && ( - <input - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'} - maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN} - /> - )} - {item.type === 'paragraph' && ( - <textarea - className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50" - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - /> - )} - {item.type === 'number' && ( - <input - type='number' - placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} - value={inputs?.[item.key] || ''} - onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }} - className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'} - /> - )} - </div> - ))} - </div> - ) - } - - const canChat = () => { - const prompt_variables = promptConfig?.prompt_variables - if (!inputs || !prompt_variables || prompt_variables?.length === 0) - return true - - let hasEmptyInput = '' - const requiredVars = prompt_variables?.filter(({ key, name, required }) => { - const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) - return res - }) || [] // compatible with old version - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs?.[key]) - hasEmptyInput = name - }) - - if (hasEmptyInput) { - logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput })) - return false - } - return !hasEmptyInput - } - - const handleChat = () => { - if (!canChat()) - return - - onStartChat(inputs) - } - - const renderNoVarPanel = () => { - if (isPublicVersion) { - return ( - <div> - <AppInfo siteInfo={siteInfo} /> - <TemplateVarPanel - isFold={false} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - </> - } - > - <ChatBtn onClick={handleChat} /> - </TemplateVarPanel> - </div> - ) - } - // private version - return ( - <TemplateVarPanel - isFold={false} - header={ - <AppInfo siteInfo={siteInfo} /> - } - > - <ChatBtn onClick={handleChat} /> - </TemplateVarPanel> - ) - } - - const renderVarPanel = () => { - return ( - <TemplateVarPanel - isFold={false} - header={ - <AppInfo siteInfo={siteInfo} /> - } - > - {renderInputs()} - <ChatBtn - className='mt-3 mobile:ml-0 tablet:ml-[128px]' - onClick={handleChat} - /> - </TemplateVarPanel> - ) - } - - const renderVarOpBtnGroup = () => { - return ( - <VarOpBtnGroup - onConfirm={() => { - if (!canChat()) - return - - onInputsChange(inputs) - setIsFold(true) - }} - onCancel={() => { - setInputs(savedInputs) - setIsFold(true) - }} - /> - ) - } - - const renderHasSetInputsPublic = () => { - if (!canEditInputs) { - return ( - <TemplateVarPanel - isFold={false} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - </> - } - /> - ) - } - - return ( - <TemplateVarPanel - isFold={isFold} - header={ - <> - <PanelTitle - title={t('share.chat.publicPromptConfigTitle')} - className='mb-1' - /> - <PromptTemplate html={highLightPromoptTemplate} /> - {isFold && ( - <div className='flex items-center justify-between mt-3 border-t border-indigo-100 pt-4 text-xs text-indigo-600'> - <span className='text-gray-700'>{t('share.chat.configStatusDes')}</span> - <EditBtn onClick={() => setIsFold(false)} /> - </div> - )} - </> - } - > - {renderInputs()} - {renderVarOpBtnGroup()} - </TemplateVarPanel> - ) - } - - const renderHasSetInputsPrivate = () => { - if (!canEditInputs || !hasVar) - return null - - return ( - <TemplateVarPanel - isFold={isFold} - header={ - <div className='flex items-center justify-between text-indigo-600'> - <PanelTitle - title={!isFold ? t('share.chat.privatePromptConfigTitle') : t('share.chat.configStatusDes')} - /> - {isFold && ( - <EditBtn onClick={() => setIsFold(false)} /> - )} - </div> - } - > - {renderInputs()} - {renderVarOpBtnGroup()} - </TemplateVarPanel> - ) - } - - const renderHasSetInputs = () => { - if ((!isPublicVersion && !canEditInputs) || !hasVar) - return null - - return ( - <div - className='pt-[88px] mb-5' - > - {isPublicVersion ? renderHasSetInputsPublic() : renderHasSetInputsPrivate()} - </div>) - } - - return ( - <div className='relative tablet:min-h-[64px]'> - {/* {hasSetInputs && renderHeader()} */} - <div className='mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'> - {/* Has't set inputs */} - { - !hasSetInputs && ( - <div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'> - {hasVar - ? ( - renderVarPanel() - ) - : ( - renderNoVarPanel() - )} - </div> - ) - } - - {/* Has set inputs */} - {hasSetInputs && renderHasSetInputs()} - - {/* foot */} - {!hasSetInputs && ( - <div className='mt-4 flex justify-between items-center h-8 text-xs text-gray-400'> - - {siteInfo.privacy_policy - ? <div>{t('share.chat.privacyPolicyLeft')} - <a - className='text-gray-500 px-1' - href={siteInfo.privacy_policy} - target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a> - {t('share.chat.privacyPolicyRight')} - </div> - : <div> - </div>} - { - customConfig?.remove_webapp_brand - ? null - : ( - <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank"> - <span className='uppercase'>{t('share.chat.powerBy')}</span> - { - customConfig?.replace_webapp_logo - ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' /> - : <FootLogo /> - } - </a> - ) - } - </div> - )} - </div> - </div > - ) -} - -export default React.memo(Welcome) diff --git a/web/app/components/share/chatbot/welcome/massive-component.tsx b/web/app/components/share/chatbot/welcome/massive-component.tsx deleted file mode 100644 index 0ed2af5aaf..0000000000 --- a/web/app/components/share/chatbot/welcome/massive-component.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import cn from 'classnames' -import { useTranslation } from 'react-i18next' -import { - PencilIcon, -} from '@heroicons/react/24/solid' -import s from './style.module.css' -import type { SiteInfo } from '@/models/share' -import Button from '@/app/components/base/button' -import LogoSite from '@/app/components/base/logo/logo-site' - -export const AppInfo: FC<{ siteInfo: SiteInfo }> = ({ siteInfo }) => { - const { t } = useTranslation() - return ( - <div> - <div className='flex items-center py-2 text-xl font-medium text-gray-700 rounded-md'>👏 {t('share.common.welcome')} {siteInfo.title}</div> - <p className='text-sm text-gray-500'>{siteInfo.description}</p> - </div> - ) -} - -export const PromptTemplate: FC<{ html: string }> = ({ html }) => { - return ( - <div - className={' box-border text-sm text-gray-700'} - dangerouslySetInnerHTML={{ __html: html }} - ></div> - ) -} - -export const StarIcon = () => ( - <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z" fill="#444CE7" /> - <path d="M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z" fill="#444CE7" /> - <path d="M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z" fill="#444CE7" /> - </svg> -) - -export const ChatBtn: FC<{ onClick: () => void; className?: string }> = ({ - className, - onClick, -}) => { - const { t } = useTranslation() - return ( - <Button - variant='primary' - className={cn(className, `!p-0 space-x-2 flex items-center ${s.customBtn}`)} - onClick={onClick}> - <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path fillRule="evenodd" clipRule="evenodd" d="M18 10.5C18 14.366 14.418 17.5 10 17.5C8.58005 17.506 7.17955 17.1698 5.917 16.52L2 17.5L3.338 14.377C2.493 13.267 2 11.934 2 10.5C2 6.634 5.582 3.5 10 3.5C14.418 3.5 18 6.634 18 10.5ZM7 9.5H5V11.5H7V9.5ZM15 9.5H13V11.5H15V9.5ZM9 9.5H11V11.5H9V9.5Z" fill="white" /> - </svg> - {t('share.chat.startChat')} - </Button> - ) -} - -export const EditBtn = ({ className, onClick }: { className?: string; onClick: () => void }) => { - const { t } = useTranslation() - - return ( - <div - className={cn('px-2 flex space-x-1 items-center rounded-md cursor-pointer', className)} - onClick={onClick} - > - <PencilIcon className='w-3 h-3' /> - <span>{t('common.operation.edit')}</span> - </div> - ) -} - -export const FootLogo = () => ( - <LogoSite className='!h-5' /> -) diff --git a/web/app/components/share/chatbot/welcome/style.module.css b/web/app/components/share/chatbot/welcome/style.module.css deleted file mode 100644 index 458a112ca3..0000000000 --- a/web/app/components/share/chatbot/welcome/style.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.boxShodow { - box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} - -.bgGrayColor { - background-color: #F9FAFB; -} - -.headerBg { - height: 3.5rem; - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.formLabel { - width: 120px; - margin-right: 8px; -} - -.customBtn { - width: 136px; -} \ No newline at end of file diff --git a/web/app/components/share/header.tsx b/web/app/components/share/header.tsx deleted file mode 100644 index d4b41bc1a4..0000000000 --- a/web/app/components/share/header.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { FC } from 'react' -import React from 'react' -import { - Bars3Icon, - PencilSquareIcon, -} from '@heroicons/react/24/solid' -import { useTranslation } from 'react-i18next' -import AppIcon from '@/app/components/base/app-icon' -import { ReplayIcon } from '@/app/components/app/chat/icon-component' -import Tooltip from '@/app/components/base/tooltip' - -export type IHeaderProps = { - title: string - customerIcon?: React.ReactNode - icon: string - icon_background: string - isMobile?: boolean - isEmbedScene?: boolean - onShowSideBar?: () => void - onCreateNewChat?: () => void -} -const Header: FC<IHeaderProps> = ({ - title, - isMobile, - customerIcon, - icon, - icon_background, - isEmbedScene = false, - onShowSideBar, - onCreateNewChat, -}) => { - const { t } = useTranslation() - if (!isMobile) - return null - - if (isEmbedScene) { - return ( - <div - className={` - shrink-0 flex items-center justify-between h-14 px-4 bg-gray-100 - bg-gradient-to-r from-blue-600 to-sky-500 - `} - > - <div className="flex items-center space-x-2"> - {customerIcon || <AppIcon size="small" icon={icon} background={icon_background} />} - <div - className={'text-sm font-bold text-white'} - > - {title} - </div> - </div> - <Tooltip - selector={'embed-scene-restart-button'} - htmlContent={t('share.chat.resetChat')} - position='top' - > - <div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => { - onCreateNewChat?.() - }}> - <ReplayIcon className="h-4 w-4 text-sm font-bold text-white" /> - </div> - </Tooltip> - </div> - ) - } - - return ( - <div className="shrink-0 flex items-center justify-between h-14 px-4 bg-gray-100"> - <div - className='flex items-center justify-center h-8 w-8 cursor-pointer' - onClick={() => onShowSideBar?.()} - > - <Bars3Icon className="h-4 w-4 text-gray-500" /> - </div> - <div className='flex items-center space-x-2'> - <AppIcon size="small" icon={icon} background={icon_background} /> - <div className=" text-sm text-gray-800 font-bold">{title}</div> - </div> - <div className='flex items-center justify-center h-8 w-8 cursor-pointer' - onClick={() => onCreateNewChat?.()} - > - <PencilSquareIcon className="h-4 w-4 text-gray-500" /> - </div> - </div> - ) -} - -export default React.memo(Header) diff --git a/web/app/components/share/text-generation/result/content.tsx b/web/app/components/share/text-generation/result/content.tsx index 21fd5fb6a0..17cce0fae5 100644 --- a/web/app/components/share/text-generation/result/content.tsx +++ b/web/app/components/share/text-generation/result/content.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import React from 'react' import Header from './header' -import type { Feedbacktype } from '@/app/components/app/chat/type' +import type { Feedbacktype } from '@/app/components/base/chat/chat/type' import { format } from '@/service/base' export type IResultProps = { diff --git a/web/app/components/share/text-generation/result/header.tsx b/web/app/components/share/text-generation/result/header.tsx index cfbea69341..bb3c5695f3 100644 --- a/web/app/components/share/text-generation/result/header.tsx +++ b/web/app/components/share/text-generation/result/header.tsx @@ -4,7 +4,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import copy from 'copy-to-clipboard' -import type { Feedbacktype } from '@/app/components/app/chat/type' +import type { Feedbacktype } from '@/app/components/base/chat/chat/type' import Button from '@/app/components/base/button' import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 67210a9856..e40a1f3c8f 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -9,7 +9,7 @@ import TextGenerationRes from '@/app/components/app/text-generate/item' import NoData from '@/app/components/share/text-generation/no-data' import Toast from '@/app/components/base/toast' import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share' -import type { Feedbacktype } from '@/app/components/app/chat/type' +import type { Feedbacktype } from '@/app/components/base/chat/chat/type' import Loading from '@/app/components/base/loading' import type { PromptConfig } from '@/models/debug' import type { InstalledApp } from '@/models/explore' diff --git a/web/app/components/tools/add-tool-modal/index.tsx b/web/app/components/tools/add-tool-modal/index.tsx index a3a3694e8c..02e4c656ba 100644 --- a/web/app/components/tools/add-tool-modal/index.tsx +++ b/web/app/components/tools/add-tool-modal/index.tsx @@ -83,7 +83,9 @@ const AddToolModal: FC<Props> = ({ return toolWithProvider.labels.includes(currentCategory) }).filter((toolWithProvider) => { return toolWithProvider.tools.some((tool) => { - return tool.label[language].toLowerCase().includes(keywords.toLowerCase()) + return Object.values(tool.label).some((label) => { + return label.toLowerCase().includes(keywords.toLowerCase()) + }) }) }) }, [currentType, currentCategory, toolList, keywords, language]) diff --git a/web/app/components/tools/edit-custom-collection-modal/index.tsx b/web/app/components/tools/edit-custom-collection-modal/index.tsx index c06c3af123..8de57bbafa 100644 --- a/web/app/components/tools/edit-custom-collection-modal/index.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/index.tsx @@ -302,7 +302,7 @@ const EditCustomCollectionModal: FC<Props> = ({ <div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} > { isEdit && ( - <Button onClick={onRemove}>{t('common.operation.remove')}</Button> + <Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button> ) } <div className='flex space-x-2 '> diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index ba96fb1126..31d9aefc71 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -8,6 +8,7 @@ import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderReq import ToolItem from './tool-item' import I18n from '@/context/i18n' import { getLanguage } from '@/i18n/language' +import Confirm from '@/app/components/base/confirm' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Indicator from '@/app/components/header/indicator' @@ -83,6 +84,8 @@ const ProviderDetail = ({ // custom provider const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | WorkflowToolProviderResponse | null>(null) const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false) + const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [deleteAction, setDeleteAction] = useState(null) const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => { await updateCustomCollection(data) onRefreshData() @@ -158,6 +161,23 @@ const ProviderDetail = ({ }) setIsShowEditWorkflowToolModal(false) } + const onClickCustomToolDelete = () => { + setDeleteAction('customTool') + setShowConfirmDelete(true) + } + const onClickWorkflowToolDelete = () => { + setDeleteAction('workflowTool') + setShowConfirmDelete(true) + } + const handleConfirmDelete = () => { + if (deleteAction === 'customTool') + doRemoveCustomToolCollection() + + else if (deleteAction === 'workflowTool') + removeWorkflowToolProvider() + + setShowConfirmDelete(false) + } // ToolList const [toolList, setToolList] = useState<Tool[]>([]) @@ -330,17 +350,27 @@ const ProviderDetail = ({ payload={customCollection} onHide={() => setIsShowEditCustomCollectionModal(false)} onEdit={doUpdateCustomToolCollection} - onRemove={doRemoveCustomToolCollection} + onRemove={onClickCustomToolDelete} /> )} {isShowEditWorkflowToolModal && ( <WorkflowToolModal payload={customCollection} onHide={() => setIsShowEditWorkflowToolModal(false)} - onRemove={removeWorkflowToolProvider} + onRemove={onClickWorkflowToolDelete} onSave={updateWorkflowToolProvider} /> )} + {showConfirmDelete && ( + <Confirm + title={t('tools.createTool.deleteToolConfirmTitle')} + content={t('tools.createTool.deleteToolConfirmContent')} + isShow={showConfirmDelete} + onClose={() => setShowConfirmDelete(false)} + onConfirm={handleConfirmDelete} + onCancel={() => setShowConfirmDelete(false)} + /> + )} </div> ) } diff --git a/web/app/components/tools/utils/index.ts b/web/app/components/tools/utils/index.ts index c4e1a3858e..0c462aa6fc 100644 --- a/web/app/components/tools/utils/index.ts +++ b/web/app/components/tools/utils/index.ts @@ -1,4 +1,4 @@ -import type { ThoughtItem } from '../../app/chat/type' +import type { ThoughtItem } from '@/app/components/base/chat/chat/type' import type { VisionFile } from '@/types/app' export const sortAgentSorts = (list: ThoughtItem[]) => { diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 9462fef964..80f786835b 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -244,7 +244,7 @@ const WorkflowToolAsModal: FC<Props> = ({ </div> <div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} > {!isAdd && onRemove && ( - <Button onClick={onRemove}>{t('common.operation.remove')}</Button> + <Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button> )} <div className='flex space-x-2 '> <Button onClick={onHide}>{t('common.operation.cancel')}</Button> diff --git a/web/app/components/workflow/candidate-node.tsx b/web/app/components/workflow/candidate-node.tsx index 8528e61daa..faae545b3b 100644 --- a/web/app/components/workflow/candidate-node.tsx +++ b/web/app/components/workflow/candidate-node.tsx @@ -12,7 +12,7 @@ import { useStore, useWorkflowStore, } from './store' -import { useNodesInteractions } from './hooks' +import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks' import { CUSTOM_NODE } from './constants' import CustomNode from './nodes' import CustomNoteNode from './note-node' @@ -26,6 +26,7 @@ const CandidateNode = () => { const mousePosition = useStore(s => s.mousePosition) const { zoom } = useViewport() const { handleNodeSelect } = useNodesInteractions() + const { saveStateToHistory } = useWorkflowHistory() useEventListener('click', (e) => { const { candidateNode, mousePosition } = workflowStore.getState() @@ -53,6 +54,11 @@ const CandidateNode = () => { }) }) setNodes(newNodes) + if (candidateNode.type === CUSTOM_NOTE_NODE) + saveStateToHistory(WorkflowHistoryEvent.NoteAdd) + else + saveStateToHistory(WorkflowHistoryEvent.NodeAdd) + workflowStore.setState({ candidateNode: undefined }) if (candidateNode.type === CUSTOM_NOTE_NODE) diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index a6f313e98e..1786ca4b47 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -377,6 +377,10 @@ export const TOOL_OUTPUT_STRUCT: Var[] = [ variable: 'files', type: VarType.arrayFile, }, + { + variable: 'json', + type: VarType.arrayObject, + }, ] export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [ diff --git a/web/app/components/workflow/header/undo-redo.tsx b/web/app/components/workflow/header/undo-redo.tsx new file mode 100644 index 0000000000..bd27e09173 --- /dev/null +++ b/web/app/components/workflow/header/undo-redo.tsx @@ -0,0 +1,65 @@ +import type { FC } from 'react' +import { memo, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowGoBackLine, + RiArrowGoForwardFill, +} from '@remixicon/react' +import TipPopup from '../operator/tip-popup' +import { useWorkflowHistoryStore } from '../workflow-history-store' +import { useNodesReadOnly } from '@/app/components/workflow/hooks' +import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history' + +export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void } +const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => { + const { t } = useTranslation() + const { store } = useWorkflowHistoryStore() + const [buttonsDisabled, setButtonsDisabled] = useState({ undo: true, redo: true }) + + useEffect(() => { + const unsubscribe = store.temporal.subscribe((state) => { + setButtonsDisabled({ + undo: state.pastStates.length === 0, + redo: state.futureStates.length === 0, + }) + }) + return () => unsubscribe() + }, [store]) + + const { nodesReadOnly } = useNodesReadOnly() + + return ( + <div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'> + <TipPopup title={t('workflow.common.undo')!} > + <div + data-tooltip-id='workflow.undo' + className={` + flex items-center px-1.5 w-8 h-8 rounded-md text-[13px] font-medium + hover:bg-black/5 hover:text-gray-700 cursor-pointer select-none + ${(nodesReadOnly || buttonsDisabled.undo) && 'hover:bg-transparent opacity-50 !cursor-not-allowed'} + `} + onClick={() => !nodesReadOnly && !buttonsDisabled.undo && handleUndo()} + > + <RiArrowGoBackLine className='h-4 w-4' /> + </div> + </TipPopup> + <TipPopup title={t('workflow.common.redo')!} > + <div + data-tooltip-id='workflow.redo' + className={` + flex items-center px-1.5 w-8 h-8 rounded-md text-[13px] font-medium + hover:bg-black/5 hover:text-gray-700 cursor-pointer select-none + ${(nodesReadOnly || buttonsDisabled.redo) && 'hover:bg-transparent opacity-50 !cursor-not-allowed'} + `} + onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()} + > + <RiArrowGoForwardFill className='h-4 w-4' /> + </div> + </TipPopup> + <div className="mx-[3px] w-[1px] h-3.5 bg-gray-200"></div> + <ViewWorkflowHistory /> + </div> + ) +} + +export default memo(UndoRedo) diff --git a/web/app/components/workflow/header/view-workflow-history.tsx b/web/app/components/workflow/header/view-workflow-history.tsx new file mode 100644 index 0000000000..8fa49d59e5 --- /dev/null +++ b/web/app/components/workflow/header/view-workflow-history.tsx @@ -0,0 +1,273 @@ +import { + memo, + useCallback, + useMemo, + useState, +} from 'react' +import cn from 'classnames' +import { + RiCloseLine, + RiHistoryLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useShallow } from 'zustand/react/shallow' +import { useStoreApi } from 'reactflow' +import { + useNodesReadOnly, + useWorkflowHistory, +} from '../hooks' +import TipPopup from '../operator/tip-popup' +import type { WorkflowHistoryState } from '../workflow-history-store' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import { useStore as useAppStore } from '@/app/components/app/store' + +type ChangeHistoryEntry = { + label: string + index: number + state: Partial<WorkflowHistoryState> +} + +type ChangeHistoryList = { + pastStates: ChangeHistoryEntry[] + futureStates: ChangeHistoryEntry[] + statesCount: number +} + +const ViewWorkflowHistory = () => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const { nodesReadOnly } = useNodesReadOnly() + const { setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ + appDetail: state.appDetail, + setCurrentLogItem: state.setCurrentLogItem, + setShowMessageLogModal: state.setShowMessageLogModal, + }))) + const reactflowStore = useStoreApi() + const { store, getHistoryLabel } = useWorkflowHistory() + + const { pastStates, futureStates, undo, redo, clear } = store.temporal.getState() + const [currentHistoryStateIndex, setCurrentHistoryStateIndex] = useState<number>(0) + + const handleClearHistory = useCallback(() => { + clear() + setCurrentHistoryStateIndex(0) + }, [clear]) + + const handleSetState = useCallback(({ index }: ChangeHistoryEntry) => { + const { setEdges, setNodes } = reactflowStore.getState() + const diff = currentHistoryStateIndex + index + if (diff === 0) + return + + if (diff < 0) + undo(diff * -1) + else + redo(diff) + + const { edges, nodes } = store.getState() + if (edges.length === 0 && nodes.length === 0) + return + + setEdges(edges) + setNodes(nodes) + }, [currentHistoryStateIndex, reactflowStore, redo, store, undo]) + + const calculateStepLabel = useCallback((index: number) => { + if (!index) + return + + const count = index < 0 ? index * -1 : index + return `${index > 0 ? t('workflow.changeHistory.stepForward', { count }) : t('workflow.changeHistory.stepBackward', { count })}` + } + , [t]) + + const calculateChangeList: ChangeHistoryList = useMemo(() => { + const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => { + return { + label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent), + index: reverse ? list.length - 1 - index - startIndex : index - startIndex, + state, + } + }).filter(Boolean) + + const historyData = { + pastStates: filterList(pastStates, pastStates.length).reverse(), + futureStates: filterList([...futureStates, (!pastStates.length && !futureStates.length) ? undefined : store.getState()].filter(Boolean), 0, true), + statesCount: 0, + } + + historyData.statesCount = pastStates.length + futureStates.length + + return { + ...historyData, + statesCount: pastStates.length + futureStates.length, + } + }, [futureStates, getHistoryLabel, pastStates, store]) + + return ( + ( + <PortalToFollowElem + placement='bottom-end' + offset={{ + mainAxis: 4, + crossAxis: 131, + }} + open={open} + onOpenChange={setOpen} + > + <PortalToFollowElemTrigger onClick={() => !nodesReadOnly && setOpen(v => !v)}> + <TipPopup + title={t('workflow.changeHistory.title')} + > + <div + className={` + flex items-center justify-center w-8 h-8 rounded-md hover:bg-black/5 cursor-pointer + ${open && 'bg-primary-50'} ${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'} + `} + onClick={() => { + if (nodesReadOnly) + return + setCurrentLogItem() + setShowMessageLogModal(false) + }} + > + <RiHistoryLine className={`w-4 h-4 hover:bg-black/5 hover:text-gray-700 ${open ? 'text-primary-600' : 'text-gray-500'}`} /> + </div> + </TipPopup> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-[12]'> + <div + className='flex flex-col ml-2 min-w-[240px] max-w-[360px] bg-white border-[0.5px] border-gray-200 shadow-xl rounded-xl overflow-y-auto' + > + <div className='sticky top-0 bg-white flex items-center justify-between px-4 pt-3 text-base font-semibold text-gray-900'> + <div className='grow'>{t('workflow.changeHistory.title')}</div> + <div + className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer' + onClick={() => { + setCurrentLogItem() + setShowMessageLogModal(false) + setOpen(false) + }} + > + <RiCloseLine className='w-4 h-4 text-gray-500' /> + </div> + </div> + { + ( + <div + className='p-2 overflow-y-auto' + style={{ + maxHeight: 'calc(1 / 2 * 100vh)', + }} + > + { + !calculateChangeList.statesCount && ( + <div className='py-12'> + <RiHistoryLine className='mx-auto mb-2 w-8 h-8 text-gray-300' /> + <div className='text-center text-[13px] text-gray-400'> + {t('workflow.changeHistory.placeholder')} + </div> + </div> + ) + } + <div className='flex flex-col'> + { + calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => ( + <div + key={item?.index} + className={cn( + 'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', + item?.index === currentHistoryStateIndex && 'bg-primary-50', + )} + onClick={() => { + handleSetState(item) + setOpen(false) + }} + > + <div> + <div + className={cn( + 'flex items-center text-[13px] font-medium leading-[18px]', + item?.index === currentHistoryStateIndex && 'text-primary-600', + )} + > + {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')}) + </div> + </div> + </div> + )) + } + { + calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => ( + <div + key={item?.index} + className={cn( + 'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', + item?.index === calculateChangeList.statesCount - 1 && 'bg-primary-50', + )} + onClick={() => { + handleSetState(item) + setOpen(false) + }} + > + <div> + <div + className={cn( + 'flex items-center text-[13px] font-medium leading-[18px]', + item?.index === calculateChangeList.statesCount - 1 && 'text-primary-600', + )} + > + {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}) + </div> + </div> + </div> + )) + } + </div> + </div> + ) + } + { + !!calculateChangeList.statesCount && ( + <> + <div className="h-[1px] bg-gray-100" /> + <div + className={cn( + 'flex my-0.5 px-2 py-[7px] rounded-lg cursor-pointer', + 'hover:bg-red-50 hover:text-red-600', + )} + onClick={() => { + handleClearHistory() + setOpen(false) + }} + > + <div> + <div + className={cn( + 'flex items-center text-[13px] font-medium leading-[18px]', + )} + > + {t('workflow.changeHistory.clearHistory')} + </div> + </div> + </div> + </> + ) + } + <div className="px-3 w-[240px] py-2 text-xs text-gray-500" > + <div className="flex items-center mb-1 h-[22px] font-medium uppercase">{t('workflow.changeHistory.hint')}</div> + <div className="mb-1 text-gray-700 leading-[18px]">{t('workflow.changeHistory.hintText')}</div> + </div> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) + ) +} + +export default memo(ViewWorkflowHistory) diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index 41fb7845a6..e355aece39 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -13,3 +13,4 @@ export * from './use-selection-interactions' export * from './use-panel-interactions' export * from './use-workflow-start-run' export * from './use-nodes-layout' +export * from './use-workflow-history' diff --git a/web/app/components/workflow/hooks/use-edges-interactions.ts b/web/app/components/workflow/hooks/use-edges-interactions.ts index e7544c2ab5..bc3fb0e8bf 100644 --- a/web/app/components/workflow/hooks/use-edges-interactions.ts +++ b/web/app/components/workflow/hooks/use-edges-interactions.ts @@ -13,11 +13,13 @@ import type { import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useNodesReadOnly } from './use-workflow' +import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' export const useEdgesInteractions = () => { const store = useStoreApi() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { getNodesReadOnly } = useNodesReadOnly() + const { saveStateToHistory } = useWorkflowHistory() const handleEdgeEnter = useCallback<EdgeMouseHandler>((_, edge) => { if (getNodesReadOnly()) @@ -83,7 +85,8 @@ export const useEdgesInteractions = () => { }) setEdges(newEdges) handleSyncWorkflowDraft() - }, [store, handleSyncWorkflowDraft, getNodesReadOnly]) + saveStateToHistory(WorkflowHistoryEvent.EdgeDeleteByDeleteBranch) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) const handleEdgeDelete = useCallback(() => { if (getNodesReadOnly()) @@ -123,7 +126,8 @@ export const useEdgesInteractions = () => { }) setEdges(newEdges) handleSyncWorkflowDraft() - }, [store, getNodesReadOnly, handleSyncWorkflowDraft]) + saveStateToHistory(WorkflowHistoryEvent.EdgeDelete) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) const handleEdgesChange = useCallback<OnEdgesChange>((changes) => { if (getNodesReadOnly()) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index c63d662e01..914901ebb1 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -42,18 +42,21 @@ import { CUSTOM_NOTE_NODE } from '../note-node/constants' import type { IterationNodeType } from '../nodes/iteration/types' import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types' import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions' +import { useWorkflowHistoryStore } from '../workflow-history-store' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useHelpline } from './use-helpline' import { useNodesReadOnly, useWorkflow, } from './use-workflow' +import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' export const useNodesInteractions = () => { const { t } = useTranslation() const store = useStoreApi() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() + const { store: workflowHistoryStore } = useWorkflowHistoryStore() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { getAfterNodesInSameBranch, @@ -66,6 +69,8 @@ export const useNodesInteractions = () => { } = useNodeIterationInteractions() const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number }) + const { saveStateToHistory, undo, redo } = useWorkflowHistory() + const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => { workflowStore.setState({ nodeAnimation: false }) @@ -137,8 +142,13 @@ export const useNodesInteractions = () => { setHelpLineHorizontal() setHelpLineVertical() handleSyncWorkflowDraft() + + if (x !== 0 && y !== 0) { + // selecting a note will trigger a drag stop event with x and y as 0 + saveStateToHistory(WorkflowHistoryEvent.NodeDragStop) + } } - }, [handleSyncWorkflowDraft, workflowStore, getNodesReadOnly]) + }, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft]) const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => { if (getNodesReadOnly()) @@ -359,8 +369,10 @@ export const useNodesInteractions = () => { return filtered }) setEdges(newEdges) + handleSyncWorkflowDraft() - }, [store, handleSyncWorkflowDraft, getNodesReadOnly]) + saveStateToHistory(WorkflowHistoryEvent.NodeConnect) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => { if (getNodesReadOnly()) @@ -544,7 +556,13 @@ export const useNodesInteractions = () => { }) setEdges(newEdges) handleSyncWorkflowDraft() - }, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore, t]) + + if (currentNode.type === 'custom-note') + saveStateToHistory(WorkflowHistoryEvent.NoteDelete) + + else + saveStateToHistory(WorkflowHistoryEvent.NodeDelete) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) const handleNodeAdd = useCallback<OnNodeAdd>(( { @@ -877,7 +895,8 @@ export const useNodesInteractions = () => { setEdges(newEdges) } handleSyncWorkflowDraft() - }, [store, workflowStore, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t]) + saveStateToHistory(WorkflowHistoryEvent.NodeAdd) + }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch]) const handleNodeChange = useCallback(( currentNodeId: string, @@ -955,7 +974,9 @@ export const useNodesInteractions = () => { }) setEdges(newEdges) handleSyncWorkflowDraft() - }, [store, handleSyncWorkflowDraft, getNodesReadOnly, t]) + + saveStateToHistory(WorkflowHistoryEvent.NodeChange) + }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory]) const handleNodeCancelRunningStatus = useCallback(() => { const { @@ -1107,9 +1128,10 @@ export const useNodesInteractions = () => { }) setNodes([...nodes, ...nodesToPaste]) + saveStateToHistory(WorkflowHistoryEvent.NodePaste) handleSyncWorkflowDraft() } - }, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow, handleNodeIterationChildrenCopy]) + }, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy]) const handleNodesDuplicate = useCallback(() => { if (getNodesReadOnly()) @@ -1208,7 +1230,52 @@ export const useNodesInteractions = () => { }) setNodes(newNodes) handleSyncWorkflowDraft() - }, [store, getNodesReadOnly, handleSyncWorkflowDraft]) + saveStateToHistory(WorkflowHistoryEvent.NodeResize) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) + + const handleHistoryBack = useCallback(() => { + if (getNodesReadOnly()) + return + + const { + shortcutsDisabled, + } = workflowStore.getState() + + if (shortcutsDisabled) + return + + const { setEdges, setNodes } = store.getState() + undo() + + const { edges, nodes } = workflowHistoryStore.getState() + if (edges.length === 0 && nodes.length === 0) + return + + setEdges(edges) + setNodes(nodes) + }, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly]) + + const handleHistoryForward = useCallback(() => { + if (getNodesReadOnly()) + return + + const { + shortcutsDisabled, + } = workflowStore.getState() + + if (shortcutsDisabled) + return + + const { setEdges, setNodes } = store.getState() + redo() + + const { edges, nodes } = workflowHistoryStore.getState() + if (edges.length === 0 && nodes.length === 0) + return + + setEdges(edges) + setNodes(nodes) + }, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly]) return { handleNodeDragStart, @@ -1232,5 +1299,7 @@ export const useNodesInteractions = () => { handleNodesDuplicate, handleNodesDelete, handleNodeResize, + handleHistoryBack, + handleHistoryForward, } } diff --git a/web/app/components/workflow/hooks/use-workflow-history.ts b/web/app/components/workflow/hooks/use-workflow-history.ts new file mode 100644 index 0000000000..592c0b01cd --- /dev/null +++ b/web/app/components/workflow/hooks/use-workflow-history.ts @@ -0,0 +1,150 @@ +import { + useCallback, + useRef, useState, +} from 'react' +import { debounce } from 'lodash-es' +import { + useStoreApi, +} from 'reactflow' +import { useTranslation } from 'react-i18next' +import { useWorkflowHistoryStore } from '../workflow-history-store' + +/** + * All supported Events that create a new history state. + * Current limitations: + * - InputChange events in Node Panels do not trigger state changes. + * - Resizing UI elements does not trigger state changes. + */ +export enum WorkflowHistoryEvent { + NodeTitleChange = 'NodeTitleChange', + NodeDescriptionChange = 'NodeDescriptionChange', + NodeDragStop = 'NodeDragStop', + NodeChange = 'NodeChange', + NodeConnect = 'NodeConnect', + NodePaste = 'NodePaste', + NodeDelete = 'NodeDelete', + EdgeDelete = 'EdgeDelete', + EdgeDeleteByDeleteBranch = 'EdgeDeleteByDeleteBranch', + NodeAdd = 'NodeAdd', + NodeResize = 'NodeResize', + NoteAdd = 'NoteAdd', + NoteChange = 'NoteChange', + NoteDelete = 'NoteDelete', + LayoutOrganize = 'LayoutOrganize', +} + +export const useWorkflowHistory = () => { + const store = useStoreApi() + const { store: workflowHistoryStore } = useWorkflowHistoryStore() + const { t } = useTranslation() + + const [undoCallbacks, setUndoCallbacks] = useState<any[]>([]) + const [redoCallbacks, setRedoCallbacks] = useState<any[]>([]) + + const onUndo = useCallback((callback: unknown) => { + setUndoCallbacks((prev: any) => [...prev, callback]) + return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback)) + }, []) + + const onRedo = useCallback((callback: unknown) => { + setRedoCallbacks((prev: any) => [...prev, callback]) + return () => setRedoCallbacks(prev => prev.filter(cb => cb !== callback)) + }, []) + + const undo = useCallback(() => { + workflowHistoryStore.temporal.getState().undo() + undoCallbacks.forEach(callback => callback()) + }, [undoCallbacks, workflowHistoryStore.temporal]) + + const redo = useCallback(() => { + workflowHistoryStore.temporal.getState().redo() + redoCallbacks.forEach(callback => callback()) + }, [redoCallbacks, workflowHistoryStore.temporal]) + + // Some events may be triggered multiple times in a short period of time. + // We debounce the history state update to avoid creating multiple history states + // with minimal changes. + const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent) => { + workflowHistoryStore.setState({ + workflowHistoryEvent: event, + nodes: store.getState().getNodes(), + edges: store.getState().edges, + }) + }, 500)) + + const saveStateToHistory = useCallback((event: WorkflowHistoryEvent) => { + switch (event) { + case WorkflowHistoryEvent.NoteChange: + // Hint: Note change does not trigger when note text changes, + // because the note editors have their own history states. + saveStateToHistoryRef.current(event) + break + case WorkflowHistoryEvent.NodeTitleChange: + case WorkflowHistoryEvent.NodeDescriptionChange: + case WorkflowHistoryEvent.NodeDragStop: + case WorkflowHistoryEvent.NodeChange: + case WorkflowHistoryEvent.NodeConnect: + case WorkflowHistoryEvent.NodePaste: + case WorkflowHistoryEvent.NodeDelete: + case WorkflowHistoryEvent.EdgeDelete: + case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch: + case WorkflowHistoryEvent.NodeAdd: + case WorkflowHistoryEvent.NodeResize: + case WorkflowHistoryEvent.NoteAdd: + case WorkflowHistoryEvent.LayoutOrganize: + case WorkflowHistoryEvent.NoteDelete: + saveStateToHistoryRef.current(event) + break + default: + // We do not create a history state for every event. + // Some events of reactflow may change things the user would not want to undo/redo. + // For example: UI state changes like selecting a node. + break + } + }, []) + + const getHistoryLabel = useCallback((event: WorkflowHistoryEvent) => { + switch (event) { + case WorkflowHistoryEvent.NodeTitleChange: + return t('workflow.changeHistory.nodeTitleChange') + case WorkflowHistoryEvent.NodeDescriptionChange: + return t('workflow.changeHistory.nodeDescriptionChange') + case WorkflowHistoryEvent.LayoutOrganize: + case WorkflowHistoryEvent.NodeDragStop: + return t('workflow.changeHistory.nodeDragStop') + case WorkflowHistoryEvent.NodeChange: + return t('workflow.changeHistory.nodeChange') + case WorkflowHistoryEvent.NodeConnect: + return t('workflow.changeHistory.nodeConnect') + case WorkflowHistoryEvent.NodePaste: + return t('workflow.changeHistory.nodePaste') + case WorkflowHistoryEvent.NodeDelete: + return t('workflow.changeHistory.nodeDelete') + case WorkflowHistoryEvent.NodeAdd: + return t('workflow.changeHistory.nodeAdd') + case WorkflowHistoryEvent.EdgeDelete: + case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch: + return t('workflow.changeHistory.edgeDelete') + case WorkflowHistoryEvent.NodeResize: + return t('workflow.changeHistory.nodeResize') + case WorkflowHistoryEvent.NoteAdd: + return t('workflow.changeHistory.noteAdd') + case WorkflowHistoryEvent.NoteChange: + return t('workflow.changeHistory.noteChange') + case WorkflowHistoryEvent.NoteDelete: + return t('workflow.changeHistory.noteDelete') + default: + return 'Unknown Event' + } + }, [t]) + + return { + store: workflowHistoryStore, + saveStateToHistory, + getHistoryLabel, + undo, + redo, + onUndo, + onRedo, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index e110d7c44a..109771dc87 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' import { useReactFlow } from 'reactflow' import { useWorkflowStore } from '../store' import { WORKFLOW_DATA_UPDATE } from '../constants' @@ -11,6 +12,9 @@ import { useEdgesInteractions } from './use-edges-interactions' import { useNodesInteractions } from './use-nodes-interactions' import { useEventEmitterContextContext } from '@/context/event-emitter' import { fetchWorkflowDraft } from '@/service/workflow' +import { exportAppConfig } from '@/service/apps' +import { useToastContext } from '@/app/components/base/toast' +import { useStore as useAppStore } from '@/app/components/app/store' export const useWorkflowInteractions = () => { const workflowStore = useWorkflowStore() @@ -71,3 +75,29 @@ export const useWorkflowUpdate = () => { handleRefreshWorkflowDraft, } } + +export const useDSL = () => { + const { t } = useTranslation() + const { notify } = useToastContext() + const appDetail = useAppStore(s => s.appDetail) + + const handleExportDSL = useCallback(async () => { + if (!appDetail) + return + try { + const { data } = await exportAppConfig(appDetail.id) + const a = document.createElement('a') + const file = new Blob([data], { type: 'application/yaml' }) + a.href = URL.createObjectURL(file) + a.download = `${appDetail.name}.yml` + a.click() + } + catch (e) { + notify({ type: 'error', message: t('app.exportFailed') }) + } + }, [appDetail, notify, t]) + + return { + handleExportDSL, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index fc995ed976..effd6d757c 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -42,6 +42,7 @@ import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_b import { useNodesExtraData } from './use-nodes-data' import { useWorkflowTemplate } from './use-workflow-template' import { useNodesSyncDraft } from './use-nodes-sync-draft' +import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import { useStore as useAppStore } from '@/app/components/app/store' import { fetchNodesDefaultConfigs, @@ -71,6 +72,7 @@ export const useWorkflow = () => { const workflowStore = useWorkflowStore() const nodesExtraData = useNodesExtraData() const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { saveStateToHistory } = useWorkflowHistory() const setPanelWidth = useCallback((width: number) => { localStorage.setItem('workflow-node-panel-width', `${width}`) @@ -122,10 +124,11 @@ export const useWorkflow = () => { y: 0, zoom, }) + saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize) setTimeout(() => { handleSyncWorkflowDraft() }) - }, [store, reactflow, handleSyncWorkflowDraft, workflowStore]) + }, [workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft]) const getTreeLeafNodes = useCallback((nodeId: string) => { const { @@ -485,7 +488,9 @@ export const useWorkflowInit = () => { nodes: nodesTemplate, edges: edgesTemplate, }, - features: {}, + features: { + retriever_resource: { enabled: true }, + }, }, }).then((res) => { workflowStore.getState().setDraftUpdatedAt(res.updated_at) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 79e681b561..a9a4b40ef3 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -20,6 +20,7 @@ import ReactFlow, { useEdgesState, useNodesState, useOnViewportChange, + useReactFlow, } from 'reactflow' import type { Viewport, @@ -32,6 +33,7 @@ import type { } from './types' import { WorkflowContextProvider } from './context' import { + useDSL, useEdgesInteractions, useNodesInteractions, useNodesReadOnly, @@ -58,6 +60,7 @@ import CandidateNode from './candidate-node' import PanelContextmenu from './panel-contextmenu' import NodeContextmenu from './node-contextmenu' import SyncingDataModal from './syncing-data-modal' +import UpdateDSLModal from './update-dsl-modal' import { useStore, useWorkflowStore, @@ -73,9 +76,11 @@ import { ITERATION_CHILDREN_Z_INDEX, WORKFLOW_DATA_UPDATE, } from './constants' +import { WorkflowHistoryProvider, useWorkflowHistoryStore } from './workflow-history-store' import Loading from '@/app/components/base/loading' import { FeaturesProvider } from '@/app/components/base/features' import type { Features as FeaturesData } from '@/app/components/base/features/types' +import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useEventEmitterContextContext } from '@/context/event-emitter' import Confirm from '@/app/components/base/confirm/common' @@ -99,15 +104,20 @@ const Workflow: FC<WorkflowProps> = memo(({ }) => { const workflowContainerRef = useRef<HTMLDivElement>(null) const workflowStore = useWorkflowStore() + const reactflow = useReactFlow() + const featuresStore = useFeaturesStore() const [nodes, setNodes] = useNodesState(originalNodes) const [edges, setEdges] = useEdgesState(originalEdges) const showFeaturesPanel = useStore(state => state.showFeaturesPanel) const controlMode = useStore(s => s.controlMode) const nodeAnimation = useStore(s => s.nodeAnimation) const showConfirm = useStore(s => s.showConfirm) + const showImportDSLModal = useStore(s => s.showImportDSLModal) const { setShowConfirm, setControlPromptEditorRerenderKey, + setShowImportDSLModal, + setSyncWorkflowDraftHash, } = workflowStore.getState() const { handleSyncWorkflowDraft, @@ -122,6 +132,19 @@ const Workflow: FC<WorkflowProps> = memo(({ if (v.type === WORKFLOW_DATA_UPDATE) { setNodes(v.payload.nodes) setEdges(v.payload.edges) + + if (v.payload.viewport) + reactflow.setViewport(v.payload.viewport) + + if (v.payload.features && featuresStore) { + const { setFeatures } = featuresStore.getState() + + setFeatures(v.payload.features) + } + + if (v.payload.hash) + setSyncWorkflowDraftHash(v.payload.hash) + setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) } }) @@ -159,6 +182,8 @@ const Workflow: FC<WorkflowProps> = memo(({ useEventListener('keydown', (e) => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) e.preventDefault() + if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) + e.preventDefault() }) useEventListener('mousemove', (e) => { const containerClientRect = workflowContainerRef.current?.getBoundingClientRect() @@ -190,6 +215,8 @@ const Workflow: FC<WorkflowProps> = memo(({ handleNodesPaste, handleNodesDuplicate, handleNodesDelete, + handleHistoryBack, + handleHistoryForward, } = useNodesInteractions() const { handleEdgeEnter, @@ -204,11 +231,15 @@ const Workflow: FC<WorkflowProps> = memo(({ } = useSelectionInteractions() const { handlePaneContextMenu, + handlePaneContextmenuCancel, } = usePanelInteractions() const { isValidConnection, } = useWorkflow() const { handleStartWorkflowRun } = useWorkflowStartRun() + const { + handleExportDSL, + } = useDSL() useOnViewportChange({ onEnd: () => { @@ -216,6 +247,8 @@ const Workflow: FC<WorkflowProps> = memo(({ }, }) + const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() + useKeyPress('delete', handleNodesDelete) useKeyPress(['delete', 'backspace'], handleEdgeDelete) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => { @@ -232,6 +265,18 @@ const Workflow: FC<WorkflowProps> = memo(({ }, { exactMatch: true, useCapture: true }) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true }) useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) + useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) + useKeyPress( + `${getKeyboardKeyCodeBySystem('ctrl')}.z`, + () => workflowHistoryShortcutsEnabled && handleHistoryBack(), + { exactMatch: true, useCapture: true }, + ) + + useKeyPress( + [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`], + () => workflowHistoryShortcutsEnabled && handleHistoryForward(), + { exactMatch: true, useCapture: true }, + ) return ( <div @@ -245,9 +290,9 @@ const Workflow: FC<WorkflowProps> = memo(({ > <SyncingDataModal /> <CandidateNode /> - <Header /> + <Header/> <Panel /> - <Operator /> + <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> { showFeaturesPanel && <Features /> } @@ -266,6 +311,15 @@ const Workflow: FC<WorkflowProps> = memo(({ /> ) } + { + showImportDSLModal && ( + <UpdateDSLModal + onCancel={() => setShowImportDSLModal(false)} + onBackup={handleExportDSL} + onImport={handlePaneContextmenuCancel} + /> + ) + } <ReactFlow nodeTypes={nodeTypes} edgeTypes={edgeTypes} @@ -368,13 +422,17 @@ const WorkflowWrap = memo(() => { return ( <ReactFlowProvider> - <FeaturesProvider features={initialFeatures}> - <Workflow - nodes={nodesData} - edges={edgesData} - viewport={data?.graph.viewport} - /> - </FeaturesProvider> + <WorkflowHistoryProvider + nodes={nodesData} + edges={edgesData} > + <FeaturesProvider features={initialFeatures}> + <Workflow + nodes={nodesData} + edges={edgesData} + viewport={data?.graph.viewport} + /> + </FeaturesProvider> + </WorkflowHistoryProvider> </ReactFlowProvider> ) }) 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 dddc0c7423..a13395dce7 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 @@ -160,6 +160,10 @@ const CodeEditor: FC<Props> = ({ // lineNumbers: (num) => { // return <div>{num}</div> // } + // hide ambiguousCharacters warning + unicodeHighlight: { + ambiguousCharacters: false, + }, }} onMount={handleEditorDidMount} /> 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 d944aade8e..09891ff05e 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 @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import cn from 'classnames' import { @@ -71,7 +71,9 @@ const VarReferencePicker: FC<Props> = ({ const isChatMode = useIsChatMode() const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow() - const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)) + const availableNodes = useMemo(() => { + return passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)) + }, [getBeforeNodesInSameBranch, getTreeLeafNodes, nodeId, onlyLeafNodeVar, passedInAvailableNodes]) const startNode = availableNodes.find((node: any) => { return node.data.type === BlockEnum.Start }) @@ -91,7 +93,7 @@ const VarReferencePicker: FC<Props> = ({ const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType) const isConstant = isSupportConstantValue && varKindType === VarKindType.constant - const outputVars = (() => { + const outputVars = useMemo(() => { if (availableVars) return availableVars @@ -104,7 +106,8 @@ const VarReferencePicker: FC<Props> = ({ }) return vars - })() + }, [iterationNode, availableNodes, isChatMode, filterVar, availableVars, t]) + const [open, setOpen] = useState(false) useEffect(() => { onOpen() @@ -112,16 +115,16 @@ const VarReferencePicker: FC<Props> = ({ }, [open]) const hasValue = !isConstant && value.length > 0 - const isIterationVar = (() => { + const isIterationVar = useMemo(() => { if (!isInIteration) return false if (value[0] === node?.parentId && ['item', 'index'].includes(value[1])) return true return false - })() + }, [isInIteration, value, node]) const outputVarNodeId = hasValue ? value[0] : '' - const outputVarNode = (() => { + const outputVarNode = useMemo(() => { if (!hasValue || isConstant) return null @@ -132,16 +135,16 @@ const VarReferencePicker: FC<Props> = ({ return startNode?.data return getNodeInfoById(availableNodes, outputVarNodeId)?.data - })() + }, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode]) - const varName = (() => { + const varName = useMemo(() => { if (hasValue) { const isSystem = isSystemVar(value as ValueSelector) const varName = value.length >= 3 ? (value as ValueSelector).slice(-2).join('.') : value[value.length - 1] return `${isSystem ? 'sys.' : ''}${varName}` } return '' - })() + }, [hasValue, value]) const varKindTypes = [ { 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 68bf6b07b0..2bfcb44006 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 @@ -8,7 +8,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => { if (language === 'zh_Hans') return 'https://docs.dify.ai/v/zh-hans/guides/workflow/node/' - return 'https://docs.dify.ai/features/workflow/node/' + return 'https://docs.dify.ai/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 ec2ce217b1..a3ffcbcc1f 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 @@ -12,6 +12,7 @@ import { getNodeInfoById, isSystemVar, toNodeOutputVars } from '@/app/components import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types' import { useStore as useAppStore } from '@/app/components/app/store' +import { useWorkflowStore } from '@/app/components/workflow/store' import { getIterationSingleNodeRunUrl, singleNodeRun } from '@/service/workflow' import Toast from '@/app/components/base/toast' import LLMDefault from '@/app/components/workflow/nodes/llm/default' @@ -164,6 +165,12 @@ const useOneStepRun = <T>({ } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data._isSingleRun]) + + const workflowStore = useWorkflowStore() + useEffect(() => { + workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun) + }, [isShowSingleRun]) + const hideSingleRun = () => { handleNodeDataUpdate({ id, 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 3f238b44e8..c7bce2ef07 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 @@ -43,8 +43,14 @@ function useOutputVarList<T>({ handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey]) }, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange]) + const generateNewKey = useCallback(() => { + let keyIndex = Object.keys((inputs as any)[varKey]).length + 1 + while (((inputs as any)[varKey])[`var_${keyIndex}`]) + keyIndex++ + return `var_${keyIndex}` + }, [inputs, varKey]) const handleAddVariable = useCallback(() => { - const newKey = `var_${Object.keys((inputs as any)[varKey]).length + 1}` + const newKey = generateNewKey() const newInputs = produce(inputs, (draft: any) => { draft[varKey] = { ...draft[varKey], @@ -56,7 +62,7 @@ function useOutputVarList<T>({ }) setInputs(newInputs) onOutputKeyOrdersChange([...outputKeyOrders, newKey]) - }, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange]) + }, [generateNewKey, inputs, setInputs, onOutputKeyOrdersChange, outputKeyOrders, varKey]) const [isShowRemoveVarConfirm, { setTrue: showRemoveVarConfirm, diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx index 05b65ee518..54eb4413f6 100644 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ b/web/app/components/workflow/nodes/_base/panel.tsx @@ -24,6 +24,7 @@ import { import { useResizePanel } from './hooks/use-resize-panel' import BlockIcon from '@/app/components/workflow/block-icon' import { + WorkflowHistoryEvent, useAvailableBlocks, useNodeDataUpdate, useNodesInteractions, @@ -31,11 +32,13 @@ import { useNodesSyncDraft, useToolIcon, useWorkflow, + useWorkflowHistory, } from '@/app/components/workflow/hooks' import { canRunBySingle } from '@/app/components/workflow/utils' import TooltipPlus from '@/app/components/base/tooltip-plus' 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: ReactElement @@ -50,6 +53,7 @@ const BasePanel: FC<BasePanelProps> = ({ const { showMessageLogModal } = useAppStore(useShallow(state => ({ showMessageLogModal: state.showMessageLogModal, }))) + const showSingleRunPanel = useStore(s => s.showSingleRunPanel) const panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420 const { setPanelWidth, @@ -75,6 +79,8 @@ const BasePanel: FC<BasePanelProps> = ({ onResize: handleResize, }) + const { saveStateToHistory } = useWorkflowHistory() + const { handleNodeDataUpdate, handleNodeDataUpdateWithSyncDraft, @@ -82,10 +88,12 @@ const BasePanel: FC<BasePanelProps> = ({ const handleTitleBlur = useCallback((title: string) => { handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) - }, [handleNodeDataUpdateWithSyncDraft, id]) + saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) const handleDescriptionChange = useCallback((desc: string) => { handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) - }, [handleNodeDataUpdateWithSyncDraft, id]) + saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) return ( <div className={cn( @@ -99,7 +107,7 @@ const BasePanel: FC<BasePanelProps> = ({ </div> <div ref={containerRef} - className='relative h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl overflow-y-auto' + className={cn('relative h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} style={{ width: `${panelWidth}px`, }} diff --git a/web/app/components/workflow/nodes/iteration/add-block.tsx b/web/app/components/workflow/nodes/iteration/add-block.tsx index c343de1c72..3c77e7d115 100644 --- a/web/app/components/workflow/nodes/iteration/add-block.tsx +++ b/web/app/components/workflow/nodes/iteration/add-block.tsx @@ -13,8 +13,10 @@ import { generateNewNode, } from '../../utils' import { + WorkflowHistoryEvent, useAvailableBlocks, useNodesReadOnly, + useWorkflowHistory, } from '../../hooks' import { NODES_INITIAL_DATA } from '../../constants' import InsertBlock from './insert-block' @@ -42,6 +44,7 @@ const AddBlock = ({ const { nodesReadOnly } = useNodesReadOnly() const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true) const { availablePrevBlocks } = useAvailableBlocks(iterationNodeData.startNodeType, true) + const { saveStateToHistory } = useWorkflowHistory() const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => { const { @@ -78,7 +81,8 @@ const AddBlock = ({ draft.push(newNode) }) setNodes(newNodes) - }, [store, t, iterationNodeId]) + saveStateToHistory(WorkflowHistoryEvent.NodeAdd) + }, [store, t, iterationNodeId, saveStateToHistory]) const renderTriggerElement = useCallback((open: boolean) => { return ( diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index e783e56ca7..1b00045c0f 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -131,6 +131,11 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ type='Array[File]' description={t(`${i18nPrefix}.outputVars.files.title`)} /> + <VarItem + name='json' + type='Array[Object]' + description={t(`${i18nPrefix}.outputVars.json`)} + /> </> </OutputVars> </div> diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts index 7606951726..04e8081692 100644 --- a/web/app/components/workflow/note-node/hooks.ts +++ b/web/app/components/workflow/note-node/hooks.ts @@ -1,14 +1,16 @@ import { useCallback } from 'react' import type { EditorState } from 'lexical' -import { useNodeDataUpdate } from '../hooks' +import { WorkflowHistoryEvent, useNodeDataUpdate, useWorkflowHistory } from '../hooks' import type { NoteTheme } from './types' export const useNote = (id: string) => { const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + const { saveStateToHistory } = useWorkflowHistory() const handleThemeChange = useCallback((theme: NoteTheme) => { handleNodeDataUpdateWithSyncDraft({ id, data: { theme } }) - }, [handleNodeDataUpdateWithSyncDraft, id]) + saveStateToHistory(WorkflowHistoryEvent.NoteChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) const handleEditorChange = useCallback((editorState: EditorState) => { if (!editorState?.isEmpty()) @@ -19,7 +21,8 @@ export const useNote = (id: string) => { const handleShowAuthorChange = useCallback((showAuthor: boolean) => { handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } }) - }, [handleNodeDataUpdateWithSyncDraft, id]) + saveStateToHistory(WorkflowHistoryEvent.NoteChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) return { handleThemeChange, diff --git a/web/app/components/workflow/note-node/note-editor/editor.tsx b/web/app/components/workflow/note-node/note-editor/editor.tsx index 189cc78c42..a065278722 100644 --- a/web/app/components/workflow/note-node/note-editor/editor.tsx +++ b/web/app/components/workflow/note-node/note-editor/editor.tsx @@ -13,6 +13,7 @@ import { ListPlugin } from '@lexical/react/LexicalListPlugin' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { useWorkflowHistoryStore } from '../../workflow-history-store' import LinkEditorPlugin from './plugins/link-editor-plugin' import FormatDetectorPlugin from './plugins/format-detector-plugin' // import TreeView from '@/app/components/base/prompt-editor/plugins/tree-view' @@ -32,12 +33,16 @@ const Editor = ({ onChange?.(editorState) }, [onChange]) + const { setShortcutsEnabled } = useWorkflowHistoryStore() + return ( <div className='relative'> <RichTextPlugin contentEditable={ <div> <ContentEditable + onFocus={() => setShortcutsEnabled(false)} + onBlur={() => setShortcutsEnabled(true)} spellCheck={false} className='w-full h-full outline-none caret-primary-600' placeholder={placeholder} diff --git a/web/app/components/workflow/operator/index.tsx b/web/app/components/workflow/operator/index.tsx index 14407f36e6..043bd60aae 100644 --- a/web/app/components/workflow/operator/index.tsx +++ b/web/app/components/workflow/operator/index.tsx @@ -1,9 +1,15 @@ import { memo } from 'react' import { MiniMap } from 'reactflow' +import UndoRedo from '../header/undo-redo' import ZoomInOut from './zoom-in-out' import Control from './control' -const Operator = () => { +export type OperatorProps = { + handleUndo: () => void + handleRedo: () => void +} + +const Operator = ({ handleUndo, handleRedo }: OperatorProps) => { return ( <> <MiniMap @@ -15,6 +21,7 @@ const Operator = () => { /> <div className='flex items-center mt-1 gap-2 absolute left-4 bottom-4 z-[9]'> <ZoomInOut /> + <UndoRedo handleUndo={handleUndo} handleRedo={handleRedo} /> <Control /> </div> </> diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx index a5e63fda4e..823a9ea6b9 100644 --- a/web/app/components/workflow/panel-contextmenu.tsx +++ b/web/app/components/workflow/panel-contextmenu.tsx @@ -8,48 +8,30 @@ import { useClickAway } from 'ahooks' import ShortcutsName from './shortcuts-name' import { useStore } from './store' import { + useDSL, useNodesInteractions, usePanelInteractions, useWorkflowStartRun, } from './hooks' import AddBlock from './operator/add-block' import { useOperator } from './operator/hooks' -import { exportAppConfig } from '@/service/apps' -import { useToastContext } from '@/app/components/base/toast' -import { useStore as useAppStore } from '@/app/components/app/store' const PanelContextmenu = () => { const { t } = useTranslation() - const { notify } = useToastContext() const ref = useRef(null) const panelMenu = useStore(s => s.panelMenu) const clipboardElements = useStore(s => s.clipboardElements) - const appDetail = useAppStore(s => s.appDetail) + const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal) const { handleNodesPaste } = useNodesInteractions() const { handlePaneContextmenuCancel } = usePanelInteractions() const { handleStartWorkflowRun } = useWorkflowStartRun() const { handleAddNote } = useOperator() + const { handleExportDSL } = useDSL() useClickAway(() => { handlePaneContextmenuCancel() }, ref) - const onExport = async () => { - if (!appDetail) - return - try { - const { data } = await exportAppConfig(appDetail.id) - const a = document.createElement('a') - const file = new Blob([data], { type: 'application/yaml' }) - a.href = URL.createObjectURL(file) - a.download = `${appDetail.name}.yml` - a.click() - } - catch (e) { - notify({ type: 'error', message: t('app.exportFailed') }) - } - } - const renderTrigger = () => { return ( <div @@ -123,10 +105,16 @@ const PanelContextmenu = () => { <div className='p-1'> <div className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50' - onClick={() => onExport()} + onClick={() => handleExportDSL()} > {t('app.export')} </div> + <div + className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50' + onClick={() => setShowImportDSLModal(true)} + > + {t('workflow.common.importDSL')} + </div> </div> </div> ) diff --git a/web/app/components/workflow/panel/chat-record/index.tsx b/web/app/components/workflow/panel/chat-record/index.tsx index a0e74441b7..2ab3165c14 100644 --- a/web/app/components/workflow/panel/chat-record/index.tsx +++ b/web/app/components/workflow/panel/chat-record/index.tsx @@ -14,7 +14,7 @@ import { useWorkflowRun } from '../../hooks' import UserInput from './user-input' import Chat from '@/app/components/base/chat/chat' import type { ChatItem } from '@/app/components/base/chat/types' -import { fetchConvesationMessages } from '@/service/debug' +import { fetchConversationMessages } from '@/service/debug' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' @@ -51,11 +51,11 @@ const ChatRecord = () => { return res }, [chatList]) - const handleFetchConvesationMessages = useCallback(async () => { + const handleFetchConversationMessages = useCallback(async () => { if (appDetail && currentConversationID) { try { setFetched(false) - const res = await fetchConvesationMessages(appDetail.id, currentConversationID) + const res = await fetchConversationMessages(appDetail.id, currentConversationID) setFetched(true) setChatList((res as any).data) } @@ -65,8 +65,8 @@ const ChatRecord = () => { } }, [appDetail, currentConversationID]) useEffect(() => { - handleFetchConvesationMessages() - }, [currentConversationID, appDetail, handleFetchConvesationMessages]) + handleFetchConversationMessages() + }, [currentConversationID, appDetail, handleFetchConversationMessages]) return ( <div diff --git a/web/app/components/workflow/run/output-panel.tsx b/web/app/components/workflow/run/output-panel.tsx index 1ebe940c94..67030a05fe 100644 --- a/web/app/components/workflow/run/output-panel.tsx +++ b/web/app/components/workflow/run/output-panel.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { Markdown } from '@/app/components/base/markdown' -import LoadingAnim from '@/app/components/app/chat/loading-anim' +import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' type OutputPanelProps = { isRunning?: boolean diff --git a/web/app/components/workflow/run/result-text.tsx b/web/app/components/workflow/run/result-text.tsx index b0c8d234c9..55977cdd2b 100644 --- a/web/app/components/workflow/run/result-text.tsx +++ b/web/app/components/workflow/run/result-text.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { ImageIndentLeft } from '@/app/components/base/icons/src/vender/line/editor' import { Markdown } from '@/app/components/base/markdown' -import LoadingAnim from '@/app/components/app/chat/loading-anim' +import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' type ResultTextProps = { isRunning?: boolean diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index 6bf31c5c85..203aa82838 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -28,6 +28,8 @@ type PreviewRunningData = WorkflowRunningData & { type Shape = { appId: string panelWidth: number + showSingleRunPanel: boolean + setShowSingleRunPanel: (showSingleRunPanel: boolean) => void workflowRunningData?: PreviewRunningData setWorkflowRunningData: (workflowData: PreviewRunningData) => void historyWorkflowData?: HistoryWorkflowData @@ -129,12 +131,16 @@ type Shape = { setIsSyncingWorkflowDraft: (isSyncingWorkflowDraft: boolean) => void controlPromptEditorRerenderKey: number setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void + showImportDSLModal: boolean + setShowImportDSLModal: (showImportDSLModal: boolean) => void } export const createWorkflowStore = () => { return createStore<Shape>(set => ({ appId: '', panelWidth: localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420, + showSingleRunPanel: false, + setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), workflowRunningData: undefined, setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), historyWorkflowData: undefined, @@ -217,6 +223,8 @@ export const createWorkflowStore = () => { setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), controlPromptEditorRerenderKey: 0, setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })), + showImportDSLModal: false, + setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), })) } diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx new file mode 100644 index 0000000000..184b47c476 --- /dev/null +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -0,0 +1,154 @@ +'use client' + +import type { MouseEventHandler } from 'react' +import { + memo, + useCallback, + useRef, + useState, +} from 'react' +import { useContext } from 'use-context-selector' +import { useTranslation } from 'react-i18next' +import { + RiAlertLine, + RiCloseLine, +} from '@remixicon/react' +import { WORKFLOW_DATA_UPDATE } from './constants' +import { + initialEdges, + initialNodes, +} from './utils' +import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' +import Button from '@/app/components/base/button' +import Modal from '@/app/components/base/modal' +import { ToastContext } from '@/app/components/base/toast' +import { updateWorkflowDraftFromDSL } from '@/service/workflow' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { useStore as useAppStore } from '@/app/components/app/store' + +type UpdateDSLModalProps = { + onCancel: () => void + onBackup: () => void + onImport?: () => void +} + +const UpdateDSLModal = ({ + onCancel, + onBackup, + onImport, +}: UpdateDSLModalProps) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const appDetail = useAppStore(s => s.appDetail) + const [currentFile, setDSLFile] = useState<File>() + const [fileContent, setFileContent] = useState<string>() + const [loading, setLoading] = useState(false) + const { eventEmitter } = useEventEmitterContextContext() + + const readFile = (file: File) => { + const reader = new FileReader() + reader.onload = function (event) { + const content = event.target?.result + setFileContent(content as string) + } + reader.readAsText(file) + } + + const handleFile = (file?: File) => { + setDSLFile(file) + if (file) + readFile(file) + if (!file) + setFileContent('') + } + + const isCreatingRef = useRef(false) + const handleImport: MouseEventHandler = useCallback(async () => { + if (isCreatingRef.current) + return + isCreatingRef.current = true + if (!currentFile) + return + try { + if (appDetail && fileContent) { + setLoading(true) + const { + graph, + features, + hash, + } = await updateWorkflowDraftFromDSL(appDetail.id, fileContent) + const { nodes, edges, viewport } = graph + eventEmitter?.emit({ + type: WORKFLOW_DATA_UPDATE, + payload: { + nodes: initialNodes(nodes, edges), + edges: initialEdges(edges, nodes), + viewport, + features, + hash, + }, + } as any) + if (onImport) + onImport() + notify({ type: 'success', message: t('workflow.common.importSuccess') }) + setLoading(false) + onCancel() + } + } + catch (e) { + setLoading(false) + notify({ type: 'error', message: t('workflow.common.importFailure') }) + } + isCreatingRef.current = false + }, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport]) + + return ( + <Modal + className='p-6 w-[520px] rounded-2xl' + isShow={true} + onClose={() => {}} + > + <div className='flex items-center justify-between mb-6'> + <div className='text-2xl font-semibold text-[#101828]'>{t('workflow.common.importDSL')}</div> + <div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}> + <RiCloseLine className='w-5 h-5 text-gray-500' /> + </div> + </div> + <div className='flex mb-4 px-4 py-3 bg-[#FFFAEB] rounded-xl border border-[#FEDF89]'> + <RiAlertLine className='shrink-0 mt-0.5 mr-2 w-4 h-4 text-[#F79009]' /> + <div> + <div className='mb-2 text-sm font-medium text-[#354052]'>{t('workflow.common.importDSLTip')}</div> + <Button + variant='secondary-accent' + onClick={onBackup} + > + {t('workflow.common.backupCurrentDraft')} + </Button> + </div> + </div> + <div className='mb-8'> + <div className='mb-1 text-[13px] font-semibold text-[#354052]'> + {t('workflow.common.chooseDSL')} + </div> + <Uploader + file={currentFile} + updateFile={handleFile} + className='!mt-0' + /> + </div> + <div className='flex justify-end'> + <Button className='mr-2' onClick={onCancel}>{t('app.newApp.Cancel')}</Button> + <Button + disabled={!currentFile || loading} + variant='warning' + onClick={handleImport} + loading={loading} + > + {t('workflow.common.overwriteAndImport')} + </Button> + </div> + </Modal> + ) +} + +export default memo(UpdateDSLModal) diff --git a/web/app/components/workflow/workflow-history-store.tsx b/web/app/components/workflow/workflow-history-store.tsx new file mode 100644 index 0000000000..a49e9b64e7 --- /dev/null +++ b/web/app/components/workflow/workflow-history-store.tsx @@ -0,0 +1,120 @@ +import { type ReactNode, createContext, useContext, useMemo, useState } from 'react' +import { type StoreApi, create } from 'zustand' +import { type TemporalState, temporal } from 'zundo' +import isDeepEqual from 'fast-deep-equal' +import type { Edge, Node } from './types' +import type { WorkflowHistoryEvent } from './hooks' + +export const WorkflowHistoryStoreContext = createContext<WorkflowHistoryStoreContextType>({ store: null, shortcutsEnabled: true, setShortcutsEnabled: () => {} }) +export const Provider = WorkflowHistoryStoreContext.Provider + +export function WorkflowHistoryProvider({ + nodes, + edges, + children, +}: WorkflowWithHistoryProviderProps) { + const [shortcutsEnabled, setShortcutsEnabled] = useState(true) + const [store] = useState(() => + createStore({ + nodes, + edges, + }), + ) + + const contextValue = { + store, + shortcutsEnabled, + setShortcutsEnabled, + } + + return ( + <Provider value={contextValue}> + {children} + </Provider> + ) +} + +export function useWorkflowHistoryStore() { + const { + store, + shortcutsEnabled, + setShortcutsEnabled, + } = useContext(WorkflowHistoryStoreContext) + if (store === null) + throw new Error('useWorkflowHistoryStoreApi must be used within a WorkflowHistoryProvider') + + return { + store: useMemo( + () => ({ + getState: store.getState, + setState: (state: WorkflowHistoryState) => { + store.setState({ + workflowHistoryEvent: state.workflowHistoryEvent, + nodes: state.nodes.map((node: Node) => ({ ...node, data: { ...node.data, selected: false } })), + edges: state.edges.map((edge: Edge) => ({ ...edge, selected: false }) as Edge), + }) + }, + subscribe: store.subscribe, + temporal: store.temporal, + }), + [store], + ), + shortcutsEnabled, + setShortcutsEnabled, + } +} + +function createStore({ + nodes: storeNodes, + edges: storeEdges, +}: { + nodes: Node[] + edges: Edge[] +}): WorkflowHistoryStoreApi { + const store = create(temporal<WorkflowHistoryState>( + (set, get) => { + return { + workflowHistoryEvent: undefined, + nodes: storeNodes, + edges: storeEdges, + getNodes: () => get().nodes, + setNodes: (nodes: Node[]) => set({ nodes }), + setEdges: (edges: Edge[]) => set({ edges }), + } + }, + { + equality: (pastState, currentState) => + isDeepEqual(pastState, currentState), + }, + ), + ) + + return store +} + +export type WorkflowHistoryStore = { + nodes: Node[] + edges: Edge[] + workflowHistoryEvent: WorkflowHistoryEvent | undefined +} + +export type WorkflowHistoryActions = { + setNodes?: (nodes: Node[]) => void + setEdges?: (edges: Edge[]) => void +} + +export type WorkflowHistoryState = WorkflowHistoryStore & WorkflowHistoryActions + +type WorkflowHistoryStoreContextType = { + store: ReturnType<typeof createStore> | null + shortcutsEnabled: boolean + setShortcutsEnabled: (enabled: boolean) => void +} + +export type WorkflowHistoryStoreApi = StoreApi<WorkflowHistoryState> & { temporal: StoreApi<TemporalState<WorkflowHistoryState>> } + +export type WorkflowWithHistoryProviderProps = { + nodes: Node[] + edges: Edge[] + children: ReactNode +} diff --git a/web/app/forgot-password/ChangePasswordForm.tsx b/web/app/forgot-password/ChangePasswordForm.tsx new file mode 100644 index 0000000000..d878660416 --- /dev/null +++ b/web/app/forgot-password/ChangePasswordForm.tsx @@ -0,0 +1,178 @@ +'use client' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import useSWR from 'swr' +import { useSearchParams } from 'next/navigation' +import cn from 'classnames' +import { CheckCircleIcon } from '@heroicons/react/24/solid' +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,}$/ + +const ChangePasswordForm = () => { + const { t } = useTranslation() + const searchParams = useSearchParams() + const token = searchParams.get('token') + + const verifyTokenParams = { + url: '/forgot-password/validity', + body: { token }, + } + const { data: verifyTokenRes, mutate: revalidateToken } = useSWR(verifyTokenParams, verifyForgotPasswordToken, { + revalidateOnFocus: false, + }) + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showSuccess, setShowSuccess] = useState(false) + + const showErrorMessage = useCallback((message: string) => { + Toast.notify({ + type: 'error', + message, + }) + }, []) + + const valid = useCallback(() => { + if (!password.trim()) { + showErrorMessage(t('login.error.passwordEmpty')) + return false + } + if (!validPassword.test(password)) { + showErrorMessage(t('login.error.passwordInvalid')) + return false + } + if (password !== confirmPassword) { + showErrorMessage(t('common.account.notEqual')) + return false + } + return true + }, [password, confirmPassword, showErrorMessage, t]) + + const handleChangePassword = useCallback(async () => { + const token = searchParams.get('token') || '' + + if (!valid()) + return + try { + await changePasswordWithToken({ + url: '/forgot-password/resets', + body: { + token, + new_password: password, + password_confirm: confirmPassword, + }, + }) + setShowSuccess(true) + } + catch { + await revalidateToken() + } + }, [password, revalidateToken, token, valid]) + + return ( + <div className={ + cn( + 'flex flex-col items-center w-full grow justify-center', + 'px-6', + 'md:px-[108px]', + ) + }> + {!verifyTokenRes && <Loading />} + {verifyTokenRes && !verifyTokenRes.is_valid && ( + <div className="flex flex-col md:w-[400px]"> + <div className="w-full mx-auto"> + <div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">🤷‍♂️</div> + <h2 className="text-[32px] font-bold text-gray-900">{t('login.invalid')}</h2> + </div> + <div className="w-full mx-auto mt-6"> + <Button variant='primary' className='w-full !text-sm'> + <a href="https://dify.ai">{t('login.explore')}</a> + </Button> + </div> + </div> + )} + {verifyTokenRes && verifyTokenRes.is_valid && !showSuccess && ( + <div className='flex flex-col md:w-[400px]'> + <div className="w-full mx-auto"> + <h2 className="text-[32px] font-bold text-gray-900"> + {t('login.changePassword')} + </h2> + <p className='mt-1 text-sm text-gray-600'> + {t('login.changePasswordTip')} + </p> + </div> + + <div className="w-full mx-auto mt-6"> + <div className="bg-white"> + {/* Password */} + <div className='mb-5'> + <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> + {t('common.account.newPassword')} + </label> + <div className="mt-1 relative rounded-md shadow-sm"> + <input + id="password" + type='password' + value={password} + onChange={e => setPassword(e.target.value)} + placeholder={t('login.passwordPlaceholder') || ''} + className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} + /> + </div> + <div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div> + </div> + {/* Confirm Password */} + <div className='mb-5'> + <label htmlFor="confirmPassword" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> + {t('common.account.confirmPassword')} + </label> + <div className="mt-1 relative rounded-md shadow-sm"> + <input + id="confirmPassword" + type='password' + value={confirmPassword} + onChange={e => setConfirmPassword(e.target.value)} + placeholder={t('login.confirmPasswordPlaceholder') || ''} + className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} + /> + </div> + </div> + <div> + <Button + variant='primary' + className='w-full !text-sm' + onClick={handleChangePassword} + > + {t('common.operation.reset')} + </Button> + </div> + </div> + </div> + </div> + )} + {verifyTokenRes && verifyTokenRes.is_valid && showSuccess && ( + <div className="flex flex-col md:w-[400px]"> + <div className="w-full mx-auto"> + <div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold"> + <CheckCircleIcon className='w-10 h-10 text-[#039855]' /> + </div> + <h2 className="text-[32px] font-bold text-gray-900"> + {t('login.passwordChangedTip')} + </h2> + </div> + <div className="w-full mx-auto mt-6"> + <Button variant='primary' className='w-full !text-sm'> + <a href="/signin">{t('login.passwordChanged')}</a> + </Button> + </div> + </div> + )} + </div> + ) +} + +export default ChangePasswordForm diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx new file mode 100644 index 0000000000..6fd69a3638 --- /dev/null +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -0,0 +1,122 @@ +'use client' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useRouter } from 'next/navigation' + +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import Loading from '../components/base/loading' +import Button from '@/app/components/base/button' + +import { + fetchInitValidateStatus, + fetchSetupStatus, + sendForgotPasswordEmail, +} from '@/service/common' +import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' + +const accountFormSchema = z.object({ + email: z + .string() + .min(1, { message: 'login.error.emailInValid' }) + .email('login.error.emailInValid'), +}) + +type AccountFormValues = z.infer<typeof accountFormSchema> + +const ForgotPasswordForm = () => { + const { t } = useTranslation() + const router = useRouter() + const [loading, setLoading] = useState(true) + const [isEmailSent, setIsEmailSent] = useState(false) + const { register, trigger, getValues, formState: { errors } } = useForm<AccountFormValues>({ + resolver: zodResolver(accountFormSchema), + defaultValues: { email: '' }, + }) + + const handleSendResetPasswordEmail = async (email: string) => { + try { + const res = await sendForgotPasswordEmail({ + url: '/forgot-password', + body: { email }, + }) + if (res.result === 'success') + setIsEmailSent(true) + + else console.error('Email verification failed') + } + catch (error) { + console.error('Request failed:', error) + } + } + + const handleSendResetPasswordClick = async () => { + if (isEmailSent) { + router.push('/signin') + } + else { + const isValid = await trigger('email') + if (isValid) { + const email = getValues('email') + await handleSendResetPasswordEmail(email) + } + } + } + + useEffect(() => { + fetchSetupStatus().then((res: SetupStatusResponse) => { + fetchInitValidateStatus().then((res: InitValidateStatusResponse) => { + if (res.status === 'not_started') + window.location.href = '/init' + }) + + setLoading(false) + }) + }, []) + + return ( + loading + ? <Loading/> + : <> + <div className="sm:mx-auto sm:w-full sm:max-w-md"> + <h2 className="text-[32px] font-bold text-gray-900"> + {isEmailSent ? t('login.resetLinkSent') : t('login.forgotPassword')} + </h2> + <p className='mt-1 text-sm text-gray-600'> + {isEmailSent ? t('login.checkEmailForResetLink') : t('login.forgotPasswordDesc')} + </p> + </div> + <div className="grow mt-8 sm:mx-auto sm:w-full sm:max-w-md"> + <div className="bg-white "> + <form> + {!isEmailSent && ( + <div className='mb-5'> + <label htmlFor="email" + className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> + {t('login.email')} + </label> + <div className="mt-1"> + <input + {...register('email')} + placeholder={t('login.emailPlaceholder') || ''} + className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'} + /> + {errors.email && <span className='text-red-400 text-sm'>{t(`${errors.email?.message}`)}</span>} + </div> + </div> + )} + <div> + <Button variant='primary' className='w-full' onClick={handleSendResetPasswordClick}> + {isEmailSent ? t('login.backToSignIn') : t('login.sendResetLink')} + </Button> + </div> + </form> + </div> + </div> + </> + ) +} + +export default ForgotPasswordForm diff --git a/web/app/forgot-password/page.tsx b/web/app/forgot-password/page.tsx new file mode 100644 index 0000000000..fa44d1a20c --- /dev/null +++ b/web/app/forgot-password/page.tsx @@ -0,0 +1,38 @@ +'use client' +import React from 'react' +import classNames from 'classnames' +import { useSearchParams } from 'next/navigation' +import Header from '../signin/_header' +import style from '../signin/page.module.css' +import ForgotPasswordForm from './ForgotPasswordForm' +import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm' + +const ForgotPassword = () => { + const searchParams = useSearchParams() + const token = searchParams.get('token') + + return ( + <div className={classNames( + style.background, + 'flex w-full min-h-screen', + 'p-4 lg:p-8', + 'gap-x-20', + 'justify-center lg:justify-start', + )}> + <div className={ + classNames( + 'flex w-full flex-col bg-white shadow rounded-2xl shrink-0', + 'md:w-[608px] space-between', + ) + }> + <Header /> + {token ? <ChangePasswordForm /> : <ForgotPasswordForm />} + <div className='px-8 py-6 text-sm font-normal text-gray-500'> + © {new Date().getFullYear()} Dify, Inc. All rights reserved. + </div> + </div> + </div> + ) +} + +export default ForgotPassword diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index f23a04e4e4..40912c6e1f 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -224,21 +224,9 @@ const NormalForm = () => { <div className='mb-4'> <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> <span>{t('login.password')}</span> - {/* <Tooltip - selector='forget-password' - htmlContent={ - <div> - <div className='font-medium'>{t('login.forget')}</div> - <div className='font-medium text-gray-500'> - <code> - sudo rm -rf / - </code> - </div> - </div> - } - > - <span className='cursor-pointer text-primary-600'>{t('login.forget')}</span> - </Tooltip> */} + <Link href='/forgot-password' className='text-primary-600'> + {t('login.forget')} + </Link> </label> <div className="relative mt-1"> <input diff --git a/web/i18n/de-DE/app-overview.ts b/web/i18n/de-DE/app-overview.ts index 6c043df21b..99100cf868 100644 --- a/web/i18n/de-DE/app-overview.ts +++ b/web/i18n/de-DE/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Anzeigen', hide: 'Verbergen', }, + chatColorTheme: 'Chat-Farbschema', + chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest', + chatColorThemeInverted: 'Invertiert', + invalidHexMessage: 'Ungültiger Hex-Wert', more: { entry: 'Mehr Einstellungen anzeigen', copyright: 'Urheberrecht', diff --git a/web/i18n/de-DE/dataset.ts b/web/i18n/de-DE/dataset.ts index e7844a5f3b..53e8cdd447 100644 --- a/web/i18n/de-DE/dataset.ts +++ b/web/i18n/de-DE/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Dieses Wissen löschen?', deleteDatasetConfirmContent: 'Das Löschen des Wissens ist unwiderruflich. Benutzer werden nicht mehr auf Ihr Wissen zugreifen können und alle Eingabeaufforderungen, Konfigurationen und Protokolle werden dauerhaft gelöscht.', + datasetUsedByApp: 'Das Wissen wird von einigen Apps verwendet. Apps werden dieses Wissen nicht mehr nutzen können, und alle Prompt-Konfigurationen und Protokolle werden dauerhaft gelöscht.', datasetDeleted: 'Wissen gelöscht', datasetDeleteFailed: 'Löschen des Wissens fehlgeschlagen', didYouKnow: 'Wusstest du schon?', diff --git a/web/i18n/de-DE/login.ts b/web/i18n/de-DE/login.ts index 06f9d6366d..f932f92976 100644 --- a/web/i18n/de-DE/login.ts +++ b/web/i18n/de-DE/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'Hast du nicht?', invalidInvitationCode: 'Ungültiger Einladungscode', accountAlreadyInited: 'Konto bereits initialisiert', + forgotPassword: 'Passwort vergessen?', + resetLinkSent: 'Link zum Zurücksetzen gesendet', + sendResetLink: 'Link zum Zurücksetzen senden', + backToSignIn: 'Zurück zur Anmeldung', + forgotPasswordDesc: 'Bitte geben Sie Ihre E-Mail-Adresse ein, um Ihr Passwort zurückzusetzen. Wir senden Ihnen eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts.', + checkEmailForResetLink: 'Bitte überprüfen Sie Ihre E-Mails auf einen Link zum Zurücksetzen Ihres Passworts. Wenn er nicht innerhalb weniger Minuten erscheint, überprüfen Sie bitte Ihren Spam-Ordner.', + passwordChanged: 'Jetzt anmelden', + changePassword: 'Passwort ändern', + changePasswordTip: 'Bitte geben Sie ein neues Passwort für Ihr Konto ein', + invalidToken: 'Ungültiges oder abgelaufenes Token', + confirmPassword: 'Passwort bestätigen', + confirmPasswordPlaceholder: 'Bestätigen Sie Ihr neues Passwort', + passwordChangedTip: 'Ihr Passwort wurde erfolgreich geändert', error: { emailEmpty: 'E-Mail-Adresse wird benötigt', emailInValid: 'Bitte gib eine gültige E-Mail-Adresse ein', diff --git a/web/i18n/de-DE/tools.ts b/web/i18n/de-DE/tools.ts index 2c15a3f9ad..a45d0da1b1 100644 --- a/web/i18n/de-DE/tools.ts +++ b/web/i18n/de-DE/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: 'Bitte Datenschutzrichtlinie eingeben', customDisclaimer: 'Benutzer Haftungsausschluss', customDisclaimerPlaceholder: 'Bitte benutzerdefinierten Haftungsausschluss eingeben', + deleteToolConfirmTitle: 'Löschen Sie dieses Werkzeug?', + deleteToolConfirmContent: 'Das Löschen des Werkzeugs ist irreversibel. Benutzer können Ihr Werkzeug nicht mehr verwenden.', }, test: { title: 'Test', diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index a156a1d11b..4d3ac07f4f 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Rückgängig', + redo: 'Wiederholen', editing: 'Bearbeitung', autoSaved: 'Automatisch gespeichert', unpublished: 'Unveröffentlicht', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'Nach dem Workflow-Update ist eine Neukonfiguration des Tools erforderlich.', viewDetailInTracingPanel: 'Details anzeigen', }, + changeHistory: { + title: 'Änderungsverlauf', + placeholder: 'Du hast noch nichts geändert', + clearHistory: 'Änderungsverlauf löschen', + hint: 'Hinweis', + hintText: 'Änderungen werden im Änderungsverlauf aufgezeichnet, der für die Dauer dieser Sitzung auf Ihrem Gerät gespeichert wird. Dieser Verlauf wird gelöscht, wenn Sie den Editor verlassen.', + stepBackward_one: '{{count}} Schritt zurück', + stepBackward_other: '{{count}} Schritte zurück', + stepForward_one: '{{count}} Schritt vorwärts', + 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', + }, errorMsg: { fieldRequired: '{{field}} ist erforderlich', authRequired: 'Autorisierung ist erforderlich', diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index 0887ef25b7..b8c7999415 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/web/i18n/en-US/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Show', hide: 'Hide', }, + chatColorTheme: 'Chat color theme', + chatColorThemeDesc: 'Set the color theme of the chatbot', + chatColorThemeInverted: 'Inverted', + invalidHexMessage: 'Invalid hex value', more: { entry: 'Show more settings', copyright: 'Copyright', diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index c180443a6d..6153c11873 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -85,6 +85,42 @@ const translation = { workflow: 'Workflow', completion: 'Completion', }, + tracing: { + title: 'Tracing app performance', + description: 'Configuring a Third-Party LLMOps provider and tracing app performance.', + config: 'Config', + collapse: 'Collapse', + expand: 'Expand', + tracing: 'Tracing', + disabled: 'Disabled', + disabledTip: 'Please config provider first', + enabled: 'In Service', + tracingDescription: 'Capture the full context of app execution, including LLM calls, context, prompts, HTTP requests, and more, to a third-party tracing platform.', + configProviderTitle: { + configured: 'Configured', + notConfigured: 'Config provider to enable tracing', + moreProvider: 'More Provider', + }, + langsmith: { + title: 'LangSmith', + description: 'An all-in-one developer platform for every step of the LLM-powered application lifecycle.', + }, + langfuse: { + title: 'Langfuse', + description: 'Traces, evals, prompt management and metrics to debug and improve your LLM application.', + }, + inUse: 'In use', + configProvider: { + title: 'Config ', + placeholder: 'Enter your {{key}}', + project: 'Project', + publicKey: 'Public Key', + secretKey: 'Secret Key', + viewDocsLink: 'View {{key}} docs', + removeConfirmTitle: 'Remove {{key}} configuration?', + removeConfirmContent: 'The current configuration is in use, removing it will turn off the Tracing feature.', + }, + }, } export default translation diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 4515ad7a21..2f583dc033 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -12,6 +12,7 @@ const translation = { cancel: 'Cancel', clear: 'Clear', save: 'Save', + saveAndEnable: 'Save & Enable', edit: 'Edit', add: 'Add', added: 'Added', @@ -439,7 +440,7 @@ const translation = { latestAvailable: 'Dify {{version}} is the latest version available.', }, appMenus: { - overview: 'Overview', + overview: 'Monitoring', promptEng: 'Orchestrate', apiAccess: 'API Access', logAndAnn: 'Logs & Ann.', diff --git a/web/i18n/en-US/dataset-creation.ts b/web/i18n/en-US/dataset-creation.ts index b2090c3339..23e1aab89f 100644 --- a/web/i18n/en-US/dataset-creation.ts +++ b/web/i18n/en-US/dataset-creation.ts @@ -42,7 +42,7 @@ const translation = { notionSyncTitle: 'Notion is not connected', notionSyncTip: 'To sync with Notion, connection to Notion must be established first.', connect: 'Go to connect', - button: 'next', + button: 'Next', emptyDatasetCreation: 'I want to create an empty Knowledge', modal: { title: 'Create an empty Knowledge', diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts index dee495fbd4..8251693154 100644 --- a/web/i18n/en-US/dataset.ts +++ b/web/i18n/en-US/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Delete this Knowledge?', deleteDatasetConfirmContent: 'Deleting the Knowledge is irreversible. Users will no longer be able to access your Knowledge, and all prompt configurations and logs will be permanently deleted.', + datasetUsedByApp: 'The knowledge is being used by some apps. Apps will no longer be able to use this Knowledge, and all prompt configurations and logs will be permanently deleted.', datasetDeleted: 'Knowledge deleted', datasetDeleteFailed: 'Failed to delete Knowledge', didYouKnow: 'Did you know?', diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index 98ba95462d..2cb6ecb785 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -35,6 +35,19 @@ const translation = { donthave: 'Don\'t have?', invalidInvitationCode: 'Invalid invitation code', accountAlreadyInited: 'Account already initialized', + forgotPassword: 'Forgot your password?', + resetLinkSent: 'Reset link sent', + sendResetLink: 'Send reset link', + backToSignIn: 'Return to sign in', + forgotPasswordDesc: 'Please enter your email address to reset your password. We will send you an email with instructions on how to reset your password.', + checkEmailForResetLink: 'Please check your email for a link to reset your password. If it doesn\'t appear within a few minutes, make sure to check your spam folder.', + passwordChanged: 'Sign in now', + changePassword: 'Change Password', + changePasswordTip: 'Please enter a new password for your account', + invalidToken: 'Invalid or expired token', + confirmPassword: 'Confirm Password', + confirmPasswordPlaceholder: 'Confirm your new password', + passwordChangedTip: 'Your password has been successfully changed', error: { emailEmpty: 'Email address is required', emailInValid: 'Please enter a valid email address', diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index d425f8d74b..e50086c3eb 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -105,6 +105,8 @@ const translation = { customDisclaimerPlaceholder: 'Please enter custom disclaimer', confirmTitle: 'Confirm to save ?', confirmTip: 'Apps using this tool will be affected', + deleteToolConfirmTitle: 'Delete this Tool?', + deleteToolConfirmContent: 'Deleting the Tool is irreversible. Users will no longer be able to access your Tool.', }, test: { title: 'Test', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 5d0edcf6ce..4ac3e82a95 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Undo', + redo: 'Redo', editing: 'Editing', autoSaved: 'Auto-Saved', unpublished: 'Unpublished', @@ -68,6 +70,39 @@ const translation = { workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.', viewDetailInTracingPanel: 'View details', syncingData: 'Syncing data, just a few seconds.', + importDSL: 'Import DSL', + importDSLTip: 'Current draft will be overwritten. Export workflow as backup before importing.', + backupCurrentDraft: 'Backup Current Draft', + chooseDSL: 'Choose DSL(yml) file', + overwriteAndImport: 'Overwrite and Import', + importFailure: 'Import failure', + importSuccess: 'Import success', + }, + changeHistory: { + title: 'Change History', + placeholder: 'You haven\'t changed anything yet', + clearHistory: 'Clear History', + hint: 'Hint', + hintText: 'Your editing actions are tracked in a change history, which is stored on your device for the duration of this session. This history will be cleared when you leave the editor.', + stepBackward_one: '{{count}} step backward', + stepBackward_other: '{{count}} steps backward', + stepForward_one: '{{count}} step forward', + 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', + noteAdd: 'Note added', + noteChange: 'Note changed', + noteDelete: 'Note deleted', + edgeDelete: 'Block disconnected', }, errorMsg: { fieldRequired: '{{field}} is required', @@ -361,6 +396,7 @@ const translation = { url: 'Image url', upload_file_id: 'Upload file id', }, + json: 'tool generated json', }, }, questionClassifiers: { diff --git a/web/i18n/es-ES/app-annotation.ts b/web/i18n/es-ES/app-annotation.ts new file mode 100644 index 0000000000..e090c46122 --- /dev/null +++ b/web/i18n/es-ES/app-annotation.ts @@ -0,0 +1,87 @@ +const translation = { + title: 'Anotaciones', + name: 'Respuesta de Anotación', + editBy: 'Respuesta editada por {{author}}', + noData: { + title: 'Sin anotaciones', + description: 'Puedes editar anotaciones durante la depuración de la aplicación o importar anotaciones en masa aquí para obtener una respuesta de alta calidad.', + }, + table: { + header: { + question: 'pregunta', + answer: 'respuesta', + createdAt: 'creado el', + hits: 'aciertos', + actions: 'acciones', + addAnnotation: 'Agregar Anotación', + bulkImport: 'Importar en Masa', + bulkExport: 'Exportar en Masa', + clearAll: 'Borrar Todas las Anotaciones', + }, + }, + editModal: { + title: 'Editar Respuesta de Anotación', + queryName: 'Consulta del Usuario', + answerName: 'Bot Narrador', + yourAnswer: 'Tu Respuesta', + answerPlaceholder: 'Escribe tu respuesta aquí', + yourQuery: 'Tu Consulta', + queryPlaceholder: 'Escribe tu consulta aquí', + removeThisCache: 'Eliminar esta Anotación', + createdAt: 'Creado el', + }, + addModal: { + title: 'Agregar Respuesta de Anotación', + queryName: 'Pregunta', + answerName: 'Respuesta', + answerPlaceholder: 'Escribe la respuesta aquí', + queryPlaceholder: 'Escribe la pregunta aquí', + createNext: 'Agregar otra respuesta anotada', + }, + batchModal: { + title: 'Importación en Masa', + csvUploadTitle: 'Arrastra y suelta tu archivo CSV aquí, o ', + browse: 'navega', + tip: 'El archivo CSV debe cumplir con la siguiente estructura:', + question: 'pregunta', + answer: 'respuesta', + contentTitle: 'contenido del fragmento', + content: 'contenido', + template: 'Descarga la plantilla aquí', + cancel: 'Cancelar', + run: 'Ejecutar Lote', + runError: 'Error al ejecutar el lote', + processing: 'En proceso de lote', + completed: 'Importación completada', + error: 'Error de importación', + ok: 'OK', + }, + errorMessage: { + answerRequired: 'Se requiere una respuesta', + queryRequired: 'Se requiere una pregunta', + }, + viewModal: { + annotatedResponse: 'Respuesta de Anotación', + hitHistory: 'Historial de Aciertos', + hit: 'Acierto', + hits: 'Aciertos', + noHitHistory: 'Sin historial de aciertos', + }, + hitHistoryTable: { + query: 'Consulta', + match: 'Coincidencia', + response: 'Respuesta', + source: 'Fuente', + score: 'Puntuación', + time: 'Tiempo', + }, + initSetup: { + title: 'Configuración Inicial de Respuesta de Anotación', + configTitle: 'Configuración de Respuesta de Anotación', + confirmBtn: 'Guardar y Habilitar', + configConfirmBtn: 'Guardar', + }, + embeddingModelSwitchTip: 'Modelo de vectorización de texto de anotación, cambiar de modelo volverá a incrustar, lo que resultará en costos adicionales.', +} + +export default translation diff --git a/web/i18n/es-ES/app-api.ts b/web/i18n/es-ES/app-api.ts new file mode 100644 index 0000000000..5d2bc078e3 --- /dev/null +++ b/web/i18n/es-ES/app-api.ts @@ -0,0 +1,83 @@ +const translation = { + apiServer: 'Servidor de API', + apiKey: 'Clave de API', + status: 'Estado', + disabled: 'Desactivado', + ok: 'En servicio', + copy: 'Copiar', + copied: 'Copiado', + play: 'Reproducir', + pause: 'Pausa', + playing: 'Reproduciendo', + loading: 'Cargando', + merMaind: { + rerender: 'Rehacer Rerender', + }, + never: 'Nunca', + apiKeyModal: { + apiSecretKey: 'Clave secreta de API', + apiSecretKeyTips: 'Para evitar el abuso de la API, protege tu clave de API. Evita usarla como texto plano en el código del frontend. :)', + createNewSecretKey: 'Crear nueva clave secreta', + secretKey: 'Clave secreta', + created: 'CREADA', + lastUsed: 'ÚLTIMO USO', + generateTips: 'Guarda esta clave en un lugar seguro y accesible.', + }, + actionMsg: { + deleteConfirmTitle: '¿Eliminar esta clave secreta?', + deleteConfirmTips: 'Esta acción no se puede deshacer.', + ok: 'OK', + }, + completionMode: { + title: 'Completar App API', + info: 'Para generar texto de alta calidad, como artículos, resúmenes y traducciones, utiliza la API de mensajes de completado con la entrada del usuario. La generación de texto depende de los parámetros del modelo y las plantillas de inicio establecidas en Dify Prompt Engineering.', + createCompletionApi: 'Crear mensaje de completado', + createCompletionApiTip: 'Crea un mensaje de completado para admitir el modo de pregunta y respuesta.', + inputsTips: '(Opcional) Proporciona campos de entrada de usuario como pares clave-valor, que corresponden a las variables en Prompt Eng. La clave es el nombre de la variable, el valor es el valor del parámetro. Si el tipo de campo es Select, el valor enviado debe ser una de las opciones predefinidas.', + queryTips: 'Contenido de texto de entrada del usuario.', + blocking: 'Tipo de bloqueo, esperando a que se complete la ejecución y devuelva los resultados. (Las solicitudes pueden interrumpirse si el proceso es largo)', + streaming: 'devoluciones de transmisión. Implementación de la devolución de transmisión basada en SSE (Eventos enviados por el servidor).', + messageFeedbackApi: 'Comentarios de mensajes (me gusta)', + messageFeedbackApiTip: 'Califica los mensajes recibidos en nombre de los usuarios finales con me gusta o no me gusta. Estos datos son visibles en la página de Registros y Anotaciones y se utilizan para ajustar el modelo en el futuro.', + messageIDTip: 'ID del mensaje', + ratingTip: 'me gusta o no me gusta, null es deshacer', + parametersApi: 'Obtener información de parámetros de la aplicación', + parametersApiTip: 'Recupera los parámetros de entrada configurados, incluidos los nombres de variables, los nombres de campos, los tipos y los valores predeterminados. Normalmente se utiliza para mostrar estos campos en un formulario o completar los valores predeterminados después de que el cliente se carga.', + }, + chatMode: { + title: 'Chat App API', + info: 'Para aplicaciones de conversación versátiles que utilizan un formato de preguntas y respuestas, llama a la API de mensajes de chat para iniciar el diálogo. Mantén conversaciones en curso pasando el conversation_id devuelto. Los parámetros de respuesta y las plantillas dependen de la configuración de Dify Prompt Eng.', + createChatApi: 'Crear mensaje de chat', + createChatApiTip: 'Crea un nuevo mensaje de conversación o continúa un diálogo existente.', + inputsTips: '(Opcional) Proporciona campos de entrada de usuario como pares clave-valor, que corresponden a las variables en Prompt Eng. La clave es el nombre de la variable, el valor es el valor del parámetro. Si el tipo de campo es Select, el valor enviado debe ser una de las opciones predefinidas.', + queryTips: 'Contenido de entrada/pregunta del usuario', + blocking: 'Tipo de bloqueo, esperando a que se complete la ejecución y devuelva los resultados. (Las solicitudes pueden interrumpirse si el proceso es largo)', + streaming: 'devoluciones de transmisión. Implementación de la devolución de transmisión basada en SSE (Eventos enviados por el servidor).', + conversationIdTip: '(Opcional) ID de conversación: dejar vacío para la primera conversación; pasar conversation_id del contexto para continuar el diálogo.', + messageFeedbackApi: 'Comentarios terminales de mensajes, me gusta', + messageFeedbackApiTip: 'Califica los mensajes recibidos en nombre de los usuarios finales con me gusta o no me gusta. Estos datos son visibles en la página de Registros y Anotaciones y se utilizan para ajustar el modelo en el futuro.', + messageIDTip: 'ID del mensaje', + ratingTip: 'me gusta o no me gusta, null es deshacer', + chatMsgHistoryApi: 'Obtener el historial de mensajes de chat', + chatMsgHistoryApiTip: 'La primera página devuelve las últimas `limit` barras, en orden inverso.', + chatMsgHistoryConversationIdTip: 'ID de conversación', + chatMsgHistoryFirstId: 'ID del primer registro de chat en la página actual. El valor predeterminado es ninguno.', + chatMsgHistoryLimit: 'Cuántos chats se devuelven en una solicitud', + conversationsListApi: 'Obtener lista de conversaciones', + conversationsListApiTip: 'Obtiene la lista de sesiones del usuario actual. De forma predeterminada, se devuelven las últimas 20 sesiones.', + conversationsListFirstIdTip: 'ID del último registro en la página actual, predeterminado ninguno.', + conversationsListLimitTip: 'Cuántos chats se devuelven en una solicitud', + conversationRenamingApi: 'Renombrar conversación', + conversationRenamingApiTip: 'Cambia el nombre de las conversaciones; el nombre se muestra en las interfaces de cliente de múltiples sesiones.', + conversationRenamingNameTip: 'Nuevo nombre', + parametersApi: 'Obtener información de parámetros de la aplicación', + parametersApiTip: 'Recupera los parámetros de entrada configurados, incluidos los nombres de variables, los nombres de campos, los tipos y los valores predeterminados. Normalmente se utiliza para mostrar estos campos en un formulario o completar los valores predeterminados después de que el cliente se carga.', + }, + develop: { + requestBody: 'Cuerpo de la solicitud', + pathParams: 'Parámetros de ruta', + query: 'Consulta', + }, +} + +export default translation diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts new file mode 100644 index 0000000000..ecc4805059 --- /dev/null +++ b/web/i18n/es-ES/app-debug.ts @@ -0,0 +1,416 @@ +const translation = { + pageTitle: { + line1: 'INDICACIÓN', + line2: 'Ingeniería', + }, + orchestrate: 'Orquestar', + promptMode: { + simple: 'Cambia a Modo Experto para editar toda la INDICACIÓN', + advanced: 'Modo Experto', + switchBack: 'Volver', + advancedWarning: { + title: 'Has cambiado a Modo Experto, y una vez que modifiques la INDICACIÓN, NO PODRÁS regresar al modo básico.', + description: 'En Modo Experto, puedes editar toda la INDICACIÓN.', + learnMore: 'Aprender más', + ok: 'OK', + }, + operation: { + addMessage: 'Agregar Mensaje', + }, + contextMissing: 'Componente de contexto faltante, la efectividad de la indicación puede no ser buena.', + }, + operation: { + applyConfig: 'Publicar', + resetConfig: 'Restablecer', + debugConfig: 'Depurar', + addFeature: 'Agregar Función', + automatic: 'Automático', + stopResponding: 'Dejar de responder', + agree: 'Me gusta', + disagree: 'No me gusta', + cancelAgree: 'Cancelar Me gusta', + cancelDisagree: 'Cancelar No me gusta', + userAction: 'Usuario ', + }, + notSetAPIKey: { + title: 'La clave del proveedor LLM no se ha establecido', + trailFinished: 'Prueba terminada', + description: 'La clave del proveedor LLM no se ha establecido, y debe configurarse antes de depurar.', + settingBtn: 'Ir a configuración', + }, + trailUseGPT4Info: { + title: 'No se admite GPT-4 ahora', + description: 'Para usar GPT-4, configure la clave API.', + }, + feature: { + groupChat: { + title: 'Mejorar chat', + description: 'Agregar configuraciones previas a la conversación en aplicaciones puede mejorar la experiencia del usuario.', + }, + groupExperience: { + title: 'Mejorar experiencia', + }, + conversationOpener: { + title: 'Iniciadores de conversación', + description: 'En una aplicación de chat, la primera oración que la IA dice al usuario suele usarse como bienvenida.', + }, + suggestedQuestionsAfterAnswer: { + title: 'Seguimiento', + description: 'Configurar sugerencias de próximas preguntas puede proporcionar una mejor conversación.', + resDes: '3 sugerencias para la próxima pregunta del usuario.', + tryToAsk: 'Intenta preguntar', + }, + moreLikeThis: { + title: 'Más como esto', + description: 'Genera múltiples textos a la vez, luego edítalos y continúa generando', + generateNumTip: 'Número de veces generado cada vez', + tip: 'Usar esta función incurrirá en un costo adicional de tokens', + }, + speechToText: { + title: 'Voz a Texto', + description: 'Una vez habilitado, puedes usar la entrada de voz.', + resDes: 'Entrada de voz habilitada', + }, + textToSpeech: { + title: 'Texto a Voz', + description: 'Una vez habilitado, el texto puede convertirse en voz.', + resDes: 'Texto a Audio habilitado', + }, + citation: { + title: 'Citas y Atribuciones', + description: 'Una vez habilitado, muestra el documento fuente y la sección atribuida del contenido generado.', + resDes: 'Citas y Atribuciones habilitadas', + }, + annotation: { + title: 'Respuesta de Anotación', + description: 'Puedes agregar manualmente una respuesta de alta calidad a la caché para una coincidencia prioritaria con preguntas similares de los usuarios.', + resDes: 'Respuesta de Anotación habilitada', + scoreThreshold: { + title: 'Umbral de Puntuación', + description: 'Usado para establecer el umbral de similitud para la respuesta de anotación.', + easyMatch: 'Coincidencia Fácil', + accurateMatch: 'Coincidencia Precisa', + }, + matchVariable: { + title: 'Variable de Coincidencia', + choosePlaceholder: 'Elige la variable de coincidencia', + }, + cacheManagement: 'Anotaciones', + cached: 'Anotado', + remove: 'Eliminar', + removeConfirm: '¿Eliminar esta anotación?', + add: 'Agregar anotación', + edit: 'Editar anotación', + }, + dataSet: { + title: 'Contexto', + noData: 'Puedes importar Conocimiento como contexto', + words: 'Palabras', + textBlocks: 'Bloques de Texto', + selectTitle: 'Seleccionar Conocimiento de referencia', + selected: 'Conocimiento seleccionado', + noDataSet: 'No se encontró Conocimiento', + toCreate: 'Ir a crear', + notSupportSelectMulti: 'Actualmente solo se admite un Conocimiento', + queryVariable: { + title: 'Variable de Consulta', + tip: 'Esta variable se utilizará como entrada de consulta para la recuperación de contexto, obteniendo información de contexto relacionada con la entrada de esta variable.', + choosePlaceholder: 'Elige la variable de consulta', + noVar: 'No hay variables', + noVarTip: 'por favor, crea una variable en la sección Variables', + unableToQueryDataSet: 'No se puede consultar el Conocimiento', + unableToQueryDataSetTip: 'No se puede consultar el Conocimiento con éxito, por favor elige una variable de consulta de contexto en la sección de contexto.', + ok: 'OK', + contextVarNotEmpty: 'La variable de consulta de contexto no puede estar vacía', + deleteContextVarTitle: '¿Eliminar variable "{{varName}}"?', + deleteContextVarTip: 'Esta variable ha sido establecida como una variable de consulta de contexto, y eliminarla afectará el uso normal del Conocimiento. Si aún necesitas eliminarla, por favor vuelve a seleccionarla en la sección de contexto.', + }, + }, + tools: { + title: 'Herramientas', + tips: 'Las herramientas proporcionan un método estándar de llamada API, tomando la entrada del usuario o variables como parámetros de solicitud para consultar datos externos como contexto.', + toolsInUse: '{{count}} herramientas en uso', + modal: { + title: 'Herramienta', + toolType: { + title: 'Tipo de Herramienta', + placeholder: 'Por favor selecciona el tipo de herramienta', + }, + name: { + title: 'Nombre', + placeholder: 'Por favor ingresa el nombre', + }, + variableName: { + title: 'Nombre de la Variable', + placeholder: 'Por favor ingresa el nombre de la variable', + }, + }, + }, + conversationHistory: { + title: 'Historial de Conversaciones', + description: 'Establecer nombres de prefijo para los roles de conversación', + tip: 'El Historial de Conversaciones no está habilitado, por favor agrega <histories> en la indicación arriba.', + learnMore: 'Aprender más', + editModal: { + title: 'Editar Nombres de Roles de Conversación', + userPrefix: 'Prefijo de Usuario', + assistantPrefix: 'Prefijo de Asistente', + }, + }, + toolbox: { + title: 'CAJA DE HERRAMIENTAS', + }, + moderation: { + title: 'Moderación de contenido', + description: 'Asegura la salida del modelo utilizando API de moderación o manteniendo una lista de palabras sensibles.', + allEnabled: 'Contenido de ENTRADA/SALIDA Habilitado', + inputEnabled: 'Contenido de ENTRADA Habilitado', + outputEnabled: 'Contenido de SALIDA Habilitado', + modal: { + title: 'Configuración de moderación de contenido', + provider: { + title: 'Proveedor', + openai: 'Moderación de OpenAI', + openaiTip: { + prefix: 'La Moderación de OpenAI requiere una clave API de OpenAI configurada en la ', + suffix: '.', + }, + keywords: 'Palabras clave', + }, + keywords: { + tip: 'Una por línea, separadas por saltos de línea. Hasta 100 caracteres por línea.', + placeholder: 'Una por línea, separadas por saltos de línea', + line: 'Línea', + }, + content: { + input: 'Moderar Contenido de ENTRADA', + output: 'Moderar Contenido de SALIDA', + preset: 'Respuestas predefinidas', + placeholder: 'Contenido de respuestas predefinidas aquí', + condition: 'Moderar Contenido de ENTRADA y SALIDA habilitado al menos uno', + fromApi: 'Las respuestas predefinidas son devueltas por la API', + errorMessage: 'Las respuestas predefinidas no pueden estar vacías', + supportMarkdown: 'Markdown soportado', + }, + openaiNotConfig: { + before: 'La Moderación de OpenAI requiere una clave API de OpenAI configurada en la', + after: '', + }, + }, + }, + }, + automatic: { + title: 'Orquestación automatizada de aplicaciones', + description: 'Describe tu escenario, Dify orquestará una aplicación para ti.', + intendedAudience: '¿Quién es el público objetivo?', + intendedAudiencePlaceHolder: 'p.ej. Estudiante', + solveProblem: '¿Qué problemas esperan que la IA pueda resolver para ellos?', + solveProblemPlaceHolder: 'p.ej. Extraer ideas y resumir información de informes y artículos largos', + generate: 'Generar', + audiencesRequired: 'Audiencia requerida', + problemRequired: 'Problema requerido', + resTitle: 'Hemos orquestado la siguiente aplicación para ti.', + apply: 'Aplicar esta orquestación', + noData: 'Describe tu caso de uso a la izquierda, la vista previa de la orquestación se mostrará aquí.', + loading: 'Orquestando la aplicación para ti...', + overwriteTitle: '¿Sobrescribir configuración existente?', + overwriteMessage: 'Aplicar esta orquestación sobrescribirá la configuración existente.', + }, + resetConfig: { + title: '¿Confirmar restablecimiento?', + message: 'Restablecer descarta cambios, restaurando la última configuración publicada.', + }, + errorMessage: { + nameOfKeyRequired: 'nombre de la clave: {{key}} requerido', + valueOfVarRequired: 'el valor de {{key}} no puede estar vacío', + queryRequired: 'Se requiere texto de solicitud.', + waitForResponse: 'Por favor espera la respuesta al mensaje anterior para completar.', + waitForBatchResponse: 'Por favor espera la respuesta a la tarea por lotes para completar.', + notSelectModel: 'Por favor elige un modelo', + waitForImgUpload: 'Por favor espera a que la imagen se cargue', + }, + chatSubTitle: 'Instrucciones', + completionSubTitle: 'Prefijo de la Indicación', + promptTip: 'Las indicaciones guían las respuestas de la IA con instrucciones y restricciones. Inserta variables como {{input}}. Esta indicación no será visible para los usuarios.', + formattingChangedTitle: 'Formato cambiado', + formattingChangedText: 'Modificar el formato restablecerá el área de depuración, ¿estás seguro?', + variableTitle: 'Variables', + variableTip: 'Los usuarios completan las variables en un formulario, reemplazando automáticamente las variables en la indicación.', + notSetVar: 'Las variables permiten a los usuarios introducir palabras de indicación u observaciones de apertura al completar formularios. Puedes intentar ingresar "{{input}}" en las palabras de indicación.', + autoAddVar: 'Variables no definidas referenciadas en la pre-indicación, ¿quieres agregarlas en el formulario de entrada del usuario?', + variableTable: { + key: 'Clave de Variable', + name: 'Nombre del Campo de Entrada del Usuario', + optional: 'Opcional', + type: 'Tipo de Entrada', + action: 'Acciones', + typeString: 'Cadena', + typeSelect: 'Seleccionar', + }, + varKeyError: { + canNoBeEmpty: 'La clave de la variable no puede estar vacía', + tooLong: 'Clave de la variable: {{key}} demasiado larga. No puede tener más de 30 caracteres', + notValid: 'Clave de la variable: {{key}} no es válida. Solo puede contener letras, números y guiones bajos', + notStartWithNumber: 'Clave de la variable: {{key}} no puede comenzar con un número', + keyAlreadyExists: 'Clave de la variable: {{key}} ya existe', + }, + otherError: { + promptNoBeEmpty: 'La indicación no puede estar vacía', + historyNoBeEmpty: 'El historial de conversaciones debe establecerse en la indicación', + queryNoBeEmpty: 'La consulta debe establecerse en la indicación', + }, + variableConig: { + 'addModalTitle': 'Agregar Campo de Entrada', + 'editModalTitle': 'Editar Campo de Entrada', + 'description': 'Configuración para la variable {{varName}}', + 'fieldType': 'Tipo de campo', + 'string': 'Texto corto', + 'text-input': 'Texto corto', + 'paragraph': 'Párrafo', + 'select': 'Seleccionar', + 'number': 'Número', + 'notSet': 'No configurado, intenta escribir {{input}} en la indicación de prefijo', + 'stringTitle': 'Opciones de cuadro de texto de formulario', + 'maxLength': 'Longitud máxima', + 'options': 'Opciones', + 'addOption': 'Agregar opción', + 'apiBasedVar': 'Variable basada en API', + 'varName': 'Nombre de la Variable', + 'labelName': 'Nombre de la Etiqueta', + 'inputPlaceholder': 'Por favor ingresa', + 'content': 'Contenido', + 'required': 'Requerido', + 'errorMsg': { + varNameRequired: 'Nombre de la variable es requerido', + labelNameRequired: 'Nombre de la etiqueta es requerido', + varNameCanBeRepeat: 'El nombre de la variable no puede repetirse', + atLeastOneOption: 'Se requiere al menos una opción', + optionRepeat: 'Hay opciones repetidas', + }, + }, + vision: { + name: 'Visión', + description: 'Habilitar Visión permitirá al modelo recibir imágenes y responder preguntas sobre ellas.', + settings: 'Configuraciones', + visionSettings: { + title: 'Configuraciones de Visión', + resolution: 'Resolución', + resolutionTooltip: `Baja resolución permitirá que el modelo reciba una versión de baja resolución de 512 x 512 de la imagen, y represente la imagen con un presupuesto de 65 tokens. Esto permite que la API devuelva respuestas más rápidas y consuma menos tokens de entrada para casos de uso que no requieren alta detalle. + \n + Alta resolución permitirá primero que el modelo vea la imagen de baja resolución y luego crea recortes detallados de las imágenes de entrada como cuadrados de 512px basados en el tamaño de la imagen de entrada. Cada uno de los recortes detallados usa el doble del presupuesto de tokens para un total de 129 tokens.`, + high: 'Alta', + low: 'Baja', + uploadMethod: 'Método de carga', + both: 'Ambos', + localUpload: 'Carga Local', + url: 'URL', + uploadLimit: 'Límite de carga', + }, + }, + voice: { + name: 'Voz', + defaultDisplay: 'Voz Predeterminada', + description: 'Configuraciones de voz a texto', + settings: 'Configuraciones', + voiceSettings: { + title: 'Configuraciones de Voz', + language: 'Idioma', + resolutionTooltip: 'Soporte de idioma para voz a texto.', + voice: 'Voz', + }, + }, + openingStatement: { + title: 'Apertura de Conversación', + add: 'Agregar', + writeOpener: 'Escribir apertura', + placeholder: 'Escribe tu mensaje de apertura aquí, puedes usar variables, intenta escribir {{variable}}.', + openingQuestion: 'Preguntas de Apertura', + 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.', + notIncludeKey: 'La indicación inicial no incluye la variable: {{key}}. Por favor agrégala a la indicación inicial.', + }, + modelConfig: { + model: 'Modelo', + setTone: 'Establecer tono de respuestas', + title: 'Modelo y Parámetros', + modeType: { + chat: 'Chat', + completion: 'Completar', + }, + }, + inputs: { + title: 'Depurar y Previsualizar', + noPrompt: 'Intenta escribir alguna indicación en la entrada de pre-indicación', + userInputField: 'Campo de Entrada del Usuario', + noVar: 'Completa el valor de la variable, que se reemplazará automáticamente en la palabra de indicación cada vez que se inicie una nueva sesión.', + chatVarTip: 'Completa el valor de la variable, que se reemplazará automáticamente en la palabra de indicación cada vez que se inicie una nueva sesión', + completionVarTip: 'Completa el valor de la variable, que se reemplazará automáticamente en las palabras de indicación cada vez que se envíe una pregunta.', + previewTitle: 'Vista previa de la indicación', + queryTitle: 'Contenido de la consulta', + queryPlaceholder: 'Por favor ingresa el texto de la solicitud.', + run: 'EJECUTAR', + }, + result: 'Texto de salida', + datasetConfig: { + settingTitle: 'Configuraciones de Recuperación', + knowledgeTip: 'Haz clic en el botón “+” para agregar conocimiento', + retrieveOneWay: { + title: 'Recuperación N-a-1', + description: 'Basado en la intención del usuario y las descripciones de Conocimiento, el Agente selecciona autónomamente el mejor Conocimiento para consultar. Ideal para aplicaciones con Conocimiento limitado y distintivo.', + }, + retrieveMultiWay: { + title: 'Recuperación Multi-camino', + description: 'Basado en la intención del usuario, consulta a través de todo el Conocimiento, recupera texto relevante de múltiples fuentes y selecciona los mejores resultados que coinciden con la consulta del usuario después de reordenar. Se requiere configuración de la API del modelo de Reordenar.', + }, + rerankModelRequired: 'Se requiere modelo de Reordenar', + params: 'Parámetros', + top_k: 'Top K', + top_kTip: 'Usado para filtrar fragmentos que son más similares a las preguntas del usuario. El sistema también ajustará dinámicamente el valor de Top K, de acuerdo con los max_tokens del modelo seleccionado.', + score_threshold: 'Umbral de Puntuación', + score_thresholdTip: 'Usado para establecer el umbral de similitud para la filtración de fragmentos.', + retrieveChangeTip: 'Modificar el modo de índice y el modo de recuperación puede afectar las aplicaciones asociadas con este Conocimiento.', + }, + debugAsSingleModel: 'Depurar como Modelo Único', + debugAsMultipleModel: 'Depurar como Múltiples Modelos', + duplicateModel: 'Duplicar', + publishAs: 'Publicar como', + assistantType: { + name: 'Tipo de Asistente', + chatAssistant: { + name: 'Asistente Básico', + description: 'Construye un asistente basado en chat usando un Modelo de Lenguaje Grande', + }, + agentAssistant: { + name: 'Asistente Agente', + description: 'Construye un Agente inteligente que puede elegir herramientas autónomamente para completar tareas', + }, + }, + agent: { + agentMode: 'Modo Agente', + agentModeDes: 'Establecer el tipo de modo de inferencia para el agente', + agentModeType: { + ReACT: 'ReAct', + functionCall: 'Llamada de Función', + }, + setting: { + name: 'Configuraciones del Agente', + description: 'Las configuraciones del Asistente Agente permiten establecer el modo del agente y funciones avanzadas como indicaciones integradas, disponibles solo en el tipo Agente.', + maximumIterations: { + name: 'Iteraciones Máximas', + description: 'Limitar el número de iteraciones que un asistente agente puede ejecutar', + }, + }, + buildInPrompt: 'Indicación Integrada', + firstPrompt: 'Primera Indicación', + nextIteration: 'Próxima Iteración', + promptPlaceholder: 'Escribe tu indicación aquí', + tools: { + name: 'Herramientas', + description: 'El uso de herramientas puede extender las capacidades del LLM, como buscar en internet o realizar cálculos científicos', + enabled: 'Habilitado', + }, + }, +} + +export default translation diff --git a/web/i18n/es-ES/app-log.ts b/web/i18n/es-ES/app-log.ts new file mode 100644 index 0000000000..2a6c9f57da --- /dev/null +++ b/web/i18n/es-ES/app-log.ts @@ -0,0 +1,91 @@ +const translation = { + title: 'Registros', + description: 'Los registros registran el estado de ejecución de la aplicación, incluyendo las entradas de usuario y las respuestas de la IA.', + dateTimeFormat: 'MM/DD/YYYY hh:mm A', + table: { + header: { + time: 'Tiempo', + endUser: 'Usuario Final', + input: 'Entrada', + output: 'Salida', + summary: 'Título', + messageCount: 'Cantidad de Mensajes', + userRate: 'Tasa de Usuario', + adminRate: 'Tasa de Op.', + startTime: 'HORA DE INICIO', + status: 'ESTADO', + runtime: 'TIEMPO DE EJECUCIÓN', + tokens: 'TOKENS', + user: 'USUARIO FINAL', + version: 'VERSIÓN', + }, + pagination: { + previous: 'Anterior', + next: 'Siguiente', + }, + empty: { + noChat: 'Aún no hay conversación', + noOutput: 'Sin salida', + element: { + title: '¿Hay alguien ahí?', + content: 'Observa y anota las interacciones entre los usuarios finales y las aplicaciones de IA aquí para mejorar continuamente la precisión de la IA. Puedes probar <shareLink>compartiendo</shareLink> o <testLink>probando</testLink> la aplicación web tú mismo, y luego regresar a esta página.', + }, + }, + }, + detail: { + time: 'Tiempo', + conversationId: 'ID de Conversación', + promptTemplate: 'Plantilla de Indicación', + promptTemplateBeforeChat: 'Plantilla de Indicación Antes de la Conversación · Como Mensaje del Sistema', + annotationTip: 'Mejoras Marcadas por {{user}}', + timeConsuming: '', + second: 's', + tokenCost: 'Tokens gastados', + loading: 'cargando', + operation: { + like: 'me gusta', + dislike: 'no me gusta', + addAnnotation: 'Agregar Mejora', + editAnnotation: 'Editar Mejora', + annotationPlaceholder: 'Ingresa la respuesta esperada que deseas que la IA responda, lo cual se puede utilizar para el ajuste del modelo y la mejora continua de la calidad de generación de texto en el futuro.', + }, + variables: 'Variables', + uploadImages: 'Imágenes Cargadas', + }, + filter: { + period: { + today: 'Hoy', + last7days: 'Últimos 7 Días', + last4weeks: 'Últimas 4 semanas', + last3months: 'Últimos 3 meses', + last12months: 'Últimos 12 meses', + monthToDate: 'Mes hasta la fecha', + quarterToDate: 'Trimestre hasta la fecha', + yearToDate: 'Año hasta la fecha', + allTime: 'Todo el tiempo', + }, + annotation: { + all: 'Todos', + annotated: 'Mejoras Anotadas ({{count}} elementos)', + not_annotated: 'No Anotadas', + }, + }, + workflowTitle: 'Registros de Flujo de Trabajo', + workflowSubtitle: 'El registro registró la operación de Automate.', + runDetail: { + title: 'Registro de Conversación', + workflowTitle: 'Detalle del Registro', + }, + promptLog: 'Registro de Indicación', + agentLog: 'Registro de Agente', + viewLog: 'Ver Registro', + agentLogDetail: { + agentMode: 'Modo de Agente', + toolUsed: 'Herramienta Utilizada', + iterations: 'Iteraciones', + iteration: 'Iteración', + finalProcessing: 'Procesamiento Final', + }, +} + +export default translation diff --git a/web/i18n/es-ES/app-overview.ts b/web/i18n/es-ES/app-overview.ts new file mode 100644 index 0000000000..f3aaf1f737 --- /dev/null +++ b/web/i18n/es-ES/app-overview.ts @@ -0,0 +1,156 @@ +const translation = { + welcome: { + firstStepTip: 'Para comenzar,', + enterKeyTip: 'ingresa tu clave de API de OpenAI a continuación', + getKeyTip: 'Obtén tu clave de API desde el panel de control de OpenAI', + placeholder: 'Tu clave de API de OpenAI (ej. sk-xxxx)', + }, + apiKeyInfo: { + cloud: { + trial: { + title: 'Estás utilizando la cuota de prueba de {{providerName}}.', + description: 'La cuota de prueba se proporciona para su uso de prueba. Antes de que se agoten las llamadas de la cuota de prueba, configure su propio proveedor de modelos o compre cuota adicional.', + }, + exhausted: { + title: 'Tu cuota de prueba se ha agotado, por favor configura tu APIKey.', + description: 'Tu cuota de prueba se ha agotado. Por favor, configure su propio proveedor de modelos o compre cuota adicional.', + }, + }, + selfHost: { + title: { + row1: 'Para comenzar,', + row2: 'configura primero tu proveedor de modelos.', + }, + }, + callTimes: 'Veces llamadas', + usedToken: 'Token utilizados', + setAPIBtn: 'Ir a configurar proveedor de modelos', + tryCloud: 'O prueba la versión en la nube de Dify con una cotización gratuita', + }, + overview: { + title: 'Resumen', + appInfo: { + explanation: 'Aplicación web de IA lista para usar', + accessibleAddress: 'URL pública', + preview: 'Vista previa', + regenerate: 'Regenerar', + regenerateNotice: '¿Deseas regenerar la URL pública?', + preUseReminder: 'Por favor, habilita la aplicación web antes de continuar.', + settings: { + entry: 'Configuración', + title: 'Configuración de la aplicación web', + webName: 'Nombre de la aplicación web', + webDesc: 'Descripción de la aplicación web', + webDescTip: 'Este texto se mostrará en el lado del cliente, proporcionando una guía básica sobre cómo usar la aplicación', + webDescPlaceholder: 'Ingresa la descripción de la aplicación web', + language: 'Idioma', + workflow: { + title: 'Pasos del flujo de trabajo', + show: 'Mostrar', + hide: 'Ocultar', + }, + chatColorTheme: 'Tema de color del chat', + chatColorThemeDesc: 'Establece el tema de color del chatbot', + chatColorThemeInverted: 'Invertido', + invalidHexMessage: 'Valor hexadecimal no válido', + more: { + entry: 'Mostrar más configuraciones', + copyright: 'Derechos de autor', + copyRightPlaceholder: 'Ingresa el nombre del autor o la organización', + privacyPolicy: 'Política de privacidad', + privacyPolicyPlaceholder: 'Ingresa el enlace de la política de privacidad', + privacyPolicyTip: 'Ayuda a los visitantes a comprender los datos que recopila la aplicación, consulta la <privacyPolicyLink>Política de privacidad</privacyPolicyLink> de Dify.', + customDisclaimer: 'Descargo de responsabilidad personalizado', + customDisclaimerPlaceholder: 'Ingresa el texto de descargo de responsabilidad personalizado', + customDisclaimerTip: 'El texto de descargo de responsabilidad personalizado se mostrará en el lado del cliente, proporcionando información adicional sobre la aplicación', + }, + }, + embedded: { + entry: 'Incrustado', + title: 'Incrustar en el sitio web', + explanation: 'Elige la forma de incrustar la aplicación de chat en tu sitio web', + iframe: 'Para agregar la aplicación de chat en cualquier lugar de tu sitio web, agrega este iframe a tu código HTML.', + scripts: 'Para agregar una aplicación de chat en la esquina inferior derecha de tu sitio web, agrega este código a tu HTML.', + chromePlugin: 'Instalar la extensión de Chrome de Dify Chatbot', + copied: 'Copiado', + copy: 'Copiar', + }, + qrcode: { + title: 'Código QR para compartir', + scan: 'Escanear para compartir la aplicación', + download: 'Descargar código QR', + }, + customize: { + way: 'forma', + entry: 'Personalizar', + title: 'Personalizar la aplicación web de IA', + explanation: 'Puedes personalizar el frontend de la aplicación web para adaptarlo a tus necesidades y estilo.', + way1: { + name: 'Bifurca el código del cliente, modifícalo y despliégalo en Vercel (recomendado)', + step1: 'Bifurca el código del cliente y modifícalo', + step1Tip: 'Haz clic aquí para bifurcar el código fuente en tu cuenta de GitHub y modificar el código', + step1Operation: 'Dify-WebClient', + step2: 'Despliégalo en Vercel', + step2Tip: 'Haz clic aquí para importar el repositorio en Vercel y desplegarlo', + step2Operation: 'Importar repositorio', + step3: 'Configura las variables de entorno', + step3Tip: 'Agrega las siguientes variables de entorno en Vercel', + }, + way2: { + name: 'Escribe código del lado del cliente para llamar a la API y despliégalo en un servidor', + operation: 'Documentación', + }, + }, + }, + apiInfo: { + title: 'API del servicio backend', + explanation: 'Fácilmente integrable en tu aplicación', + accessibleAddress: 'Punto de conexión de la API del servicio', + doc: 'Referencia de la API', + }, + status: { + running: 'En servicio', + disable: 'Deshabilitar', + }, + }, + analysis: { + title: 'Análisis', + ms: 'ms', + tokenPS: 'Token/s', + totalMessages: { + title: 'Mensajes totales', + explanation: 'Recuento diario de interacciones de IA; excluye la ingeniería/depuración de prompts.', + }, + activeUsers: { + title: 'Usuarios activos', + explanation: 'Usuarios únicos que interactúan en preguntas y respuestas con IA; excluye la ingeniería/depuración de prompts.', + }, + tokenUsage: { + title: 'Uso de tokens', + explanation: 'Refleja el uso diario de tokens del modelo de lenguaje para la aplicación, útil para el control de costos.', + consumed: 'Consumidos', + }, + avgSessionInteractions: { + title: 'Interacciones promedio por sesión', + explanation: 'Recuento continuo de comunicación usuario-IA; para aplicaciones basadas en conversaciones.', + }, + avgUserInteractions: { + title: 'Interacciones promedio por usuario', + explanation: 'Refleja la frecuencia de uso diario de los usuarios. Esta métrica refleja la fidelidad del usuario.', + }, + userSatisfactionRate: { + title: 'Tasa de satisfacción del usuario', + explanation: 'El número de likes por cada 1,000 mensajes. Esto indica la proporción de respuestas con las que los usuarios están muy satisfechos.', + }, + avgResponseTime: { + title: 'Tiempo promedio de respuesta', + explanation: 'Tiempo (ms) que tarda la IA en procesar/responder; para aplicaciones basadas en texto.', + }, + tps: { + title: 'Velocidad de salida de tokens', + explanation: 'Mide el rendimiento del LLM. Cuenta la velocidad de salida de tokens del LLM desde el inicio de la solicitud hasta la finalización de la salida.', + }, + }, +} + +export default translation diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts new file mode 100644 index 0000000000..82359b40b0 --- /dev/null +++ b/web/i18n/es-ES/app.ts @@ -0,0 +1,126 @@ +const translation = { + createApp: 'CREAR APP', + types: { + all: 'Todos', + chatbot: 'Chatbot', + agent: 'Agente', + workflow: 'Flujo de trabajo', + completion: 'Finalización', + }, + duplicate: 'Duplicar', + duplicateTitle: 'Duplicar App', + export: 'Exportar DSL', + exportFailed: 'Error al exportar DSL.', + importDSL: 'Importar archivo DSL', + createFromConfigFile: 'Crear desde archivo DSL', + deleteAppConfirmTitle: '¿Eliminar esta app?', + deleteAppConfirmContent: + 'Eliminar la app es irreversible. Los usuarios ya no podrán acceder a tu app y todas las configuraciones y registros de prompts se eliminarán permanentemente.', + appDeleted: 'App eliminada', + appDeleteFailed: 'Error al eliminar app', + join: 'Únete a la comunidad', + communityIntro: + 'Discute con miembros del equipo, colaboradores y desarrolladores en diferentes canales.', + roadmap: 'Ver nuestro plan de desarrollo', + newApp: { + startFromBlank: 'Crear desde cero', + startFromTemplate: 'Crear desde plantilla', + captionAppType: '¿Qué tipo de app quieres crear?', + chatbotDescription: 'Crea una aplicación basada en chat. Esta app utiliza un formato de pregunta y respuesta, permitiendo múltiples rondas de conversación continua.', + completionDescription: 'Crea una aplicación que genera texto de alta calidad basado en prompts, como la generación de artículos, resúmenes, traducciones y más.', + completionWarning: 'Este tipo de app ya no será compatible.', + agentDescription: 'Crea un Agente inteligente que puede elegir herramientas de forma autónoma para completar tareas', + workflowDescription: 'Crea una aplicación que genera texto de alta calidad basado en flujos de trabajo con un alto grado de personalización. Es adecuado para usuarios experimentados.', + workflowWarning: 'Actualmente en beta', + chatbotType: 'Método de orquestación del Chatbot', + basic: 'Básico', + basicTip: 'Para principiantes, se puede cambiar a Chatflow más adelante', + basicFor: 'PARA PRINCIPIANTES', + basicDescription: 'La Orquestación Básica permite la orquestación de una app de Chatbot utilizando configuraciones simples, sin la capacidad de modificar los prompts incorporados. Es adecuado para principiantes.', + advanced: 'Chatflow', + advancedFor: 'Para usuarios avanzados', + advancedDescription: 'La Orquestación de Flujo de Trabajo orquesta Chatbots en forma de flujos de trabajo, ofreciendo un alto grado de personalización, incluida la capacidad de editar los prompts incorporados. Es adecuado para usuarios experimentados.', + captionName: 'Icono y nombre de la app', + appNamePlaceholder: 'Asigna un nombre a tu app', + captionDescription: 'Descripción', + appDescriptionPlaceholder: 'Ingresa la descripción de la app', + useTemplate: 'Usar esta plantilla', + previewDemo: 'Vista previa de demostración', + chatApp: 'Asistente', + chatAppIntro: + 'Quiero construir una aplicación basada en chat. Esta app utiliza un formato de pregunta y respuesta, permitiendo múltiples rondas de conversación continua.', + agentAssistant: 'Nuevo Asistente de Agente', + completeApp: 'Generador de Texto', + completeAppIntro: + 'Quiero crear una aplicación que genera texto de alta calidad basado en prompts, como la generación de artículos, resúmenes, traducciones y más.', + showTemplates: 'Quiero elegir una plantilla', + hideTemplates: 'Volver a la selección de modo', + Create: 'Crear', + Cancel: 'Cancelar', + nameNotEmpty: 'El nombre no puede estar vacío', + appTemplateNotSelected: 'Por favor, selecciona una plantilla', + appTypeRequired: 'Por favor, selecciona un tipo de app', + appCreated: 'App creada', + appCreateFailed: 'Error al crear app', + }, + editApp: 'Editar información', + editAppTitle: 'Editar información de la app', + editDone: 'Información de la app actualizada', + editFailed: 'Error al actualizar información de la app', + emoji: { + ok: 'OK', + cancel: 'Cancelar', + }, + switch: 'Cambiar a Orquestación de Flujo de Trabajo', + switchTipStart: 'Se creará una nueva copia de la app para ti y la nueva copia cambiará a Orquestación de Flujo de Trabajo. La nueva copia no permitirá', + switchTip: 'volver', + switchTipEnd: ' a la Orquestación Básica.', + switchLabel: 'La copia de la app a crear', + removeOriginal: 'Eliminar la app original', + switchStart: 'Iniciar cambio', + typeSelector: { + all: 'Todos los tipos', + chatbot: 'Chatbot', + agent: 'Agente', + workflow: 'Flujo de trabajo', + completion: 'Finalización', + }, + tracing: { + title: 'Rastreo del rendimiento de la app', + description: 'Configuración de un proveedor de LLMOps de terceros y rastreo del rendimiento de la app.', + config: 'Configurar', + collapse: 'Contraer', + expand: 'Expandir', + tracing: 'Rastreo', + disabled: 'Deshabilitado', + disabledTip: 'Por favor, configura el proveedor primero', + enabled: 'En servicio', + tracingDescription: 'Captura el contexto completo de la ejecución de la app, incluyendo llamadas LLM, contexto, prompts, solicitudes HTTP y más, en una plataforma de rastreo de terceros.', + configProviderTitle: { + configured: 'Configurado', + notConfigured: 'Configurar proveedor para habilitar el rastreo', + moreProvider: 'Más proveedores', + }, + 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.', + }, + langfuse: { + title: 'Langfuse', + description: 'Rastrea, evalúa, gestiona prompts y métricas para depurar y mejorar tu aplicación LLM.', + }, + inUse: 'En uso', + configProvider: { + title: 'Configurar ', + placeholder: 'Ingresa tu {{key}}', + project: 'Proyecto', + publicKey: 'Clave pública', + secretKey: 'Clave secreta', + viewDocsLink: 'Ver documentación de {{key}}', + removeConfirmTitle: '¿Eliminar la configuración de {{key}}?', + removeConfirmContent: 'La configuración actual está en uso, eliminarla desactivará la función de rastreo.', + }, + }, +} + +export default translation diff --git a/web/i18n/es-ES/billing.ts b/web/i18n/es-ES/billing.ts new file mode 100644 index 0000000000..8dcee420f5 --- /dev/null +++ b/web/i18n/es-ES/billing.ts @@ -0,0 +1,118 @@ +const translation = { + currentPlan: 'Plan Actual', + upgradeBtn: { + plain: 'Actualizar Plan', + encourage: 'Actualizar Ahora', + encourageShort: 'Actualizar', + }, + viewBilling: 'Administrar facturación y suscripciones', + buyPermissionDeniedTip: 'Por favor, contacta al administrador de tu empresa para suscribirte', + plansCommon: { + title: 'Elige un plan que sea adecuado para ti', + yearlyTip: '¡Obtén 2 meses gratis al suscribirte anualmente!', + mostPopular: 'Más Popular', + planRange: { + monthly: 'Mensual', + yearly: 'Anual', + }, + month: 'mes', + year: 'año', + save: 'Ahorra ', + free: 'Gratis', + currentPlan: 'Plan Actual', + contractSales: 'Contactar ventas', + contractOwner: 'Contactar al administrador del equipo', + startForFree: 'Empezar gratis', + getStartedWith: 'Empezar con ', + contactSales: 'Contactar Ventas', + talkToSales: 'Hablar con Ventas', + modelProviders: 'Proveedores de Modelos', + teamMembers: 'Miembros del Equipo', + annotationQuota: 'Cuota de Anotación', + buildApps: 'Crear Aplicaciones', + vectorSpace: 'Espacio Vectorial', + vectorSpaceBillingTooltip: 'Cada 1MB puede almacenar aproximadamente 1.2 millones de caracteres de datos vectorizados (estimado utilizando OpenAI Embeddings, varía según los modelos).', + vectorSpaceTooltip: 'El Espacio Vectorial es el sistema de memoria a largo plazo necesario para que los LLMs comprendan tus datos.', + documentsUploadQuota: 'Cuota de Carga de Documentos', + documentProcessingPriority: 'Prioridad de Procesamiento de Documentos', + documentProcessingPriorityTip: 'Para una mayor prioridad de procesamiento de documentos, por favor actualiza tu plan.', + documentProcessingPriorityUpgrade: 'Procesa más datos con mayor precisión y velocidad.', + priority: { + 'standard': 'Estándar', + 'priority': 'Prioridad', + 'top-priority': 'Prioridad Máxima', + }, + logsHistory: 'Historial de Registros', + customTools: 'Herramientas Personalizadas', + unavailable: 'No disponible', + days: 'días', + unlimited: 'Ilimitado', + support: 'Soporte', + supportItems: { + communityForums: 'Foros Comunitarios', + emailSupport: 'Soporte por Correo Electrónico', + priorityEmail: 'Soporte Prioritario por Correo Electrónico y Chat', + logoChange: 'Cambio de Logotipo', + SSOAuthentication: 'Autenticación SSO', + personalizedSupport: 'Soporte Personalizado', + dedicatedAPISupport: 'Soporte API Dedicado', + customIntegration: 'Integración y Soporte Personalizado', + ragAPIRequest: 'Solicitudes API RAG', + bulkUpload: 'Carga Masiva de Documentos', + agentMode: 'Modo Agente', + workflow: 'Flujo de Trabajo', + llmLoadingBalancing: 'Balanceo de Carga LLM', + llmLoadingBalancingTooltip: 'Agrega múltiples claves API a los modelos, evitando efectivamente los límites de velocidad de API.', + }, + comingSoon: 'Próximamente', + member: 'Miembro', + memberAfter: 'Miembro', + messageRequest: { + title: 'Créditos de Mensajes', + tooltip: 'Cuotas de invocación de mensajes para varios planes utilizando modelos de OpenAI (excepto gpt4). Los mensajes que excedan el límite utilizarán tu clave API de OpenAI.', + }, + annotatedResponse: { + title: 'Límites de Cuota de Anotación', + tooltip: 'Edición manual y anotación de respuestas proporciona habilidades de respuesta a preguntas personalizadas y de alta calidad para aplicaciones (aplicable solo en aplicaciones de chat).', + }, + ragAPIRequestTooltip: 'Se refiere al número de llamadas API que invocan solo las capacidades de procesamiento de base de conocimientos de Dify.', + receiptInfo: 'Solo el propietario del equipo y el administrador del equipo pueden suscribirse y ver la información de facturación.', + }, + plans: { + sandbox: { + name: 'Sandbox', + description: 'Prueba gratuita de 200 veces GPT', + includesTitle: 'Incluye:', + }, + professional: { + name: 'Profesional', + description: 'Para individuos y pequeños equipos que desean desbloquear más poder de manera asequible.', + includesTitle: 'Todo en el plan gratuito, más:', + }, + team: { + name: 'Equipo', + description: 'Colabora sin límites y disfruta de un rendimiento de primera categoría.', + includesTitle: 'Todo en el plan Profesional, más:', + }, + enterprise: { + name: 'Empresa', + description: 'Obtén capacidades completas y soporte para sistemas críticos a gran escala.', + includesTitle: 'Todo en el plan Equipo, más:', + }, + }, + vectorSpace: { + fullTip: 'El Espacio Vectorial está lleno.', + fullSolution: 'Actualiza tu plan para obtener más espacio.', + }, + apps: { + fullTipLine1: 'Actualiza tu plan para', + fullTipLine2: 'crear más aplicaciones.', + }, + annotatedResponse: { + fullTipLine1: 'Actualiza tu plan para', + fullTipLine2: 'anotar más conversaciones.', + quotaTitle: 'Cuota de Respuesta Anotada', + }, +} + +export default translation diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts new file mode 100644 index 0000000000..e60c5441a7 --- /dev/null +++ b/web/i18n/es-ES/common.ts @@ -0,0 +1,571 @@ +const translation = { + api: { + success: 'Éxito', + actionSuccess: 'Acción exitosa', + saved: 'Guardado', + create: 'Creado', + remove: 'Eliminado', + }, + operation: { + create: 'Crear', + confirm: 'Confirmar', + cancel: 'Cancelar', + clear: 'Limpiar', + save: 'Guardar', + saveAndEnable: 'Guardar y habilitar', + edit: 'Editar', + add: 'Agregar', + added: 'Agregado', + refresh: 'Reiniciar', + reset: 'Restablecer', + search: 'Buscar', + change: 'Cambiar', + remove: 'Eliminar', + send: 'Enviar', + copy: 'Copiar', + lineBreak: 'Salto de línea', + sure: 'Estoy seguro', + download: 'Descargar', + delete: 'Eliminar', + settings: 'Configuraciones', + setup: 'Configurar', + getForFree: 'Obtener gratis', + reload: 'Recargar', + ok: 'OK', + log: 'Registro', + learnMore: 'Aprender más', + params: 'Parámetros', + duplicate: 'Duplicar', + rename: 'Renombrar', + }, + errorMsg: { + fieldRequired: '{{field}} es requerido', + urlError: 'la URL debe comenzar con http:// o https://', + }, + placeholder: { + input: 'Por favor ingresa', + select: 'Por favor selecciona', + }, + voice: { + language: { + zhHans: 'Chino', + zhHant: 'Chino Tradicional', + enUS: 'Inglés', + deDE: 'Alemán', + frFR: 'Francés', + esES: 'Español', + itIT: 'Italiano', + thTH: 'Tailandés', + idID: 'Indonesio', + jaJP: 'Japonés', + koKR: 'Coreano', + ptBR: 'Portugués', + ruRU: 'Ruso', + ukUA: 'Ucraniano', + viVN: 'Vietnamita', + plPL: 'Polaco', + }, + }, + unit: { + char: 'caracteres', + }, + actionMsg: { + noModification: 'No hay modificaciones en este momento.', + modifiedSuccessfully: 'Modificado exitosamente', + modifiedUnsuccessfully: 'Modificación no exitosa', + copySuccessfully: 'Copiado exitosamente', + paySucceeded: 'Pago exitoso', + payCancelled: 'Pago cancelado', + generatedSuccessfully: 'Generado exitosamente', + generatedUnsuccessfully: 'Generación no exitosa', + }, + model: { + params: { + temperature: 'Temperatura', + temperatureTip: + 'Controla la aleatoriedad: Reducir resulta en completaciones menos aleatorias. A medida que la temperatura se acerca a cero, el modelo se vuelve determinista y repetitivo.', + top_p: 'Top P', + top_pTip: + 'Controla la diversidad mediante el muestreo de núcleo: 0.5 significa que se consideran la mitad de todas las opciones ponderadas por probabilidad.', + presence_penalty: 'Penalización por presencia', + presence_penaltyTip: + 'Cuánto penalizar los nuevos tokens según si aparecen en el texto hasta ahora.\nAumenta la probabilidad del modelo de hablar sobre nuevos temas.', + frequency_penalty: 'Penalización por frecuencia', + frequency_penaltyTip: + 'Cuánto penalizar los nuevos tokens según su frecuencia existente en el texto hasta ahora.\nDisminuye la probabilidad del modelo de repetir la misma línea literalmente.', + max_tokens: 'Tokens máximos', + max_tokensTip: + 'Se usa para limitar la longitud máxima de la respuesta, en tokens. \nValores más grandes pueden limitar el espacio disponible para palabras de indicación, registros de chat y Conocimiento. \nSe recomienda configurarlo por debajo de dos tercios\ngpt-4-1106-preview, gpt-4-vision-preview tokens máximos (entrada 128k salida 4k)', + maxTokenSettingTip: 'Tu configuración de tokens máximos es alta, lo que puede limitar el espacio para indicaciones, consultas y datos. Considera configurarlo por debajo de 2/3.', + setToCurrentModelMaxTokenTip: 'Tokens máximos actualizados al 80% del máximo de tokens del modelo actual {{maxToken}}.', + stop_sequences: 'Secuencias de parada', + stop_sequencesTip: 'Hasta cuatro secuencias donde la API dejará de generar más tokens. El texto devuelto no contendrá la secuencia de parada.', + stop_sequencesPlaceholder: 'Ingresa la secuencia y presiona Tab', + }, + tone: { + Creative: 'Creativo', + Balanced: 'Equilibrado', + Precise: 'Preciso', + Custom: 'Personalizado', + }, + addMoreModel: 'Ir a configuraciones para agregar más modelos', + }, + menus: { + status: 'beta', + explore: 'Explorar', + apps: 'Estudio', + plugins: 'Plugins', + pluginsTips: 'Integrar plugins de terceros o crear Plugins AI compatibles con ChatGPT.', + datasets: 'Conocimiento', + datasetsTips: 'PRÓXIMAMENTE: Importa tus propios datos de texto o escribe datos en tiempo real a través de Webhook para la mejora del contexto LLM.', + newApp: 'Nueva App', + newDataset: 'Crear Conocimiento', + tools: 'Herramientas', + }, + userProfile: { + settings: 'Configuraciones', + workspace: 'Espacio de trabajo', + createWorkspace: 'Crear espacio de trabajo', + helpCenter: 'Ayuda', + roadmapAndFeedback: 'Comentarios', + community: 'Comunidad', + about: 'Acerca de', + logout: 'Cerrar sesión', + }, + settings: { + accountGroup: 'CUENTA', + workplaceGroup: 'ESPACIO DE TRABAJO', + account: 'Mi cuenta', + members: 'Miembros', + billing: 'Facturación', + integrations: 'Integraciones', + language: 'Idioma', + provider: 'Proveedor de Modelo', + dataSource: 'Fuente de Datos', + plugin: 'Plugins', + apiBasedExtension: 'Extensión basada en API', + }, + account: { + avatar: 'Avatar', + name: 'Nombre', + email: 'Correo electrónico', + password: 'Contraseña', + passwordTip: 'Puedes establecer una contraseña permanente si no deseas usar códigos de inicio de sesión temporales', + setPassword: 'Establecer una contraseña', + resetPassword: 'Restablecer contraseña', + currentPassword: 'Contraseña actual', + newPassword: 'Nueva contraseña', + confirmPassword: 'Confirmar contraseña', + notEqual: 'Las dos contraseñas son diferentes.', + langGeniusAccount: 'Cuenta Dify', + langGeniusAccountTip: 'Tu cuenta Dify y los datos de usuario asociados.', + editName: 'Editar Nombre', + showAppLength: 'Mostrar {{length}} apps', + delete: 'Eliminar cuenta', + deleteTip: 'Eliminar tu cuenta borrará permanentemente todos tus datos y no se podrán recuperar.', + deleteConfirmTip: 'Para confirmar, por favor envía lo siguiente desde tu correo electrónico registrado a ', + }, + members: { + team: 'Equipo', + invite: 'Agregar', + name: 'NOMBRE', + lastActive: 'ÚLTIMA ACTIVIDAD', + role: 'ROLES', + pending: 'Pendiente...', + owner: 'Propietario', + admin: 'Administrador', + adminTip: 'Puede crear aplicaciones y administrar configuraciones del equipo', + normal: 'Normal', + normalTip: 'Solo puede usar aplicaciones, no puede crear aplicaciones', + builder: 'Constructor', + builderTip: 'Puede crear y editar sus propias aplicaciones', + editor: 'Editor', + editorTip: 'Puede crear y editar aplicaciones', + datasetOperator: 'Administrador de Conocimiento', + datasetOperatorTip: 'Solo puede administrar la base de conocimiento', + inviteTeamMember: 'Agregar miembro del equipo', + inviteTeamMemberTip: 'Pueden acceder a tus datos del equipo directamente después de iniciar sesión.', + email: 'Correo electrónico', + emailInvalid: 'Formato de correo electrónico inválido', + emailPlaceholder: 'Por favor ingresa correos electrónicos', + sendInvite: 'Enviar invitación', + invitedAsRole: 'Invitado como usuario {{role}}', + invitationSent: 'Invitación enviada', + invitationSentTip: 'Invitación enviada, y pueden iniciar sesión en Dify para acceder a tus datos del equipo.', + invitationLink: 'Enlace de invitación', + failedinvitationEmails: 'Los siguientes usuarios no fueron invitados exitosamente', + ok: 'OK', + removeFromTeam: 'Eliminar del equipo', + removeFromTeamTip: 'Se eliminará el acceso al equipo', + setAdmin: 'Establecer como administrador', + setMember: 'Establecer como miembro ordinario', + setBuilder: 'Establecer como constructor', + setEditor: 'Establecer como editor', + disinvite: 'Cancelar la invitación', + deleteMember: 'Eliminar miembro', + you: '(Tú)', + }, + integrations: { + connected: 'Conectado', + google: 'Google', + googleAccount: 'Iniciar sesión con cuenta de Google', + github: 'GitHub', + githubAccount: 'Iniciar sesión con cuenta de GitHub', + connect: 'Conectar', + }, + language: { + displayLanguage: 'Idioma de visualización', + timezone: 'Zona horaria', + }, + provider: { + apiKey: 'Clave API', + enterYourKey: 'Ingresa tu clave API aquí', + invalidKey: 'Clave API de OpenAI inválida', + validatedError: 'Validación fallida: ', + validating: 'Validando clave...', + saveFailed: 'Error al guardar la clave API', + apiKeyExceedBill: 'Esta CLAVE API no tiene cuota disponible, por favor lee', + addKey: 'Agregar Clave', + comingSoon: 'Próximamente', + editKey: 'Editar', + invalidApiKey: 'Clave API inválida', + azure: { + apiBase: 'Base API', + apiBasePlaceholder: 'La URL base de la API de tu Endpoint de Azure OpenAI.', + apiKey: 'Clave API', + apiKeyPlaceholder: 'Ingresa tu clave API aquí', + helpTip: 'Aprender sobre el Servicio Azure OpenAI', + }, + openaiHosted: { + openaiHosted: 'OpenAI Hospedado', + onTrial: 'EN PRUEBA', + exhausted: 'CUOTA AGOTADA', + desc: 'El servicio de hospedaje OpenAI proporcionado por Dify te permite usar modelos como GPT-3.5. Antes de que se agote tu cuota de prueba, necesitas configurar otros proveedores de modelos.', + callTimes: 'Tiempos de llamada', + usedUp: 'Cuota de prueba agotada. Agrega tu propio proveedor de modelos.', + useYourModel: 'Actualmente usando tu propio proveedor de modelos.', + close: 'Cerrar', + }, + anthropicHosted: { + anthropicHosted: 'Claude de Anthropíc', + onTrial: 'EN PRUEBA', + exhausted: 'CUOTA AGOTADA', + desc: 'Modelo poderoso, que se destaca en una amplia gama de tareas, desde diálogos sofisticados y generación de contenido creativo hasta instrucciones detalladas.', + callTimes: 'Tiempos de llamada', + usedUp: 'Cuota de prueba agotada. Agrega tu propio proveedor de modelos.', + useYourModel: 'Actualmente usando tu propio proveedor de modelos.', + close: 'Cerrar', + }, + anthropic: { + using: 'La capacidad de incrustación está usando', + enableTip: 'Para habilitar el modelo de Anthropíc, primero necesitas vincularte al Servicio OpenAI o Azure OpenAI.', + notEnabled: 'No habilitado', + keyFrom: 'Obtén tu clave API de Anthropíc', + }, + encrypted: { + front: 'Tu CLAVE API será encriptada y almacenada usando', + back: ' tecnología.', + }, + }, + modelProvider: { + notConfigured: 'El modelo del sistema aún no ha sido completamente configurado, y algunas funciones pueden no estar disponibles.', + systemModelSettings: 'Configuraciones del Modelo del Sistema', + systemModelSettingsLink: '¿Por qué es necesario configurar un modelo del sistema?', + selectModel: 'Selecciona tu modelo', + setupModelFirst: 'Por favor configura tu modelo primero', + systemReasoningModel: { + key: 'Modelo de Razonamiento del Sistema', + tip: 'Establece el modelo de inferencia predeterminado para ser usado en la creación de aplicaciones, así como características como la generación de nombres de diálogo y sugerencias de la próxima pregunta también usarán el modelo de inferencia predeterminado.', + }, + embeddingModel: { + key: 'Modelo de Incrustación', + tip: 'Establece el modelo predeterminado para el procesamiento de incrustación de documentos del Conocimiento, tanto la recuperación como la importación del Conocimiento utilizan este modelo de Incrustación para el procesamiento de vectorización. Cambiarlo causará que la dimensión del vector entre el Conocimiento importado y la pregunta sea inconsistente, resultando en fallos en la recuperación. Para evitar fallos en la recuperación, por favor no cambies este modelo a voluntad.', + required: 'El Modelo de Incrustación es requerido', + }, + speechToTextModel: { + key: 'Modelo de Voz a Texto', + tip: 'Establece el modelo predeterminado para la entrada de voz a texto en la conversación.', + }, + ttsModel: { + key: 'Modelo de Texto a Voz', + tip: 'Establece el modelo predeterminado para la entrada de texto a voz en la conversación.', + }, + rerankModel: { + key: 'Modelo de Reordenar', + tip: 'El modelo de reordenar reordenará la lista de documentos candidatos basada en la coincidencia semántica con la consulta del usuario, mejorando los resultados de clasificación semántica', + }, + apiKey: 'CLAVE API', + quota: 'Cuota', + searchModel: 'Modelo de búsqueda', + noModelFound: 'No se encontró modelo para {{model}}', + models: 'Modelos', + showMoreModelProvider: 'Mostrar más proveedores de modelos', + selector: { + tip: 'Este modelo ha sido eliminado. Por favor agrega un modelo o selecciona otro modelo.', + emptyTip: 'No hay modelos disponibles', + emptySetting: 'Por favor ve a configuraciones para configurar', + rerankTip: 'Por favor configura el modelo de Reordenar', + }, + card: { + quota: 'CUOTA', + onTrial: 'En prueba', + paid: 'Pagado', + quotaExhausted: 'Cuota agotada', + callTimes: 'Tiempos de llamada', + tokens: 'Tokens', + buyQuota: 'Comprar Cuota', + priorityUse: 'Uso prioritario', + removeKey: 'Eliminar CLAVE API', + tip: 'Se dará prioridad al uso de la cuota pagada. La cuota de prueba se utilizará después de que se agote la cuota pagada.', + }, + item: { + deleteDesc: '{{modelName}} se está utilizando como modelo de razonamiento del sistema. Algunas funciones no estarán disponibles después de la eliminación. Por favor confirma.', + freeQuota: 'CUOTA GRATUITA', + }, + addApiKey: 'Agrega tu CLAVE API', + invalidApiKey: 'Clave API inválida', + encrypted: { + front: 'Tu CLAVE API será encriptada y almacenada usando', + back: ' tecnología.', + }, + freeQuota: { + howToEarn: 'Cómo ganar', + }, + addMoreModelProvider: 'AGREGAR MÁS PROVEEDORES DE MODELOS', + addModel: 'Agregar Modelo', + modelsNum: '{{num}} Modelos', + showModels: 'Mostrar Modelos', + showModelsNum: 'Mostrar {{num}} Modelos', + collapse: 'Colapsar', + config: 'Configurar', + modelAndParameters: 'Modelo y Parámetros', + model: 'Modelo', + featureSupported: '{{feature}} soportado', + callTimes: 'Tiempos de llamada', + credits: 'Créditos de Mensaje', + buyQuota: 'Comprar Cuota', + getFreeTokens: 'Obtener Tokens gratis', + priorityUsing: 'Uso prioritario', + deprecated: 'Desaprobado', + confirmDelete: '¿Confirmar eliminación?', + quotaTip: 'Tokens gratuitos restantes disponibles', + loadPresets: 'Cargar Presets', + parameters: 'PARÁMETROS', + loadBalancing: 'Balanceo de carga', + loadBalancingDescription: 'Reduce la presión con múltiples conjuntos de credenciales.', + loadBalancingHeadline: 'Balanceo de Carga', + configLoadBalancing: 'Configurar Balanceo de Carga', + modelHasBeenDeprecated: 'Este modelo ha sido desaprobado', + providerManaged: 'Gestionado por el proveedor', + providerManagedDescription: 'Usa el único conjunto de credenciales proporcionado por el proveedor del modelo.', + defaultConfig: 'Configuración Predeterminada', + apiKeyStatusNormal: 'El estado de la CLAVE API es normal', + apiKeyRateLimit: 'Se alcanzó el límite de velocidad, disponible después de {{seconds}}s', + addConfig: 'Agregar Configuración', + editConfig: 'Editar Configuración', + loadBalancingLeastKeyWarning: 'Para habilitar el balanceo de carga se deben habilitar al menos 2 claves.', + loadBalancingInfo: 'Por defecto, el balanceo de carga usa la estrategia Round-robin. Si se activa el límite de velocidad, se aplicará un período de enfriamiento de 1 minuto.', + upgradeForLoadBalancing: 'Actualiza tu plan para habilitar el Balanceo de Carga.', + }, + dataSource: { + add: 'Agregar una fuente de datos', + connect: 'Conectar', + configure: 'Configurar', + notion: { + title: 'Notion', + description: 'Usando Notion como fuente de datos para el Conocimiento.', + connectedWorkspace: 'Espacio de trabajo conectado', + addWorkspace: 'Agregar espacio de trabajo', + connected: 'Conectado', + disconnected: 'Desconectado', + changeAuthorizedPages: 'Cambiar páginas autorizadas', + pagesAuthorized: 'Páginas autorizadas', + sync: 'Sincronizar', + remove: 'Eliminar', + selector: { + pageSelected: 'Páginas seleccionadas', + searchPages: 'Buscar páginas...', + noSearchResult: 'No hay resultados de búsqueda', + addPages: 'Agregar páginas', + preview: 'VISTA PREVIA', + }, + }, + website: { + title: 'Sitio web', + description: 'Importar contenido de sitios web usando un rastreador web.', + with: 'Con', + configuredCrawlers: 'Rastreadores configurados', + active: 'Activo', + inactive: 'Inactivo', + }, + }, + plugin: { + serpapi: { + apiKey: 'Clave API', + apiKeyPlaceholder: 'Ingresa tu clave API', + keyFrom: 'Obtén tu clave API de SerpAPI en la página de cuenta de SerpAPI', + }, + }, + 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/features/extension/api_based_extension', + add: 'Agregar Extensión API', + selector: { + title: 'Extensión API', + placeholder: 'Por favor selecciona extensión API', + manage: 'Gestionar Extensión API', + }, + modal: { + title: 'Agregar Extensión API', + editTitle: 'Editar Extensión API', + name: { + title: 'Nombre', + placeholder: 'Por favor ingresa el nombre', + }, + apiEndpoint: { + title: 'Punto final de la API', + placeholder: 'Por favor ingresa el punto final de la API', + }, + apiKey: { + title: 'Clave API', + placeholder: 'Por favor ingresa la clave API', + lengthError: 'La longitud de la clave API no puede ser menor a 5 caracteres', + }, + }, + type: 'Tipo', + }, + about: { + changeLog: 'Registro de cambios', + updateNow: 'Actualizar ahora', + nowAvailable: 'Dify {{version}} ya está disponible.', + latestAvailable: 'Dify {{version}} es la última versión disponible.', + }, + appMenus: { + overview: 'Monitoreo', + promptEng: 'Orquestar', + apiAccess: 'Acceso API', + logAndAnn: 'Registros y Anuncios', + logs: 'Registros', + }, + environment: { + testing: 'PRUEBAS', + development: 'DESARROLLO', + }, + appModes: { + completionApp: 'Generador de Texto', + chatApp: 'Aplicación de Chat', + }, + datasetMenus: { + documents: 'Documentos', + hitTesting: 'Pruebas de Recuperación', + settings: 'Configuraciones', + emptyTip: 'El Conocimiento no ha sido asociado, por favor ve a la aplicación o plugin para completar la asociación.', + viewDoc: 'Ver documentación', + relatedApp: 'aplicaciones vinculadas', + }, + voiceInput: { + speaking: 'Habla ahora...', + converting: 'Convirtiendo a texto...', + notAllow: 'micrófono no autorizado', + }, + modelName: { + 'gpt-3.5-turbo': 'GPT-3.5-Turbo', + 'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K', + 'gpt-4': 'GPT-4', + 'gpt-4-32k': 'GPT-4-32K', + 'text-davinci-003': 'Text-Davinci-003', + 'text-embedding-ada-002': 'Text-Embedding-Ada-002', + 'whisper-1': 'Whisper-1', + 'claude-instant-1': 'Claude-Instant', + 'claude-2': 'Claude-2', + }, + chat: { + renameConversation: 'Renombrar Conversación', + conversationName: 'Nombre de la conversación', + conversationNamePlaceholder: 'Por favor ingresa el nombre de la conversación', + conversationNameCanNotEmpty: 'Nombre de la conversación requerido', + citation: { + title: 'CITAS', + linkToDataset: 'Enlace al Conocimiento', + characters: 'Caracteres:', + hitCount: 'Conteo de recuperaciones:', + vectorHash: 'Hash de vector:', + hitScore: 'Puntuación de recuperación:', + }, + }, + promptEditor: { + placeholder: 'Escribe tu palabra de indicación aquí, ingresa \'{\' para insertar una variable, ingresa \'/\' para insertar un bloque de contenido de indicación', + context: { + item: { + title: 'Contexto', + desc: 'Insertar plantilla de contexto', + }, + modal: { + title: '{{num}} Conocimiento en Contexto', + add: 'Agregar Contexto ', + footer: 'Puedes gestionar contextos en la sección de Contexto abajo.', + }, + }, + history: { + item: { + title: 'Historial de Conversación', + desc: 'Insertar plantilla de mensaje histórico', + }, + modal: { + title: 'EJEMPLO', + user: 'Hola', + assistant: '¡Hola! ¿Cómo puedo asistirte hoy?', + edit: 'Editar Nombres de Roles de Conversación', + }, + }, + variable: { + item: { + title: 'Variables y Herramientas Externas', + desc: 'Insertar Variables y Herramientas Externas', + }, + outputToolDisabledItem: { + title: 'Variables', + desc: 'Insertar Variables', + }, + modal: { + add: 'Nueva variable', + addTool: 'Nueva herramienta', + }, + }, + query: { + item: { + title: 'Consulta', + desc: 'Insertar plantilla de consulta del usuario', + }, + }, + existed: 'Ya existe en la indicación', + }, + imageUploader: { + uploadFromComputer: 'Cargar desde la Computadora', + uploadFromComputerReadError: 'Lectura de imagen fallida, por favor intenta nuevamente.', + uploadFromComputerUploadError: 'Carga de imagen fallida, por favor carga nuevamente.', + uploadFromComputerLimit: 'Las imágenes cargadas no pueden exceder {{size}} MB', + pasteImageLink: 'Pegar enlace de imagen', + pasteImageLinkInputPlaceholder: 'Pega el enlace de imagen aquí', + pasteImageLinkInvalid: 'Enlace de imagen inválido', + imageUpload: 'Carga de Imagen', + }, + tag: { + placeholder: 'Todas las Etiquetas', + addNew: 'Agregar nueva etiqueta', + noTag: 'Sin etiquetas', + noTagYet: 'Aún sin etiquetas', + addTag: 'Agregar etiquetas', + editTag: 'Editar etiquetas', + manageTags: 'Gestionar Etiquetas', + selectorPlaceholder: 'Escribe para buscar o crear', + create: 'Crear', + delete: 'Eliminar etiqueta', + deleteTip: 'La etiqueta se está utilizando, ¿eliminarla?', + created: 'Etiqueta creada exitosamente', + failed: 'Creación de etiqueta fallida', + }, +} + +export default translation diff --git a/web/i18n/es-ES/custom.ts b/web/i18n/es-ES/custom.ts new file mode 100644 index 0000000000..0dd6512589 --- /dev/null +++ b/web/i18n/es-ES/custom.ts @@ -0,0 +1,30 @@ +const translation = { + custom: 'Personalización', + upgradeTip: { + prefix: 'Actualiza tu plan para', + suffix: 'personalizar tu marca.', + }, + webapp: { + title: 'Personalizar marca de WebApp', + removeBrand: 'Eliminar Powered by Dify', + changeLogo: 'Cambiar Imagen de Marca Powered by', + changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 40x40px', + }, + app: { + title: 'Personalizar encabezado de la aplicación', + changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 80x80px', + }, + upload: 'Subir', + uploading: 'Subiendo', + uploadedFail: 'Error al subir la imagen, por favor vuelve a intentar.', + change: 'Cambiar', + apply: 'Aplicar', + restore: 'Restaurar valores predeterminados', + customize: { + contactUs: ' contáctanos ', + prefix: 'Para personalizar el logotipo de la marca dentro de la aplicación, por favor', + suffix: 'para actualizar a la edición Enterprise.', + }, +} + +export default translation diff --git a/web/i18n/es-ES/dataset-creation.ts b/web/i18n/es-ES/dataset-creation.ts new file mode 100644 index 0000000000..f8ef14f89b --- /dev/null +++ b/web/i18n/es-ES/dataset-creation.ts @@ -0,0 +1,161 @@ +const translation = { + steps: { + header: { + creation: 'Crear conocimiento', + update: 'Agregar datos', + }, + one: 'Elegir fuente de datos', + two: 'Preprocesamiento y limpieza de texto', + three: 'Ejecutar y finalizar', + }, + error: { + unavailable: 'Este conocimiento no está disponible', + }, + firecrawl: { + configFirecrawl: 'Configurar 🔥Firecrawl', + apiKeyPlaceholder: 'Clave de API de firecrawl.dev', + getApiKeyLinkText: 'Obtener tu clave de API de firecrawl.dev', + }, + stepOne: { + filePreview: 'Vista previa del archivo', + pagePreview: 'Vista previa de la página', + dataSourceType: { + file: 'Importar desde archivo', + notion: 'Sincronizar desde Notion', + web: 'Sincronizar desde sitio web', + }, + uploader: { + title: 'Cargar archivo', + button: 'Arrastra y suelta el archivo, o', + browse: 'Buscar', + tip: 'Soporta {{supportTypes}}. Máximo {{size}}MB cada uno.', + validation: { + typeError: 'Tipo de archivo no soportado', + size: 'Archivo demasiado grande. El máximo es {{size}}MB', + count: 'No se admiten varios archivos', + filesNumber: 'Has alcanzado el límite de carga por lotes de {{filesNumber}}.', + }, + cancel: 'Cancelar', + change: 'Cambiar', + failed: 'Error al cargar', + }, + notionSyncTitle: 'Notion no está conectado', + notionSyncTip: 'Para sincronizar con Notion, primero se debe establecer la conexión con Notion.', + connect: 'Ir a conectar', + button: 'Siguiente', + emptyDatasetCreation: 'Quiero crear un conocimiento vacío', + modal: { + title: 'Crear un conocimiento vacío', + tip: 'Un conocimiento vacío no contendrá documentos y podrás cargar documentos en cualquier momento.', + input: 'Nombre del conocimiento', + placeholder: 'Por favor ingresa', + nameNotEmpty: 'El nombre no puede estar vacío', + nameLengthInvaild: 'El nombre debe tener entre 1 y 40 caracteres', + cancelButton: 'Cancelar', + confirmButton: 'Crear', + failed: 'Error al crear', + }, + website: { + fireCrawlNotConfigured: 'Firecrawl no está configurado', + fireCrawlNotConfiguredDescription: 'Configura Firecrawl con la clave de API para poder utilizarlo.', + configure: 'Configurar', + run: 'Ejecutar', + firecrawlTitle: 'Extraer contenido web con 🔥Firecrawl', + firecrawlDoc: 'Documentación de Firecrawl', + firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync_from_website', + options: 'Opciones', + crawlSubPage: 'Rastrear subpáginas', + limit: 'Límite', + maxDepth: 'Profundidad máxima', + excludePaths: 'Excluir rutas', + includeOnlyPaths: 'Incluir solo rutas', + extractOnlyMainContent: 'Extraer solo el contenido principal (sin encabezados, navegación, pies de página, etc.)', + exceptionErrorTitle: 'Se produjo una excepción al ejecutar el trabajo de Firecrawl:', + unknownError: 'Error desconocido', + totalPageScraped: 'Total de páginas extraídas:', + selectAll: 'Seleccionar todo', + resetAll: 'Restablecer todo', + scrapTimeInfo: 'Se extrajeron {{total}} páginas en total en {{time}}s', + preview: 'Vista previa', + maxDepthTooltip: 'Profundidad máxima para rastrear en relación con la URL ingresada. La profundidad 0 solo extrae la página de la URL ingresada, la profundidad 1 extrae la URL y todo lo después de la URL ingresada + una /, y así sucesivamente.', + }, + }, + stepTwo: { + segmentation: 'Configuración de fragmentos', + auto: 'Automático', + autoDescription: 'Configura automáticamente las reglas de fragmentación y preprocesamiento. Se recomienda seleccionar esto para usuarios no familiarizados.', + custom: 'Personalizado', + customDescription: 'Personaliza las reglas de fragmentación, longitud de fragmentos y reglas de preprocesamiento, etc.', + separator: 'Identificador de segmento', + separatorPlaceholder: 'Por ejemplo, salto de línea (\\\\n) o separador especial (como "***")', + maxLength: 'Longitud máxima del fragmento', + overlap: 'Superposición de fragmentos', + overlapTip: 'Configurar la superposición de fragmentos puede mantener la relevancia semántica entre ellos, mejorando el efecto de recuperación. Se recomienda configurar el 10%-25% del tamaño máximo del fragmento.', + overlapCheck: 'La superposición de fragmentos no debe ser mayor que la longitud máxima del fragmento', + rules: 'Reglas de preprocesamiento de texto', + removeExtraSpaces: 'Reemplazar espacios, saltos de línea y tabulaciones consecutivas', + removeUrlEmails: 'Eliminar todas las URL y direcciones de correo electrónico', + removeStopwords: 'Eliminar palabras vacías como "un", "una", "el"', + preview: 'Confirmar y vista previa', + reset: 'Restablecer', + indexMode: 'Modo de índice', + qualified: 'Alta calidad', + recommend: 'Recomendado', + qualifiedTip: 'Llama a la interfaz de incrustación del sistema por defecto para proporcionar una mayor precisión cuando los usuarios realizan consultas.', + warning: 'Por favor, configura primero la clave de API del proveedor del modelo.', + click: 'Ir a configuración', + economical: 'Económico', + economicalTip: 'Utiliza motores de vector sin conexión, índices de palabras clave, etc. para reducir la precisión sin gastar tokens', + QATitle: 'Segmentación en formato de pregunta y respuesta', + QATip: 'Habilitar esta opción consumirá más tokens', + QALanguage: 'Segmentar usando', + emstimateCost: 'Estimación', + emstimateSegment: 'Fragmentos estimados', + segmentCount: 'fragmentos', + calculating: 'Calculando...', + fileSource: 'Preprocesar documentos', + notionSource: 'Preprocesar páginas', + websiteSource: 'Preprocesar sitio web', + other: 'y otros ', + fileUnit: ' archivos', + notionUnit: ' páginas', + webpageUnit: ' páginas', + previousStep: 'Paso anterior', + nextStep: 'Guardar y procesar', + save: 'Guardar y procesar', + cancel: 'Cancelar', + sideTipTitle: '¿Por qué fragmentar y preprocesar?', + sideTipP1: 'Al procesar datos de texto, la fragmentación y la limpieza son dos pasos de preprocesamiento importantes.', + sideTipP2: 'La segmentación divide el texto largo en párrafos para que los modelos puedan entenderlo mejor. Esto mejora la calidad y relevancia de los resultados del modelo.', + sideTipP3: 'La limpieza elimina caracteres y formatos innecesarios, haciendo que el conocimiento sea más limpio y fácil de analizar.', + sideTipP4: 'Una fragmentación y limpieza adecuadas mejoran el rendimiento del modelo, proporcionando resultados más precisos y valiosos.', + previewTitle: 'Vista previa', + previewTitleButton: 'Vista previa', + previewButton: 'Cambiar a formato de pregunta y respuesta', + previewSwitchTipStart: 'La vista previa actual del fragmento está en formato de texto, cambiar a una vista previa en formato de pregunta y respuesta', + previewSwitchTipEnd: ' consumirá tokens adicionales', + characters: 'caracteres', + indexSettedTip: 'Para cambiar el método de índice, por favor ve a la ', + retrivalSettedTip: 'Para cambiar el método de índice, por favor ve a la ', + datasetSettingLink: 'configuración del conocimiento.', + }, + stepThree: { + creationTitle: '🎉 Conocimiento creado', + creationContent: 'Hemos asignado automáticamente un nombre al conocimiento, puedes modificarlo en cualquier momento', + label: 'Nombre del conocimiento', + additionTitle: '🎉 Documento cargado', + additionP1: 'El documento se ha cargado en el conocimiento', + additionP2: ', puedes encontrarlo en la lista de documentos del conocimiento.', + stop: 'Detener procesamiento', + resume: 'Reanudar procesamiento', + navTo: 'Ir al documento', + sideTipTitle: '¿Qué sigue?', + sideTipContent: 'Después de que el documento termine de indexarse, el conocimiento se puede integrar en la aplicación como contexto. Puedes encontrar la configuración de contexto en la página de orquestación de indicaciones. También puedes crearlo como un plugin de indexación ChatGPT independiente para su lanzamiento.', + modelTitle: '¿Estás seguro de detener la incrustación?', + modelContent: 'Si necesitas reanudar el procesamiento más tarde, continuarás desde donde lo dejaste.', + modelButtonConfirm: 'Confirmar', + modelButtonCancel: 'Cancelar', + }, +} + +export default translation diff --git a/web/i18n/es-ES/dataset-documents.ts b/web/i18n/es-ES/dataset-documents.ts new file mode 100644 index 0000000000..6a5191ce53 --- /dev/null +++ b/web/i18n/es-ES/dataset-documents.ts @@ -0,0 +1,352 @@ +const translation = { + list: { + title: 'Documentos', + desc: 'Aquí se muestran todos los archivos del Conocimiento, y todo el Conocimiento se puede vincular a citas de Dify o indexarse a través del complemento de Chat.', + addFile: 'Agregar archivo', + addPages: 'Agregar páginas', + addUrl: 'Agregar URL', + table: { + header: { + fileName: 'NOMBRE DEL ARCHIVO', + words: 'PALABRAS', + hitCount: 'CANTIDAD DE RECUPERACIÓN', + uploadTime: 'TIEMPO DE CARGA', + status: 'ESTADO', + action: 'ACCIÓN', + }, + rename: 'Renombrar', + name: 'Nombre', + }, + action: { + uploadFile: 'Subir nuevo archivo', + settings: 'Configuración de segmento', + addButton: 'Agregar fragmento', + add: 'Agregar un fragmento', + batchAdd: 'Agregar en lotes', + archive: 'Archivar', + unarchive: 'Desarchivar', + delete: 'Eliminar', + enableWarning: 'El archivo archivado no puede habilitarse', + sync: 'Sincronizar', + }, + index: { + enable: 'Habilitar', + disable: 'Deshabilitar', + all: 'Todos', + enableTip: 'El archivo se puede indexar', + disableTip: 'El archivo no se puede indexar', + }, + status: { + queuing: 'En cola', + indexing: 'Indexando', + paused: 'Pausado', + error: 'Error', + available: 'Disponible', + enabled: 'Habilitado', + disabled: 'Deshabilitado', + archived: 'Archivado', + }, + empty: { + title: 'Aún no hay documentación', + upload: { + tip: 'Puedes subir archivos, sincronizar desde el sitio web o desde aplicaciones web como Notion, GitHub, etc.', + }, + sync: { + tip: 'Dify descargará periódicamente archivos desde tu Notion y completará el procesamiento.', + }, + }, + delete: { + title: '¿Seguro que deseas eliminar?', + content: 'Si necesitas reanudar el procesamiento más tarde, continuarás desde donde lo dejaste.', + }, + batchModal: { + title: 'Agregar fragmentos en lotes', + csvUploadTitle: 'Arrastra y suelta tu archivo CSV aquí, o ', + browse: 'navega', + tip: 'El archivo CSV debe cumplir con la siguiente estructura:', + question: 'pregunta', + answer: 'respuesta', + contentTitle: 'contenido del fragmento', + content: 'contenido', + template: 'Descarga la plantilla aquí', + cancel: 'Cancelar', + run: 'Ejecutar en lotes', + runError: 'Error al ejecutar en lotes', + processing: 'Procesamiento en lotes', + completed: 'Importación completada', + error: 'Error de importación', + ok: 'Aceptar', + }, + }, + metadata: { + title: 'Metadatos', + desc: 'Etiquetar metadatos para documentos permite que la IA acceda a ellos de manera oportuna y expone la fuente de referencias para los usuarios.', + dateTimeFormat: 'MMMM D, YYYY hh:mm A', + docTypeSelectTitle: 'Por favor, selecciona un tipo de documento', + docTypeChangeTitle: 'Cambiar tipo de documento', + docTypeSelectWarning: + 'Si se cambia el tipo de documento, los metadatos ahora llenos ya no se conservarán.', + firstMetaAction: 'Vamos D', + placeholder: { + add: 'Agregar ', + select: 'Seleccionar ', + }, + source: { + upload_file: 'Subir archivo', + notion: 'Sincronizar desde Notion', + github: 'Sincronizar desde GitHub', + }, + type: { + book: 'Libro', + webPage: 'Página Web', + paper: 'Artículo', + socialMediaPost: 'Publicación en Redes Sociales', + personalDocument: 'Documento Personal', + businessDocument: 'Documento de Negocios', + IMChat: 'Chat IM', + wikipediaEntry: 'Entrada de Wikipedia', + notion: 'Sincronizar desde Notion', + github: 'Sincronizar desde GitHub', + technicalParameters: 'Parámetros Técnicos', + }, + field: { + processRule: { + processDoc: 'Procesar documento', + segmentRule: 'Regla de segmentación', + segmentLength: 'Longitud de fragmentos', + processClean: 'Limpieza de texto procesado', + }, + book: { + title: 'Título', + language: 'Idioma', + author: 'Autor', + publisher: 'Editorial', + publicationDate: 'Fecha de publicación', + ISBN: 'ISBN', + category: 'Categoría', + }, + webPage: { + title: 'Título', + url: 'URL', + language: 'Idioma', + authorPublisher: 'Autor/Editorial', + publishDate: 'Fecha de publicación', + topicsKeywords: 'Temas/Palabras clave', + description: 'Descripción', + }, + paper: { + title: 'Título', + language: 'Idioma', + author: 'Autor', + publishDate: 'Fecha de publicación', + journalConferenceName: 'Nombre de la revista/conferencia', + volumeIssuePage: 'Volumen/Número/Página', + DOI: 'DOI', + topicsKeywords: 'Temas/Palabras clave', + abstract: 'Resumen', + }, + socialMediaPost: { + platform: 'Plataforma', + authorUsername: 'Autor/Nombre de usuario', + publishDate: 'Fecha de publicación', + postURL: 'URL de la publicación', + topicsTags: 'Temas/Etiquetas', + }, + personalDocument: { + title: 'Título', + author: 'Autor', + creationDate: 'Fecha de creación', + lastModifiedDate: 'Última fecha de modificación', + documentType: 'Tipo de documento', + tagsCategory: 'Etiquetas/Categoría', + }, + businessDocument: { + title: 'Título', + author: 'Autor', + creationDate: 'Fecha de creación', + lastModifiedDate: 'Última fecha de modificación', + documentType: 'Tipo de documento', + departmentTeam: 'Departamento/Equipo', + }, + IMChat: { + chatPlatform: 'Plataforma de chat', + chatPartiesGroupName: 'Partes de chat/Nombre del grupo', + participants: 'Participantes', + startDate: 'Fecha de inicio', + endDate: 'Fecha de fin', + topicsKeywords: 'Temas/Palabras clave', + fileType: 'Tipo de archivo', + }, + wikipediaEntry: { + title: 'Título', + language: 'Idioma', + webpageURL: 'URL de la página web', + editorContributor: 'Editor/Contribuidor', + lastEditDate: 'Última fecha de edición', + summaryIntroduction: 'Resumen/Introducción', + }, + notion: { + title: 'Título', + language: 'Idioma', + author: 'Autor', + createdTime: 'Fecha de creación', + lastModifiedTime: 'Última fecha de modificación', + url: 'URL', + tag: 'Etiqueta', + description: 'Descripción', + }, + github: { + repoName: 'Nombre del repositorio', + repoDesc: 'Descripción del repositorio', + repoOwner: 'Propietario del repositorio', + fileName: 'Nombre del archivo', + filePath: 'Ruta del archivo', + programmingLang: 'Lenguaje de programación', + url: 'URL', + license: 'Licencia', + lastCommitTime: 'Última hora de compromiso', + lastCommitAuthor: 'Último autor del compromiso', + }, + originInfo: { + originalFilename: 'Nombre de archivo original', + originalFileSize: 'Tamaño de archivo original', + uploadDate: 'Fecha de carga', + lastUpdateDate: 'Última fecha de actualización', + source: 'Fuente', + }, + technicalParameters: { + segmentSpecification: 'Especificación de fragmentos', + segmentLength: 'Longitud de fragmentos', + avgParagraphLength: 'Longitud promedio del párrafo', + paragraphs: 'Párrafos', + hitCount: 'Cantidad de recuperación', + embeddingTime: 'Tiempo de incrustación', + embeddedSpend: 'Gasto incrustado', + }, + }, + languageMap: { + zh: 'Chino', + en: 'Inglés', + es: 'Español', + fr: 'Francés', + de: 'Alemán', + ja: 'Japonés', + ko: 'Coreano', + ru: 'Ruso', + ar: 'Árabe', + pt: 'Portugués', + it: 'Italiano', + nl: 'Holandés', + pl: 'Polaco', + sv: 'Sueco', + tr: 'Turco', + he: 'Hebreo', + hi: 'Hindi', + da: 'Danés', + fi: 'Finlandés', + no: 'Noruego', + hu: 'Húngaro', + el: 'Griego', + cs: 'Checo', + th: 'Tailandés', + id: 'Indonesio', + }, + categoryMap: { + book: { + fiction: 'Ficción', + biography: 'Biografía', + history: 'Historia', + science: 'Ciencia', + technology: 'Tecnología', + education: 'Educación', + philosophy: 'Filosofía', + religion: 'Religión', + socialSciences: 'Ciencias Sociales', + art: 'Arte', + travel: 'Viaje', + health: 'Salud', + selfHelp: 'Autoayuda', + businessEconomics: 'Negocios y Economía', + cooking: 'Cocina', + childrenYoungAdults: 'Niños y Jóvenes Adultos', + comicsGraphicNovels: 'Cómics y Novelas Gráficas', + poetry: 'Poesía', + drama: 'Drama', + other: 'Otros', + }, + personalDoc: { + notes: 'Notas', + blogDraft: 'Borrador de blog', + diary: 'Diario', + researchReport: 'Informe de investigación', + bookExcerpt: 'Extracto de libro', + schedule: 'Horario', + list: 'Lista', + projectOverview: 'Visión general del proyecto', + photoCollection: 'Colección de fotos', + creativeWriting: 'Escritura creativa', + codeSnippet: 'Fragmento de código', + designDraft: 'Borrador de diseño', + personalResume: 'Currículum personal', + other: 'Otros', + }, + businessDoc: { + meetingMinutes: 'Minutos de reunión', + researchReport: 'Informe de investigación', + proposal: 'Propuesta', + employeeHandbook: 'Manual del empleado', + trainingMaterials: 'Materiales de capacitación', + requirementsDocument: 'Documento de requisitos', + designDocument: 'Documento de diseño', + productSpecification: 'Especificación del producto', + financialReport: 'Informe financiero', + marketAnalysis: 'Análisis de mercado', + projectPlan: 'Plan de proyecto', + teamStructure: 'Estructura del equipo', + policiesProcedures: 'Políticas y procedimientos', + contractsAgreements: 'Contratos y acuerdos', + emailCorrespondence: 'Correspondencia por correo electrónico', + other: 'Otros', + }, + }, + }, + embedding: { + processing: 'Procesando incrustación...', + paused: 'Incrustación pausada', + completed: 'Incrustación completada', + error: 'Error de incrustación', + docName: 'Preprocesamiento del documento', + mode: 'Regla de segmentación', + segmentLength: 'Longitud de fragmentos', + textCleaning: 'Definición de texto y limpieza previa', + segments: 'Párrafos', + highQuality: 'Modo de alta calidad', + economy: 'Modo económico', + estimate: 'Consumo estimado', + stop: 'Detener procesamiento', + resume: 'Reanudar procesamiento', + automatic: 'Automático', + custom: 'Personalizado', + previewTip: 'La vista previa del párrafo estará disponible después de que se complete la incrustación', + }, + segment: { + paragraphs: 'Párrafos', + keywords: 'Palabras clave', + addKeyWord: 'Agregar palabra clave', + keywordError: 'La longitud máxima de la palabra clave es 20', + characters: 'caracteres', + hitCount: 'Cantidad de recuperación', + vectorHash: 'Hash de vector: ', + questionPlaceholder: 'agregar pregunta aquí', + questionEmpty: 'La pregunta no puede estar vacía', + answerPlaceholder: 'agregar respuesta aquí', + answerEmpty: 'La respuesta no puede estar vacía', + contentPlaceholder: 'agregar contenido aquí', + contentEmpty: 'El contenido no puede estar vacío', + newTextSegment: 'Nuevo segmento de texto', + newQaSegment: 'Nuevo segmento de preguntas y respuestas', + delete: '¿Eliminar este fragmento?', + }, +} + +export default translation diff --git a/web/i18n/es-ES/dataset-hit-testing.ts b/web/i18n/es-ES/dataset-hit-testing.ts new file mode 100644 index 0000000000..4ebdd03b9d --- /dev/null +++ b/web/i18n/es-ES/dataset-hit-testing.ts @@ -0,0 +1,28 @@ +const translation = { + title: 'Prueba de recuperación', + desc: 'Prueba del efecto de impacto del conocimiento basado en el texto de consulta proporcionado.', + dateTimeFormat: 'MM/DD/YYYY hh:mm A', + recents: 'Recientes', + table: { + header: { + source: 'Fuente', + text: 'Texto', + time: 'Tiempo', + }, + }, + input: { + title: 'Texto fuente', + placeholder: 'Por favor ingrese un texto, se recomienda una oración declarativa corta.', + countWarning: 'Hasta 200 caracteres.', + indexWarning: 'Solo conocimiento de alta calidad.', + testing: 'Prueba', + }, + hit: { + title: 'PÁRRAFOS DE RECUPERACIÓN', + emptyTip: 'Los resultados de la prueba de recuperación se mostrarán aquí', + }, + noRecentTip: 'No hay resultados de consulta recientes aquí', + viewChart: 'Ver GRÁFICO VECTORIAL', +} + +export default translation diff --git a/web/i18n/es-ES/dataset-settings.ts b/web/i18n/es-ES/dataset-settings.ts new file mode 100644 index 0000000000..984b378376 --- /dev/null +++ b/web/i18n/es-ES/dataset-settings.ts @@ -0,0 +1,35 @@ +const translation = { + title: 'Configuración del conjunto de datos', + desc: 'Aquí puedes modificar las propiedades y los métodos de trabajo del conjunto de datos.', + form: { + name: 'Nombre del conjunto de datos', + namePlaceholder: 'Por favor ingresa el nombre del conjunto de datos', + nameError: 'El nombre no puede estar vacío', + desc: 'Descripción del conjunto de datos', + descInfo: 'Por favor escribe una descripción textual clara para delinear el contenido del conjunto de datos. Esta descripción se utilizará como base para la coincidencia al seleccionar entre múltiples conjuntos de datos para la inferencia.', + descPlaceholder: 'Describe lo que hay en este conjunto de datos. Una descripción detallada permite que la IA acceda al contenido del conjunto de datos de manera oportuna. Si está vacío, Dify utilizará la estrategia de coincidencia predeterminada.', + descWrite: 'Aprende cómo escribir una buena descripción del conjunto de datos.', + permissions: 'Permisos', + permissionsOnlyMe: 'Solo yo', + permissionsAllMember: 'Todos los miembros del equipo', + permissionsInvitedMembers: 'Miembros del equipo invitados', + me: '(Tú)', + indexMethod: 'Método de indexación', + indexMethodHighQuality: 'Alta calidad', + indexMethodHighQualityTip: 'Llama al modelo de incrustación para procesar y proporcionar una mayor precisión cuando los usuarios realizan consultas.', + indexMethodEconomy: 'Económico', + indexMethodEconomyTip: 'Utiliza motores de vectores sin conexión, índices de palabras clave, etc. para reducir la precisión sin gastar tokens.', + embeddingModel: 'Modelo de incrustación', + embeddingModelTip: 'Cambia el modelo de incrustación, por favor ve a ', + embeddingModelTipLink: 'Configuración', + retrievalSetting: { + title: 'Configuración de recuperación', + learnMore: 'Aprende más', + description: ' sobre el método de recuperación.', + longDescription: ' sobre el método de recuperación, puedes cambiar esto en cualquier momento en la configuración del conjunto de datos.', + }, + save: 'Guardar', + }, +} + +export default translation diff --git a/web/i18n/es-ES/dataset.ts b/web/i18n/es-ES/dataset.ts new file mode 100644 index 0000000000..307187b605 --- /dev/null +++ b/web/i18n/es-ES/dataset.ts @@ -0,0 +1,50 @@ +const translation = { + knowledge: 'Conocimiento', + documentCount: ' documentos', + wordCount: ' mil palabras', + appCount: ' aplicaciones vinculadas', + createDataset: 'Crear Conocimiento', + createDatasetIntro: 'Importa tus propios datos de texto o escribe datos en tiempo real a través de Webhook para mejorar el contexto de LLM.', + deleteDatasetConfirmTitle: '¿Eliminar este Conocimiento?', + deleteDatasetConfirmContent: + 'Eliminar el Conocimiento es irreversible. Los usuarios ya no podrán acceder a tu Conocimiento y todas las configuraciones y registros de las sugerencias se eliminarán permanentemente.', + datasetUsedByApp: 'El conocimiento está siendo utilizado por algunas aplicaciones. Las aplicaciones ya no podrán utilizar este Conocimiento y todas las configuraciones y registros de las sugerencias se eliminarán permanentemente.', + datasetDeleted: 'Conocimiento eliminado', + datasetDeleteFailed: 'Error al eliminar el Conocimiento', + didYouKnow: '¿Sabías?', + intro1: 'El Conocimiento se puede integrar en la aplicación Dify ', + intro2: 'como contexto', + intro3: ',', + intro4: 'o ', + intro5: 'se puede crear', + intro6: ' como un complemento independiente de ChatGPT para publicar', + unavailable: 'No disponible', + unavailableTip: 'El modelo de incrustación no está disponible, es necesario configurar el modelo de incrustación predeterminado', + datasets: 'CONOCIMIENTO', + datasetsApi: 'ACCESO A LA API', + retrieval: { + semantic_search: { + title: 'Búsqueda Vectorial', + description: 'Genera incrustaciones de consulta y busca el fragmento de texto más similar a su representación vectorial.', + }, + full_text_search: { + title: 'Búsqueda de Texto Completo', + description: 'Indexa todos los términos del documento, lo que permite a los usuarios buscar cualquier término y recuperar el fragmento de texto relevante que contiene esos términos.', + }, + hybrid_search: { + title: 'Búsqueda Híbrida', + description: 'Ejecuta búsquedas de texto completo y búsquedas vectoriales simultáneamente, reordena para seleccionar la mejor coincidencia para la consulta del usuario. Es necesaria la configuración de las API del modelo de reordenamiento.', + recommend: 'Recomendar', + }, + invertedIndex: { + title: 'Índice Invertido', + description: 'El Índice Invertido es una estructura utilizada para la recuperación eficiente. Organizado por términos, cada término apunta a documentos o páginas web que lo contienen.', + }, + change: 'Cambiar', + changeRetrievalMethod: 'Cambiar método de recuperación', + }, + docsFailedNotice: 'no se pudieron indexar los documentos', + retry: 'Reintentar', +} + +export default translation diff --git a/web/i18n/es-ES/explore.ts b/web/i18n/es-ES/explore.ts new file mode 100644 index 0000000000..5f85d42362 --- /dev/null +++ b/web/i18n/es-ES/explore.ts @@ -0,0 +1,41 @@ +const translation = { + title: 'Explorar', + sidebar: { + discovery: 'Descubrimiento', + chat: 'Chat', + workspace: 'Espacio de trabajo', + action: { + pin: 'Anclar', + unpin: 'Desanclar', + rename: 'Renombrar', + delete: 'Eliminar', + }, + delete: { + title: 'Eliminar aplicación', + content: '¿Estás seguro de que quieres eliminar esta aplicación?', + }, + }, + apps: { + title: 'Explorar aplicaciones de Dify', + description: 'Utiliza estas aplicaciones de plantilla al instante o personaliza tus propias aplicaciones basadas en las plantillas.', + allCategories: 'Recomendado', + }, + appCard: { + addToWorkspace: 'Agregar al espacio de trabajo', + customize: 'Personalizar', + }, + appCustomize: { + title: 'Crear aplicación a partir de {{name}}', + subTitle: 'Icono y nombre de la aplicación', + nameRequired: 'El nombre de la aplicación es obligatorio', + }, + category: { + Assistant: 'Asistente', + Writing: 'Escritura', + Translate: 'Traducción', + Programming: 'Programación', + HR: 'Recursos Humanos', + }, +} + +export default translation diff --git a/web/i18n/es-ES/layout.ts b/web/i18n/es-ES/layout.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/es-ES/layout.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/es-ES/login.ts b/web/i18n/es-ES/login.ts new file mode 100644 index 0000000000..dc12cfc32f --- /dev/null +++ b/web/i18n/es-ES/login.ts @@ -0,0 +1,75 @@ +const translation = { + pageTitle: '¡Hola, vamos a empezar!👋', + welcome: 'Bienvenido a Dify, por favor inicia sesión para continuar.', + email: 'Correo electrónico', + emailPlaceholder: 'Tu correo electrónico', + password: 'Contraseña', + passwordPlaceholder: 'Tu contraseña', + name: 'Nombre de usuario', + namePlaceholder: 'Tu nombre de usuario', + forget: '¿Olvidaste tu contraseña?', + signBtn: 'Iniciar sesión', + sso: 'Continuar con SSO', + installBtn: 'Configurar', + setAdminAccount: 'Configurando una cuenta de administrador', + setAdminAccountDesc: 'Privilegios máximos para la cuenta de administrador, que se puede utilizar para crear aplicaciones y administrar proveedores de LLM, etc.', + createAndSignIn: 'Crear e iniciar sesión', + oneMoreStep: 'Un paso más', + createSample: 'Con esta información, crearemos una aplicación de muestra para ti', + invitationCode: 'Código de invitación', + invitationCodePlaceholder: 'Tu código de invitación', + interfaceLanguage: 'Idioma de interfaz', + timezone: 'Zona horaria', + go: 'Ir a Dify', + sendUsMail: 'Envíanos un correo electrónico con tu presentación y nosotros nos encargaremos de la solicitud de invitación.', + acceptPP: 'He leído y acepto la política de privacidad', + reset: 'Por favor, ejecuta el siguiente comando para restablecer tu contraseña', + withGitHub: 'Continuar con GitHub', + withGoogle: 'Continuar con Google', + rightTitle: 'Desbloquea todo el potencial de LLM', + rightDesc: 'Construye de manera sencilla aplicaciones de IA visualmente cautivadoras, operables y mejorables.', + tos: 'Términos de servicio', + pp: 'Política de privacidad', + tosDesc: 'Al registrarte, aceptas nuestros', + goToInit: 'Si no has inicializado la cuenta, por favor ve a la página de inicialización', + donthave: '¿No tienes?', + invalidInvitationCode: 'Código de invitación inválido', + accountAlreadyInited: 'La cuenta ya está inicializada', + forgotPassword: '¿Olvidaste tu contraseña?', + resetLinkSent: 'Enlace de restablecimiento enviado', + sendResetLink: 'Enviar enlace de restablecimiento', + backToSignIn: 'Volver a iniciar sesión', + forgotPasswordDesc: 'Por favor, ingresa tu dirección de correo electrónico para restablecer tu contraseña. Te enviaremos un correo electrónico con instrucciones sobre cómo restablecer tu contraseña.', + checkEmailForResetLink: 'Por favor, revisa tu correo electrónico para encontrar un enlace para restablecer tu contraseña. Si no aparece en unos minutos, asegúrate de revisar tu carpeta de spam.', + passwordChanged: 'Inicia sesión ahora', + changePassword: 'Cambiar contraseña', + changePasswordTip: 'Por favor, ingresa una nueva contraseña para tu cuenta', + invalidToken: 'Token inválido o expirado', + confirmPassword: 'Confirmar contraseña', + confirmPasswordPlaceholder: 'Confirma tu nueva contraseña', + passwordChangedTip: 'Tu contraseña se ha cambiado correctamente', + error: { + emailEmpty: 'Se requiere una dirección de correo electrónico', + emailInValid: 'Por favor, ingresa una dirección de correo electrónico válida', + nameEmpty: 'Se requiere un nombre', + passwordEmpty: 'Se requiere una contraseña', + passwordLengthInValid: 'La contraseña debe tener al menos 8 caracteres', + passwordInvalid: 'La contraseña debe contener letras y números, y tener una longitud mayor a 8', + }, + license: { + tip: 'Antes de comenzar con Dify Community Edition, lee la', + link: 'Licencia de código abierto de GitHub', + }, + join: 'Unirse', + joinTipStart: 'Te invita a unirte al equipo de', + joinTipEnd: 'en Dify', + invalid: 'El enlace ha expirado', + explore: 'Explorar Dify', + activatedTipStart: 'Te has unido al equipo de', + activatedTipEnd: '', + activated: 'Inicia sesión ahora', + adminInitPassword: 'Contraseña de inicialización de administrador', + validate: 'Validar', +} + +export default translation diff --git a/web/i18n/es-ES/register.ts b/web/i18n/es-ES/register.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/es-ES/register.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/es-ES/run-log.ts b/web/i18n/es-ES/run-log.ts new file mode 100644 index 0000000000..134764e60d --- /dev/null +++ b/web/i18n/es-ES/run-log.ts @@ -0,0 +1,29 @@ +const translation = { + input: 'ENTRADA', + result: 'RESULTADO', + detail: 'DETALLE', + tracing: 'TRAZADO', + resultPanel: { + status: 'ESTADO', + time: 'TIEMPO TRANSCURRIDO', + tokens: 'TOTAL DE TOKENS', + }, + meta: { + title: 'METADATOS', + status: 'Estado', + version: 'Versión', + executor: 'Ejecutor', + startTime: 'Hora de inicio', + time: 'Tiempo transcurrido', + tokens: 'Total de tokens', + steps: 'Pasos de ejecución', + }, + resultEmpty: { + title: 'Esta ejecución solo produce formato JSON,', + tipLeft: 'por favor ve al ', + link: 'panel de detalle', + tipRight: ' para verlo.', + }, +} + +export default translation diff --git a/web/i18n/es-ES/share-app.ts b/web/i18n/es-ES/share-app.ts new file mode 100644 index 0000000000..ad242df478 --- /dev/null +++ b/web/i18n/es-ES/share-app.ts @@ -0,0 +1,74 @@ +const translation = { + common: { + welcome: 'Bienvenido/a al uso', + appUnavailable: 'La aplicación no está disponible', + appUnkonwError: 'La aplicación no está disponible', + }, + chat: { + newChat: 'Nuevo chat', + pinnedTitle: 'Fijados', + unpinnedTitle: 'Chats', + newChatDefaultName: 'Nueva conversación', + resetChat: 'Reiniciar conversación', + powerBy: 'Desarrollado por', + prompt: 'Indicación', + privatePromptConfigTitle: 'Configuración de la conversación', + publicPromptConfigTitle: 'Indicación inicial', + configStatusDes: 'Antes de comenzar, puedes modificar la configuración de la conversación', + configDisabled: + 'Se han utilizado las configuraciones de la sesión anterior para esta sesión.', + startChat: 'Iniciar chat', + privacyPolicyLeft: + 'Por favor, lee la ', + privacyPolicyMiddle: + 'política de privacidad', + privacyPolicyRight: + ' proporcionada por el desarrollador de la aplicación.', + deleteConversation: { + title: 'Eliminar conversación', + content: '¿Estás seguro/a de que quieres eliminar esta conversación?', + }, + tryToSolve: 'Intentar resolver', + temporarySystemIssue: 'Lo sentimos, hay un problema temporal del sistema.', + }, + generation: { + tabs: { + create: 'Ejecutar una vez', + batch: 'Ejecutar en lote', + saved: 'Guardado', + }, + savedNoData: { + title: '¡Aún no has guardado ningún resultado!', + description: 'Comienza a generar contenido y encuentra tus resultados guardados aquí.', + startCreateContent: 'Comenzar a crear contenido', + }, + title: 'Completado por IA', + queryTitle: 'Contenido de la consulta', + completionResult: 'Resultado del completado', + queryPlaceholder: 'Escribe tu contenido de consulta...', + run: 'Ejecutar', + copy: 'Copiar', + resultTitle: 'Completado por IA', + noData: 'La IA te dará lo que deseas aquí.', + csvUploadTitle: 'Arrastra y suelta tu archivo CSV aquí, o ', + browse: 'navega', + csvStructureTitle: 'El archivo CSV debe cumplir con la siguiente estructura:', + downloadTemplate: 'Descarga la plantilla aquí', + field: 'Campo', + batchFailed: { + info: '{{num}} ejecuciones fallidas', + retry: 'Reintentar', + outputPlaceholder: 'Sin contenido de salida', + }, + errorMsg: { + empty: 'Por favor, ingresa contenido en el archivo cargado.', + fileStructNotMatch: 'El archivo CSV cargado no coincide con la estructura.', + emptyLine: 'La fila {{rowIndex}} está vacía', + invalidLine: 'Fila {{rowIndex}}: el valor de {{varName}} no puede estar vacío', + moreThanMaxLengthLine: 'Fila {{rowIndex}}: el valor de {{varName}} no puede tener más de {{maxLength}} caracteres', + atLeastOne: 'Por favor, ingresa al menos una fila en el archivo cargado.', + }, + }, +} + +export default translation diff --git a/web/i18n/es-ES/tools.ts b/web/i18n/es-ES/tools.ts new file mode 100644 index 0000000000..546591f1aa --- /dev/null +++ b/web/i18n/es-ES/tools.ts @@ -0,0 +1,153 @@ +const translation = { + title: 'Herramientas', + createCustomTool: 'Crear Herramienta Personalizada', + customToolTip: 'Aprende más sobre las herramientas personalizadas de Dify', + type: { + all: 'Todas', + builtIn: 'Incorporadas', + custom: 'Personalizadas', + workflow: 'Flujo de Trabajo', + }, + contribute: { + line1: 'Estoy interesado en ', + line2: 'contribuir herramientas a Dify.', + viewGuide: 'Ver la guía', + }, + author: 'Por', + auth: { + unauthorized: 'Para Autorizar', + authorized: 'Autorizado', + setup: 'Configurar la autorización para usar', + setupModalTitle: 'Configurar Autorización', + setupModalTitleDescription: 'Después de configurar las credenciales, todos los miembros dentro del espacio de trabajo pueden usar esta herramienta al orquestar aplicaciones.', + }, + includeToolNum: '{{num}} herramientas incluidas', + addTool: 'Agregar Herramienta', + addToolModal: { + type: 'tipo', + category: 'categoría', + 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"', + }, + createTool: { + title: 'Crear Herramienta Personalizada', + editAction: 'Configurar', + editTitle: 'Editar Herramienta Personalizada', + name: 'Nombre', + toolNamePlaceHolder: 'Ingresa el nombre de la herramienta', + nameForToolCall: 'Nombre de llamada de la herramienta', + nameForToolCallPlaceHolder: 'Utilizado para el reconocimiento automático, como getCurrentWeather, list_pets', + nameForToolCallTip: 'Solo soporta números, letras y guiones bajos.', + description: 'Descripción', + descriptionPlaceholder: 'Breve descripción del propósito de la herramienta, por ejemplo, obtener la temperatura de una ubicación específica.', + schema: 'Esquema', + schemaPlaceHolder: 'Ingresa tu esquema OpenAPI aquí', + viewSchemaSpec: 'Ver la Especificación OpenAPI-Swagger', + importFromUrl: 'Importar desde URL', + importFromUrlPlaceHolder: 'https://...', + urlError: 'Por favor, ingresa una URL válida', + examples: 'Ejemplos', + exampleOptions: { + json: 'Clima (JSON)', + yaml: 'Tienda de Mascotas (YAML)', + blankTemplate: 'Plantilla en Blanco', + }, + availableTools: { + title: 'Herramientas Disponibles', + name: 'Nombre', + description: 'Descripción', + method: 'Método', + path: 'Ruta', + action: 'Acciones', + test: 'Probar', + }, + authMethod: { + title: 'Método de Autorización', + type: 'Tipo de Autorización', + keyTooltip: 'Clave del encabezado HTTP, puedes dejarla como "Authorization" si no tienes idea de qué es o configurarla con un valor personalizado', + types: { + none: 'Ninguno', + api_key: 'Clave API', + apiKeyPlaceholder: 'Nombre del encabezado HTTP para la Clave API', + apiValuePlaceholder: 'Ingresa la Clave API', + }, + key: 'Clave', + value: 'Valor', + }, + authHeaderPrefix: { + title: 'Tipo de Autenticación', + types: { + basic: 'Básica', + bearer: 'Bearer', + custom: 'Personalizada', + }, + }, + privacyPolicy: 'Política de Privacidad', + privacyPolicyPlaceholder: 'Por favor, ingresa la política de privacidad', + toolInput: { + title: 'Entrada de la Herramienta', + name: 'Nombre', + required: 'Requerido', + method: 'Método', + methodSetting: 'Configuración', + methodSettingTip: 'El usuario completa la configuración de la herramienta', + methodParameter: 'Parámetro', + methodParameterTip: 'LLM completa durante la inferencia', + label: 'Etiquetas', + labelPlaceholder: 'Elige etiquetas (opcional)', + description: 'Descripción', + descriptionPlaceholder: 'Descripción del significado del parámetro', + }, + customDisclaimer: 'Descargo de responsabilidad personalizado', + customDisclaimerPlaceholder: 'Por favor, ingresa el descargo de responsabilidad personalizado', + confirmTitle: '¿Confirmar para guardar?', + confirmTip: 'Las aplicaciones que usen esta herramienta se verán afectadas', + deleteToolConfirmTitle: '¿Eliminar esta Herramienta?', + deleteToolConfirmContent: 'Eliminar la herramienta es irreversible. Los usuarios ya no podrán acceder a tu herramienta.', + }, + test: { + title: 'Probar', + parametersValue: 'Parámetros y Valor', + parameters: 'Parámetros', + value: 'Valor', + testResult: 'Resultados de la Prueba', + testResultPlaceholder: 'El resultado de la prueba se mostrará aquí', + }, + thought: { + using: 'Usando', + used: 'Usado', + requestTitle: 'Solicitud a', + responseTitle: 'Respuesta de', + }, + setBuiltInTools: { + info: 'Información', + setting: 'Ajuste', + toolDescription: 'Descripción de la herramienta', + parameters: 'parámetros', + string: 'cadena', + number: 'número', + required: 'Requerido', + infoAndSetting: 'Información y Ajustes', + }, + noCustomTool: { + title: '¡Sin herramientas personalizadas!', + content: 'Agrega y administra tus herramientas personalizadas aquí para construir aplicaciones de inteligencia artificial.', + createTool: 'Crear Herramienta', + }, + noSearchRes: { + title: '¡Lo sentimos, no hay resultados!', + content: 'No encontramos herramientas que coincidan con tu búsqueda.', + reset: 'Restablecer Búsqueda', + }, + builtInPromptTitle: 'Aviso', + toolRemoved: 'Herramienta eliminada', + notAuthorized: 'Herramienta no autorizada', + howToGet: 'Cómo obtener', + openInStudio: 'Abrir en Studio', + toolNameUsageTip: 'Nombre de llamada de la herramienta para razonamiento y promoción de agentes', +} + +export default translation diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts new file mode 100644 index 0000000000..5db18939fe --- /dev/null +++ b/web/i18n/es-ES/workflow.ts @@ -0,0 +1,476 @@ +const translation = { + common: { + undo: 'Deshacer', + redo: 'Rehacer', + editing: 'Editando', + autoSaved: 'Guardado automático', + unpublished: 'No publicado', + published: 'Publicado', + publish: 'Publicar', + update: 'Actualizar', + run: 'Ejecutar', + running: 'Ejecutando', + inRunMode: 'En modo de ejecución', + inPreview: 'En vista previa', + inPreviewMode: 'En modo de vista previa', + preview: 'Vista previa', + viewRunHistory: 'Ver historial de ejecución', + runHistory: 'Historial de ejecución', + goBackToEdit: 'Volver al editor', + conversationLog: 'Registro de conversación', + features: 'Funcionalidades', + debugAndPreview: 'Depurar y previsualizar', + restart: 'Reiniciar', + currentDraft: 'Borrador actual', + currentDraftUnpublished: 'Borrador actual no publicado', + latestPublished: 'Último publicado', + publishedAt: 'Publicado el', + restore: 'Restaurar', + runApp: 'Ejecutar aplicación', + batchRunApp: 'Ejecutar aplicación en lote', + accessAPIReference: 'Acceder a la referencia de la API', + embedIntoSite: 'Insertar en el sitio', + addTitle: 'Agregar título...', + addDescription: 'Agregar descripción...', + noVar: 'Sin variable', + searchVar: 'Buscar variable', + variableNamePlaceholder: 'Nombre de la variable', + 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', + effectVarConfirm: { + title: 'Eliminar variable', + content: 'La variable se utiliza en otros nodos. ¿Aún quieres eliminarla?', + }, + insertVarTip: 'Presiona la tecla \'/\' para insertar rápidamente', + processData: 'Procesar datos', + input: 'Entrada', + output: 'Salida', + jinjaEditorPlaceholder: 'Escribe \'/\' o \'{\' para insertar una variable', + viewOnly: 'Solo vista', + showRunHistory: 'Mostrar historial de ejecución', + enableJinja: 'Habilitar soporte de plantillas Jinja', + learnMore: 'Más información', + copy: 'Copiar', + duplicate: 'Duplicar', + addBlock: 'Agregar bloque', + pasteHere: 'Pegar aquí', + pointerMode: 'Modo puntero', + handMode: 'Modo mano', + model: 'Modelo', + workflowAsTool: 'Flujo de trabajo como herramienta', + configureRequired: 'Configuración requerida', + configure: 'Configurar', + manageInTools: 'Administrar en Herramientas', + workflowAsToolTip: 'Se requiere la reconfiguración de la herramienta después de la actualización del flujo de trabajo.', + viewDetailInTracingPanel: 'Ver detalles', + syncingData: 'Sincronizando datos, solo unos segundos.', + importDSL: 'Importar DSL', + importDSLTip: 'El borrador actual se sobrescribirá. Exporta el flujo de trabajo como respaldo antes de importar.', + backupCurrentDraft: 'Respaldar borrador actual', + chooseDSL: 'Elegir archivo DSL (yml)', + overwriteAndImport: 'Sobrescribir e importar', + importFailure: 'Error al importar', + importSuccess: 'Importación exitosa', + }, + changeHistory: { + title: 'Historial de cambios', + placeholder: 'Aún no has realizado cambios', + clearHistory: 'Borrar historial', + hint: 'Sugerencia', + hintText: 'Tus acciones de edición se registran en un historial de cambios, que se almacena en tu dispositivo durante esta sesión. Este historial se borrará cuando salgas del editor.', + stepBackward_one: '{{count}} paso hacia atrás', + stepBackward_other: '{{count}} pasos hacia atrás', + stepForward_one: '{{count}} paso hacia adelante', + 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', + }, + errorMsg: { + fieldRequired: 'Se requiere {{field}}', + authRequired: 'Se requiere autorización', + invalidJson: '{{field}} no es un JSON válido', + fields: { + variable: 'Nombre de la variable', + variableValue: 'Valor de la variable', + code: 'Código', + model: 'Modelo', + rerankModel: 'Modelo de reordenamiento', + }, + invalidVariable: 'Variable no válida', + }, + singleRun: { + testRun: 'Ejecución de prueba', + startRun: 'Iniciar ejecución', + running: 'Ejecutando', + testRunIteration: 'Iteración de ejecución de prueba', + back: 'Atrás', + iteration: 'Iteración', + }, + tabs: { + 'searchBlock': 'Buscar bloque', + 'blocks': 'Bloques', + 'tools': 'Herramientas', + 'allTool': 'Todos', + 'builtInTool': 'Incorporadas', + 'customTool': 'Personalizadas', + 'workflowTool': 'Flujo de trabajo', + 'question-understand': 'Entender pregunta', + 'logic': 'Lógica', + 'transform': 'Transformar', + 'utilities': 'Utilidades', + 'noResult': 'No se encontraron coincidencias', + }, + blocks: { + 'start': 'Inicio', + 'end': 'Fin', + 'answer': 'Respuesta', + 'llm': 'LLM', + 'knowledge-retrieval': 'Recuperación de conocimiento', + 'question-classifier': 'Clasificador de preguntas', + 'if-else': 'SI/SINO', + 'code': 'Código', + 'template-transform': 'Plantilla', + 'http-request': 'Solicitud HTTP', + 'variable-assigner': 'Asignador de variables', + 'variable-aggregator': 'Agregador de variables', + 'iteration-start': 'Inicio de iteración', + 'iteration': 'Iteración', + 'parameter-extractor': 'Extractor de parámetros', + }, + blocksAbout: { + 'start': 'Define los parámetros iniciales para iniciar un flujo de trabajo', + 'end': 'Define el final y el tipo de resultado de un flujo de trabajo', + 'answer': 'Define el contenido de respuesta de una conversación de chat', + 'llm': 'Invoca modelos de lenguaje grandes para responder preguntas o procesar lenguaje natural', + 'knowledge-retrieval': 'Te permite consultar contenido de texto relacionado con las preguntas de los usuarios desde el conocimiento', + 'question-classifier': 'Define las condiciones de clasificación de las preguntas de los usuarios, LLM puede definir cómo progresa la conversación en función de la descripción de clasificación', + 'if-else': 'Te permite dividir el flujo de trabajo en dos ramas basadas en condiciones SI/SINO', + 'code': 'Ejecuta un fragmento de código Python o NodeJS para implementar lógica personalizada', + 'template-transform': 'Convierte datos en una cadena utilizando la sintaxis de plantillas Jinja', + 'http-request': 'Permite enviar solicitudes al servidor a través del protocolo HTTP', + 'variable-assigner': 'Agrega variables de múltiples ramas en una sola variable para configurar de manera unificada los nodos descendentes.', + 'variable-aggregator': 'Agrega variables de múltiples ramas en una sola variable para configurar de manera unificada los nodos descendentes.', + 'iteration': 'Realiza múltiples pasos en un objeto de lista hasta que se generen todos los resultados.', + 'parameter-extractor': 'Utiliza LLM para extraer parámetros estructurados del lenguaje natural para invocaciones de herramientas o solicitudes HTTP.', + }, + operator: { + zoomIn: 'Acercar', + zoomOut: 'Alejar', + zoomTo50: 'Zoom al 50%', + zoomTo100: 'Zoom al 100%', + zoomToFit: 'Ajustar al tamaño', + }, + 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', + }, + nodes: { + common: { + outputVars: 'Variables de salida', + insertVarTip: 'Insertar variable', + memory: { + memory: 'Memoria', + memoryTip: 'Configuración de memoria de chat', + windowSize: 'Tamaño de ventana', + conversationRoleName: 'Nombre del rol de conversación', + user: 'Prefijo de usuario', + assistant: 'Prefijo de asistente', + }, + memories: { + title: 'Memorias', + tip: 'Memoria de chat', + builtIn: 'Incorporada', + }, + }, + start: { + required: 'requerido', + inputField: 'Campo de entrada', + builtInVar: 'Variables incorporadas', + outputVars: { + query: 'Entrada del usuario', + memories: { + des: 'Historial de conversación', + type: 'tipo de mensaje', + content: 'contenido del mensaje', + }, + files: 'Lista de archivos', + }, + noVarTip: 'Establece las entradas que se pueden utilizar en el flujo de trabajo', + }, + end: { + outputs: 'Salidas', + output: { + type: 'tipo de salida', + variable: 'variable de salida', + }, + type: { + 'none': 'Ninguno', + 'plain-text': 'Texto sin formato', + 'structured': 'Estructurado', + }, + }, + answer: { + answer: 'Respuesta', + outputVars: 'Variables de salida', + }, + llm: { + model: 'modelo', + variables: 'variables', + context: 'contexto', + contextTooltip: 'Puedes importar el conocimiento como contexto', + notSetContextInPromptTip: 'Para habilitar la función de contexto, completa la variable de contexto en PROMPT.', + prompt: 'indicación', + roleDescription: { + system: 'Proporciona instrucciones generales para la conversación', + user: 'Proporciona instrucciones, consultas o cualquier entrada basada en texto al modelo', + assistant: 'Las respuestas del modelo basadas en los mensajes del usuario', + }, + addMessage: 'Agregar mensaje', + vision: 'visión', + files: 'Archivos', + resolution: { + name: 'Resolución', + high: 'Alta', + low: 'Baja', + }, + outputVars: { + output: 'Generar contenido', + usage: 'Información de uso del modelo', + }, + singleRun: { + variable: 'Variable', + }, + sysQueryInUser: 'se requiere sys.query en el mensaje del usuario', + }, + knowledgeRetrieval: { + queryVariable: 'Variable de consulta', + knowledge: 'Conocimiento', + outputVars: { + output: 'Datos segmentados de recuperación', + content: 'Contenido segmentado', + title: 'Título segmentado', + icon: 'Ícono segmentado', + url: 'URL segmentada', + metadata: 'Metadatos adicionales', + }, + }, + http: { + inputVars: 'Variables de entrada', + api: 'API', + apiPlaceholder: 'Ingresa la URL, escribe \'/\' para insertar una variable', + notStartWithHttp: 'La API debe comenzar con http:// o https://', + key: 'Clave', + value: 'Valor', + bulkEdit: 'Edición masiva', + keyValueEdit: 'Edición clave-valor', + headers: 'Encabezados', + params: 'Parámetros', + body: 'Cuerpo', + outputVars: { + body: 'Contenido de la respuesta', + statusCode: 'Código de estado de la respuesta', + headers: 'Lista de encabezados de respuesta en formato JSON', + files: 'Lista de archivos', + }, + authorization: { + 'authorization': 'Autorización', + 'authorizationType': 'Tipo de autorización', + 'no-auth': 'Ninguna', + 'api-key': 'Clave de API', + 'auth-type': 'Tipo de autenticación', + 'basic': 'Básica', + 'bearer': 'Bearer', + 'custom': 'Personalizada', + 'api-key-title': 'Clave de API', + 'header': 'Encabezado', + }, + insertVarPlaceholder: 'escribe \'/\' para insertar una variable', + timeout: { + title: 'Tiempo de espera', + connectLabel: 'Tiempo de espera de conexión', + connectPlaceholder: 'Ingresa el tiempo de espera de conexión en segundos', + readLabel: 'Tiempo de espera de lectura', + readPlaceholder: 'Ingresa el tiempo de espera de lectura en segundos', + writeLabel: 'Tiempo de espera de escritura', + writePlaceholder: 'Ingresa el tiempo de espera de escritura en segundos', + }, + }, + code: { + inputVars: 'Variables de entrada', + outputVars: 'Variables de salida', + advancedDependencies: 'Dependencias avanzadas', + advancedDependenciesTip: 'Agrega algunas dependencias precargadas que consumen más tiempo o no son incorporadas por defecto aquí', + searchDependencies: 'Buscar dependencias', + }, + templateTransform: { + inputVars: 'Variables de entrada', + code: 'Código', + codeSupportTip: 'Solo admite Jinja2', + outputVars: { + output: 'Contenido transformado', + }, + }, + ifElse: { + if: 'Si', + else: 'Sino', + elseDescription: 'Se utiliza para definir la lógica que se debe ejecutar cuando no se cumple la condición del si.', + and: 'y', + or: 'o', + operator: 'Operador', + notSetVariable: 'Por favor, establece primero la variable', + comparisonOperator: { + 'contains': 'contiene', + 'not contains': 'no contiene', + 'start with': 'comienza con', + 'end with': 'termina con', + 'is': 'es', + 'is not': 'no es', + 'empty': 'está vacío', + 'not empty': 'no está vacío', + 'null': 'es nulo', + 'not null': 'no es nulo', + }, + enterValue: 'Ingresa un valor', + addCondition: 'Agregar condición', + conditionNotSetup: 'Condición NO configurada', + }, + variableAssigner: { + title: 'Asignar variables', + outputType: 'Tipo de salida', + varNotSet: 'Variable no establecida', + noVarTip: 'Agrega las variables que se asignarán', + type: { + string: 'Cadena', + number: 'Número', + object: 'Objeto', + array: 'Arreglo', + }, + aggregationGroup: 'Grupo de agregación', + aggregationGroupTip: 'Al habilitar esta función, el agregador de variables puede agregar múltiples conjuntos de variables.', + addGroup: 'Agregar grupo', + outputVars: { + varDescribe: 'Salida de {{groupName}}', + }, + setAssignVariable: 'Establecer variable asignada', + }, + tool: { + toAuthorize: 'Para autorizar', + inputVars: 'Variables de entrada', + outputVars: { + text: 'Contenido generado por la herramienta', + files: { + title: 'Archivos generados por la herramienta', + type: 'Tipo de soporte. Ahora solo admite imágenes', + transfer_method: 'Método de transferencia. El valor es remote_url o local_file', + url: 'URL de la imagen', + upload_file_id: 'ID de archivo cargado', + }, + json: 'JSON generado por la herramienta', + }, + }, + questionClassifiers: { + model: 'modelo', + inputVars: 'Variables de entrada', + outputVars: { + className: 'Nombre de la clase', + }, + class: 'Clase', + classNamePlaceholder: 'Escribe el nombre de tu clase', + advancedSetting: 'Configuración avanzada', + topicName: 'Nombre del tema', + topicPlaceholder: 'Escribe el nombre de tu tema', + addClass: 'Agregar clase', + instruction: 'Instrucción', + instructionTip: 'Input additional instructions to help the question classifier better understand how to categorize questions.', + instructionPlaceholder: 'Write your instruction', + }, + parameterExtractor: { + inputVar: 'Variable de entrada', + extractParameters: 'Extraer parámetros', + importFromTool: 'Importar desde herramientas', + addExtractParameter: 'Agregar parámetro de extracción', + addExtractParameterContent: { + name: 'Nombre', + namePlaceholder: 'Nombre del parámetro de extracción', + type: 'Tipo', + typePlaceholder: 'Tipo de parámetro de extracción', + description: 'Descripción', + descriptionPlaceholder: 'Descripción del parámetro de extracción', + required: 'Requerido', + requiredContent: 'El campo requerido se utiliza solo como referencia para la inferencia del modelo, y no para la validación obligatoria de la salida del parámetro.', + }, + extractParametersNotSet: 'Parámetros de extracción no configurados', + instruction: 'Instrucción', + instructionTip: 'Ingrese instrucciones adicionales para ayudar al extractor de parámetros a entender cómo extraer parámetros.', + 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?', + deleteDesc: 'Eliminar el nodo de iteración eliminará todos los nodos secundarios', + input: 'Entrada', + output: 'Variables de salida', + iteration_one: '{{count}} Iteración', + iteration_other: '{{count}} Iteraciones', + currentIteration: 'Iteración actual', + }, + note: { + addNote: 'Agregar nota', + editor: { + placeholder: 'Escribe tu nota...', + small: 'Pequeño', + medium: 'Mediano', + large: 'Grande', + bold: 'Negrita', + italic: 'Itálica', + strikethrough: 'Tachado', + link: 'Enlace', + openLink: 'Abrir', + unlink: 'Quitar enlace', + enterUrl: 'Introducir URL...', + invalidUrl: 'URL inválida', + bulletList: 'Lista de viñetas', + showAuthor: 'Mostrar autor', + }, + }, + tracing: { + stopBy: 'Detenido por {{user}}', + }, + }, +} + +export default translation diff --git a/web/i18n/fr-FR/app-overview.ts b/web/i18n/fr-FR/app-overview.ts index 316af61629..23032f9897 100644 --- a/web/i18n/fr-FR/app-overview.ts +++ b/web/i18n/fr-FR/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Afficher', hide: 'Masquer', }, + chatColorTheme: 'Thème de couleur du chatbot', + chatColorThemeDesc: 'Définir le thème de couleur du chatbot', + chatColorThemeInverted: 'Inversé', + invalidHexMessage: 'Valeur hexadécimale invalide', more: { entry: 'Afficher plus de paramètres', copyright: 'Droits d\'auteur', diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts index b0c4c9bbaf..6d15638ff1 100644 --- a/web/i18n/fr-FR/common.ts +++ b/web/i18n/fr-FR/common.ts @@ -10,7 +10,7 @@ const translation = { create: 'Créer', confirm: 'Confirmer', cancel: 'Annuler', - clear: 'Clair', + clear: 'Effacer', save: 'Enregistrer', edit: 'Modifier', add: 'Ajouter', diff --git a/web/i18n/fr-FR/dataset.ts b/web/i18n/fr-FR/dataset.ts index 2176a070a3..2ba5819c24 100644 --- a/web/i18n/fr-FR/dataset.ts +++ b/web/i18n/fr-FR/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Supprimer cette Connaissance ?', deleteDatasetConfirmContent: 'La suppression de la Connaissance est irréversible. Les utilisateurs ne pourront plus accéder à votre Savoir, et toutes les configurations de prompt et les journaux seront supprimés de façon permanente.', + datasetUsedByApp: 'La connaissance est utilisée par certaines applications. Les applications ne pourront plus utiliser cette Connaissance, et toutes les configurations de prompts et les journaux seront définitivement supprimés.', datasetDeleted: 'Connaissance supprimée', datasetDeleteFailed: 'Échec de la suppression de la Connaissance', didYouKnow: 'Saviez-vous ?', diff --git a/web/i18n/fr-FR/login.ts b/web/i18n/fr-FR/login.ts index 71cc15f61a..c905320b22 100644 --- a/web/i18n/fr-FR/login.ts +++ b/web/i18n/fr-FR/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'Vous n\'avez pas ?', invalidInvitationCode: 'Code d\'invitation invalide', accountAlreadyInited: 'Compte déjà initialisé', + forgotPassword: 'Mot de passe oublié?', + resetLinkSent: 'Lien de réinitialisation envoyé', + sendResetLink: 'Envoyer le lien de réinitialisation', + backToSignIn: 'Retour à la connexion', + forgotPasswordDesc: 'Veuillez entrer votre adresse e-mail pour réinitialiser votre mot de passe. Nous vous enverrons un e-mail avec des instructions sur la réinitialisation de votre mot de passe.', + checkEmailForResetLink: 'Veuillez vérifier votre e-mail pour un lien de réinitialisation de votre mot de passe. S\'il n\'apparaît pas dans quelques minutes, assurez-vous de vérifier votre dossier de spam.', + passwordChanged: 'Connectez-vous maintenant', + changePassword: 'Changer le mot de passe', + changePasswordTip: 'Veuillez entrer un nouveau mot de passe pour votre compte', + invalidToken: 'Token invalide ou expiré', + confirmPassword: 'Confirmez le mot de passe', + confirmPasswordPlaceholder: 'Confirmez votre nouveau mot de passe', + passwordChangedTip: 'Votre mot de passe a été changé avec succès', error: { emailEmpty: 'Une adresse e-mail est requise', emailInValid: 'Veuillez entrer une adresse email valide', diff --git a/web/i18n/fr-FR/tools.ts b/web/i18n/fr-FR/tools.ts index 634739144f..5e2c770fea 100644 --- a/web/i18n/fr-FR/tools.ts +++ b/web/i18n/fr-FR/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: 'Veuillez entrer la politique de confidentialité', customDisclaimer: 'Clause de non-responsabilité personnalisée', customDisclaimerPlaceholder: 'Entrez le texte de la clause de non-responsabilité personnalisée', + deleteToolConfirmTitle: 'Supprimer cet outil ?', + deleteToolConfirmContent: 'La suppression de l\'outil est irréversible. Les utilisateurs ne pourront plus accéder à votre outil.', }, test: { title: 'Test', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 6303e2d5d7..12a9f7817d 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Défaire', + redo: 'Réexécuter', editing: 'Édition', autoSaved: 'Sauvegardé automatiquement', unpublished: 'Non publié', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'Reconfiguration de l\'outil requise après la mise à jour du flux de travail.', viewDetailInTracingPanel: 'Voir les détails', }, + changeHistory: { + title: 'Historique des modifications', + placeholder: 'Vous n\'avez encore rien modifié', + clearHistory: 'Effacer l\'historique', + hint: 'Conseil', + hintText: 'Vos actions d\'édition sont suivies dans un historique des modifications, qui est stocké sur votre appareil pour la durée de cette session. Cet historique sera effacé lorsque vous quitterez l\'éditeur.', + stepBackward_one: '{{count}} pas en arrière', + stepBackward_other: '{{count}} pas en arrière', + stepForward_one: '{{count}} pas en avant', + 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é', + }, errorMsg: { fieldRequired: '{{field}} est requis', authRequired: 'Autorisation requise', diff --git a/web/i18n/hi-IN/app-overview.ts b/web/i18n/hi-IN/app-overview.ts index a51023ae15..b75206e032 100644 --- a/web/i18n/hi-IN/app-overview.ts +++ b/web/i18n/hi-IN/app-overview.ts @@ -53,6 +53,10 @@ const translation = { show: 'दिखाएं', hide: 'छुपाएं', }, + chatColorTheme: 'चैटबॉट का रंग थीम', + chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें', + chatColorThemeInverted: 'उल्टा', + invalidHexMessage: 'अमान्य हेक्स मान', more: { entry: 'अधिक सेटिंग्स दिखाएं', copyright: 'कॉपीराइट', diff --git a/web/i18n/hi-IN/dataset.ts b/web/i18n/hi-IN/dataset.ts index 777a816356..859887a54d 100644 --- a/web/i18n/hi-IN/dataset.ts +++ b/web/i18n/hi-IN/dataset.ts @@ -9,6 +9,7 @@ const translation = { deleteDatasetConfirmTitle: 'क्या आप यह ज्ञान हटाना चाहते हैं?', deleteDatasetConfirmContent: 'ज्ञान को हटाना अपरिवर्तनीय है। उपयोगकर्ता अब आपके ज्ञान को प्राप्त नहीं कर पाएंगे, और सभी प्रॉम्प्ट कॉन्फ़िगरेशन और लॉग स्थायी रूप से मिटा दिए जाएंगे।', + datasetUsedByApp: 'यह ज्ञान कुछ ऐप्स द्वारा उपयोग किया जा रहा है। ऐप्स अब इस ज्ञान का उपयोग नहीं कर पाएंगे, और सभी प्रॉम्प्ट कॉन्फ़िगरेशन और लॉग स्थायी रूप से हटा दिए जाएंगे।', datasetDeleted: 'ज्ञान हटा दिया गया', datasetDeleteFailed: 'ज्ञान हटाने में विफल', didYouKnow: 'क्या आप जानते हैं?', diff --git a/web/i18n/hi-IN/login.ts b/web/i18n/hi-IN/login.ts index 06edd2c088..3ecba9a186 100644 --- a/web/i18n/hi-IN/login.ts +++ b/web/i18n/hi-IN/login.ts @@ -39,6 +39,19 @@ const translation = { donthave: 'नहीं है?', invalidInvitationCode: 'अवैध निमंत्रण कोड', accountAlreadyInited: 'खाता पहले से प्रारंभ किया गया है', + forgotPassword: 'क्या आपने अपना पासवर्ड भूल गए हैं?', + resetLinkSent: 'रीसेट लिंक भेजी गई', + sendResetLink: 'रीसेट लिंक भेजें', + backToSignIn: 'साइन इन पर वापस जाएं', + forgotPasswordDesc: 'कृपया अपना ईमेल पता दर्ज करें ताकि हम आपको अपना पासवर्ड रीसेट करने के निर्देशों के साथ एक ईमेल भेज सकें।', + checkEmailForResetLink: 'कृपया अपना पासवर्ड रीसेट करने के लिए लिंक के लिए अपना ईमेल चेक करें। अगर यह कुछ मिनटों के भीतर नहीं आता है, तो कृपया अपना स्पैम फोल्डर भी चेक करें।', + passwordChanged: 'अब साइन इन करें', + changePassword: 'पासवर्ड बदलें', + changePasswordTip: 'कृपया अपने खाते के लिए नया पासवर्ड दर्ज करें', + invalidToken: 'अमान्य या समाप्त टोकन', + confirmPassword: 'पासवर्ड की पुष्टि करें', + confirmPasswordPlaceholder: 'अपना नया पासवर्ड पुष्टि करें', + passwordChangedTip: 'आपका पासवर्ड सफलतापूर्वक बदल दिया गया है', error: { emailEmpty: 'ईमेल पता आवश्यक है', emailInValid: 'कृपया एक मान्य ईमेल पता दर्ज करें', diff --git a/web/i18n/hi-IN/tools.ts b/web/i18n/hi-IN/tools.ts index cc81e3efc6..ea8e915ea3 100644 --- a/web/i18n/hi-IN/tools.ts +++ b/web/i18n/hi-IN/tools.ts @@ -108,6 +108,8 @@ const translation = { customDisclaimerPlaceholder: 'कस्टम अस्वीकरण दर्ज करें', confirmTitle: 'सहेजने की पुष्टि करें ?', confirmTip: 'इस उपकरण का उपयोग करने वाले ऐप्स प्रभावित होंगे', + deleteToolConfirmTitle: 'इस उपकरण को हटाएं?', + deleteToolConfirmContent: 'इस उपकरण को हटाने से वापस नहीं आ सकता है। उपयोगकर्ता अब तक आपके उपकरण पर अन्तराल नहीं कर सकेंगे।', }, test: { title: 'परीक्षण', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index da60f0ea20..7a0843863f 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'पूर्ववत करें', + redo: 'फिर से करें', editing: 'संपादन', autoSaved: 'स्वतः सहेजा गया', unpublished: 'अप्रकाशित', @@ -72,6 +74,32 @@ const translation = { viewDetailInTracingPanel: 'विवरण देखें', syncingData: 'डेटा सिंक हो रहा है, बस कुछ सेकंड।', }, + changeHistory: { + title: 'परिवर्तन इतिहास', + placeholder: 'आपने अभी तक कुछ भी नहीं बदला है', + clearHistory: 'इतिहास साफ़ करें', + hint: 'संकेत', + 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: 'ब्लॉक डिस्कनेक्ट किया गया', + }, errorMsg: { fieldRequired: '{{field}} आवश्यक है', authRequired: 'प्राधिकरण आवश्यक है', diff --git a/web/i18n/ja-JP/app-overview.ts b/web/i18n/ja-JP/app-overview.ts index 32b2768d2c..b7c87c94d1 100644 --- a/web/i18n/ja-JP/app-overview.ts +++ b/web/i18n/ja-JP/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: '表示', hide: '非表示', }, + chatColorTheme: 'チャットボットのカラーテーマ', + chatColorThemeDesc: 'チャットボットのカラーテーマを設定します', + chatColorThemeInverted: '反転', + invalidHexMessage: '無効な16進数値', more: { entry: 'その他の設定を表示', copyright: '著作権', diff --git a/web/i18n/ja-JP/dataset.ts b/web/i18n/ja-JP/dataset.ts index d6ec8483ee..869a169027 100644 --- a/web/i18n/ja-JP/dataset.ts +++ b/web/i18n/ja-JP/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'この知識を削除しますか?', deleteDatasetConfirmContent: '知識を削除すると元に戻すことはできません。ユーザーはもはやあなたの知識にアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。', + datasetUsedByApp: 'この知識は一部のアプリによって使用されています。アプリはこの知識を使用できなくなり、すべてのプロンプト設定とログは永久に削除されます。', datasetDeleted: '知識が削除されました', datasetDeleteFailed: '知識の削除に失敗しました', didYouKnow: 'ご存知ですか?', diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index ef87dd3df6..4015bf448d 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'お持ちでない場合', invalidInvitationCode: '無効な招待コード', accountAlreadyInited: 'アカウントは既に初期化されています', + forgotPassword: 'パスワードを忘れましたか?', + resetLinkSent: 'リセットリンクが送信されました', + sendResetLink: 'リセットリンクを送信', + backToSignIn: 'サインインに戻る', + forgotPasswordDesc: 'パスワードをリセットするためにメールアドレスを入力してください。パスワードのリセット方法に関する指示が記載されたメールを送信します。', + checkEmailForResetLink: 'パスワードリセットリンクを確認するためにメールを確認してください。数分以内に表示されない場合は、スパムフォルダーを確認してください。', + passwordChanged: '今すぐサインイン', + changePassword: 'パスワードを変更する', + changePasswordTip: 'アカウントの新しいパスワードを入力してください', + invalidToken: '無効または期限切れのトークン', + confirmPassword: 'パスワードを確認', + confirmPasswordPlaceholder: '新しいパスワードを確認してください', + passwordChangedTip: 'パスワードが正常に変更されました', error: { emailEmpty: 'メールアドレスは必須です', emailInValid: '有効なメールアドレスを入力してください', diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index 65c60d8e3f..8173a9903f 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: 'プライバシーポリシーを入力してください', customDisclaimer: 'カスタム免責事項', customDisclaimerPlaceholder: 'カスタム免責事項を入力してください', + deleteToolConfirmTitle: 'このツールを削除しますか?', + deleteToolConfirmContent: 'ツールの削除は取り消しできません。ユーザーはもうあなたのツールにアクセスできません。', }, test: { title: 'テスト', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index bd363d40fe..2e1f8e0807 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: '元に戻す', + redo: 'やり直し', editing: '編集中', autoSaved: '自動保存済み', unpublished: '未公開', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'ワークフローの更新後、ツールの再設定が必要です。', viewDetailInTracingPanel: '詳細を表示', }, + changeHistory: { + title: '変更履歴', + placeholder: 'まだ何も変更していません', + clearHistory: '履歴をクリア', + hint: 'ヒント', + 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: 'ブロックが切断されました', + }, errorMsg: { fieldRequired: '{{field}}は必須です', authRequired: '認証が必要です', diff --git a/web/i18n/ko-KR/app-overview.ts b/web/i18n/ko-KR/app-overview.ts index ca58a3c535..47342984b3 100644 --- a/web/i18n/ko-KR/app-overview.ts +++ b/web/i18n/ko-KR/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: '표시', hide: '숨기기', }, + chatColorTheme: '챗봇 색상 테마', + chatColorThemeDesc: '챗봇의 색상 테마를 설정하세요', + chatColorThemeInverted: '반전', + invalidHexMessage: '잘못된 16진수 값', more: { entry: '추가 설정 보기', copyright: '저작권', diff --git a/web/i18n/ko-KR/dataset.ts b/web/i18n/ko-KR/dataset.ts index d80d9925ca..27a5d7320e 100644 --- a/web/i18n/ko-KR/dataset.ts +++ b/web/i18n/ko-KR/dataset.ts @@ -7,6 +7,7 @@ const translation = { createDatasetIntro: '자체 텍스트 데이터를 가져오거나 LLM 컨텍스트를 강화하기 위해 웹훅을 통해 실시간 데이터를 기록할 수 있습니다.', deleteDatasetConfirmTitle: '이 지식을 삭제하시겠습니까?', deleteDatasetConfirmContent: '지식을 삭제하면 다시 되돌릴 수 없습니다. 사용자는 더 이상 귀하의 지식에 액세스할 수 없으며 모든 프롬프트 설정과 로그가 영구적으로 삭제됩니다.', + datasetUsedByApp: '이 지식은 일부 앱에서 사용 중입니다. 앱에서 더 이상 이 지식을 사용할 수 없게 되며, 모든 프롬프트 구성 및 로그가 영구적으로 삭제됩니다.', datasetDeleted: '지식이 삭제되었습니다', datasetDeleteFailed: '지식 삭제에 실패했습니다', didYouKnow: '알고 계셨나요?', diff --git a/web/i18n/ko-KR/login.ts b/web/i18n/ko-KR/login.ts index ee0867d1db..01d1f538fe 100644 --- a/web/i18n/ko-KR/login.ts +++ b/web/i18n/ko-KR/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: '계정이 없으신가요?', invalidInvitationCode: '유효하지 않은 초대 코드입니다.', accountAlreadyInited: '계정은 이미 초기화되었습니다.', + forgotPassword: '비밀번호를 잊으셨나요?', + resetLinkSent: '재설정 링크가 전송되었습니다', + sendResetLink: '재설정 링크 보내기', + backToSignIn: '로그인으로 돌아가기', + forgotPasswordDesc: '비밀번호를 재설정하려면 이메일 주소를 입력하세요. 비밀번호 재설정 방법에 대한 이메일을 보내드리겠습니다.', + checkEmailForResetLink: '비밀번호 재설정 링크를 확인하려면 이메일을 확인하세요. 몇 분 내에 나타나지 않으면 스팸 폴더를 확인하세요.', + passwordChanged: '지금 로그인', + changePassword: '비밀번호 변경', + changePasswordTip: '계정의 새 비밀번호를 입력하세요', + invalidToken: '유효하지 않거나 만료된 토큰', + confirmPassword: '비밀번호 확인', + confirmPasswordPlaceholder: '새 비밀번호를 확인하세요', + passwordChangedTip: '비밀번호가 성공적으로 변경되었습니다', error: { emailEmpty: '이메일 주소를 입력하세요.', emailInValid: '유효한 이메일 주소를 입력하세요.', diff --git a/web/i18n/ko-KR/tools.ts b/web/i18n/ko-KR/tools.ts index ab47c7e2a0..c896a17a4f 100644 --- a/web/i18n/ko-KR/tools.ts +++ b/web/i18n/ko-KR/tools.ts @@ -105,6 +105,8 @@ const translation = { customDisclaimerPlaceholder: '사용자 정의 권리 포기 문구를 입력해주세요.', confirmTitle: '저장하시겠습니까?', confirmTip: '이 도구를 사용하는 앱은 영향을 받습니다.', + deleteToolConfirmTitle: '이 도구를 삭제하시겠습니까?', + deleteToolConfirmContent: '이 도구를 삭제하면 되돌릴 수 없습니다. 사용자는 더 이상 당신의 도구에 액세스할 수 없습니다.', }, test: { title: '테스트', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index f8405a1ffd..5eef217cac 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: '실행 취소', + redo: '다시 실행', editing: '편집 중', autoSaved: '자동 저장됨', unpublished: '미발행', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: '워크플로우 업데이트 후 도구 재구성이 필요합니다.', viewDetailInTracingPanel: '세부 정보 보기', }, + changeHistory: { + title: '변경 기록', + placeholder: '아직 아무 것도 변경하지 않았습니다', + clearHistory: '기록 지우기', + hint: '힌트', + 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: '블록 연결 해제됨', + }, errorMsg: { fieldRequired: '{{field}}가 필요합니다', authRequired: '인증이 필요합니다', diff --git a/web/i18n/languages.json b/web/i18n/languages.json index 1017344471..d93d163db0 100644 --- a/web/i18n/languages.json +++ b/web/i18n/languages.json @@ -33,7 +33,7 @@ "name": "Español (España)", "prompt_name": "Spanish", "example": "Saluton, Dify!", - "supported": false + "supported": true }, { "value": "fr-FR", diff --git a/web/i18n/pl-PL/app-overview.ts b/web/i18n/pl-PL/app-overview.ts index 08aa7547ab..95e8aadb70 100644 --- a/web/i18n/pl-PL/app-overview.ts +++ b/web/i18n/pl-PL/app-overview.ts @@ -53,6 +53,10 @@ const translation = { show: 'Pokaż', hide: 'Ukryj', }, + chatColorTheme: 'Motyw kolorystyczny czatu', + chatColorThemeDesc: 'Ustaw motyw kolorystyczny czatu', + chatColorThemeInverted: 'Odwrócony', + invalidHexMessage: 'Nieprawidłowa wartość heksadecymalna', more: { entry: 'Pokaż więcej ustawień', copyright: 'Prawa autorskie', diff --git a/web/i18n/pl-PL/dataset.ts b/web/i18n/pl-PL/dataset.ts index 2401004a22..5351b3c739 100644 --- a/web/i18n/pl-PL/dataset.ts +++ b/web/i18n/pl-PL/dataset.ts @@ -9,6 +9,7 @@ const translation = { deleteDatasetConfirmTitle: 'Czy na pewno usunąć tę Wiedzę?', deleteDatasetConfirmContent: 'Usunięcie Wiedzy jest nieodwracalne. Użytkownicy nie będą już mieli dostępu do Twojej Wiedzy, a wszystkie konfiguracje i logi zostaną trwale usunięte.', + datasetUsedByApp: 'Ta wiedza jest wykorzystywana przez niektóre aplikacje. Aplikacje nie będą już mogły korzystać z tej Wiedzy, a wszystkie konfiguracje podpowiedzi i logi zostaną trwale usunięte.', datasetDeleted: 'Wiedza usunięta', datasetDeleteFailed: 'Nie udało się usunąć Wiedzy', didYouKnow: 'Czy wiedziałeś?', diff --git a/web/i18n/pl-PL/login.ts b/web/i18n/pl-PL/login.ts index b629ee598a..075b79b913 100644 --- a/web/i18n/pl-PL/login.ts +++ b/web/i18n/pl-PL/login.ts @@ -39,6 +39,19 @@ const translation = { donthave: 'Nie masz?', invalidInvitationCode: 'Niewłaściwy kod zaproszenia', accountAlreadyInited: 'Konto już zainicjowane', + forgotPassword: 'Zapomniałeś hasła?', + resetLinkSent: 'Link resetujący został wysłany', + sendResetLink: 'Wyślij link resetujący', + backToSignIn: 'Powrót do logowania', + forgotPasswordDesc: 'Proszę podać swój adres e-mail, aby zresetować hasło. Wyślemy Ci e-mail z instrukcjami, jak zresetować hasło.', + checkEmailForResetLink: 'Proszę sprawdzić swój e-mail w poszukiwaniu linku do resetowania hasła. Jeśli nie pojawi się w ciągu kilku minut, sprawdź folder spam.', + passwordChanged: 'Zaloguj się teraz', + changePassword: 'Zmień hasło', + changePasswordTip: 'Wprowadź nowe hasło do swojego konta', + invalidToken: 'Nieprawidłowy lub wygasły token', + confirmPassword: 'Potwierdź hasło', + confirmPasswordPlaceholder: 'Potwierdź nowe hasło', + passwordChangedTip: 'Twoje hasło zostało pomyślnie zmienione', error: { emailEmpty: 'Adres e-mail jest wymagany', emailInValid: 'Proszę wpisać prawidłowy adres e-mail', diff --git a/web/i18n/pl-PL/tools.ts b/web/i18n/pl-PL/tools.ts index 77749544f6..f5d3226e8c 100644 --- a/web/i18n/pl-PL/tools.ts +++ b/web/i18n/pl-PL/tools.ts @@ -75,6 +75,8 @@ const translation = { privacyPolicyPlaceholder: 'Proszę wprowadzić politykę prywatności', customDisclaimer: 'Oświadczenie niestandardowe', customDisclaimerPlaceholder: 'Proszę wprowadzić oświadczenie niestandardowe', + deleteToolConfirmTitle: 'Skasuj ten przyrząd?', + deleteToolConfirmContent: 'Usunięcie narzędzia jest nieodwracalne. Użytkownicy nie będą mieli już dostępu do Twojego narzędzia.', }, test: { title: 'Test', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 3bba30c295..0b56b41ad7 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Cofnij', + redo: 'Ponów', editing: 'Edytowanie', autoSaved: 'Automatycznie zapisane', unpublished: 'Nieopublikowane', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'Wymagana rekonfiguracja narzędzia po aktualizacji przepływu pracy.', viewDetailInTracingPanel: 'Zobacz szczegóły', }, + changeHistory: { + title: 'Historia Zmian', + placeholder: 'Nie dokonano jeszcze żadnych zmian', + clearHistory: 'Wyczyść Historię', + hint: 'Wskazówka', + hintText: 'Działania edycji są śledzone w historii zmian, która jest przechowywana na urządzeniu przez czas trwania tej sesji. Ta historia zostanie usunięta po opuszczeniu edytora.', + stepBackward_one: '{{count}} krok do tyłu', + stepBackward_other: '{{count}} kroki do tyłu', + stepForward_one: '{{count}} krok do przodu', + 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', + }, errorMsg: { fieldRequired: '{{field}} jest wymagane', authRequired: 'Wymagana autoryzacja', diff --git a/web/i18n/pt-BR/app-overview.ts b/web/i18n/pt-BR/app-overview.ts index b8c6feb846..d288e331b3 100644 --- a/web/i18n/pt-BR/app-overview.ts +++ b/web/i18n/pt-BR/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Mostrar', hide: 'Ocultar', }, + chatColorTheme: 'Tema de cor do chatbot', + chatColorThemeDesc: 'Defina o tema de cor do chatbot', + chatColorThemeInverted: 'Inve', + invalidHexMessage: 'Valor hex inválido', more: { entry: 'Mostrar mais configurações', copyright: 'Direitos autorais', diff --git a/web/i18n/pt-BR/dataset.ts b/web/i18n/pt-BR/dataset.ts index eacdce33e9..f6f76e4626 100644 --- a/web/i18n/pt-BR/dataset.ts +++ b/web/i18n/pt-BR/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Excluir este Conhecimento?', deleteDatasetConfirmContent: 'A exclusão do Conhecimento é irreversível. Os usuários não poderão mais acessar seu Conhecimento e todas as configurações e registros de prompt serão excluídos permanentemente.', + datasetUsedByApp: 'O conhecimento está sendo usado por alguns aplicativos. Os aplicativos não poderão mais usar esse Conhecimento, e todas as configurações de prompt e logs serão excluídos permanentemente.', datasetDeleted: 'Conhecimento excluído', datasetDeleteFailed: 'Falha ao excluir o Conhecimento', didYouKnow: 'Você sabia?', diff --git a/web/i18n/pt-BR/login.ts b/web/i18n/pt-BR/login.ts index 722c7eecf2..88312778c3 100644 --- a/web/i18n/pt-BR/login.ts +++ b/web/i18n/pt-BR/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'Não tem?', invalidInvitationCode: 'Código de convite inválido', accountAlreadyInited: 'Conta já iniciada', + forgotPassword: 'Esqueceu sua senha?', + resetLinkSent: 'Link de redefinição enviado', + sendResetLink: 'Enviar link de redefinição', + backToSignIn: 'Voltar para login', + forgotPasswordDesc: 'Por favor, insira seu endereço de e-mail para redefinir sua senha. Enviaremos um e-mail com instruções sobre como redefinir sua senha.', + checkEmailForResetLink: 'Verifique seu e-mail para um link para redefinir sua senha. Se não aparecer dentro de alguns minutos, verifique sua pasta de spam.', + passwordChanged: 'Entre agora', + changePassword: 'Mudar a senha', + changePasswordTip: 'Por favor, insira uma nova senha para sua conta', + invalidToken: 'Token inválido ou expirado', + confirmPassword: 'Confirme a Senha', + confirmPasswordPlaceholder: 'Confirme sua nova senha', + passwordChangedTip: 'Sua senha foi alterada com sucesso', error: { emailEmpty: 'O endereço de e-mail é obrigatório', emailInValid: 'Digite um endereço de e-mail válido', diff --git a/web/i18n/pt-BR/tools.ts b/web/i18n/pt-BR/tools.ts index 1e646bb3cf..28b6e82176 100644 --- a/web/i18n/pt-BR/tools.ts +++ b/web/i18n/pt-BR/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: 'Digite a política de privacidade', customDisclaimer: 'Aviso Personalizado', customDisclaimerPlaceholder: 'Digite o aviso personalizado', + deleteToolConfirmTitle: 'Excluir esta ferramenta?', + deleteToolConfirmContent: 'Excluir a ferramenta é irreversível. Os usuários não poderão mais acessar sua ferramenta.', }, test: { title: 'Testar', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 83667ed796..e5fd21dd4d 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Desfazer', + redo: 'Refazer', editing: 'Editando', autoSaved: 'Salvo automaticamente', unpublished: 'Não publicado', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'É necessária a reconfiguração da ferramenta após a atualização do fluxo de trabalho.', viewDetailInTracingPanel: 'Ver detalhes', }, + changeHistory: { + title: 'Histórico de alterações', + placeholder: 'Você ainda não alterou nada', + clearHistory: 'Limpar histórico', + hint: 'Dica', + hintText: 'As ações de edição são rastreadas em um histórico de alterações, que é armazenado em seu dispositivo para a duração desta sessão. Este histórico será apagado quando você sair do editor.', + stepBackward_one: '{{count}} passo para trás', + stepBackward_other: '{{count}} passos para trás', + stepForward_one: '{{count}} passo para frente', + 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', + }, errorMsg: { fieldRequired: '{{field}} é obrigatório', authRequired: 'Autorização é necessária', diff --git a/web/i18n/ro-RO/app-overview.ts b/web/i18n/ro-RO/app-overview.ts index 5bb4aabb17..007a76a10e 100644 --- a/web/i18n/ro-RO/app-overview.ts +++ b/web/i18n/ro-RO/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Afișați', hide: 'Ascundeți', }, + chatColorTheme: 'Tema de culoare a chatului', + chatColorThemeDesc: 'Setați tema de culoare a chatbotului', + chatColorThemeInverted: 'Inversat', + invalidHexMessage: 'Valoare hex nevalidă', more: { entry: 'Afișați mai multe setări', copyright: 'Drepturi de autor', diff --git a/web/i18n/ro-RO/dataset.ts b/web/i18n/ro-RO/dataset.ts index eb6f8e84b4..363d882b09 100644 --- a/web/i18n/ro-RO/dataset.ts +++ b/web/i18n/ro-RO/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Ștergeți această Cunoștință?', deleteDatasetConfirmContent: 'Ștergerea Cunoștințelor este ireversibilă. Utilizatorii nu vor mai putea accesa Cunoștințele, iar toate configurațiile și jurnalele prompt vor fi șterse permanent.', + datasetUsedByApp: 'Cunoștințele sunt utilizate de unele aplicații. Aplicațiile nu vor mai putea utiliza aceste Cunoștințe, iar toate configurațiile de prompt și jurnalele vor fi șterse definitiv.', datasetDeleted: 'Cunoștințe șterse', datasetDeleteFailed: 'Eșec la ștergerea Cunoștințelor', didYouKnow: 'Știați că?', diff --git a/web/i18n/ro-RO/login.ts b/web/i18n/ro-RO/login.ts index 3f22f8d169..c8a0fad91c 100644 --- a/web/i18n/ro-RO/login.ts +++ b/web/i18n/ro-RO/login.ts @@ -35,6 +35,19 @@ const translation = { donthave: 'Nu ai?', invalidInvitationCode: 'Cod de invitație invalid', accountAlreadyInited: 'Contul este deja inițializat', + forgotPassword: 'Ați uitat parola?', + resetLinkSent: 'Link de resetare trimis', + sendResetLink: 'Trimiteți linkul de resetare', + backToSignIn: 'Înapoi la autentificare', + forgotPasswordDesc: 'Vă rugăm să introduceți adresa de e-mail pentru a reseta parola. Vă vom trimite un e-mail cu instrucțiuni despre cum să resetați parola.', + checkEmailForResetLink: 'Vă rugăm să verificați e-mailul pentru un link de resetare a parolei. Dacă nu apare în câteva minute, verificați folderul de spam.', + passwordChanged: 'Conectează-te acum', + changePassword: 'Schimbă parola', + changePasswordTip: 'Vă rugăm să introduceți o nouă parolă pentru contul dvs', + invalidToken: 'Token invalid sau expirat', + confirmPassword: 'Confirmă parola', + confirmPasswordPlaceholder: 'Confirmați noua parolă', + passwordChangedTip: 'Parola dvs. a fost schimbată cu succes', error: { emailEmpty: 'Adresa de email este obligatorie', emailInValid: 'Te rugăm să introduci o adresă de email validă', diff --git a/web/i18n/ro-RO/tools.ts b/web/i18n/ro-RO/tools.ts index a35633fa95..e878162426 100644 --- a/web/i18n/ro-RO/tools.ts +++ b/web/i18n/ro-RO/tools.ts @@ -71,6 +71,8 @@ const translation = { }, privacyPolicy: 'Politica de Confidențialitate', privacyPolicyPlaceholder: 'Vă rugăm să introduceți politica de confidențialitate', + deleteToolConfirmTitle: 'Ștergeți această unealtă?', + deleteToolConfirmContent: ' Ștergerea uneltă este irreversibilă. Utilizatorii nu vor mai putea accesa uneltă dvs.', }, test: { title: 'Testează', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index d9afddae22..b97d043c0a 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Anulează', + redo: 'Refă', editing: 'Editare', autoSaved: 'Salvat automat', unpublished: 'Nepublicat', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'Reconfigurarea instrumentului este necesară după actualizarea fluxului de lucru.', viewDetailInTracingPanel: 'Vezi detalii', }, + changeHistory: { + title: 'Istoric modificări', + placeholder: 'Nu ați schimbat nimic încă', + clearHistory: 'Șterge istoricul', + hint: 'Sfat', + hintText: 'Acțiunile dvs. de editare sunt urmărite într-un istoric al modificărilor, care este stocat pe dispozitivul dvs. pe durata acestei sesiuni. Acest istoric va fi șters când veți părăsi editorul.', + stepBackward_one: '{{count}} pas înapoi', + stepBackward_other: '{{count}} pași înapoi', + stepForward_one: '{{count}} pas înainte', + 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', + }, errorMsg: { fieldRequired: '{{field}} este obligatoriu', authRequired: 'Autorizarea este necesară', diff --git a/web/i18n/uk-UA/app-overview.ts b/web/i18n/uk-UA/app-overview.ts index b1fcc92d4a..8bd1f0fb39 100644 --- a/web/i18n/uk-UA/app-overview.ts +++ b/web/i18n/uk-UA/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Показати', hide: 'Приховати', }, + chatColorTheme: 'Тема кольору чату', + chatColorThemeDesc: 'Встановіть тему кольору чат-бота', + chatColorThemeInverted: 'Інвертовано', + invalidHexMessage: 'Недійсне шістнадцяткове значення', more: { entry: 'Показати додаткові налаштування', copyright: 'Авторське право', diff --git a/web/i18n/uk-UA/dataset.ts b/web/i18n/uk-UA/dataset.ts index e4f26f739c..fb44b4107a 100644 --- a/web/i18n/uk-UA/dataset.ts +++ b/web/i18n/uk-UA/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Видалити це Знання?', deleteDatasetConfirmContent: 'Видалення "Знання" є незворотнім. Користувачі більше не матимуть доступу до Знань, а всі конфігурації підказок і журнали будуть безповоротно видалені.', + datasetUsedByApp: 'Ці знання використовуються деякими додатками. Додатки більше не зможуть використовувати ці Знання, а всі конфігурації підказок та журнали будуть остаточно видалені.', datasetDeleted: 'Знання видалено', datasetDeleteFailed: 'Не вдалося видалити Знання', didYouKnow: 'Чи знаєте ви?', diff --git a/web/i18n/uk-UA/login.ts b/web/i18n/uk-UA/login.ts index 95bb7ccfea..46de22bec2 100644 --- a/web/i18n/uk-UA/login.ts +++ b/web/i18n/uk-UA/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'Не маєте?', invalidInvitationCode: 'Недійсний код запрошення', accountAlreadyInited: 'Обліковий запис уже ініціалізовано', + forgotPassword: 'Забули пароль?', + resetLinkSent: 'Посилання для скидання надіслано', + sendResetLink: 'Надіслати посилання для скидання', + backToSignIn: 'Повернутися до входу', + forgotPasswordDesc: 'Будь ласка, введіть свою електронну адресу, щоб скинути пароль. Ми надішлемо вам електронного листа з інструкціями щодо скидання пароля.', + checkEmailForResetLink: 'Будь ласка, перевірте свою електронну пошту на наявність посилання для скидання пароля. Якщо протягом кількох хвилин не з’явиться, перевірте папку зі спамом.', + passwordChanged: 'Увійдіть зараз', + changePassword: 'Змінити пароль', + changePasswordTip: 'Будь ласка, введіть новий пароль для свого облікового запису', + invalidToken: 'Недійсний або прострочений токен', + confirmPassword: 'Підтвердити пароль', + confirmPasswordPlaceholder: 'Підтвердьте новий пароль', + passwordChangedTip: 'Ваш пароль було успішно змінено', error: { emailEmpty: 'Адреса електронної пошти обов\'язкова', emailInValid: 'Введіть дійсну адресу електронної пошти', diff --git a/web/i18n/uk-UA/tools.ts b/web/i18n/uk-UA/tools.ts index ac6bcca919..313332e3a4 100644 --- a/web/i18n/uk-UA/tools.ts +++ b/web/i18n/uk-UA/tools.ts @@ -72,6 +72,8 @@ const translation = { privacyPolicyPlaceholder: 'Введіть політику конфіденційності', customDisclaimer: 'Власний відомості', customDisclaimerPlaceholder: 'Введіть власні відомості', + deleteToolConfirmTitle: 'Видалити цей інструмент?', + deleteToolConfirmContent: 'Видалення інструменту є незворотнім. Користувачі більше не зможуть отримати доступ до вашого інструменту.', }, test: { diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index f551b5c3e5..b8e43bf46f 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: 'Скасувати', + redo: 'Повторити', editing: 'Редагування', autoSaved: 'Автоматично збережено', unpublished: 'Неопубліковано', @@ -68,6 +70,32 @@ const translation = { workflowAsToolTip: 'Після оновлення робочого потоку необхідна переконфігурація інструменту.', viewDetailInTracingPanel: 'Переглянути деталі', }, + changeHistory: { + title: 'Історія змін', + placeholder: 'Ви ще нічого не змінили', + clearHistory: 'Очистити історію', + hint: 'Підказка', + 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: 'Блок відключено', + }, errorMsg: { fieldRequired: '{{field}} є обов\'язковим', authRequired: 'Потрібна авторизація', diff --git a/web/i18n/vi-VN/app-overview.ts b/web/i18n/vi-VN/app-overview.ts index 09005e46d9..c023850d71 100644 --- a/web/i18n/vi-VN/app-overview.ts +++ b/web/i18n/vi-VN/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: 'Hiển thị', hide: 'Ẩn', }, + chatColorTheme: 'Chủ đề màu sắc trò chuyện', + chatColorThemeDesc: 'Thiết lập chủ đề màu sắc của chatbot', + chatColorThemeInverted: 'Đảo ngược', + invalidHexMessage: 'Giá trị không hợp lệ của hệ màu hex', more: { entry: 'Hiển thị thêm cài đặt', copyright: 'Bản quyền', diff --git a/web/i18n/vi-VN/dataset.ts b/web/i18n/vi-VN/dataset.ts index 017cf08236..6e32079bb2 100644 --- a/web/i18n/vi-VN/dataset.ts +++ b/web/i18n/vi-VN/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: 'Xóa Kiến thức này?', deleteDatasetConfirmContent: 'Xóa Kiến thức là không thể đảo ngược. Người dùng sẽ không còn có khả năng truy cập Kiến thức của bạn nữa, và tất cả các cấu hình và nhật ký nhắc nhở sẽ bị xóa vĩnh viễn.', + datasetUsedByApp: 'Kiến thức này đang được sử dụng bởi một số ứng dụng. Các ứng dụng sẽ không thể sử dụng Kiến thức này nữa, và tất cả cấu hình lời nhắc và nhật ký sẽ bị xóa vĩnh viễn.', datasetDeleted: 'Kiến thức đã bị xóa', datasetDeleteFailed: 'Xóa Kiến thức không thành công', didYouKnow: 'Bạn đã biết chưa?', diff --git a/web/i18n/vi-VN/login.ts b/web/i18n/vi-VN/login.ts index b8ae56a017..1fd3e55dfe 100644 --- a/web/i18n/vi-VN/login.ts +++ b/web/i18n/vi-VN/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: 'Chưa có?', invalidInvitationCode: 'Mã mời không hợp lệ', accountAlreadyInited: 'Tài khoản đã được khởi tạo', + forgotPassword: 'Quên mật khẩu?', + resetLinkSent: 'Đã gửi liên kết đặt lại mật khẩu', + sendResetLink: 'Gửi liên kết đặt lại mật khẩu', + backToSignIn: 'Quay lại đăng nhập', + forgotPasswordDesc: 'Vui lòng nhập địa chỉ email của bạn để đặt lại mật khẩu. Chúng tôi sẽ gửi cho bạn một email với hướng dẫn về cách đặt lại mật khẩu.', + checkEmailForResetLink: 'Vui lòng kiểm tra email của bạn để nhận liên kết đặt lại mật khẩu. Nếu không thấy trong vài phút, hãy kiểm tra thư mục spam.', + passwordChanged: 'Đăng nhập ngay', + changePassword: 'Đổi mật khẩu', + changePasswordTip: 'Vui lòng nhập mật khẩu mới cho tài khoản của bạn', + invalidToken: 'Mã thông báo không hợp lệ hoặc đã hết hạn', + confirmPassword: 'Xác nhận mật khẩu', + confirmPasswordPlaceholder: 'Xác nhận mật khẩu mới của bạn', + passwordChangedTip: 'Mật khẩu của bạn đã được thay đổi thành công', error: { emailEmpty: 'Địa chỉ Email là bắt buộc', emailInValid: 'Vui lòng nhập một địa chỉ email hợp lệ', diff --git a/web/i18n/vi-VN/tools.ts b/web/i18n/vi-VN/tools.ts index a774003eaa..faf491d892 100644 --- a/web/i18n/vi-VN/tools.ts +++ b/web/i18n/vi-VN/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: 'Vui lòng nhập chính sách bảo mật', customDisclaimer: 'Tuyên bố Tùy chỉnh', customDisclaimerPlaceholder: 'Vui lòng nhập tuyên bố tùy chỉnh', + deleteToolConfirmTitle: 'Xóa công cụ này?', + deleteToolConfirmContent: 'Xóa công cụ là không thể hồi tơi. Người dùng sẽ không thể truy cập lại công cụ của bạn.', }, test: { title: 'Kiểm tra', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index e36bda2f79..40fd91144e 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -1,5 +1,33 @@ const translation = { common: { + undo: 'Hoàn tác', + redo: 'Làm lại', + changeHistory: { + title: 'Lịch sử thay đổi', + placeholder: 'Bạn chưa thay đổi gì cả', + clearHistory: 'Xóa lịch sử', + hint: 'Gợi ý', + hintText: 'Các hành động chỉnh sửa của bạn được theo dõi trong lịch sử thay đổi, được lưu trên thiết bị của bạn trong suốt phiên làm việc này. Lịch sử này sẽ bị xóa khi bạn thoát khỏi trình soạn thảo.', + stepBackward_one: '{{count}} bước lùi', + stepBackward_other: '{{count}} bước lùi', + stepForward_one: '{{count}} bước tiến', + 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', + }, editing: 'Chỉnh sửa', autoSaved: 'Tự động lưu', unpublished: 'Chưa xuất bản', diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index 16638cab70..556dfe539e 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: '显示', hide: '隐藏', }, + chatColorTheme: '聊天颜色主题', + chatColorThemeDesc: '设置聊天机器人的颜色主题', + chatColorThemeInverted: '反转', + invalidHexMessage: '无效的十六进制值', more: { entry: '展示更多设置', copyright: '版权', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index dd603951f2..4008a247c9 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -84,6 +84,42 @@ const translation = { workflow: '工作流', completion: '文本生成', }, + tracing: { + title: '追踪应用性能', + description: '配置第三方 LLMOps 提供商并跟踪应用程序性能。', + config: '配置', + collapse: '折叠', + expand: '展开', + tracing: '追踪', + disabled: '已禁用', + disabledTip: '请先配置提供商', + enabled: '已启用', + tracingDescription: '捕获应用程序执行的完整上下文,包括 LLM 调用、上下文、提示、HTTP 请求等,发送到第三方跟踪平台。', + configProviderTitle: { + configured: '已配置', + notConfigured: '配置提供商以启用追踪', + moreProvider: '更多提供商', + }, + langsmith: { + title: 'LangSmith', + description: '一个全方位的开发者平台,适用于 LLM 驱动应用程序生命周期的每个步骤。', + }, + langfuse: { + title: 'Langfuse', + description: '跟踪、评估、提示管理和指标,以调试和改进您的 LLM 应用程序。', + }, + inUse: '使用中', + configProvider: { + title: '配置 ', + placeholder: '输入你的{{key}}', + project: '项目', + publicKey: '公钥', + secretKey: '密钥', + viewDocsLink: '查看 {{key}} 的文档', + removeConfirmTitle: '删除 {{key}} 配置?', + removeConfirmContent: '当前配置正在使用中,删除它将关闭追踪功能。', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 07b4367502..49fe6f6cad 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -12,6 +12,7 @@ const translation = { cancel: '取消', clear: '清空', save: '保存', + saveAndEnable: '保存并启用', edit: '编辑', add: '添加', added: '已添加', @@ -435,7 +436,7 @@ const translation = { latestAvailable: 'Dify {{version}} 已是最新版本。', }, appMenus: { - overview: '概览', + overview: '监测', promptEng: '编排', apiAccess: '访问 API', logAndAnn: '日志与标注', diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 57b20a1abc..20881ab57e 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: '要删除知识库吗?', deleteDatasetConfirmContent: '删除知识库是不可逆的。用户将无法再访问您的知识库,所有的提示配置和日志将被永久删除。', + datasetUsedByApp: '某些应用正在使用该知识库。应用将无法再使用该知识库,所有的提示配置和日志将被永久删除。', datasetDeleted: '知识库已删除', datasetDeleteFailed: '删除知识库失败', didYouKnow: '你知道吗?', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index 10c5ee4497..5ac9b9fcb4 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: '还没有邀请码?', invalidInvitationCode: '无效的邀请码', accountAlreadyInited: '账户已经初始化', + forgotPassword: '忘记密码?', + resetLinkSent: '重置链接已发送', + sendResetLink: '发送重置链接', + backToSignIn: '返回登录', + forgotPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。', + checkEmailForResetLink: '请检查您的电子邮件以获取重置密码的链接。如果几分钟内没有收到,请检查您的垃圾邮件文件夹。', + passwordChanged: '立即登录', + changePassword: '更改密码', + changePasswordTip: '请输入您的新密码', + invalidToken: '无效或已过期的令牌', + confirmPassword: '确认密码', + confirmPasswordPlaceholder: '确认您的新密码', + passwordChangedTip: '您的密码已成功更改', error: { emailEmpty: '邮箱不能为空', emailInValid: '请输入有效的邮箱地址', diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 46451d25b4..9064bbd263 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -105,6 +105,8 @@ const translation = { customDisclaimerPlaceholder: '请输入自定义免责声明', confirmTitle: '确认保存?', confirmTip: '发布新的工具版本可能会影响该工具已关联的应用', + deleteToolConfirmTitle: '删除这个工具?', + deleteToolConfirmContent: '删除工具是不可逆的。用户将无法再访问您的工具。', }, test: { title: '测试', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 1fbaf38cc5..a71b22c8e0 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: '撤销', + redo: '重做', editing: '编辑中', autoSaved: '自动保存', unpublished: '未发布', @@ -68,6 +70,39 @@ const translation = { workflowAsToolTip: '工作流更新后需要重新配置工具参数', viewDetailInTracingPanel: '查看详细信息', syncingData: '同步数据中,只需几秒钟。', + importDSL: '导入 DSL', + importDSLTip: '当前草稿将被覆盖。在导入之前请导出工作流作为备份。', + backupCurrentDraft: '备份当前草稿', + chooseDSL: '选择 DSL(yml) 文件', + overwriteAndImport: '覆盖并导入', + importFailure: '导入失败', + importSuccess: '导入成功', + }, + changeHistory: { + title: '变更历史', + placeholder: '尚未更改任何内容', + clearHistory: '清除历史记录', + hint: '提示', + 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: '块已断开连接', }, errorMsg: { fieldRequired: '{{field}} 不能为空', @@ -361,6 +396,7 @@ const translation = { url: '图片链接', upload_file_id: '上传文件ID', }, + json: '工具生成的json', }, }, questionClassifiers: { diff --git a/web/i18n/zh-Hant/app-overview.ts b/web/i18n/zh-Hant/app-overview.ts index e93c24ed8a..ecf2b5c3b1 100644 --- a/web/i18n/zh-Hant/app-overview.ts +++ b/web/i18n/zh-Hant/app-overview.ts @@ -49,6 +49,10 @@ const translation = { show: '展示', hide: '隱藏', }, + chatColorTheme: '聊天顏色主題', + chatColorThemeDesc: '設定聊天機器人的顏色主題', + chatColorThemeInverted: '反轉', + invalidHexMessage: '無效的十六進制值', more: { entry: '展示更多設定', copyright: '版權', diff --git a/web/i18n/zh-Hant/dataset.ts b/web/i18n/zh-Hant/dataset.ts index 410335d0ee..8de7bc487f 100644 --- a/web/i18n/zh-Hant/dataset.ts +++ b/web/i18n/zh-Hant/dataset.ts @@ -8,6 +8,7 @@ const translation = { deleteDatasetConfirmTitle: '要刪除知識庫嗎?', deleteDatasetConfirmContent: '刪除知識庫是不可逆的。使用者將無法再訪問您的知識庫,所有的提示配置和日誌將被永久刪除。', + datasetUsedByApp: '這些知識正被一些應用程序使用。應用程序將無法再使用這些知識,所有提示配置和日誌將被永久刪除。', datasetDeleted: '知識庫已刪除', datasetDeleteFailed: '刪除知識庫失敗', didYouKnow: '你知道嗎?', diff --git a/web/i18n/zh-Hant/login.ts b/web/i18n/zh-Hant/login.ts index 3b8a986fd0..cce869f38a 100644 --- a/web/i18n/zh-Hant/login.ts +++ b/web/i18n/zh-Hant/login.ts @@ -34,6 +34,19 @@ const translation = { donthave: '還沒有邀請碼?', invalidInvitationCode: '無效的邀請碼', accountAlreadyInited: '賬戶已經初始化', + forgotPassword: '忘記密碼?', + resetLinkSent: '重設連結已發送', + sendResetLink: '發送重設連結', + backToSignIn: '返回登錄', + forgotPasswordDesc: '請輸入您的電子郵件地址以重設密碼。我們將向您發送一封電子郵件,說明如何重設密碼。', + checkEmailForResetLink: '請檢查您的電子郵件以獲取重設密碼的連結。如果幾分鐘內沒有收到,請檢查您的垃圾郵件文件夾。', + passwordChanged: '立即登入', + changePassword: '更改密碼', + changePasswordTip: '請輸入您的新密碼', + invalidToken: '無效或已過期的令牌', + confirmPassword: '確認密碼', + confirmPasswordPlaceholder: '確認您的新密碼', + passwordChangedTip: '您的密碼已成功更改', error: { emailEmpty: '郵箱不能為空', emailInValid: '請輸入有效的郵箱地址', diff --git a/web/i18n/zh-Hant/tools.ts b/web/i18n/zh-Hant/tools.ts index 1b6d7cedd2..58ba9f5c81 100644 --- a/web/i18n/zh-Hant/tools.ts +++ b/web/i18n/zh-Hant/tools.ts @@ -73,6 +73,8 @@ const translation = { privacyPolicyPlaceholder: '請輸入隱私協議', customDisclaimer: '自定義免責聲明', customDisclaimerPlaceholder: '請輸入自定義免責聲明', + deleteToolConfirmTitle: '刪除這個工具?', + deleteToolConfirmContent: '刪除工具是不可逆的。用戶將無法再訪問您的工具。', }, test: { title: '測試', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index d960586198..86974f75e6 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -1,5 +1,7 @@ const translation = { common: { + undo: '復原', + redo: '重做', editing: '編輯中', autoSaved: '自動保存', unpublished: '未發佈', @@ -68,6 +70,31 @@ const translation = { workflowAsToolTip: '工作流更新後需要重新配置工具參數', viewDetailInTracingPanel: '查看詳細信息', }, + changeHistory: { + title: '變更履歷', + placeholder: '尚未更改任何內容', + clearHistory: '清除歷史記錄', + hint: '提示', + 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: '註釋已更改', + edgeDelete: '區塊已斷開連接', + }, errorMsg: { fieldRequired: '{{field}} 不能為空', authRequired: '請先授權', diff --git a/web/models/app.ts b/web/models/app.ts index ac8f502fd2..80d121c7a3 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -1,3 +1,4 @@ +import type { LangFuseConfig, LangSmithConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' import type { App, AppTemplate, SiteConfig } from '@/types/app' /* export type App = { @@ -129,3 +130,13 @@ export type AppVoicesListResponse = [{ name: string value: string }] + +export type TracingStatus = { + enabled: boolean + tracing_provider: TracingProvider | null +} + +export type TracingConfig = { + tracing_provider: TracingProvider + tracing_config: LangSmithConfig | LangFuseConfig +} diff --git a/web/models/share.ts b/web/models/share.ts index 32b565eb65..47a87e2fdb 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -11,6 +11,8 @@ export type ConversationItem = { export type SiteInfo = { title: string + chat_color_theme?: string + chat_color_theme_inverted?: boolean icon?: string icon_background?: string description?: string diff --git a/web/package.json b/web/package.json index f796ef1a67..71819c176c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "0.6.11", + "version": "0.6.12-fix1", "private": true, "scripts": { "dev": "next dev", @@ -42,6 +42,7 @@ "echarts": "^5.4.1", "echarts-for-react": "^3.0.2", "emoji-mart": "^5.5.2", + "fast-deep-equal": "^3.1.3", "i18next": "^22.4.13", "i18next-resources-to-backend": "^1.1.3", "immer": "^9.0.19", @@ -89,7 +90,8 @@ "use-context-selector": "^1.4.1", "uuid": "^9.0.1", "zod": "^3.23.6", - "zustand": "^4.5.1" + "zundo": "^2.1.0", + "zustand": "^4.5.2" }, "devDependencies": { "@antfu/eslint-config": "^0.36.0", diff --git a/web/service/apps.ts b/web/service/apps.ts index b20739898e..cd71ceadae 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -1,8 +1,9 @@ import type { Fetcher } from 'swr' -import { del, get, post, put } from './base' -import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' +import { del, get, patch, post, put } from './base' +import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' import type { CommonResponse } from '@/models/common' import type { AppMode, ModelConfig } from '@/types/app' +import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => { return get<AppListResponse>(url, { params }) @@ -121,3 +122,32 @@ export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { u export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => { return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`) } + +// Tracing +export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => { + return get(`/apps/${appId}/trace`) +} + +export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => { + return post(`/apps/${appId}/trace`, { body }) +} + +export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => { + return get(`/apps/${appId}/trace-config`, { + params: { + tracing_provider: provider, + }, + }) +} + +export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => { + return post(`/apps/${appId}/trace-config`, { body }) +} + +export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => { + return patch(`/apps/${appId}/trace-config`, { body }) +} + +export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => { + return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`) +} diff --git a/web/service/base.ts b/web/service/base.ts index c4cea6f995..ccf731f476 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -1,6 +1,6 @@ import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config' import Toast from '@/app/components/base/toast' -import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/app/chat/type' +import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type' import type { VisionFile } from '@/types/app' import type { IterationFinishedResponse, diff --git a/web/service/common.ts b/web/service/common.ts index a68aeb2256..3fbcde2a2a 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -298,3 +298,13 @@ export const enableModel = (url: string, body: { model: string; model_type: Mode export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) => patch<CommonResponse>(url, { body }) + +export const sendForgotPasswordEmail: Fetcher<CommonResponse, { url: string; body: { email: string } }> = ({ url, body }) => + post<CommonResponse>(url, { body }) + +export const verifyForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => { + return post(url, { body }) as Promise<CommonResponse & { is_valid: boolean; email: string }> +} + +export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => + post<CommonResponse>(url, { body }) diff --git a/web/service/datasets.ts b/web/service/datasets.ts index 35330a0dec..a0905208fa 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -72,6 +72,12 @@ export const createEmptyDataset: Fetcher<DataSet, { name: string }> = ({ name }) return post<DataSet>('/datasets', { body: { name } }) } +export const checkIsUsedInApp: Fetcher<{ is_using: boolean }, string> = (id) => { + return get<{ is_using: boolean }>(`/datasets/${id}/use-check`, {}, { + silent: true, + }) +} + export const deleteDataset: Fetcher<DataSet, string> = (datasetID) => { return del<DataSet>(`/datasets/${datasetID}`) } diff --git a/web/service/debug.ts b/web/service/debug.ts index 6899990f71..a373a0dd6a 100644 --- a/web/service/debug.ts +++ b/web/service/debug.ts @@ -55,7 +55,7 @@ export const fetchSuggestedQuestions = (appId: string, messageId: string, getAbo ) } -export const fetchConvesationMessages = (appId: string, conversation_id: string, getAbortController?: any) => { +export const fetchConversationMessages = (appId: string, conversation_id: string, getAbortController?: any) => { return get(`apps/${appId}/chat-messages`, { params: { conversation_id, diff --git a/web/service/share.ts b/web/service/share.ts index 14a14e266d..d4de81ddc7 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -3,7 +3,7 @@ import { del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost, delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost, } from './base' -import type { Feedbacktype } from '@/app/components/app/chat/type' +import type { Feedbacktype } from '@/app/components/base/chat/chat/type' import type { AppConversationData, AppData, diff --git a/web/service/workflow.ts b/web/service/workflow.ts index 3cf524d481..4a47c99947 100644 --- a/web/service/workflow.ts +++ b/web/service/workflow.ts @@ -54,3 +54,7 @@ export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {} params: { q: JSON.stringify(query) }, }) } + +export const updateWorkflowDraftFromDSL = (appId: string, data: string) => { + return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } }) +} diff --git a/web/types/app.ts b/web/types/app.ts index 3ad4592e67..294d2980a8 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -246,6 +246,12 @@ export type SiteConfig = { title: string /** Application Description will be shown in the Client */ description: string + /** Define the color in hex for different elements of the chatbot, such as: + * The header, the button , etc. + */ + chat_color_theme: string + /** Invert the color of the theme set in chat_color_theme */ + chat_color_theme_inverted: boolean /** Author */ author: string /** User Support Email Address */ diff --git a/web/utils/index.ts b/web/utils/index.ts index 1fe2e82b9d..8afd8afae7 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -1,3 +1,5 @@ +import { escape } from 'lodash-es' + export const sleep = (ms: number) => { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -35,5 +37,5 @@ export const getPurifyHref = (href: string) => { if (!href) return '' - return href.replace(/javascript:/ig, '').replace(/vbscript:/ig, '').replace(/data:/ig, '') + return escape(href) } diff --git a/web/yarn.lock b/web/yarn.lock index 997f961e3d..393e81cf97 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" @@ -68,23 +73,23 @@ yaml-eslint-parser "^1.1.0" "@babel/code-frame@^7.0.0": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz" - integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + version "7.22.5" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.22.5" -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== dependencies: - "@babel/helper-validator-identifier" "^7.18.6" + "@babel/helper-validator-identifier" "^7.22.5" chalk "^2.0.0" js-tokens "^4.0.0" @@ -94,11 +99,18 @@ integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.21.5", "@babel/runtime@^7.22.3", "@babel/runtime@^7.3.1": - version "7.22.3" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz" - integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ== + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz" + integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.23.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" "@braintree/sanitize-url@^6.0.1": version "6.0.4" @@ -137,18 +149,18 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0": - version "4.5.1" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz" - integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + version "4.6.2" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== "@eslint/eslintrc@^2.0.1": - version "2.0.3" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz" - integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== + version "2.1.0" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz" + integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.2" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -166,7 +178,14 @@ resolved "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz" integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== -"@floating-ui/core@^1.1.0", "@floating-ui/core@^1.4.1": +"@floating-ui/core@^1.0.0": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a" + integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg== + dependencies: + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/core@^1.1.0": version "1.4.1" resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz" integrity sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ== @@ -180,27 +199,27 @@ dependencies: "@floating-ui/core" "^1.1.0" -"@floating-ui/dom@^1.5.1": - version "1.5.1" - resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz" - integrity sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw== +"@floating-ui/dom@^1.0.0": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" + integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== dependencies: - "@floating-ui/core" "^1.4.1" - "@floating-ui/utils" "^0.1.1" + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" -"@floating-ui/react-dom@^2.0.1": - version "2.0.2" - resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz" - integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ== +"@floating-ui/react-dom@^2.0.2": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" + integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== dependencies: - "@floating-ui/dom" "^1.5.1" + "@floating-ui/dom" "^1.0.0" "@floating-ui/react@^0.25.2": - version "0.25.2" - resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.25.2.tgz" - integrity sha512-3e10G9LFOgl32/SMWLBOwT7oVCtB+d5zBsU2GxTSVOvRgZexwno5MlYbc0BaXr+TR5EEGpqe9tg9OUbjlrVRnQ== + version "0.25.3" + resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.25.3.tgz" + integrity sha512-Ti3ClVZIUqZq1OCkfbhsBA8u3m8jJ0h9gAInFwdrLaa+yTAZx3bFH8YR+/wQwPmRrpgJJ3cRhCfx4puz0PqVIA== dependencies: - "@floating-ui/react-dom" "^2.0.1" + "@floating-ui/react-dom" "^2.0.2" "@floating-ui/utils" "^0.1.1" tabbable "^6.0.1" @@ -209,6 +228,11 @@ resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz" integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== +"@floating-ui/utils@^0.2.0": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" + integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== + "@formatjs/intl-localematcher@^0.5.4": version "0.5.4" resolved "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz" @@ -217,9 +241,9 @@ tslib "^2.4.0" "@headlessui/react@^1.7.13": - version "1.7.15" - resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.15.tgz" - integrity sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw== + version "1.7.17" + resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz" + integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow== dependencies: client-only "^0.0.1" @@ -365,18 +389,6 @@ resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz#148e96dfd6e68747da41a311b9ee4559bb1b1471" integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" @@ -386,33 +398,28 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + version "0.3.22" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@lexical/clipboard@0.16.0": version "0.16.0" @@ -673,17 +680,17 @@ resolved "https://registry.npmjs.org/@next/env/-/env-14.2.4.tgz#5546813dc4f809884a37d257b254a5ce1b0248d7" integrity sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg== -"@next/eslint-plugin-next@14.1.0": - version "14.1.0" - resolved "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz" - integrity sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q== +"@next/eslint-plugin-next@14.0.4": + version "14.0.4" + resolved "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.4.tgz" + integrity sha512-U3qMNHmEZoVmHA0j/57nRfi3AscXNvkOnxDmle/69Jz/G0o/gWjXTDdlgILZdrxQ0Lw/jv2mPW8PGy0EGIHXhQ== dependencies: - glob "10.3.10" + glob "7.1.7" "@next/mdx@^14.0.4": - version "14.1.0" - resolved "https://registry.npmjs.org/@next/mdx/-/mdx-14.1.0.tgz" - integrity sha512-YLYsViq91+H8+3oCtK1iuMWdeN14K70Hy6/tYScY+nfo5bQ84A/A+vA6UdNC9MkbWQ/373hQubx2p4JvUjlb2Q== + version "14.0.4" + resolved "https://registry.npmjs.org/@next/mdx/-/mdx-14.0.4.tgz" + integrity sha512-w0b+A2LRdlqqTIzmaeqPOaafid2cYYYjETA+G+3ZFwkNbBQjvZp57P1waOexF3MGHzcCEoXEnhYpAc+FO6S0Rg== dependencies: source-map "^0.7.0" @@ -753,22 +760,17 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - "@pkgr/utils@^2.3.1": - version "2.4.1" - resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz" - integrity sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w== + version "2.4.2" + resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz" + integrity sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw== dependencies: cross-spawn "^7.0.3" - fast-glob "^3.2.12" + fast-glob "^3.3.0" is-glob "^4.0.3" open "^9.1.0" picocolors "^1.0.0" - tslib "^2.5.0" + tslib "^2.6.0" "@reactflow/background@11.3.13": version "11.3.13" @@ -847,73 +849,73 @@ integrity sha512-pBiltENdy8SfI0AeR1e5TRpS9/9Gl0eiOEt6ful2jQfzsgvZYWqsKiBWaOCLdocQuk0wS7KOHI37n0C1pnKqTw== "@rushstack/eslint-patch@^1.3.3": - version "1.7.2" - resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz" - integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== + version "1.6.1" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz" + integrity sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw== -"@sentry-internal/tracing@7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.54.0.tgz" - integrity sha512-JsyhZ0wWZ+VqbHJg+azqRGdYJDkcI5R9+pnkO6SzbzxrRewqMAIwzkpPee3oI7vG99uhMEkOkMjHu0nQGwkOQw== +"@sentry-internal/tracing@7.60.1": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.60.1.tgz" + integrity sha512-2vM+3/ddzmoBfi92OOD9FFTHXf0HdQhKtNM26+/RsmkKnTid+/inbvA7nKi+Qa7ExcnlC6eclEHQEg+0X3yDkQ== dependencies: - "@sentry/core" "7.54.0" - "@sentry/types" "7.54.0" - "@sentry/utils" "7.54.0" - tslib "^1.9.3" + "@sentry/core" "7.60.1" + "@sentry/types" "7.60.1" + "@sentry/utils" "7.60.1" + tslib "^2.4.1 || ^1.9.3" -"@sentry/browser@7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/browser/-/browser-7.54.0.tgz" - integrity sha512-EvLAw03N9WE2m1CMl2/1YMeIs1icw9IEOVJhWmf3uJEysNJOFWXu6ZzdtHEz1E6DiJYhc1HzDya0ExZeJxNARA== +"@sentry/browser@7.60.1": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/browser/-/browser-7.60.1.tgz" + integrity sha512-opZQee3S0c459LXt8YGpwOM/qiTlzluHEEnfW2q+D2yVCWh8iegsDX3kbRiv4i/mtQu9yPhM9M761KDnc/0eZw== dependencies: - "@sentry-internal/tracing" "7.54.0" - "@sentry/core" "7.54.0" - "@sentry/replay" "7.54.0" - "@sentry/types" "7.54.0" - "@sentry/utils" "7.54.0" - tslib "^1.9.3" + "@sentry-internal/tracing" "7.60.1" + "@sentry/core" "7.60.1" + "@sentry/replay" "7.60.1" + "@sentry/types" "7.60.1" + "@sentry/utils" "7.60.1" + tslib "^2.4.1 || ^1.9.3" -"@sentry/core@7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/core/-/core-7.54.0.tgz" - integrity sha512-MAn0E2EwgNn1pFQn4qxhU+1kz6edullWg6VE5wCmtpXWOVw6sILBUsQpeIG5djBKMcneJCdOlz5jeqcKPrLvZQ== +"@sentry/core@7.60.1": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/core/-/core-7.60.1.tgz" + integrity sha512-yr/0VFYWOJyXj+F2nifkRYxXskotsNnDggUnFOZZN2ZgTG94IzRFsOZQ6RslHJ8nrYPTBNO74reU0C0GB++xRw== dependencies: - "@sentry/types" "7.54.0" - "@sentry/utils" "7.54.0" - tslib "^1.9.3" + "@sentry/types" "7.60.1" + "@sentry/utils" "7.60.1" + tslib "^2.4.1 || ^1.9.3" "@sentry/react@^7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/react/-/react-7.54.0.tgz" - integrity sha512-qUbwmRRpTh05m2rbC8A2zAFQYsoHhwIpxT5UXxh0P64ZlA3cSg1/DmTTgwnd1l+7gzKrc31UikXQ4y0YDbMNKg== + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/react/-/react-7.60.1.tgz" + integrity sha512-977wb5gp7SHv9kHPs1HZtL60slt2WBFY9/YJI9Av7BjjJ/A89OhtBwbVhIcKXZ4hwHQVWuOiFCJdMrIfZXpFPA== dependencies: - "@sentry/browser" "7.54.0" - "@sentry/types" "7.54.0" - "@sentry/utils" "7.54.0" + "@sentry/browser" "7.60.1" + "@sentry/types" "7.60.1" + "@sentry/utils" "7.60.1" hoist-non-react-statics "^3.3.2" - tslib "^1.9.3" + tslib "^2.4.1 || ^1.9.3" -"@sentry/replay@7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/replay/-/replay-7.54.0.tgz" - integrity sha512-C0F0568ybphzGmKGe23duB6n5wJcgM7WLYhoeqW3o2bHeqpj1dGPSka/K3s9KzGaAgzn1zeOUYXJsOs+T/XdsA== +"@sentry/replay@7.60.1": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/replay/-/replay-7.60.1.tgz" + integrity sha512-WHQxEpJbHICs12L17LGgS/ql91yn9wJDH/hgb+1H90HaasjoR54ofWCKul29OvYV0snTWuHd6xauwtzyv9tzvg== dependencies: - "@sentry/core" "7.54.0" - "@sentry/types" "7.54.0" - "@sentry/utils" "7.54.0" + "@sentry/core" "7.60.1" + "@sentry/types" "7.60.1" + "@sentry/utils" "7.60.1" -"@sentry/types@7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/types/-/types-7.54.0.tgz" - integrity sha512-D+i9xogBeawvQi2r0NOrM7zYcUaPuijeME4O9eOTrDF20tj71hWtJLilK+KTGLYFtpGg1h+9bPaz7OHEIyVopg== +"@sentry/types@7.60.1": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/types/-/types-7.60.1.tgz" + integrity sha512-8lKKSCOhZ953cWxwnfZwoR3ZFFlZG4P3PQFTaFt/u4LxLh/0zYbdtgvtUqXRURjMCi5P6ddeE9Uw9FGnTJCsTw== -"@sentry/utils@7.54.0", "@sentry/utils@^7.54.0": - version "7.54.0" - resolved "https://registry.npmjs.org/@sentry/utils/-/utils-7.54.0.tgz" - integrity sha512-3Yf5KlKjIcYLddOexSt2ovu2TWlR4Fi7M+aCK8yUTzwNzf/xwFSWOstHlD/WiDy9HvfhWAOB/ukNTuAeJmtasw== +"@sentry/utils@7.60.1", "@sentry/utils@^7.54.0": + version "7.60.1" + resolved "https://registry.npmjs.org/@sentry/utils/-/utils-7.60.1.tgz" + integrity sha512-ik+5sKGBx4DWuvf6UUKPSafaDiASxP+Xvjg3C9ppop2I/JWxP1FfZ5g22n5ZmPmNahD6clTSoTWly8qyDUlUOw== dependencies: - "@sentry/types" "7.54.0" - tslib "^1.9.3" + "@sentry/types" "7.60.1" + tslib "^2.4.1 || ^1.9.3" "@swc/counter@^0.1.3": version "0.1.3" @@ -1044,9 +1046,9 @@ "@types/geojson" "*" "@types/d3-hierarchy@*": - version "3.1.6" - resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz" - integrity sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw== + version "3.1.7" + resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== "@types/d3-interpolate@*": version "3.0.4" @@ -1056,9 +1058,9 @@ "@types/d3-color" "*" "@types/d3-path@*": - version "3.0.2" - resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz" - integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA== + version "3.1.0" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== "@types/d3-polygon@*": version "3.0.2" @@ -1185,9 +1187,9 @@ "@types/estree" "*" "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + version "1.0.5" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/geojson@*": version "7946.0.14" @@ -1195,11 +1197,11 @@ integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== "@types/hast@^2.0.0": - version "2.3.4" - resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz" - integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== + version "2.3.5" + resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz" + integrity sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg== dependencies: - "@types/unist" "*" + "@types/unist" "^2" "@types/js-cookie@^2.x.x": version "2.2.7" @@ -1227,28 +1229,28 @@ integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA== "@types/katex@^0.16.0": - version "0.16.0" - resolved "https://registry.npmjs.org/@types/katex/-/katex-0.16.0.tgz" - integrity sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw== + version "0.16.2" + resolved "https://registry.npmjs.org/@types/katex/-/katex-0.16.2.tgz" + integrity sha512-dHsSjSlU/EWEEbeNADr3FtZZOAXPkFPUO457QCnoNqcZQXNqNEu/svQd0Nritvd3wNff4vvC/f4e6xgX3Llt8A== "@types/lodash-es@^4.17.7": - version "4.17.7" - resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz" - integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ== + version "4.17.8" + resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.8.tgz" + integrity sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog== dependencies: "@types/lodash" "*" "@types/lodash@*": - version "4.14.195" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz" - integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== + version "4.14.196" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz" + integrity sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ== "@types/mdast@^3.0.0": - version "3.0.11" - resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz" - integrity sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw== + version "3.0.12" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz" + integrity sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg== dependencies: - "@types/unist" "*" + "@types/unist" "^2" "@types/mdx@^2.0.0": version "2.0.5" @@ -1351,10 +1353,15 @@ resolved "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.1.tgz" integrity sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ== -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": - version "2.0.6" - resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/unist@^2": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" + integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== + +"@types/unist@^2.0.0", "@types/unist@^2.0.2": + version "2.0.7" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz" + integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g== "@types/uuid@^9.0.8": version "9.0.8" @@ -1362,87 +1369,87 @@ integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== "@typescript-eslint/eslint-plugin@^5.53.0": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz" - integrity sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA== + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/type-utils" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" debug "^4.3.4" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" natural-compare-lite "^1.4.0" semver "^7.3.7" tsutils "^3.21.0" "@typescript-eslint/parser@^5.4.2 || ^6.0.0", "@typescript-eslint/parser@^5.53.0": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz" - integrity sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ== + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.9": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz" - integrity sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ== +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/type-utils@5.59.9": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz" - integrity sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q== +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: - "@typescript-eslint/typescript-estree" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.59.9": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz" - integrity sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/typescript-estree@5.59.9": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz" - integrity sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.9", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.53.0": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz" - integrity sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg== +"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.53.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.59.9": - version "5.59.9" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz" - integrity sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q== +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: - "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" "@vue/compiler-core@3.4.25": @@ -1474,10 +1481,15 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.0.0, acorn@^8.5.0, acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.0.0, acorn@^8.5.0: + version "8.10.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +acorn@^8.9.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" + integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== aggregate-error@^3.0.0: version "3.1.0" @@ -1493,9 +1505,9 @@ ahooks-v3-count@^1.0.0: integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== ahooks@^3.7.5: - version "3.7.7" - resolved "https://registry.npmjs.org/ahooks/-/ahooks-3.7.7.tgz" - integrity sha512-5e5WlPq81Y84UnTLOKIQeq2cJw4aa7yj8fR2Nb/oMmXPrWMjIMCbPS1o+fpxSfCaNA3AzOnnMc8AehWRZltkJQ== + version "3.7.8" + resolved "https://registry.npmjs.org/ahooks/-/ahooks-3.7.8.tgz" + integrity sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA== dependencies: "@babel/runtime" "^7.21.0" "@types/js-cookie" "^2.x.x" @@ -1549,7 +1561,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.0.0, ansi-styles@^6.1.0: +ansi-styles@^6.0.0: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -1577,12 +1589,12 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.1.3: - version "5.1.3" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: - deep-equal "^2.0.5" + dequal "^2.0.3" array-buffer-byte-length@^1.0.0: version "1.0.0" @@ -1592,7 +1604,15 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7: +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -1619,7 +1639,7 @@ array.prototype.findlastindex@^1.2.3: es-shim-unscopables "^1.0.0" get-intrinsic "^1.2.1" -array.prototype.flat@^1.3.2: +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: version "1.3.2" resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz" integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== @@ -1640,15 +1660,15 @@ array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: es-shim-unscopables "^1.0.0" array.prototype.tosorted@^1.1.1: - version "1.1.2" - resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz" - integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== + version "1.1.1" + resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz" + integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== dependencies: call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" - get-intrinsic "^1.2.1" + get-intrinsic "^1.1.3" arraybuffer.prototype.slice@^1.0.2: version "1.0.2" @@ -1663,10 +1683,24 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== astral-regex@^2.0.0: version "2.0.0" @@ -1709,17 +1743,24 @@ available-typed-arrays@^1.0.5: resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axe-core@^4.6.2: - version "4.7.2" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz" - integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== - -axobject-query@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz" - integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: - deep-equal "^2.0.5" + possible-typed-array-names "^1.0.0" + +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== + +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" bail@^2.0.0: version "2.0.2" @@ -1761,29 +1802,22 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.21.5: - version "4.21.7" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz" - integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== + version "4.22.3" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== dependencies: - caniuse-lite "^1.0.30001489" - electron-to-chromium "^1.4.411" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" builtin-modules@^3.3.0: version "3.3.0" @@ -1820,6 +1854,17 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -1830,17 +1875,22 @@ camelcase-css@^2.0.1: resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001489, caniuse-lite@^1.0.30001579: +caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001580: version "1.0.30001581" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz" integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== +caniuse-lite@^1.0.30001579: + version "1.0.30001636" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" + integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@4.1.1, chalk@^4.1.1: +chalk@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz" integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== @@ -1862,7 +1912,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1933,9 +1983,9 @@ class-variance-authority@^0.7.0: clsx "2.0.0" classcat@^5.0.3, classcat@^5.0.4: - version "5.0.4" - resolved "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz" - integrity sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g== + version "5.0.5" + resolved "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz" + integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== classnames@2.3.1: version "2.3.1" @@ -2119,7 +2169,7 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2449,10 +2499,37 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + dayjs@^1.11.7, dayjs@^1.9.1: - version "1.11.8" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz" - integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== + version "1.11.9" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== debug@^3.2.7: version "3.2.7" @@ -2475,30 +2552,6 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" -deep-equal@^2.0.5: - version "2.2.1" - resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz" - integrity sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.2" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.0" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.0" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -2531,6 +2584,15 @@ define-data-property@^1.0.1, define-data-property@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz" @@ -2552,7 +2614,7 @@ delaunator@5: dependencies: robust-predicates "^3.0.0" -dequal@^2.0.0: +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -2647,17 +2709,17 @@ echarts-for-react@^3.0.2: size-sensor "^1.0.1" echarts@^5.4.1: - version "5.4.2" - resolved "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz" - integrity sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA== + version "5.4.3" + resolved "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz" + integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA== dependencies: tslib "2.3.0" - zrender "5.4.3" + zrender "5.4.4" -electron-to-chromium@^1.4.411: - version "1.4.423" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.423.tgz" - integrity sha512-y4A7YfQcDGPAeSWM1IuoWzXpg9RY1nwHzHSwRtCSQFp9FgAVDgdWlFf0RbdWfLWQ2WUI+bddUgk5RgTjqRE6FQ== +electron-to-chromium@^1.4.648: + version "1.4.650" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz" + integrity sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ== elkjs@^0.8.2: version "0.8.2" @@ -2680,9 +2742,9 @@ emoji-regex@^9.2.2: integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== enhanced-resolve@^5.12.0: - version "5.14.1" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz" - integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== + version "5.15.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2744,20 +2806,69 @@ es-abstract@^1.20.4, es-abstract@^1.22.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.13" -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== +es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-iterator-helpers@^1.0.12: version "1.0.15" @@ -2779,6 +2890,33 @@ es-iterator-helpers@^1.0.12: iterator.prototype "^1.1.2" safe-array-concat "^1.0.1" +es-iterator-helpers@^1.0.15: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz" @@ -2788,6 +2926,15 @@ es-set-tostringtag@^2.0.1: has "^1.0.3" has-tostringtag "^1.0.0" +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" @@ -2825,11 +2972,11 @@ escape-string-regexp@^5.0.0: integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== eslint-config-next@^14.0.4: - version "14.1.0" - resolved "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.1.0.tgz" - integrity sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg== + version "14.0.4" + resolved "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.4.tgz" + integrity sha512-9/xbOHEQOmQtqvQ1UsTQZpnA7SlDMBtuKJ//S4JnoyK3oGLhILKXdBgu/UO7lQo/2xOykQULS1qQ6p2+EpHgAQ== dependencies: - "@next/eslint-plugin-next" "14.1.0" + "@next/eslint-plugin-next" "14.0.4" "@rushstack/eslint-patch" "^1.3.3" "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" eslint-import-resolver-node "^0.3.6" @@ -2923,42 +3070,42 @@ eslint-plugin-import@^2.27.5, eslint-plugin-import@^2.28.1: tsconfig-paths "^3.15.0" eslint-plugin-jest@^27.2.1: - version "27.2.1" - resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz" - integrity sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg== + version "27.2.3" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.3.tgz" + integrity sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ== dependencies: "@typescript-eslint/utils" "^5.10.0" eslint-plugin-jsonc@^2.6.0: - version "2.8.0" - resolved "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.8.0.tgz" - integrity sha512-K4VsnztnNwpm+V49CcCu5laq8VjclJpuhfI9LFkOrOyK+BKdQHMzkWo43B4X4rYaVrChm4U9kw/tTU5RHh5Wtg== + version "2.9.0" + resolved "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.9.0.tgz" + integrity sha512-RK+LeONVukbLwT2+t7/OY54NJRccTXh/QbnXzPuTLpFMVZhPuq1C9E07+qWenGx7rrQl0kAalAWl7EmB+RjpGA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" jsonc-eslint-parser "^2.0.4" natural-compare "^1.4.0" eslint-plugin-jsx-a11y@^6.7.1: - version "6.7.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== + version "6.8.0" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== dependencies: - "@babel/runtime" "^7.20.7" - aria-query "^5.1.3" - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - ast-types-flow "^0.0.7" - axe-core "^4.6.2" - axobject-query "^3.1.1" + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.3" - language-tags "=1.0.5" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - semver "^6.3.0" + object.entries "^1.1.7" + object.fromentries "^2.0.7" eslint-plugin-markdown@^3.0.0: version "3.0.0" @@ -2992,9 +3139,9 @@ eslint-plugin-promise@^6.1.1: integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== "eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": - version "5.0.0-canary-7118f5dd7-20230705" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz" - integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== + version "4.6.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.33.2: version "7.33.2" @@ -3048,9 +3195,9 @@ eslint-plugin-unused-imports@^2.0.0: eslint-rule-composer "^0.3.0" eslint-plugin-vue@^9.9.0: - version "9.14.1" - resolved "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz" - integrity sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw== + version "9.15.1" + resolved "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.15.1.tgz" + integrity sha512-CJE/oZOslvmAR9hf8SClTdQ9JLweghT6JCBQNrT2Iel1uVw0W0OLJxzvPd6CxmABKCvLrtyDnqGV37O7KQv6+A== dependencies: "@eslint-community/eslint-utils" "^4.3.0" natural-compare "^1.4.0" @@ -3061,9 +3208,9 @@ eslint-plugin-vue@^9.9.0: xml-name-validator "^4.0.0" eslint-plugin-yml@^1.5.0: - version "1.7.0" - resolved "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.7.0.tgz" - integrity sha512-qq61FQJk+qIgWl0R06bec7UQQEIBrUH22jS+MroTbFUKu+3/iVlGRpZd8mjpOAm/+H/WEDFwy4x/+kKgVGbsWw== + version "1.8.0" + resolved "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.8.0.tgz" + integrity sha512-fgBiJvXD0P2IN7SARDJ2J7mx8t0bLdG6Zcig4ufOqW5hOvSiFxeUyc2g5I1uIm8AExbo26NNYCcTGZT0MXTsyg== dependencies: debug "^4.3.2" lodash "^4.17.21" @@ -3084,9 +3231,9 @@ eslint-scope@^5.1.1: estraverse "^4.1.1" eslint-scope@^7.1.1: - version "7.2.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz" - integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== + version "7.2.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz" + integrity sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -3166,12 +3313,12 @@ eslint@^8.36.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.0.0, espree@^9.3.1, espree@^9.5.0, espree@^9.5.2: - version "9.5.2" - resolved "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz" - integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== +espree@^9.0.0, espree@^9.3.1, espree@^9.5.0, espree@^9.6.0: + version "9.6.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" @@ -3270,9 +3417,9 @@ execa@^5.0.0: strip-final-newline "^2.0.0" execa@^7.0.0, execa@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz" - integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== + version "7.2.0" + resolved "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz" + integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== dependencies: cross-spawn "^7.0.3" get-stream "^6.0.1" @@ -3294,10 +3441,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== +fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3336,10 +3483,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3379,14 +3526,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - format@^0.2.0: version "0.2.2" resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" @@ -3437,6 +3576,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" @@ -3450,10 +3600,19 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + get-tsconfig@^4.5.0: - version "4.6.0" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.0.tgz" - integrity sha512-lgbo68hHTQnFddybKbbs/RDRJnJT5YyGy2kQzVwbq+g67X73i+5MVTval34QxGkOe9X5Ujf1UYpCaphLyltjEg== + version "4.6.2" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz" + integrity sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg== dependencies: resolve-pkg-maps "^1.0.0" @@ -3471,17 +3630,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@10.3.10: - version "10.3.10" - resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - glob@7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" @@ -3494,15 +3642,15 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +glob@7.1.7, glob@^7.1.3: + version "7.1.7" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.1.1" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -3533,13 +3681,13 @@ globby@^11.1.0: slash "^3.0.0" globby@^13.1.3: - version "13.1.4" - resolved "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz" - integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== + version "13.2.2" + resolved "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== dependencies: dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" + fast-glob "^3.3.0" + ignore "^5.2.4" merge2 "^1.4.1" slash "^4.0.0" @@ -3560,6 +3708,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" @@ -3575,18 +3728,30 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz" - integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== dependencies: - get-intrinsic "^1.2.2" + get-intrinsic "^1.1.1" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" has-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== +has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" @@ -3599,6 +3764,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -3613,6 +3785,13 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-util-from-dom@^4.0.0: version "4.2.0" resolved "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-4.2.0.tgz" @@ -3808,7 +3987,7 @@ iconv-lite@0.6: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ignore@^5.0.5, ignore@^5.1.1, ignore@^5.2.0: +ignore@^5.0.5, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -3819,9 +3998,9 @@ immer@^9.0.19: integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== immutable@^4.0.0: - version "4.3.0" - resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz" - integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== + version "4.3.1" + resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.1.tgz" + integrity sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -3859,7 +4038,7 @@ inline-style-parser@0.1.1: resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== -internal-slot@^1.0.4, internal-slot@^1.0.5: +internal-slot@^1.0.3, internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz" integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== @@ -3868,6 +4047,15 @@ internal-slot@^1.0.4, internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + "internmap@1 - 2": version "2.0.3" resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" @@ -3909,14 +4097,6 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz" @@ -3926,6 +4106,14 @@ is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: get-intrinsic "^1.2.0" is-typed-array "^1.1.10" +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -3982,13 +4170,20 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.9.0: version "2.13.1" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: hasown "^2.0.0" +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" @@ -4069,7 +4264,7 @@ is-inside-container@^1.0.0: dependencies: is-docker "^3.0.0" -is-map@^2.0.1, is-map@^2.0.2: +is-map@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz" integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== @@ -4079,6 +4274,11 @@ is-negative-zero@^2.0.2: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + is-number-object@^1.0.4: version "1.0.7" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" @@ -4116,7 +4316,7 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-set@^2.0.1, is-set@^2.0.2: +is-set@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz" integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== @@ -4128,6 +4328,13 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" @@ -4159,6 +4366,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: dependencies: which-typed-array "^1.1.11" +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz" @@ -4207,19 +4421,10 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - jiti@^1.18.2: - version "1.18.2" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz" - integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== + version "1.19.1" + resolved "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz" + integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== js-audio-recorder@^1.0.7: version "1.0.7" @@ -4237,9 +4442,9 @@ js-cookie@^3.0.1: integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== js-sdsl@^4.1.4: - version "4.4.0" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz" - integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== + version "4.4.2" + resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz" + integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -4295,13 +4500,15 @@ jsonc-eslint-parser@^2.0.4, jsonc-eslint-parser@^2.1.0: espree "^9.0.0" semver "^7.3.5" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: - version "3.3.3" - resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" - integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== dependencies: - array-includes "^3.1.5" - object.assign "^4.1.3" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" katex@^0.16.0, katex@^0.16.10: version "0.16.10" @@ -4327,17 +4534,17 @@ lamejs@^1.2.1: dependencies: use-strict "1.0.1" -language-subtag-registry@~0.3.2: - version "0.3.22" - resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== -language-tags@=1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" layout-base@^1.0.0: version "1.0.2" @@ -4373,9 +4580,9 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@^13.2.2: - version "13.2.2" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.2.tgz" - integrity sha512-71gSwXKy649VrSU09s10uAT0rWCcY3aewhMaHyl2N84oBk4Xs9HgxvUp3AYu+bNsK4NrOYYxvSgg7FyGJ+jGcA== + version "13.2.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz" + integrity sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg== dependencies: chalk "5.2.0" cli-truncate "^3.1.0" @@ -4486,11 +4693,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - markdown-extensions@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz" @@ -5175,30 +5377,18 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.4" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== - mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" @@ -5275,10 +5465,10 @@ next@^14.1.1: "@next/swc-win32-ia32-msvc" "14.2.4" "@next/swc-win32-x64-msvc" "14.2.4" -node-releases@^2.0.12: - version "2.0.12" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz" - integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== non-layered-tidy-tree-layout@^2.0.2: version "2.0.2" @@ -5346,20 +5536,12 @@ object-inspect@^1.12.3, object-inspect@^1.13.1, object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.3, object.assign@^4.1.4: +object.assign@^4.1.4: version "4.1.4" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== @@ -5369,14 +5551,33 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz" - integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== + version "1.1.7" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" object.fromentries@^2.0.6, object.fromentries@^2.0.7: version "2.0.7" @@ -5398,12 +5599,12 @@ object.groupby@^1.0.1: get-intrinsic "^1.2.1" object.hasown@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== + version "1.1.2" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" object.values@^1.1.6, object.values@^1.1.7: version "1.1.7" @@ -5446,16 +5647,16 @@ open@^9.1.0: is-wsl "^2.2.0" optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + version "0.9.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" p-limit@^2.2.0: version "2.3.0" @@ -5577,14 +5778,6 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== - dependencies: - lru-cache "^9.1.1 || ^10.0.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -5620,9 +5813,9 @@ pify@^2.3.0: integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pirates@^4.0.1: - version "4.0.5" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + version "4.0.6" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== pluralize@^8.0.0: version "8.0.0" @@ -5638,6 +5831,11 @@ portfinder@^1.0.28: debug "^3.2.7" mkdirp "^0.5.6" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-import@^15.1.0: version "15.1.0" resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz" @@ -5758,9 +5956,9 @@ queue-microtask@^1.2.2: integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== rc-input@~1.3.5: - version "1.3.6" - resolved "https://registry.npmjs.org/rc-input/-/rc-input-1.3.6.tgz" - integrity sha512-/HjTaKi8/Ts4zNbYaB5oWCquxFyFQO4Co1MnMgoCeGJlpe7k8Eir2HN0a0F9IHDmmo+GYiGgPpz7w/d/krzsJA== + version "1.3.5" + resolved "https://registry.npmjs.org/rc-input/-/rc-input-1.3.5.tgz" + integrity sha512-SPPwbTJa5ACHNoDdGZF/70AOqqm1Rir3WleuFBKq+nFby1zvpnzvWsHJgzWOr6uJ0GNt8dTMzBrmVGQJkTXqqQ== dependencies: "@babel/runtime" "^7.11.1" classnames "^2.2.1" @@ -5818,9 +6016,9 @@ react-error-boundary@^3.1.4: "@babel/runtime" "^7.12.5" react-error-boundary@^4.0.2: - version "4.0.9" - resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.9.tgz" - integrity sha512-f6DcHVdTDZmc9ixmRmuLDZpkdghYR/HKZdUzMLHD58s4cR2C4R6y4ktYztCosM6pyeK4/C8IofwqxgID25W6kw== + version "4.0.10" + resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.10.tgz" + integrity sha512-pvVKdi77j2OoPHo+p3rorgE43OjDWiqFkaqkJz8sJKK6uf/u8xtzuaVfj5qJ2JnDLIgF1De3zY5AJDijp+LVPA== dependencies: "@babel/runtime" "^7.12.5" @@ -5896,9 +6094,9 @@ react-papaparse@^4.1.0: papaparse "^5.3.1" react-slider@^2.0.4: - version "2.0.5" - resolved "https://registry.npmjs.org/react-slider/-/react-slider-2.0.5.tgz" - integrity sha512-MU5gaK1yYCKnbDDN3CMiVcgkKZwMvdqK2xUEW7fFU37NAzRgS1FZbF9N7vP08E3XXNVhiuZnwVzUa3PYQAZIMg== + version "2.0.6" + resolved "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz" + integrity sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA== dependencies: prop-types "^15.8.1" @@ -6020,17 +6218,17 @@ refractor@^3.6.0: parse-entities "^2.0.0" prismjs "~1.27.0" -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regexp-tree@^0.1.24, regexp-tree@~0.1.1: version "0.1.27" resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: +regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz" integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== @@ -6039,6 +6237,16 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + regexpp@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" @@ -6153,11 +6361,11 @@ resolve@^1.22.4: supports-preserve-symlinks-flag "^1.0.0" resolve@^2.0.0-next.4: - version "2.0.0-next.5" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz" - integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + version "2.0.0-next.4" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.9.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -6225,12 +6433,22 @@ sade@^1.7.3: mri "^1.1.0" safe-array-concat@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz" - integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== + version "1.0.1" + resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== dependencies: - call-bind "^1.0.5" - get-intrinsic "^1.2.2" + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" has-symbols "^1.0.3" isarray "^2.0.5" @@ -6243,6 +6461,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + safe-regex@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" @@ -6256,9 +6483,9 @@ safe-regex@^2.1.1: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.61.0: - version "1.62.1" - resolved "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz" - integrity sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A== + version "1.64.1" + resolved "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz" + integrity sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -6278,18 +6505,18 @@ screenfull@^5.0.0: "semver@2 || 3 || 4 || 5": version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.3.0, semver@^6.3.1: +semver@^6.3.1: version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.0.0, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + version "7.5.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -6299,15 +6526,26 @@ server-only@^0.0.1: integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== set-function-length@^1.1.1: - version "1.2.0" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz" - integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== + version "1.1.1" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== dependencies: define-data-property "^1.1.1" - function-bind "^1.1.2" - get-intrinsic "^1.2.2" + get-intrinsic "^1.2.1" gopd "^1.0.1" - has-property-descriptors "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" set-function-name@^2.0.0, set-function-name@^2.0.1: version "2.0.1" @@ -6373,11 +6611,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" @@ -6482,13 +6715,6 @@ state-local@^1.0.6: resolved "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz" integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" @@ -6499,15 +6725,6 @@ string-argv@^0.3.1: resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -6517,7 +6734,7 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: +string-width@^5.0.0: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -6527,18 +6744,17 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" string.prototype.matchall@^4.0.8: - version "4.0.10" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz" - integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + version "4.0.8" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== dependencies: call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - set-function-name "^2.0.0" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" string.prototype.trim@^1.2.8: @@ -6550,6 +6766,16 @@ string.prototype.trim@^1.2.8: define-properties "^1.2.0" es-abstract "^1.22.1" +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + string.prototype.trimend@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz" @@ -6559,6 +6785,15 @@ string.prototype.trimend@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimstart@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz" @@ -6568,6 +6803,15 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + stringify-entities@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" @@ -6576,13 +6820,6 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -6644,9 +6881,9 @@ stylis@^4.1.3: integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ== sucrase@^3.32.0: - version "3.32.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz" - integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ== + version "3.34.0" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz" + integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== dependencies: "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" @@ -6676,9 +6913,9 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== swr@^2.1.0: - version "2.1.5" - resolved "https://registry.npmjs.org/swr/-/swr-2.1.5.tgz" - integrity sha512-/OhfZMcEpuz77KavXST5q6XE9nrOBOVcBLWjMT+oAE/kQHyE3PASrevXCtQDZ8aamntOfFkbVJp7Il9tNBQWrw== + version "2.2.0" + resolved "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz" + integrity sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ== dependencies: use-sync-external-store "^1.2.0" @@ -6814,15 +7051,15 @@ tslib@2.3.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== -tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0: - version "2.5.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz" - integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3", tslib@^2.5.0, tslib@^2.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== tsutils@^3.21.0: version "3.21.0" @@ -6867,6 +7104,15 @@ typed-array-buffer@^1.0.0: get-intrinsic "^1.2.1" is-typed-array "^1.1.10" +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + typed-array-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz" @@ -6877,6 +7123,17 @@ typed-array-byte-length@^1.0.0: has-proto "^1.0.1" is-typed-array "^1.1.10" +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + typed-array-byte-offset@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz" @@ -6888,6 +7145,18 @@ typed-array-byte-offset@^1.0.0: has-proto "^1.0.1" is-typed-array "^1.1.10" +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz" @@ -6897,6 +7166,18 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + typescript@4.9.5: version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" @@ -7008,10 +7289,10 @@ untildify@^4.0.0: resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -7105,9 +7386,9 @@ void-elements@3.1.0: integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== vue-eslint-parser@^9.3.0: - version "9.3.0" - resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz" - integrity sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ== + version "9.3.1" + resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz" + integrity sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g== dependencies: debug "^4.3.4" eslint-scope "^7.1.1" @@ -7184,6 +7465,17 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -7191,20 +7483,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" @@ -7223,15 +7501,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" @@ -7276,17 +7545,22 @@ zod@^3.23.6: resolved "https://registry.npmjs.org/zod/-/zod-3.23.6.tgz#c08a977e2255dab1fdba933651584a05fcbf19e1" integrity sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA== -zrender@5.4.3: - version "5.4.3" - resolved "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz" - integrity sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ== +zrender@5.4.4: + version "5.4.4" + resolved "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz" + integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw== dependencies: tslib "2.3.0" -zustand@^4.4.1, zustand@^4.5.1: - version "4.5.1" - resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.1.tgz" - integrity sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg== +zundo@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/zundo/-/zundo-2.1.0.tgz" + integrity sha512-IMhYXDZWbyGu/p3rQb1d3orhCfAyi9hGkx6N579ZtO7mWrzvBdNyGEcxciv1jtIYPKBqLSAgzKqjLguau09f9g== + +zustand@^4.4.1, zustand@^4.5.2: + version "4.5.2" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz" + integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g== dependencies: use-sync-external-store "1.2.0"