| label: Self Checks | label: Self Checks | ||||
| description: "To make sure we get to you in time, please check the following :)" | description: "To make sure we get to you in time, please check the following :)" | ||||
| options: | options: | ||||
| - label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542). | |||||
| required: true | |||||
| - label: This is only for bug report, if you would like to ask a question, please head to [Discussions](https://github.com/langgenius/dify/discussions/categories/general). | - label: This is only for bug report, if you would like to ask a question, please head to [Discussions](https://github.com/langgenius/dify/discussions/categories/general). | ||||
| required: true | required: true | ||||
| - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | ||||
| required: true | required: true | ||||
| - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). | |||||
| - label: I confirm that I am using English to submit this report, otherwise it will be closed. | |||||
| required: true | required: true | ||||
| - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" | |||||
| - label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :) | |||||
| required: true | required: true | ||||
| - label: "Please do not modify this template :) and fill in all the required fields." | - label: "Please do not modify this template :) and fill in all the required fields." | ||||
| required: true | required: true | ||||
| attributes: | attributes: | ||||
| label: Steps to reproduce | label: Steps to reproduce | ||||
| description: We highly suggest including screenshots and a bug report log. Please use the right markdown syntax for code blocks. | description: We highly suggest including screenshots and a bug report log. Please use the right markdown syntax for code blocks. | ||||
| placeholder: Having detailed steps helps us reproduce the bug. | |||||
| placeholder: Having detailed steps helps us reproduce the bug. If you have logs, please use fenced code blocks (triple backticks ```) to format them. | |||||
| validations: | validations: | ||||
| required: true | required: true | ||||
| - type: textarea | - type: textarea | ||||
| attributes: | attributes: | ||||
| label: ✔️ Expected Behavior | label: ✔️ Expected Behavior | ||||
| placeholder: What were you expecting? | |||||
| description: Describe what you expected to happen. | |||||
| placeholder: What were you expecting? Please do not copy and paste the steps to reproduce here. | |||||
| validations: | validations: | ||||
| required: false | |||||
| required: true | |||||
| - type: textarea | - type: textarea | ||||
| attributes: | attributes: | ||||
| label: ❌ Actual Behavior | label: ❌ Actual Behavior | ||||
| placeholder: What happened instead? | |||||
| description: Describe what actually happened. | |||||
| placeholder: What happened instead? Please do not copy and paste the steps to reproduce here. | |||||
| validations: | validations: | ||||
| required: false | required: false |
| blank_issues_enabled: false | blank_issues_enabled: false | ||||
| contact_links: | contact_links: | ||||
| - name: "\U0001F4A1 Model Providers & Plugins" | |||||
| url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose" | |||||
| about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details. | |||||
| - name: "\U0001F4AC Documentation Issues" | |||||
| url: "https://github.com/langgenius/dify-docs/issues/new" | |||||
| about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue. | |||||
| - name: "\U0001F4E7 Discussions" | - name: "\U0001F4E7 Discussions" | ||||
| url: https://github.com/langgenius/dify/discussions/categories/general | url: https://github.com/langgenius/dify/discussions/categories/general | ||||
| about: General discussions and request help from the community | |||||
| about: General discussions and seek help from the community |
| name: "📚 Documentation Issue" | |||||
| description: Report issues in our documentation | |||||
| labels: | |||||
| - documentation | |||||
| body: | |||||
| - type: checkboxes | |||||
| attributes: | |||||
| label: Self Checks | |||||
| description: "To make sure we get to you in time, please check the following :)" | |||||
| options: | |||||
| - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | |||||
| required: true | |||||
| - label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). | |||||
| required: true | |||||
| - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" | |||||
| required: true | |||||
| - label: "Please do not modify this template :) and fill in all the required fields." | |||||
| required: true | |||||
| - type: textarea | |||||
| attributes: | |||||
| label: Provide a description of requested docs changes | |||||
| placeholder: Briefly describe which document needs to be corrected and why. | |||||
| validations: | |||||
| required: true |
| label: Self Checks | label: Self Checks | ||||
| description: "To make sure we get to you in time, please check the following :)" | description: "To make sure we get to you in time, please check the following :)" | ||||
| options: | options: | ||||
| - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | |||||
| - label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542). | |||||
| required: true | required: true | ||||
| - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). | |||||
| - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | |||||
| required: true | required: true | ||||
| - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" | |||||
| - label: I confirm that I am using English to submit this report, otherwise it will be closed. | |||||
| required: true | required: true | ||||
| - label: "Please do not modify this template :) and fill in all the required fields." | - label: "Please do not modify this template :) and fill in all the required fields." | ||||
| required: true | required: true |
| name: "🌐 Localization/Translation issue" | |||||
| description: Report incorrect translations. [please use English :)] | |||||
| labels: | |||||
| - translation | |||||
| body: | |||||
| - type: checkboxes | |||||
| attributes: | |||||
| label: Self Checks | |||||
| description: "To make sure we get to you in time, please check the following :)" | |||||
| options: | |||||
| - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. | |||||
| required: true | |||||
| - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). | |||||
| required: true | |||||
| - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" | |||||
| required: true | |||||
| - label: "Please do not modify this template :) and fill in all the required fields." | |||||
| required: true | |||||
| - type: input | |||||
| attributes: | |||||
| label: Dify version | |||||
| description: Hover over system tray icon or look at Settings | |||||
| validations: | |||||
| required: true | |||||
| - type: input | |||||
| attributes: | |||||
| label: Utility with translation issue | |||||
| placeholder: Some area | |||||
| description: Please input here the utility with the translation issue | |||||
| validations: | |||||
| required: true | |||||
| - type: input | |||||
| attributes: | |||||
| label: 🌐 Language affected | |||||
| placeholder: "German" | |||||
| validations: | |||||
| required: true | |||||
| - type: textarea | |||||
| attributes: | |||||
| label: ❌ Actual phrase(s) | |||||
| placeholder: What is there? Please include a screenshot as that is extremely helpful. | |||||
| validations: | |||||
| required: true | |||||
| - type: textarea | |||||
| attributes: | |||||
| label: ✔️ Expected phrase(s) | |||||
| placeholder: What was expected? | |||||
| validations: | |||||
| required: true | |||||
| - type: textarea | |||||
| attributes: | |||||
| label: ℹ Why is the current translation wrong | |||||
| placeholder: Why do you feel this is incorrect? | |||||
| validations: | |||||
| required: true |
| </br> | </br> | ||||
| The easiest way to start the Dify server is through [docker compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: | |||||
| The easiest way to start the Dify server is through [Docker Compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: | |||||
| ```bash | ```bash | ||||
| cd dify | cd dify | ||||
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Using Terraform for Deployment | #### Using Terraform for Deployment | ||||
| ## Security disclosure | ## Security disclosure | ||||
| To protect your privacy, please avoid posting security issues on GitHub. Instead, send your questions to security@dify.ai and we will provide you with a more detailed answer. | |||||
| To protect your privacy, please avoid posting security issues on GitHub. Instead, report issues to security@dify.ai, and our team will respond with detailed answer. | |||||
| ## License | ## License | ||||
| This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions. | |||||
| This repository is licensed under the [Dify Open Source License](LICENSE), based on Apache 2.0 with additional conditions. |
| - [رسم بياني Helm من قبل @magicsong](https://github.com/magicsong/ai-charts) | - [رسم بياني Helm من قبل @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [ملف YAML من قبل @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [ملف YAML من قبل @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 جديد! ملفات YAML (تدعم Dify v1.6.0) بواسطة @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### استخدام Terraform للتوزيع | #### استخدام Terraform للتوزيع | ||||
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 নতুন! YAML ফাইলসমূহ (Dify v1.6.0 সমর্থিত) তৈরি করেছেন @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### টেরাফর্ম ব্যবহার করে ডিপ্লয় | #### টেরাফর্ম ব্যবহার করে ডিপ্লয় | ||||
| 如果您需要自定义配置,请参考 [.env.example](docker/.env.example) 文件中的注释,并更新 `.env` 文件中对应的值。此外,您可能需要根据您的具体部署环境和需求对 `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 Chart 部署 | |||||
| #### 使用 Helm Chart 或 Kubernetes 资源清单(YAML)部署 | |||||
| 使用 [Helm Chart](https://helm.sh/) 版本或者 YAML 文件,可以在 Kubernetes 上部署 Dify。 | |||||
| 使用 [Helm Chart](https://helm.sh/) 版本或者 Kubernetes 资源清单(YAML),可以在 Kubernetes 上部署 Dify。 | |||||
| - [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) | - [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) | ||||
| - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | ||||
| - [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML 文件 (支持 Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### 使用 Terraform 部署 | #### 使用 Terraform 部署 | ||||
| 使用 [terraform](https://www.terraform.io/) 一键将 Dify 部署到云平台 | 使用 [terraform](https://www.terraform.io/) 一键将 Dify 部署到云平台 |
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Terraform für die Bereitstellung verwenden | #### Terraform für die Bereitstellung verwenden | ||||
| - [Gráfico Helm por @magicsong](https://github.com/magicsong/ai-charts) | - [Gráfico Helm por @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [Ficheros YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [Ficheros YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 ¡NUEVO! Archivos YAML (compatible con Dify v1.6.0) por @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Uso de Terraform para el despliegue | #### Uso de Terraform para el despliegue | ||||
| - [Helm Chart par @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart par @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [Fichier YAML par @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [Fichier YAML par @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NOUVEAU ! Fichiers YAML (compatible avec Dify v1.6.0) par @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Utilisation de Terraform pour le déploiement | #### Utilisation de Terraform pour le déploiement | ||||
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 新着!YAML ファイル(Dify v1.6.0 対応)by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Terraformを使用したデプロイ | #### Terraformを使用したデプロイ | ||||
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Terraform atorlugu pilersitsineq | #### Terraform atorlugu pilersitsineq | ||||
| - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Terraform을 사용한 배포 | #### Terraform을 사용한 배포 | ||||
| - [Helm Chart de @magicsong](https://github.com/magicsong/ai-charts) | - [Helm Chart de @magicsong](https://github.com/magicsong/ai-charts) | ||||
| - [Arquivo YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [Arquivo YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [Arquivo YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [Arquivo YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NOVO! Arquivos YAML (Compatível com Dify v1.6.0) por @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Usando o Terraform para Implantação | #### Usando o Terraform para Implantação | ||||
| - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | ||||
| - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Uporaba Terraform za uvajanje | #### Uporaba Terraform za uvajanje | ||||
| - [@BorisPolonsky tarafından Helm Chart](https://github.com/BorisPolonsky/dify-helm) | - [@BorisPolonsky tarafından Helm Chart](https://github.com/BorisPolonsky/dify-helm) | ||||
| - [@Winson-030 tarafından YAML dosyası](https://github.com/Winson-030/dify-kubernetes) | - [@Winson-030 tarafından YAML dosyası](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [@wyy-holding tarafından YAML dosyası](https://github.com/wyy-holding/dify-k8s) | - [@wyy-holding tarafından YAML dosyası](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 YENİ! YAML dosyaları (Dify v1.6.0 destekli) @Zhoneym tarafından](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Dağıtım için Terraform Kullanımı | #### Dağıtım için Terraform Kullanımı | ||||
| 如果您需要自定義配置,請參考我們的 [.env.example](docker/.env.example) 文件中的註釋,並在您的 `.env` 文件中更新相應的值。此外,根據您特定的部署環境和需求,您可能需要調整 `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 文件允許在 Kubernetes 上部署 Dify。 | |||||
| 如果您想配置高可用性設置,社區貢獻的 [Helm Charts](https://helm.sh/) 和 Kubernetes 資源清單(YAML)允許在 Kubernetes 上部署 Dify。 | |||||
| - [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify) | - [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify) | ||||
| - [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm) | - [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm) | ||||
| - [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes) | - [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [由 @wyy-holding 提供的 YAML 文件](https://github.com/wyy-holding/dify-k8s) | - [由 @wyy-holding 提供的 YAML 文件](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 NEW! YAML 檔案(支援 Dify v1.6.0)by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| ### 使用 Terraform 進行部署 | ### 使用 Terraform 進行部署 | ||||
| - [Helm Chart bởi @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | - [Helm Chart bởi @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) | ||||
| - [Tệp YAML bởi @Winson-030](https://github.com/Winson-030/dify-kubernetes) | - [Tệp YAML bởi @Winson-030](https://github.com/Winson-030/dify-kubernetes) | ||||
| - [Tệp YAML bởi @wyy-holding](https://github.com/wyy-holding/dify-k8s) | - [Tệp YAML bởi @wyy-holding](https://github.com/wyy-holding/dify-k8s) | ||||
| - [🚀 MỚI! Tệp YAML (Hỗ trợ Dify v1.6.0) bởi @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) | |||||
| #### Sử dụng Terraform để Triển khai | #### Sử dụng Terraform để Triển khai | ||||
| # hybrid: Save new data to object storage, read from both object storage and RDBMS | # hybrid: Save new data to object storage, read from both object storage and RDBMS | ||||
| WORKFLOW_NODE_EXECUTION_STORAGE=rdbms | WORKFLOW_NODE_EXECUTION_STORAGE=rdbms | ||||
| # Repository configuration | |||||
| # Core workflow execution repository implementation | |||||
| CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository | |||||
| # Core workflow node execution repository implementation | |||||
| CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository | |||||
| # API workflow node execution repository implementation | |||||
| API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository | |||||
| # API workflow run repository implementation | |||||
| API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository | |||||
| # App configuration | # App configuration | ||||
| APP_MAX_EXECUTION_TIME=1200 | APP_MAX_EXECUTION_TIME=1200 | ||||
| APP_MAX_ACTIVE_REQUESTS=0 | APP_MAX_ACTIVE_REQUESTS=0 | ||||
| # Reset password token expiry minutes | # Reset password token expiry minutes | ||||
| RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 | RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 | ||||
| CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 | |||||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 | |||||
| CREATE_TIDB_SERVICE_JOB_ENABLED=false | CREATE_TIDB_SERVICE_JOB_ENABLED=false | ||||
| # Enable OpenTelemetry | # Enable OpenTelemetry | ||||
| ENABLE_OTEL=false | ENABLE_OTEL=false | ||||
| OTLP_TRACE_ENDPOINT= | |||||
| OTLP_METRIC_ENDPOINT= | |||||
| OTLP_BASE_ENDPOINT=http://localhost:4318 | OTLP_BASE_ENDPOINT=http://localhost:4318 | ||||
| OTLP_API_KEY= | OTLP_API_KEY= | ||||
| OTEL_EXPORTER_OTLP_PROTOCOL= | OTEL_EXPORTER_OTLP_PROTOCOL= |
| description="Duration in minutes for which a password reset token remains valid", | description="Duration in minutes for which a password reset token remains valid", | ||||
| default=5, | default=5, | ||||
| ) | ) | ||||
| CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( | |||||
| description="Duration in minutes for which a change email token remains valid", | |||||
| default=5, | |||||
| ) | |||||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( | |||||
| description="Duration in minutes for which a owner transfer token remains valid", | |||||
| default=5, | |||||
| ) | |||||
| LOGIN_DISABLED: bool = Field( | LOGIN_DISABLED: bool = Field( | ||||
| description="Whether to disable login checks", | description="Whether to disable login checks", | ||||
| ) | ) | ||||
| class RepositoryConfig(BaseSettings): | |||||
| """ | |||||
| Configuration for repository implementations | |||||
| """ | |||||
| CORE_WORKFLOW_EXECUTION_REPOSITORY: str = Field( | |||||
| description="Repository implementation for WorkflowExecution. Specify as a module path", | |||||
| default="core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository", | |||||
| ) | |||||
| CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: str = Field( | |||||
| description="Repository implementation for WorkflowNodeExecution. Specify as a module path", | |||||
| default="core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository", | |||||
| ) | |||||
| API_WORKFLOW_NODE_EXECUTION_REPOSITORY: str = Field( | |||||
| description="Service-layer repository implementation for WorkflowNodeExecutionModel operations. " | |||||
| "Specify as a module path", | |||||
| default="repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository", | |||||
| ) | |||||
| API_WORKFLOW_RUN_REPOSITORY: str = Field( | |||||
| description="Service-layer repository implementation for WorkflowRun operations. Specify as a module path", | |||||
| default="repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository", | |||||
| ) | |||||
| class AuthConfig(BaseSettings): | class AuthConfig(BaseSettings): | ||||
| """ | """ | ||||
| Configuration for authentication and OAuth | Configuration for authentication and OAuth | ||||
| default=86400, | default=86400, | ||||
| ) | ) | ||||
| CHANGE_EMAIL_LOCKOUT_DURATION: PositiveInt = Field( | |||||
| description="Time (in seconds) a user must wait before retrying change email after exceeding the rate limit.", | |||||
| default=86400, | |||||
| ) | |||||
| OWNER_TRANSFER_LOCKOUT_DURATION: PositiveInt = Field( | |||||
| description="Time (in seconds) a user must wait before retrying owner transfer after exceeding the rate limit.", | |||||
| default=86400, | |||||
| ) | |||||
| class ModerationConfig(BaseSettings): | class ModerationConfig(BaseSettings): | ||||
| """ | """ | ||||
| MultiModalTransferConfig, | MultiModalTransferConfig, | ||||
| PositionConfig, | PositionConfig, | ||||
| RagEtlConfig, | RagEtlConfig, | ||||
| RepositoryConfig, | |||||
| SecurityConfig, | SecurityConfig, | ||||
| ToolConfig, | ToolConfig, | ||||
| UpdateConfig, | UpdateConfig, |
| default=3600, | default=3600, | ||||
| ) | ) | ||||
| SQLALCHEMY_POOL_USE_LIFO: bool = Field( | |||||
| description="If True, SQLAlchemy will use last-in-first-out way to retrieve connections from pool.", | |||||
| default=False, | |||||
| ) | |||||
| SQLALCHEMY_POOL_PRE_PING: bool = Field( | SQLALCHEMY_POOL_PRE_PING: bool = Field( | ||||
| description="If True, enables connection pool pre-ping feature to check connections.", | description="If True, enables connection pool pre-ping feature to check connections.", | ||||
| default=False, | default=False, | ||||
| "pool_recycle": self.SQLALCHEMY_POOL_RECYCLE, | "pool_recycle": self.SQLALCHEMY_POOL_RECYCLE, | ||||
| "pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING, | "pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING, | ||||
| "connect_args": connect_args, | "connect_args": connect_args, | ||||
| "pool_use_lifo": self.SQLALCHEMY_POOL_USE_LIFO, | |||||
| } | } | ||||
| default=False, | default=False, | ||||
| ) | ) | ||||
| OTLP_TRACE_ENDPOINT: str = Field( | |||||
| description="OTLP trace endpoint", | |||||
| default="", | |||||
| ) | |||||
| OTLP_METRIC_ENDPOINT: str = Field( | |||||
| description="OTLP metric endpoint", | |||||
| default="", | |||||
| ) | |||||
| OTLP_BASE_ENDPOINT: str = Field( | OTLP_BASE_ENDPOINT: str = Field( | ||||
| description="OTLP base endpoint", | description="OTLP base endpoint", | ||||
| default="http://localhost:4318", | default="http://localhost:4318", |
| parser.add_argument("icon", type=str, location="json") | parser.add_argument("icon", type=str, location="json") | ||||
| parser.add_argument("icon_background", type=str, location="json") | parser.add_argument("icon_background", type=str, location="json") | ||||
| parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") | parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") | ||||
| parser.add_argument("max_active_requests", type=int, location="json") | |||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| app_service = AppService() | app_service = AppService() |
| @get_app_model | @get_app_model | ||||
| @marshal_with(app_server_fields) | @marshal_with(app_server_fields) | ||||
| def post(self, app_model): | def post(self, app_model): | ||||
| # The role of the current user in the ta table must be editor, admin, or owner | |||||
| if not current_user.is_editor: | if not current_user.is_editor: | ||||
| raise NotFound() | raise NotFound() | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument("description", type=str, required=True, location="json") | |||||
| parser.add_argument("description", type=str, required=False, location="json") | |||||
| parser.add_argument("parameters", type=dict, required=True, location="json") | parser.add_argument("parameters", type=dict, required=True, location="json") | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| description = args.get("description") | |||||
| if not description: | |||||
| description = app_model.description or "" | |||||
| server = AppMCPServer( | server = AppMCPServer( | ||||
| name=app_model.name, | name=app_model.name, | ||||
| description=args["description"], | |||||
| description=description, | |||||
| parameters=json.dumps(args["parameters"], ensure_ascii=False), | parameters=json.dumps(args["parameters"], ensure_ascii=False), | ||||
| status=AppMCPServerStatus.ACTIVE, | status=AppMCPServerStatus.ACTIVE, | ||||
| app_id=app_model.id, | app_id=app_model.id, | ||||
| raise NotFound() | raise NotFound() | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument("id", type=str, required=True, location="json") | parser.add_argument("id", type=str, required=True, location="json") | ||||
| parser.add_argument("description", type=str, required=True, location="json") | |||||
| parser.add_argument("description", type=str, required=False, location="json") | |||||
| parser.add_argument("parameters", type=dict, required=True, location="json") | parser.add_argument("parameters", type=dict, required=True, location="json") | ||||
| parser.add_argument("status", type=str, required=False, location="json") | parser.add_argument("status", type=str, required=False, location="json") | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| server = db.session.query(AppMCPServer).filter(AppMCPServer.id == args["id"]).first() | server = db.session.query(AppMCPServer).filter(AppMCPServer.id == args["id"]).first() | ||||
| if not server: | if not server: | ||||
| raise NotFound() | raise NotFound() | ||||
| server.description = args["description"] | |||||
| description = args.get("description") | |||||
| if description is None: | |||||
| pass | |||||
| elif not description: | |||||
| server.description = app_model.description or "" | |||||
| else: | |||||
| server.description = description | |||||
| server.parameters = json.dumps(args["parameters"], ensure_ascii=False) | server.parameters = json.dumps(args["parameters"], ensure_ascii=False) | ||||
| if args["status"]: | if args["status"]: | ||||
| if args["status"] not in [status.value for status in AppMCPServerStatus]: | if args["status"] not in [status.value for status in AppMCPServerStatus]: |
| from decimal import Decimal | from decimal import Decimal | ||||
| import pytz | import pytz | ||||
| import sqlalchemy as sa | |||||
| from flask import jsonify | from flask import jsonify | ||||
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restful import Resource, reqparse | from flask_restful import Resource, reqparse | ||||
| from controllers.console import api | from controllers.console import api | ||||
| from controllers.console.app.wraps import get_app_model | from controllers.console.app.wraps import get_app_model | ||||
| from controllers.console.wraps import account_initialization_required, setup_required | from controllers.console.wraps import account_initialization_required, setup_required | ||||
| from core.app.entities.app_invoke_entities import InvokeFrom | |||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.helper import DatetimeString | from libs.helper import DatetimeString | ||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from models.model import AppMode | |||||
| from models import AppMode, Message | |||||
| class DailyMessageStatistic(Resource): | class DailyMessageStatistic(Resource): | ||||
| parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args") | parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args") | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| sql_query = """SELECT | |||||
| DATE(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, | |||||
| COUNT(DISTINCT messages.conversation_id) AS conversation_count | |||||
| FROM | |||||
| messages | |||||
| WHERE | |||||
| app_id = :app_id""" | |||||
| arg_dict = {"tz": account.timezone, "app_id": app_model.id} | |||||
| timezone = pytz.timezone(account.timezone) | timezone = pytz.timezone(account.timezone) | ||||
| utc_timezone = pytz.utc | utc_timezone = pytz.utc | ||||
| stmt = ( | |||||
| sa.select( | |||||
| sa.func.date( | |||||
| sa.func.date_trunc("day", sa.text("created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz")) | |||||
| ).label("date"), | |||||
| sa.func.count(sa.distinct(Message.conversation_id)).label("conversation_count"), | |||||
| ) | |||||
| .select_from(Message) | |||||
| .where(Message.app_id == app_model.id, Message.invoke_from != InvokeFrom.DEBUGGER.value) | |||||
| ) | |||||
| if args["start"]: | if args["start"]: | ||||
| start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M") | start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M") | ||||
| start_datetime = start_datetime.replace(second=0) | start_datetime = start_datetime.replace(second=0) | ||||
| start_datetime_timezone = timezone.localize(start_datetime) | start_datetime_timezone = timezone.localize(start_datetime) | ||||
| start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone) | start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone) | ||||
| sql_query += " AND created_at >= :start" | |||||
| arg_dict["start"] = start_datetime_utc | |||||
| stmt = stmt.where(Message.created_at >= start_datetime_utc) | |||||
| if args["end"]: | if args["end"]: | ||||
| end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M") | end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M") | ||||
| end_datetime = end_datetime.replace(second=0) | end_datetime = end_datetime.replace(second=0) | ||||
| end_datetime_timezone = timezone.localize(end_datetime) | end_datetime_timezone = timezone.localize(end_datetime) | ||||
| end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone) | end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone) | ||||
| stmt = stmt.where(Message.created_at < end_datetime_utc) | |||||
| sql_query += " AND created_at < :end" | |||||
| arg_dict["end"] = end_datetime_utc | |||||
| sql_query += " GROUP BY date ORDER BY date" | |||||
| stmt = stmt.group_by("date").order_by("date") | |||||
| response_data = [] | response_data = [] | ||||
| with db.engine.begin() as conn: | with db.engine.begin() as conn: | ||||
| rs = conn.execute(db.text(sql_query), arg_dict) | |||||
| for i in rs: | |||||
| response_data.append({"date": str(i.date), "conversation_count": i.conversation_count}) | |||||
| rs = conn.execute(stmt, {"tz": account.timezone}) | |||||
| for row in rs: | |||||
| response_data.append({"date": str(row.date), "conversation_count": row.conversation_count}) | |||||
| return jsonify({"data": response_data}) | return jsonify({"data": response_data}) | ||||
| return parser | return parser | ||||
| def _serialize_variable_type(workflow_draft_var: WorkflowDraftVariable) -> str: | |||||
| value_type = workflow_draft_var.value_type | |||||
| return value_type.exposed_type().value | |||||
| _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = { | _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = { | ||||
| "id": fields.String, | "id": fields.String, | ||||
| "type": fields.String(attribute=lambda model: model.get_variable_type()), | "type": fields.String(attribute=lambda model: model.get_variable_type()), | ||||
| "name": fields.String, | "name": fields.String, | ||||
| "description": fields.String, | "description": fields.String, | ||||
| "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), | "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), | ||||
| "value_type": fields.String, | |||||
| "value_type": fields.String(attribute=_serialize_variable_type), | |||||
| "edited": fields.Boolean(attribute=lambda model: model.edited), | "edited": fields.Boolean(attribute=lambda model: model.edited), | ||||
| "visible": fields.Boolean, | "visible": fields.Boolean, | ||||
| } | } | ||||
| "name": fields.String, | "name": fields.String, | ||||
| "description": fields.String, | "description": fields.String, | ||||
| "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), | "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), | ||||
| "value_type": fields.String, | |||||
| "value_type": fields.String(attribute=_serialize_variable_type), | |||||
| "edited": fields.Boolean(attribute=lambda model: model.edited), | "edited": fields.Boolean(attribute=lambda model: model.edited), | ||||
| "visible": fields.Boolean, | "visible": fields.Boolean, | ||||
| } | } | ||||
| "name": v.name, | "name": v.name, | ||||
| "description": v.description, | "description": v.description, | ||||
| "selector": v.selector, | "selector": v.selector, | ||||
| "value_type": v.value_type.value, | |||||
| "value_type": v.value_type.exposed_type().value, | |||||
| "value": v.value, | "value": v.value, | ||||
| # Do not track edited for env vars. | # Do not track edited for env vars. | ||||
| "edited": False, | "edited": False, |
| raise AppNotFoundError() | raise AppNotFoundError() | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| if app_mode == AppMode.CHANNEL: | |||||
| raise AppNotFoundError() | |||||
| if mode is not None: | if mode is not None: | ||||
| if isinstance(mode, list): | if isinstance(mode, list): |
| class PasswordResetRateLimitExceededError(BaseHTTPException): | class PasswordResetRateLimitExceededError(BaseHTTPException): | ||||
| error_code = "password_reset_rate_limit_exceeded" | error_code = "password_reset_rate_limit_exceeded" | ||||
| description = "Too many password reset emails have been sent. Please try again in 1 minutes." | |||||
| description = "Too many password reset emails have been sent. Please try again in 1 minute." | |||||
| code = 429 | |||||
| class EmailChangeRateLimitExceededError(BaseHTTPException): | |||||
| error_code = "email_change_rate_limit_exceeded" | |||||
| description = "Too many email change emails have been sent. Please try again in 1 minute." | |||||
| code = 429 | |||||
| class OwnerTransferRateLimitExceededError(BaseHTTPException): | |||||
| error_code = "owner_transfer_rate_limit_exceeded" | |||||
| description = "Too many owner transfer emails have been sent. Please try again in 1 minute." | |||||
| code = 429 | code = 429 | ||||
| error_code = "email_password_reset_limit" | error_code = "email_password_reset_limit" | ||||
| description = "Too many failed password reset attempts. Please try again in 24 hours." | description = "Too many failed password reset attempts. Please try again in 24 hours." | ||||
| code = 429 | code = 429 | ||||
| class EmailChangeLimitError(BaseHTTPException): | |||||
| error_code = "email_change_limit" | |||||
| description = "Too many failed email change attempts. Please try again in 24 hours." | |||||
| code = 429 | |||||
| class EmailAlreadyInUseError(BaseHTTPException): | |||||
| error_code = "email_already_in_use" | |||||
| description = "A user with this email already exists." | |||||
| code = 400 | |||||
| class OwnerTransferLimitError(BaseHTTPException): | |||||
| error_code = "owner_transfer_limit" | |||||
| description = "Too many failed owner transfer attempts. Please try again in 24 hours." | |||||
| code = 429 | |||||
| class NotOwnerError(BaseHTTPException): | |||||
| error_code = "not_owner" | |||||
| description = "You are not the owner of the workspace." | |||||
| code = 400 | |||||
| class CannotTransferOwnerToSelfError(BaseHTTPException): | |||||
| error_code = "cannot_transfer_owner_to_self" | |||||
| description = "You cannot transfer ownership to yourself." | |||||
| code = 400 | |||||
| class MemberNotInTenantError(BaseHTTPException): | |||||
| error_code = "member_not_in_tenant" | |||||
| description = "The member is not in the workspace." | |||||
| code = 400 |
| code = 415 | code = 415 | ||||
| class HighQualityDatasetOnlyError(BaseHTTPException): | |||||
| error_code = "high_quality_dataset_only" | |||||
| description = "Current operation only supports 'high-quality' datasets." | |||||
| code = 400 | |||||
| class DatasetNotInitializedError(BaseHTTPException): | class DatasetNotInitializedError(BaseHTTPException): | ||||
| error_code = "dataset_not_initialized" | error_code = "dataset_not_initialized" | ||||
| description = "The dataset is still being initialized or indexing. Please wait a moment." | description = "The dataset is still being initialized or indexing. Please wait a moment." |
| from flask import request | from flask import request | ||||
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restful import Resource, fields, marshal_with, reqparse | from flask_restful import Resource, fields, marshal_with, reqparse | ||||
| from sqlalchemy import select | |||||
| from sqlalchemy.orm import Session | |||||
| from configs import dify_config | from configs import dify_config | ||||
| from constants.languages import supported_language | from constants.languages import supported_language | ||||
| from controllers.console import api | from controllers.console import api | ||||
| from controllers.console.auth.error import ( | |||||
| EmailAlreadyInUseError, | |||||
| EmailChangeLimitError, | |||||
| EmailCodeError, | |||||
| InvalidEmailError, | |||||
| InvalidTokenError, | |||||
| ) | |||||
| from controllers.console.error import AccountNotFound, EmailSendIpLimitError | |||||
| from controllers.console.workspace.error import ( | from controllers.console.workspace.error import ( | ||||
| AccountAlreadyInitedError, | AccountAlreadyInitedError, | ||||
| CurrentPasswordIncorrectError, | CurrentPasswordIncorrectError, | ||||
| from controllers.console.wraps import ( | from controllers.console.wraps import ( | ||||
| account_initialization_required, | account_initialization_required, | ||||
| cloud_edition_billing_enabled, | cloud_edition_billing_enabled, | ||||
| enable_change_email, | |||||
| enterprise_license_required, | enterprise_license_required, | ||||
| only_edition_cloud, | only_edition_cloud, | ||||
| setup_required, | setup_required, | ||||
| ) | ) | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from fields.member_fields import account_fields | from fields.member_fields import account_fields | ||||
| from libs.helper import TimestampField, timezone | |||||
| from libs.helper import TimestampField, email, extract_remote_ip, timezone | |||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from models import AccountIntegrate, InvitationCode | from models import AccountIntegrate, InvitationCode | ||||
| from models.account import Account | |||||
| from services.account_service import AccountService | from services.account_service import AccountService | ||||
| from services.billing_service import BillingService | from services.billing_service import BillingService | ||||
| from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError | from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError | ||||
| return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"]) | return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"]) | ||||
| class ChangeEmailSendEmailApi(Resource): | |||||
| @enable_change_email | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("email", type=email, required=True, location="json") | |||||
| parser.add_argument("language", type=str, required=False, location="json") | |||||
| parser.add_argument("phase", type=str, required=False, location="json") | |||||
| parser.add_argument("token", type=str, required=False, location="json") | |||||
| args = parser.parse_args() | |||||
| ip_address = extract_remote_ip(request) | |||||
| if AccountService.is_email_send_ip_limit(ip_address): | |||||
| raise EmailSendIpLimitError() | |||||
| if args["language"] is not None and args["language"] == "zh-Hans": | |||||
| language = "zh-Hans" | |||||
| else: | |||||
| language = "en-US" | |||||
| account = None | |||||
| user_email = args["email"] | |||||
| if args["phase"] is not None and args["phase"] == "new_email": | |||||
| if args["token"] is None: | |||||
| raise InvalidTokenError() | |||||
| reset_data = AccountService.get_change_email_data(args["token"]) | |||||
| if reset_data is None: | |||||
| raise InvalidTokenError() | |||||
| user_email = reset_data.get("email", "") | |||||
| if user_email != current_user.email: | |||||
| raise InvalidEmailError() | |||||
| else: | |||||
| with Session(db.engine) as session: | |||||
| account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none() | |||||
| if account is None: | |||||
| raise AccountNotFound() | |||||
| token = AccountService.send_change_email_email( | |||||
| account=account, email=args["email"], old_email=user_email, language=language, phase=args["phase"] | |||||
| ) | |||||
| return {"result": "success", "data": token} | |||||
| class ChangeEmailCheckApi(Resource): | |||||
| @enable_change_email | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("email", type=email, required=True, location="json") | |||||
| parser.add_argument("code", type=str, required=True, location="json") | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| args = parser.parse_args() | |||||
| user_email = args["email"] | |||||
| is_change_email_error_rate_limit = AccountService.is_change_email_error_rate_limit(args["email"]) | |||||
| if is_change_email_error_rate_limit: | |||||
| raise EmailChangeLimitError() | |||||
| token_data = AccountService.get_change_email_data(args["token"]) | |||||
| if token_data is None: | |||||
| raise InvalidTokenError() | |||||
| if user_email != token_data.get("email"): | |||||
| raise InvalidEmailError() | |||||
| if args["code"] != token_data.get("code"): | |||||
| AccountService.add_change_email_error_rate_limit(args["email"]) | |||||
| raise EmailCodeError() | |||||
| # Verified, revoke the first token | |||||
| AccountService.revoke_change_email_token(args["token"]) | |||||
| # Refresh token data by generating a new token | |||||
| _, new_token = AccountService.generate_change_email_token( | |||||
| user_email, code=args["code"], old_email=token_data.get("old_email"), additional_data={} | |||||
| ) | |||||
| AccountService.reset_change_email_error_rate_limit(args["email"]) | |||||
| return {"is_valid": True, "email": token_data.get("email"), "token": new_token} | |||||
| class ChangeEmailResetApi(Resource): | |||||
| @enable_change_email | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| @marshal_with(account_fields) | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("new_email", type=email, required=True, location="json") | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| args = parser.parse_args() | |||||
| reset_data = AccountService.get_change_email_data(args["token"]) | |||||
| if not reset_data: | |||||
| raise InvalidTokenError() | |||||
| AccountService.revoke_change_email_token(args["token"]) | |||||
| if not AccountService.check_email_unique(args["new_email"]): | |||||
| raise EmailAlreadyInUseError() | |||||
| old_email = reset_data.get("old_email", "") | |||||
| if current_user.email != old_email: | |||||
| raise AccountNotFound() | |||||
| updated_account = AccountService.update_account(current_user, email=args["new_email"]) | |||||
| return updated_account | |||||
| class CheckEmailUnique(Resource): | |||||
| @setup_required | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("email", type=email, required=True, location="json") | |||||
| args = parser.parse_args() | |||||
| if not AccountService.check_email_unique(args["email"]): | |||||
| raise EmailAlreadyInUseError() | |||||
| return {"result": "success"} | |||||
| # Register API resources | # Register API resources | ||||
| api.add_resource(AccountInitApi, "/account/init") | api.add_resource(AccountInitApi, "/account/init") | ||||
| api.add_resource(AccountProfileApi, "/account/profile") | api.add_resource(AccountProfileApi, "/account/profile") | ||||
| api.add_resource(EducationVerifyApi, "/account/education/verify") | api.add_resource(EducationVerifyApi, "/account/education/verify") | ||||
| api.add_resource(EducationApi, "/account/education") | api.add_resource(EducationApi, "/account/education") | ||||
| api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete") | api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete") | ||||
| # Change email | |||||
| api.add_resource(ChangeEmailSendEmailApi, "/account/change-email") | |||||
| api.add_resource(ChangeEmailCheckApi, "/account/change-email/validity") | |||||
| api.add_resource(ChangeEmailResetApi, "/account/change-email/reset") | |||||
| api.add_resource(CheckEmailUnique, "/account/change-email/check-email-unique") | |||||
| # api.add_resource(AccountEmailApi, '/account/email') | # api.add_resource(AccountEmailApi, '/account/email') | ||||
| # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') | # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') |
| code = 400 | code = 400 | ||||
| class ProviderRequestFailedError(BaseHTTPException): | |||||
| error_code = "provider_request_failed" | |||||
| description = None | |||||
| code = 400 | |||||
| class InvalidInvitationCodeError(BaseHTTPException): | class InvalidInvitationCodeError(BaseHTTPException): | ||||
| error_code = "invalid_invitation_code" | error_code = "invalid_invitation_code" | ||||
| description = "Invalid invitation code." | description = "Invalid invitation code." |
| from urllib import parse | from urllib import parse | ||||
| from flask import request | |||||
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restful import Resource, abort, marshal_with, reqparse | from flask_restful import Resource, abort, marshal_with, reqparse | ||||
| import services | import services | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from controllers.console import api | from controllers.console import api | ||||
| from controllers.console.error import WorkspaceMembersLimitExceeded | |||||
| from controllers.console.auth.error import ( | |||||
| CannotTransferOwnerToSelfError, | |||||
| EmailCodeError, | |||||
| InvalidEmailError, | |||||
| InvalidTokenError, | |||||
| MemberNotInTenantError, | |||||
| NotOwnerError, | |||||
| OwnerTransferLimitError, | |||||
| ) | |||||
| from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded | |||||
| from controllers.console.wraps import ( | from controllers.console.wraps import ( | ||||
| account_initialization_required, | account_initialization_required, | ||||
| cloud_edition_billing_resource_check, | cloud_edition_billing_resource_check, | ||||
| is_allow_transfer_owner, | |||||
| setup_required, | setup_required, | ||||
| ) | ) | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from fields.member_fields import account_with_role_list_fields | from fields.member_fields import account_with_role_list_fields | ||||
| from libs.helper import extract_remote_ip | |||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from models.account import Account, TenantAccountRole | from models.account import Account, TenantAccountRole | ||||
| from services.account_service import RegisterService, TenantService | |||||
| from services.account_service import AccountService, RegisterService, TenantService | |||||
| from services.errors.account import AccountAlreadyInTenantError | from services.errors.account import AccountAlreadyInTenantError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| return {"result": "success", "accounts": members}, 200 | return {"result": "success", "accounts": members}, 200 | ||||
| class SendOwnerTransferEmailApi(Resource): | |||||
| """Send owner transfer email.""" | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| @is_allow_transfer_owner | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("language", type=str, required=False, location="json") | |||||
| args = parser.parse_args() | |||||
| ip_address = extract_remote_ip(request) | |||||
| if AccountService.is_email_send_ip_limit(ip_address): | |||||
| raise EmailSendIpLimitError() | |||||
| # check if the current user is the owner of the workspace | |||||
| if not TenantService.is_owner(current_user, current_user.current_tenant): | |||||
| raise NotOwnerError() | |||||
| if args["language"] is not None and args["language"] == "zh-Hans": | |||||
| language = "zh-Hans" | |||||
| else: | |||||
| language = "en-US" | |||||
| email = current_user.email | |||||
| token = AccountService.send_owner_transfer_email( | |||||
| account=current_user, | |||||
| email=email, | |||||
| language=language, | |||||
| workspace_name=current_user.current_tenant.name, | |||||
| ) | |||||
| return {"result": "success", "data": token} | |||||
| class OwnerTransferCheckApi(Resource): | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| @is_allow_transfer_owner | |||||
| def post(self): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("code", type=str, required=True, location="json") | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| args = parser.parse_args() | |||||
| # check if the current user is the owner of the workspace | |||||
| if not TenantService.is_owner(current_user, current_user.current_tenant): | |||||
| raise NotOwnerError() | |||||
| user_email = current_user.email | |||||
| is_owner_transfer_error_rate_limit = AccountService.is_owner_transfer_error_rate_limit(user_email) | |||||
| if is_owner_transfer_error_rate_limit: | |||||
| raise OwnerTransferLimitError() | |||||
| token_data = AccountService.get_owner_transfer_data(args["token"]) | |||||
| if token_data is None: | |||||
| raise InvalidTokenError() | |||||
| if user_email != token_data.get("email"): | |||||
| raise InvalidEmailError() | |||||
| if args["code"] != token_data.get("code"): | |||||
| AccountService.add_owner_transfer_error_rate_limit(user_email) | |||||
| raise EmailCodeError() | |||||
| # Verified, revoke the first token | |||||
| AccountService.revoke_owner_transfer_token(args["token"]) | |||||
| # Refresh token data by generating a new token | |||||
| _, new_token = AccountService.generate_owner_transfer_token(user_email, code=args["code"], additional_data={}) | |||||
| AccountService.reset_owner_transfer_error_rate_limit(user_email) | |||||
| return {"is_valid": True, "email": token_data.get("email"), "token": new_token} | |||||
| class OwnerTransfer(Resource): | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| @is_allow_transfer_owner | |||||
| def post(self, member_id): | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| args = parser.parse_args() | |||||
| # check if the current user is the owner of the workspace | |||||
| if not TenantService.is_owner(current_user, current_user.current_tenant): | |||||
| raise NotOwnerError() | |||||
| if current_user.id == str(member_id): | |||||
| raise CannotTransferOwnerToSelfError() | |||||
| transfer_token_data = AccountService.get_owner_transfer_data(args["token"]) | |||||
| if not transfer_token_data: | |||||
| raise InvalidTokenError() | |||||
| if transfer_token_data.get("email") != current_user.email: | |||||
| raise InvalidEmailError() | |||||
| AccountService.revoke_owner_transfer_token(args["token"]) | |||||
| member = db.session.get(Account, str(member_id)) | |||||
| if not member: | |||||
| abort(404) | |||||
| else: | |||||
| member_account = member | |||||
| if not TenantService.is_member(member_account, current_user.current_tenant): | |||||
| raise MemberNotInTenantError() | |||||
| try: | |||||
| assert member is not None, "Member not found" | |||||
| TenantService.update_member_role(current_user.current_tenant, member, "owner", current_user) | |||||
| AccountService.send_new_owner_transfer_notify_email( | |||||
| account=member, | |||||
| email=member.email, | |||||
| workspace_name=current_user.current_tenant.name, | |||||
| ) | |||||
| AccountService.send_old_owner_transfer_notify_email( | |||||
| account=current_user, | |||||
| email=current_user.email, | |||||
| workspace_name=current_user.current_tenant.name, | |||||
| new_owner_email=member.email, | |||||
| ) | |||||
| except Exception as e: | |||||
| raise ValueError(str(e)) | |||||
| return {"result": "success"} | |||||
| api.add_resource(MemberListApi, "/workspaces/current/members") | api.add_resource(MemberListApi, "/workspaces/current/members") | ||||
| api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email") | api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email") | ||||
| api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>") | api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>") | ||||
| api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role") | api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role") | ||||
| api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators") | api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators") | ||||
| # owner transfer | |||||
| api.add_resource(SendOwnerTransferEmailApi, "/workspaces/current/members/send-owner-transfer-confirm-email") | |||||
| api.add_resource(OwnerTransferCheckApi, "/workspaces/current/members/owner-transfer-check") | |||||
| api.add_resource(OwnerTransfer, "/workspaces/current/members/<uuid:member_id>/owner-transfer") |
| abort(403) | abort(403) | ||||
| return decorated | return decorated | ||||
| def enable_change_email(view): | |||||
| @wraps(view) | |||||
| def decorated(*args, **kwargs): | |||||
| features = FeatureService.get_system_features() | |||||
| if features.enable_change_email: | |||||
| return view(*args, **kwargs) | |||||
| # otherwise, return 403 | |||||
| abort(403) | |||||
| return decorated | |||||
| def is_allow_transfer_owner(view): | |||||
| @wraps(view) | |||||
| def decorated(*args, **kwargs): | |||||
| features = FeatureService.get_features(current_user.current_tenant_id) | |||||
| if features.is_allow_transfer_workspace: | |||||
| return view(*args, **kwargs) | |||||
| # otherwise, return 403 | |||||
| abort(403) | |||||
| return decorated |
| from dateutil.parser import isoparse | from dateutil.parser import isoparse | ||||
| from flask_restful import Resource, fields, marshal_with, reqparse | from flask_restful import Resource, fields, marshal_with, reqparse | ||||
| from flask_restful.inputs import int_range | from flask_restful.inputs import int_range | ||||
| from sqlalchemy.orm import Session | |||||
| from sqlalchemy.orm import Session, sessionmaker | |||||
| from werkzeug.exceptions import InternalServerError | from werkzeug.exceptions import InternalServerError | ||||
| from controllers.service_api import api | from controllers.service_api import api | ||||
| from libs import helper | from libs import helper | ||||
| from libs.helper import TimestampField | from libs.helper import TimestampField | ||||
| from models.model import App, AppMode, EndUser | from models.model import App, AppMode, EndUser | ||||
| from models.workflow import WorkflowRun | |||||
| from repositories.factory import DifyAPIRepositoryFactory | |||||
| from services.app_generate_service import AppGenerateService | from services.app_generate_service import AppGenerateService | ||||
| from services.errors.llm import InvokeRateLimitError | from services.errors.llm import InvokeRateLimitError | ||||
| from services.workflow_app_service import WorkflowAppService | from services.workflow_app_service import WorkflowAppService | ||||
| if app_mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]: | if app_mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]: | ||||
| raise NotWorkflowAppError() | raise NotWorkflowAppError() | ||||
| workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).first() | |||||
| # Use repository to get workflow run | |||||
| session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) | |||||
| workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) | |||||
| workflow_run = workflow_run_repo.get_workflow_run_by_id( | |||||
| tenant_id=app_model.tenant_id, | |||||
| app_id=app_model.id, | |||||
| run_id=workflow_run_id, | |||||
| ) | |||||
| return workflow_run | return workflow_run | ||||
| code = 415 | code = 415 | ||||
| class HighQualityDatasetOnlyError(BaseHTTPException): | |||||
| error_code = "high_quality_dataset_only" | |||||
| description = "Current operation only supports 'high-quality' datasets." | |||||
| code = 400 | |||||
| class DatasetNotInitializedError(BaseHTTPException): | class DatasetNotInitializedError(BaseHTTPException): | ||||
| error_code = "dataset_not_initialized" | error_code = "dataset_not_initialized" | ||||
| description = "The dataset is still being initialized or indexing. Please wait a moment." | description = "The dataset is still being initialized or indexing. Please wait a moment." |
| import uuid | import uuid | ||||
| from typing import Optional, Union, cast | from typing import Optional, Union, cast | ||||
| from sqlalchemy import select | |||||
| from core.agent.entities import AgentEntity, AgentToolEntity | from core.agent.entities import AgentEntity, AgentToolEntity | ||||
| from core.app.app_config.features.file_upload.manager import FileUploadConfigManager | from core.app.app_config.features.file_upload.manager import FileUploadConfigManager | ||||
| from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig | from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig | ||||
| if isinstance(prompt_message, SystemPromptMessage): | if isinstance(prompt_message, SystemPromptMessage): | ||||
| result.append(prompt_message) | result.append(prompt_message) | ||||
| messages: list[Message] = ( | |||||
| db.session.query(Message) | |||||
| .filter( | |||||
| Message.conversation_id == self.message.conversation_id, | |||||
| messages = ( | |||||
| ( | |||||
| db.session.execute( | |||||
| select(Message) | |||||
| .where(Message.conversation_id == self.message.conversation_id) | |||||
| .order_by(Message.created_at.desc()) | |||||
| ) | |||||
| ) | ) | ||||
| .order_by(Message.created_at.desc()) | |||||
| .scalars() | |||||
| .all() | .all() | ||||
| ) | ) | ||||
| APP_SELECTOR = CommonParameterType.APP_SELECTOR.value | APP_SELECTOR = CommonParameterType.APP_SELECTOR.value | ||||
| MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value | MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value | ||||
| TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value | TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value | ||||
| ANY = CommonParameterType.ANY.value | |||||
| # deprecated, should not use. | # deprecated, should not use. | ||||
| SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value | SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value |
| from core.model_runtime.errors.invoke import InvokeAuthorizationError | from core.model_runtime.errors.invoke import InvokeAuthorizationError | ||||
| from core.ops.ops_trace_manager import TraceQueueManager | from core.ops.ops_trace_manager import TraceQueueManager | ||||
| from core.prompt.utils.get_thread_messages_length import get_thread_messages_length | from core.prompt.utils.get_thread_messages_length import get_thread_messages_length | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.repositories.draft_variable_repository import ( | from core.workflow.repositories.draft_variable_repository import ( | ||||
| DraftVariableSaverFactory, | DraftVariableSaverFactory, | ||||
| ) | ) | ||||
| workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING | workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING | ||||
| else: | else: | ||||
| workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN | workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=workflow_triggered_from, | triggered_from=workflow_triggered_from, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| # Create session factory | # Create session factory | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | ||||
| # Create workflow execution(aka workflow run) repository | # Create workflow execution(aka workflow run) repository | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| # Create session factory | # Create session factory | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | ||||
| # Create workflow execution(aka workflow run) repository | # Create workflow execution(aka workflow run) repository | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, |
| QueueTextChunkEvent, | QueueTextChunkEvent, | ||||
| ) | ) | ||||
| from core.moderation.base import ModerationError | from core.moderation.base import ModerationError | ||||
| from core.variables.variables import VariableUnion | |||||
| from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback | from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback | ||||
| from core.workflow.entities.variable_pool import VariablePool | from core.workflow.entities.variable_pool import VariablePool | ||||
| from core.workflow.enums import SystemVariableKey | |||||
| from core.workflow.system_variable import SystemVariable | |||||
| from core.workflow.variable_loader import VariableLoader | from core.workflow.variable_loader import VariableLoader | ||||
| from core.workflow.workflow_entry import WorkflowEntry | from core.workflow.workflow_entry import WorkflowEntry | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| if not workflow: | if not workflow: | ||||
| raise ValueError("Workflow not initialized") | raise ValueError("Workflow not initialized") | ||||
| user_id = None | |||||
| user_id: str | None = None | |||||
| if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: | if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: | ||||
| end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first() | end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first() | ||||
| if end_user: | if end_user: | ||||
| session.commit() | session.commit() | ||||
| # Create a variable pool. | # Create a variable pool. | ||||
| system_inputs = { | |||||
| SystemVariableKey.QUERY: query, | |||||
| SystemVariableKey.FILES: files, | |||||
| SystemVariableKey.CONVERSATION_ID: self.conversation.id, | |||||
| SystemVariableKey.USER_ID: user_id, | |||||
| SystemVariableKey.DIALOGUE_COUNT: self._dialogue_count, | |||||
| SystemVariableKey.APP_ID: app_config.app_id, | |||||
| SystemVariableKey.WORKFLOW_ID: app_config.workflow_id, | |||||
| SystemVariableKey.WORKFLOW_EXECUTION_ID: self.application_generate_entity.workflow_run_id, | |||||
| } | |||||
| system_inputs = SystemVariable( | |||||
| query=query, | |||||
| files=files, | |||||
| conversation_id=self.conversation.id, | |||||
| user_id=user_id, | |||||
| dialogue_count=self._dialogue_count, | |||||
| app_id=app_config.app_id, | |||||
| workflow_id=app_config.workflow_id, | |||||
| workflow_execution_id=self.application_generate_entity.workflow_run_id, | |||||
| ) | |||||
| # init variable pool | # init variable pool | ||||
| variable_pool = VariablePool( | variable_pool = VariablePool( | ||||
| system_variables=system_inputs, | system_variables=system_inputs, | ||||
| user_inputs=inputs, | user_inputs=inputs, | ||||
| environment_variables=workflow.environment_variables, | environment_variables=workflow.environment_variables, | ||||
| conversation_variables=conversation_variables, | |||||
| # Based on the definition of `VariableUnion`, | |||||
| # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. | |||||
| conversation_variables=cast(list[VariableUnion], conversation_variables), | |||||
| ) | ) | ||||
| # init graph | # init graph |
| from core.model_runtime.entities.llm_entities import LLMUsage | from core.model_runtime.entities.llm_entities import LLMUsage | ||||
| from core.ops.ops_trace_manager import TraceQueueManager | from core.ops.ops_trace_manager import TraceQueueManager | ||||
| from core.workflow.entities.workflow_execution import WorkflowExecutionStatus, WorkflowType | from core.workflow.entities.workflow_execution import WorkflowExecutionStatus, WorkflowType | ||||
| from core.workflow.enums import SystemVariableKey | |||||
| from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState | from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState | ||||
| from core.workflow.nodes import NodeType | from core.workflow.nodes import NodeType | ||||
| from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | ||||
| from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | ||||
| from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | ||||
| from core.workflow.system_variable import SystemVariable | |||||
| from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager | from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager | ||||
| from events.message_event import message_was_created | from events.message_event import message_was_created | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| self._workflow_cycle_manager = WorkflowCycleManager( | self._workflow_cycle_manager = WorkflowCycleManager( | ||||
| application_generate_entity=application_generate_entity, | application_generate_entity=application_generate_entity, | ||||
| workflow_system_variables={ | |||||
| SystemVariableKey.QUERY: message.query, | |||||
| SystemVariableKey.FILES: application_generate_entity.files, | |||||
| SystemVariableKey.CONVERSATION_ID: conversation.id, | |||||
| SystemVariableKey.USER_ID: user_session_id, | |||||
| SystemVariableKey.DIALOGUE_COUNT: dialogue_count, | |||||
| SystemVariableKey.APP_ID: application_generate_entity.app_config.app_id, | |||||
| SystemVariableKey.WORKFLOW_ID: workflow.id, | |||||
| SystemVariableKey.WORKFLOW_EXECUTION_ID: application_generate_entity.workflow_run_id, | |||||
| }, | |||||
| workflow_system_variables=SystemVariable( | |||||
| query=message.query, | |||||
| files=application_generate_entity.files, | |||||
| conversation_id=conversation.id, | |||||
| user_id=user_session_id, | |||||
| dialogue_count=dialogue_count, | |||||
| app_id=application_generate_entity.app_config.app_id, | |||||
| workflow_id=workflow.id, | |||||
| workflow_execution_id=application_generate_entity.workflow_run_id, | |||||
| ), | |||||
| workflow_info=CycleManagerWorkflowInfo( | workflow_info=CycleManagerWorkflowInfo( | ||||
| workflow_id=workflow.id, | workflow_id=workflow.id, | ||||
| workflow_type=WorkflowType(workflow.type), | workflow_type=WorkflowType(workflow.type), |
| class AppRunner: | class AppRunner: | ||||
| def get_pre_calculate_rest_tokens( | |||||
| self, | |||||
| app_record: App, | |||||
| model_config: ModelConfigWithCredentialsEntity, | |||||
| prompt_template_entity: PromptTemplateEntity, | |||||
| inputs: Mapping[str, str], | |||||
| files: Sequence["File"], | |||||
| query: Optional[str] = None, | |||||
| ) -> int: | |||||
| """ | |||||
| Get pre calculate rest tokens | |||||
| :param app_record: app record | |||||
| :param model_config: model config entity | |||||
| :param prompt_template_entity: prompt template entity | |||||
| :param inputs: inputs | |||||
| :param files: files | |||||
| :param query: query | |||||
| :return: | |||||
| """ | |||||
| # Invoke model | |||||
| model_instance = ModelInstance( | |||||
| provider_model_bundle=model_config.provider_model_bundle, model=model_config.model | |||||
| ) | |||||
| model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE) | |||||
| max_tokens = 0 | |||||
| for parameter_rule in model_config.model_schema.parameter_rules: | |||||
| if parameter_rule.name == "max_tokens" or ( | |||||
| parameter_rule.use_template and parameter_rule.use_template == "max_tokens" | |||||
| ): | |||||
| max_tokens = ( | |||||
| model_config.parameters.get(parameter_rule.name) | |||||
| or model_config.parameters.get(parameter_rule.use_template or "") | |||||
| ) or 0 | |||||
| if model_context_tokens is None: | |||||
| return -1 | |||||
| if max_tokens is None: | |||||
| max_tokens = 0 | |||||
| # get prompt messages without memory and context | |||||
| prompt_messages, stop = self.organize_prompt_messages( | |||||
| app_record=app_record, | |||||
| model_config=model_config, | |||||
| prompt_template_entity=prompt_template_entity, | |||||
| inputs=inputs, | |||||
| files=files, | |||||
| query=query, | |||||
| ) | |||||
| prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages) | |||||
| rest_tokens: int = model_context_tokens - max_tokens - prompt_tokens | |||||
| if rest_tokens < 0: | |||||
| raise InvokeBadRequestError( | |||||
| "Query or prefix prompt is too long, you can reduce the prefix prompt, " | |||||
| "or shrink the max token, or switch to a llm with a larger token limit size." | |||||
| ) | |||||
| return rest_tokens | |||||
| def recalc_llm_max_tokens( | def recalc_llm_max_tokens( | ||||
| self, model_config: ModelConfigWithCredentialsEntity, prompt_messages: list[PromptMessage] | self, model_config: ModelConfigWithCredentialsEntity, prompt_messages: list[PromptMessage] | ||||
| ): | ): |
| from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse | from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse | ||||
| from core.model_runtime.errors.invoke import InvokeAuthorizationError | from core.model_runtime.errors.invoke import InvokeAuthorizationError | ||||
| from core.ops.ops_trace_manager import TraceQueueManager | from core.ops.ops_trace_manager import TraceQueueManager | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | ||||
| from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | ||||
| from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | ||||
| workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING | workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING | ||||
| else: | else: | ||||
| workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN | workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=workflow_triggered_from, | triggered_from=workflow_triggered_from, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| # Create session factory | # Create session factory | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | ||||
| # Create workflow execution(aka workflow run) repository | # Create workflow execution(aka workflow run) repository | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | |||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| # Create session factory | # Create session factory | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | ||||
| # Create workflow execution(aka workflow run) repository | # Create workflow execution(aka workflow run) repository | ||||
| workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( | |||||
| workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, | ||||
| triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, | ||||
| ) | ) | ||||
| # Create workflow node execution repository | # Create workflow node execution repository | ||||
| session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) | |||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=user, | user=user, | ||||
| app_id=application_generate_entity.app_config.app_id, | app_id=application_generate_entity.app_config.app_id, |
| ) | ) | ||||
| from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback | from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback | ||||
| from core.workflow.entities.variable_pool import VariablePool | from core.workflow.entities.variable_pool import VariablePool | ||||
| from core.workflow.enums import SystemVariableKey | |||||
| from core.workflow.system_variable import SystemVariable | |||||
| from core.workflow.variable_loader import VariableLoader | from core.workflow.variable_loader import VariableLoader | ||||
| from core.workflow.workflow_entry import WorkflowEntry | from core.workflow.workflow_entry import WorkflowEntry | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| files = self.application_generate_entity.files | files = self.application_generate_entity.files | ||||
| # Create a variable pool. | # Create a variable pool. | ||||
| system_inputs = { | |||||
| SystemVariableKey.FILES: files, | |||||
| SystemVariableKey.USER_ID: user_id, | |||||
| SystemVariableKey.APP_ID: app_config.app_id, | |||||
| SystemVariableKey.WORKFLOW_ID: app_config.workflow_id, | |||||
| SystemVariableKey.WORKFLOW_EXECUTION_ID: self.application_generate_entity.workflow_execution_id, | |||||
| } | |||||
| system_inputs = SystemVariable( | |||||
| files=files, | |||||
| user_id=user_id, | |||||
| app_id=app_config.app_id, | |||||
| workflow_id=app_config.workflow_id, | |||||
| workflow_execution_id=self.application_generate_entity.workflow_execution_id, | |||||
| ) | |||||
| variable_pool = VariablePool( | variable_pool = VariablePool( | ||||
| system_variables=system_inputs, | system_variables=system_inputs, |
| from collections.abc import Generator | from collections.abc import Generator | ||||
| from typing import Optional, Union | from typing import Optional, Union | ||||
| from sqlalchemy import select | |||||
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
| from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME | from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME | ||||
| from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk | from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk | ||||
| from core.ops.ops_trace_manager import TraceQueueManager | from core.ops.ops_trace_manager import TraceQueueManager | ||||
| from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | ||||
| from core.workflow.enums import SystemVariableKey | |||||
| from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory | ||||
| from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | ||||
| from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | ||||
| from core.workflow.system_variable import SystemVariable | |||||
| from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager | from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from models.account import Account | from models.account import Account | ||||
| Workflow, | Workflow, | ||||
| WorkflowAppLog, | WorkflowAppLog, | ||||
| WorkflowAppLogCreatedFrom, | WorkflowAppLogCreatedFrom, | ||||
| WorkflowRun, | |||||
| ) | ) | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| self._workflow_cycle_manager = WorkflowCycleManager( | self._workflow_cycle_manager = WorkflowCycleManager( | ||||
| application_generate_entity=application_generate_entity, | application_generate_entity=application_generate_entity, | ||||
| workflow_system_variables={ | |||||
| SystemVariableKey.FILES: application_generate_entity.files, | |||||
| SystemVariableKey.USER_ID: user_session_id, | |||||
| SystemVariableKey.APP_ID: application_generate_entity.app_config.app_id, | |||||
| SystemVariableKey.WORKFLOW_ID: workflow.id, | |||||
| SystemVariableKey.WORKFLOW_EXECUTION_ID: application_generate_entity.workflow_execution_id, | |||||
| }, | |||||
| workflow_system_variables=SystemVariable( | |||||
| files=application_generate_entity.files, | |||||
| user_id=user_session_id, | |||||
| app_id=application_generate_entity.app_config.app_id, | |||||
| workflow_id=workflow.id, | |||||
| workflow_execution_id=application_generate_entity.workflow_execution_id, | |||||
| ), | |||||
| workflow_info=CycleManagerWorkflowInfo( | workflow_info=CycleManagerWorkflowInfo( | ||||
| workflow_id=workflow.id, | workflow_id=workflow.id, | ||||
| workflow_type=WorkflowType(workflow.type), | workflow_type=WorkflowType(workflow.type), | ||||
| tts_publisher.publish(None) | tts_publisher.publish(None) | ||||
| def _save_workflow_app_log(self, *, session: Session, workflow_execution: WorkflowExecution) -> None: | def _save_workflow_app_log(self, *, session: Session, workflow_execution: WorkflowExecution) -> None: | ||||
| workflow_run = session.scalar(select(WorkflowRun).where(WorkflowRun.id == workflow_execution.id_)) | |||||
| assert workflow_run is not None | |||||
| invoke_from = self._application_generate_entity.invoke_from | invoke_from = self._application_generate_entity.invoke_from | ||||
| if invoke_from == InvokeFrom.SERVICE_API: | if invoke_from == InvokeFrom.SERVICE_API: | ||||
| created_from = WorkflowAppLogCreatedFrom.SERVICE_API | created_from = WorkflowAppLogCreatedFrom.SERVICE_API | ||||
| return | return | ||||
| workflow_app_log = WorkflowAppLog() | workflow_app_log = WorkflowAppLog() | ||||
| workflow_app_log.tenant_id = workflow_run.tenant_id | |||||
| workflow_app_log.app_id = workflow_run.app_id | |||||
| workflow_app_log.workflow_id = workflow_run.workflow_id | |||||
| workflow_app_log.workflow_run_id = workflow_run.id | |||||
| workflow_app_log.tenant_id = self._application_generate_entity.app_config.tenant_id | |||||
| workflow_app_log.app_id = self._application_generate_entity.app_config.app_id | |||||
| workflow_app_log.workflow_id = workflow_execution.workflow_id | |||||
| workflow_app_log.workflow_run_id = workflow_execution.id_ | |||||
| workflow_app_log.created_from = created_from.value | workflow_app_log.created_from = created_from.value | ||||
| workflow_app_log.created_by_role = self._created_by_role | workflow_app_log.created_by_role = self._created_by_role | ||||
| workflow_app_log.created_by = self._user_id | workflow_app_log.created_by = self._user_id |
| from core.workflow.graph_engine.entities.graph import Graph | from core.workflow.graph_engine.entities.graph import Graph | ||||
| from core.workflow.nodes import NodeType | from core.workflow.nodes import NodeType | ||||
| from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING | from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING | ||||
| from core.workflow.system_variable import SystemVariable | |||||
| from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool | from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool | ||||
| from core.workflow.workflow_entry import WorkflowEntry | from core.workflow.workflow_entry import WorkflowEntry | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| # init variable pool | # init variable pool | ||||
| variable_pool = VariablePool( | variable_pool = VariablePool( | ||||
| system_variables={}, | |||||
| system_variables=SystemVariable.empty(), | |||||
| user_inputs={}, | user_inputs={}, | ||||
| environment_variables=workflow.environment_variables, | environment_variables=workflow.environment_variables, | ||||
| ) | ) | ||||
| # init variable pool | # init variable pool | ||||
| variable_pool = VariablePool( | variable_pool = VariablePool( | ||||
| system_variables={}, | |||||
| system_variables=SystemVariable.empty(), | |||||
| user_inputs={}, | user_inputs={}, | ||||
| environment_variables=workflow.environment_variables, | environment_variables=workflow.environment_variables, | ||||
| ) | ) |
| class WorkflowRunNotFoundError(RecordNotFoundError): | class WorkflowRunNotFoundError(RecordNotFoundError): | ||||
| def __init__(self, workflow_run_id: str): | def __init__(self, workflow_run_id: str): | ||||
| super().__init__("WorkflowRun", workflow_run_id) | super().__init__("WorkflowRun", workflow_run_id) | ||||
| class WorkflowNodeExecutionNotFoundError(RecordNotFoundError): | |||||
| def __init__(self, workflow_node_execution_id: str): | |||||
| super().__init__("WorkflowNodeExecution", workflow_node_execution_id) |
| APP_SELECTOR = "app-selector" | APP_SELECTOR = "app-selector" | ||||
| MODEL_SELECTOR = "model-selector" | MODEL_SELECTOR = "model-selector" | ||||
| TOOLS_SELECTOR = "array[tools]" | TOOLS_SELECTOR = "array[tools]" | ||||
| ANY = "any" | |||||
| # Dynamic select parameter | # Dynamic select parameter | ||||
| # Once you are not sure about the available options until authorization is done | # Once you are not sure about the available options until authorization is done |
| _tool_file_manager_factory: Callable[[], "ToolFileManager"] | None = None | _tool_file_manager_factory: Callable[[], "ToolFileManager"] | None = None | ||||
| class ToolFileParser: | |||||
| @staticmethod | |||||
| def get_tool_file_manager() -> "ToolFileManager": | |||||
| assert _tool_file_manager_factory is not None | |||||
| return _tool_file_manager_factory() | |||||
| def set_tool_file_manager_factory(factory: Callable[[], "ToolFileManager"]) -> None: | def set_tool_file_manager_factory(factory: Callable[[], "ToolFileManager"]) -> None: | ||||
| global _tool_file_manager_factory | global _tool_file_manager_factory | ||||
| _tool_file_manager_factory = factory | _tool_file_manager_factory = factory |
| from collections.abc import Mapping | from collections.abc import Mapping | ||||
| from typing import Any | from typing import Any | ||||
| from core.variables.utils import SegmentJSONEncoder | |||||
| class TemplateTransformer(ABC): | class TemplateTransformer(ABC): | ||||
| _code_placeholder: str = "{{code}}" | _code_placeholder: str = "{{code}}" | ||||
| result_str = cls.extract_result_str_from_response(response) | result_str = cls.extract_result_str_from_response(response) | ||||
| result = json.loads(result_str) | result = json.loads(result_str) | ||||
| except json.JSONDecodeError as e: | except json.JSONDecodeError as e: | ||||
| raise ValueError(f"Failed to parse JSON response: {str(e)}. Response content: {result_str[:200]}...") | |||||
| raise ValueError(f"Failed to parse JSON response: {str(e)}.") | |||||
| except ValueError as e: | except ValueError as e: | ||||
| # Re-raise ValueError from extract_result_str_from_response | # Re-raise ValueError from extract_result_str_from_response | ||||
| raise e | raise e | ||||
| except Exception as e: | except Exception as e: | ||||
| raise ValueError(f"Unexpected error during response transformation: {str(e)}") | raise ValueError(f"Unexpected error during response transformation: {str(e)}") | ||||
| # Check if the result contains an error | |||||
| if isinstance(result, dict) and "error" in result: | |||||
| raise ValueError(f"JavaScript execution error: {result['error']}") | |||||
| if not isinstance(result, dict): | if not isinstance(result, dict): | ||||
| raise ValueError(f"Result must be a dict, got {type(result).__name__}") | raise ValueError(f"Result must be a dict, got {type(result).__name__}") | ||||
| if not all(isinstance(k, str) for k in result): | if not all(isinstance(k, str) for k in result): | ||||
| @classmethod | @classmethod | ||||
| def serialize_inputs(cls, inputs: Mapping[str, Any]) -> str: | def serialize_inputs(cls, inputs: Mapping[str, Any]) -> str: | ||||
| inputs_json_str = json.dumps(inputs, ensure_ascii=False).encode() | |||||
| inputs_json_str = json.dumps(inputs, ensure_ascii=False, cls=SegmentJSONEncoder).encode() | |||||
| input_base64_encoded = b64encode(inputs_json_str).decode("utf-8") | input_base64_encoded = b64encode(inputs_json_str).decode("utf-8") | ||||
| return input_base64_encoded | return input_base64_encoded | ||||
| import base64 | |||||
| import hashlib | |||||
| import hmac | |||||
| import os | |||||
| import time | |||||
| from pydantic import BaseModel, Field | |||||
| from configs import dify_config | |||||
| class SignedUrlParams(BaseModel): | |||||
| sign_key: str = Field(..., description="The sign key") | |||||
| timestamp: str = Field(..., description="Timestamp") | |||||
| nonce: str = Field(..., description="Nonce") | |||||
| sign: str = Field(..., description="Signature") | |||||
| class UrlSigner: | |||||
| @classmethod | |||||
| def get_signed_url(cls, url: str, sign_key: str, prefix: str) -> str: | |||||
| signed_url_params = cls.get_signed_url_params(sign_key, prefix) | |||||
| return ( | |||||
| f"{url}?timestamp={signed_url_params.timestamp}" | |||||
| f"&nonce={signed_url_params.nonce}&sign={signed_url_params.sign}" | |||||
| ) | |||||
| @classmethod | |||||
| def get_signed_url_params(cls, sign_key: str, prefix: str) -> SignedUrlParams: | |||||
| timestamp = str(int(time.time())) | |||||
| nonce = os.urandom(16).hex() | |||||
| sign = cls._sign(sign_key, timestamp, nonce, prefix) | |||||
| return SignedUrlParams(sign_key=sign_key, timestamp=timestamp, nonce=nonce, sign=sign) | |||||
| @classmethod | |||||
| def verify(cls, sign_key: str, timestamp: str, nonce: str, sign: str, prefix: str) -> bool: | |||||
| recalculated_sign = cls._sign(sign_key, timestamp, nonce, prefix) | |||||
| return sign == recalculated_sign | |||||
| @classmethod | |||||
| def _sign(cls, sign_key: str, timestamp: str, nonce: str, prefix: str) -> str: | |||||
| if not dify_config.SECRET_KEY: | |||||
| raise Exception("SECRET_KEY is not set") | |||||
| data_to_sign = f"{prefix}|{sign_key}|{timestamp}|{nonce}" | |||||
| secret_key = dify_config.SECRET_KEY.encode() | |||||
| sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest() | |||||
| encoded_sign = base64.urlsafe_b64encode(sign).decode() | |||||
| return encoded_sign |
| model_manager = ModelManager() | model_manager = ModelManager() | ||||
| model_instance = model_manager.get_default_model_instance( | |||||
| model_instance = model_manager.get_model_instance( | |||||
| tenant_id=tenant_id, | tenant_id=tenant_id, | ||||
| model_type=ModelType.LLM, | model_type=ModelType.LLM, | ||||
| provider=model_config.get("provider", ""), | |||||
| model=model_config.get("name", ""), | |||||
| ) | ) | ||||
| try: | try: |
| response = requests.post(token_url, data=params) | response = requests.post(token_url, data=params) | ||||
| if not response.ok: | if not response.ok: | ||||
| raise ValueError(f"Token refresh failed: HTTP {response.status_code}") | raise ValueError(f"Token refresh failed: HTTP {response.status_code}") | ||||
| return OAuthTokens.parse_obj(response.json()) | |||||
| return OAuthTokens.model_validate(response.json()) | |||||
| def register_client( | def register_client( |
| if not self.end_user: | if not self.end_user: | ||||
| raise ValueError("User not found") | raise ValueError("User not found") | ||||
| request = cast(types.CallToolRequest, self.request.root) | request = cast(types.CallToolRequest, self.request.root) | ||||
| args = request.params.arguments | |||||
| if not args: | |||||
| raise ValueError("No arguments provided") | |||||
| args = request.params.arguments or {} | |||||
| if self.app.mode in {AppMode.WORKFLOW.value}: | if self.app.mode in {AppMode.WORKFLOW.value}: | ||||
| args = {"inputs": args} | args = {"inputs": args} | ||||
| elif self.app.mode in {AppMode.COMPLETION.value}: | elif self.app.mode in {AppMode.COMPLETION.value}: |
| import logging | import logging | ||||
| import queue | import queue | ||||
| from collections.abc import Callable | from collections.abc import Callable | ||||
| from concurrent.futures import ThreadPoolExecutor | |||||
| from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError | |||||
| from contextlib import ExitStack | from contextlib import ExitStack | ||||
| from datetime import timedelta | from datetime import timedelta | ||||
| from types import TracebackType | from types import TracebackType | ||||
| self._session_read_timeout_seconds = read_timeout_seconds | self._session_read_timeout_seconds = read_timeout_seconds | ||||
| self._in_flight = {} | self._in_flight = {} | ||||
| self._exit_stack = ExitStack() | self._exit_stack = ExitStack() | ||||
| # Initialize executor and future to None for proper cleanup checks | |||||
| self._executor: ThreadPoolExecutor | None = None | |||||
| self._receiver_future: Future | None = None | |||||
| def __enter__(self) -> Self: | def __enter__(self) -> Self: | ||||
| self._executor = ThreadPoolExecutor() | |||||
| # The thread pool is dedicated to running `_receive_loop`. Setting `max_workers` to 1 | |||||
| # ensures no unnecessary threads are created. | |||||
| self._executor = ThreadPoolExecutor(max_workers=1) | |||||
| self._receiver_future = self._executor.submit(self._receive_loop) | self._receiver_future = self._executor.submit(self._receive_loop) | ||||
| return self | return self | ||||
| def check_receiver_status(self) -> None: | def check_receiver_status(self) -> None: | ||||
| if self._receiver_future.done(): | |||||
| """`check_receiver_status` ensures that any exceptions raised during the | |||||
| execution of `_receive_loop` are retrieved and propagated.""" | |||||
| if self._receiver_future and self._receiver_future.done(): | |||||
| self._receiver_future.result() | self._receiver_future.result() | ||||
| def __exit__( | def __exit__( | ||||
| self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None | self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None | ||||
| ) -> None: | ) -> None: | ||||
| self._exit_stack.close() | |||||
| self._read_stream.put(None) | self._read_stream.put(None) | ||||
| self._write_stream.put(None) | self._write_stream.put(None) | ||||
| # Wait for the receiver loop to finish | |||||
| if self._receiver_future: | |||||
| try: | |||||
| self._receiver_future.result(timeout=5.0) # Wait up to 5 seconds | |||||
| except TimeoutError: | |||||
| # If the receiver loop is still running after timeout, we'll force shutdown | |||||
| pass | |||||
| # Shutdown the executor | |||||
| if self._executor: | |||||
| self._executor.shutdown(wait=True) | |||||
| def send_request( | def send_request( | ||||
| self, | self, | ||||
| request: SendRequestT, | request: SendRequestT, |
| from collections.abc import Sequence | from collections.abc import Sequence | ||||
| from typing import Optional | from typing import Optional | ||||
| from sqlalchemy import select | |||||
| from core.app.app_config.features.file_upload.manager import FileUploadConfigManager | from core.app.app_config.features.file_upload.manager import FileUploadConfigManager | ||||
| from core.file import file_manager | from core.file import file_manager | ||||
| from core.model_manager import ModelInstance | from core.model_manager import ModelInstance | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from factories import file_factory | from factories import file_factory | ||||
| from models.model import AppMode, Conversation, Message, MessageFile | from models.model import AppMode, Conversation, Message, MessageFile | ||||
| from models.workflow import WorkflowRun | |||||
| from models.workflow import Workflow, WorkflowRun | |||||
| class TokenBufferMemory: | class TokenBufferMemory: | ||||
| def __init__(self, conversation: Conversation, model_instance: ModelInstance) -> None: | |||||
| def __init__( | |||||
| self, | |||||
| conversation: Conversation, | |||||
| model_instance: ModelInstance, | |||||
| ) -> None: | |||||
| self.conversation = conversation | self.conversation = conversation | ||||
| self.model_instance = model_instance | self.model_instance = model_instance | ||||
| app_record = self.conversation.app | app_record = self.conversation.app | ||||
| # fetch limited messages, and return reversed | # fetch limited messages, and return reversed | ||||
| query = ( | |||||
| db.session.query( | |||||
| Message.id, | |||||
| Message.query, | |||||
| Message.answer, | |||||
| Message.created_at, | |||||
| Message.workflow_run_id, | |||||
| Message.parent_message_id, | |||||
| Message.answer_tokens, | |||||
| ) | |||||
| .filter( | |||||
| Message.conversation_id == self.conversation.id, | |||||
| ) | |||||
| .order_by(Message.created_at.desc()) | |||||
| stmt = ( | |||||
| select(Message).where(Message.conversation_id == self.conversation.id).order_by(Message.created_at.desc()) | |||||
| ) | ) | ||||
| if message_limit and message_limit > 0: | if message_limit and message_limit > 0: | ||||
| else: | else: | ||||
| message_limit = 500 | message_limit = 500 | ||||
| messages = query.limit(message_limit).all() | |||||
| stmt = stmt.limit(message_limit) | |||||
| messages = db.session.scalars(stmt).all() | |||||
| # instead of all messages from the conversation, we only need to extract messages | # instead of all messages from the conversation, we only need to extract messages | ||||
| # that belong to the thread of last message | # that belong to the thread of last message | ||||
| files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all() | files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all() | ||||
| if files: | if files: | ||||
| file_extra_config = None | file_extra_config = None | ||||
| if self.conversation.mode not in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: | |||||
| if self.conversation.mode in {AppMode.AGENT_CHAT, AppMode.COMPLETION, AppMode.CHAT}: | |||||
| file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config) | file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config) | ||||
| elif self.conversation.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: | |||||
| workflow_run = db.session.scalar( | |||||
| select(WorkflowRun).where(WorkflowRun.id == message.workflow_run_id) | |||||
| ) | |||||
| if not workflow_run: | |||||
| raise ValueError(f"Workflow run not found: {message.workflow_run_id}") | |||||
| workflow = db.session.scalar(select(Workflow).where(Workflow.id == workflow_run.workflow_id)) | |||||
| if not workflow: | |||||
| raise ValueError(f"Workflow not found: {workflow_run.workflow_id}") | |||||
| file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) | |||||
| else: | else: | ||||
| if message.workflow_run_id: | |||||
| workflow_run = ( | |||||
| db.session.query(WorkflowRun).filter(WorkflowRun.id == message.workflow_run_id).first() | |||||
| ) | |||||
| if workflow_run and workflow_run.workflow: | |||||
| file_extra_config = FileUploadConfigManager.convert( | |||||
| workflow_run.workflow.features_dict, is_vision=False | |||||
| ) | |||||
| raise AssertionError(f"Invalid app mode: {self.conversation.mode}") | |||||
| detail = ImagePromptMessageContent.DETAIL.LOW | detail = ImagePromptMessageContent.DETAIL.LOW | ||||
| if file_extra_config and app_record: | if file_extra_config and app_record: |
| else: | else: | ||||
| node_span = self.build_workflow_task_span(trace_id, workflow_span_id, trace_info, node_execution) | node_span = self.build_workflow_task_span(trace_id, workflow_span_id, trace_info, node_execution) | ||||
| return node_span | return node_span | ||||
| except Exception: | |||||
| except Exception as e: | |||||
| logging.debug(f"Error occurred in build_workflow_node_span: {e}", exc_info=True) | |||||
| return None | return None | ||||
| def get_workflow_node_status(self, node_execution: WorkflowNodeExecution) -> Status: | def get_workflow_node_status(self, node_execution: WorkflowNodeExecution) -> Status: | ||||
| start_time=convert_datetime_to_nanoseconds(node_execution.created_at), | start_time=convert_datetime_to_nanoseconds(node_execution.created_at), | ||||
| end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), | end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), | ||||
| attributes={ | attributes={ | ||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""), | |||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", | |||||
| GEN_AI_SPAN_KIND: GenAISpanKind.TASK.value, | GEN_AI_SPAN_KIND: GenAISpanKind.TASK.value, | ||||
| GEN_AI_FRAMEWORK: "dify", | GEN_AI_FRAMEWORK: "dify", | ||||
| INPUT_VALUE: json.dumps(node_execution.inputs, ensure_ascii=False), | INPUT_VALUE: json.dumps(node_execution.inputs, ensure_ascii=False), | ||||
| start_time=convert_datetime_to_nanoseconds(node_execution.created_at), | start_time=convert_datetime_to_nanoseconds(node_execution.created_at), | ||||
| end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), | end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), | ||||
| attributes={ | attributes={ | ||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""), | |||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", | |||||
| GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value, | GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value, | ||||
| GEN_AI_FRAMEWORK: "dify", | GEN_AI_FRAMEWORK: "dify", | ||||
| GEN_AI_MODEL_NAME: process_data.get("model_name", ""), | GEN_AI_MODEL_NAME: process_data.get("model_name", ""), | ||||
| start_time=convert_datetime_to_nanoseconds(trace_info.start_time), | start_time=convert_datetime_to_nanoseconds(trace_info.start_time), | ||||
| end_time=convert_datetime_to_nanoseconds(trace_info.end_time), | end_time=convert_datetime_to_nanoseconds(trace_info.end_time), | ||||
| attributes={ | attributes={ | ||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""), | |||||
| GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", | |||||
| GEN_AI_USER_ID: str(user_id), | GEN_AI_USER_ID: str(user_id), | ||||
| GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value, | GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value, | ||||
| GEN_AI_FRAMEWORK: "dify", | GEN_AI_FRAMEWORK: "dify", |
| UnitEnum, | UnitEnum, | ||||
| ) | ) | ||||
| from core.ops.utils import filter_none_values | from core.ops.utils import filter_none_values | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from models import EndUser, WorkflowNodeExecutionTriggeredFrom | from models import EndUser, WorkflowNodeExecutionTriggeredFrom | ||||
| service_account = self.get_service_account_with_tenant(app_id) | service_account = self.get_service_account_with_tenant(app_id) | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=service_account, | user=service_account, | ||||
| app_id=trace_info.metadata.get("app_id"), | |||||
| app_id=app_id, | |||||
| triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | ||||
| ) | ) | ||||
| LangSmithRunUpdateModel, | LangSmithRunUpdateModel, | ||||
| ) | ) | ||||
| from core.ops.utils import filter_none_values, generate_dotted_order | from core.ops.utils import filter_none_values, generate_dotted_order | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | ||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| service_account = self.get_service_account_with_tenant(app_id) | service_account = self.get_service_account_with_tenant(app_id) | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=service_account, | user=service_account, | ||||
| app_id=trace_info.metadata.get("app_id"), | |||||
| app_id=app_id, | |||||
| triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | ||||
| ) | ) | ||||
| TraceTaskName, | TraceTaskName, | ||||
| WorkflowTraceInfo, | WorkflowTraceInfo, | ||||
| ) | ) | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | ||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| service_account = self.get_service_account_with_tenant(app_id) | service_account = self.get_service_account_with_tenant(app_id) | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=service_account, | user=service_account, | ||||
| app_id=trace_info.metadata.get("app_id"), | |||||
| app_id=app_id, | |||||
| triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | ||||
| ) | ) | ||||
| "trace_id": opik_trace_id, | "trace_id": opik_trace_id, | ||||
| "id": prepare_opik_uuid(created_at, node_execution_id), | "id": prepare_opik_uuid(created_at, node_execution_id), | ||||
| "parent_span_id": prepare_opik_uuid(trace_info.start_time, parent_span_id), | "parent_span_id": prepare_opik_uuid(trace_info.start_time, parent_span_id), | ||||
| "name": node_type, | |||||
| "name": node_name, | |||||
| "type": run_type, | "type": run_type, | ||||
| "start_time": created_at, | "start_time": created_at, | ||||
| "end_time": finished_at, | "end_time": finished_at, |
| WorkflowTraceInfo, | WorkflowTraceInfo, | ||||
| ) | ) | ||||
| from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel | from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | |||||
| from core.repositories import DifyCoreRepositoryFactory | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey | ||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| service_account = self.get_service_account_with_tenant(app_id) | service_account = self.get_service_account_with_tenant(app_id) | ||||
| workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( | |||||
| workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( | |||||
| session_factory=session_factory, | session_factory=session_factory, | ||||
| user=service_account, | user=service_account, | ||||
| app_id=trace_info.metadata.get("app_id"), | |||||
| app_id=app_id, | |||||
| triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | ||||
| ) | ) | ||||
| from core.entities.parameter_entities import CommonParameterType | from core.entities.parameter_entities import CommonParameterType | ||||
| from core.tools.entities.common_entities import I18nObject | from core.tools.entities.common_entities import I18nObject | ||||
| from core.workflow.nodes.base.entities import NumberType | |||||
| class PluginParameterOption(BaseModel): | class PluginParameterOption(BaseModel): | ||||
| APP_SELECTOR = CommonParameterType.APP_SELECTOR.value | APP_SELECTOR = CommonParameterType.APP_SELECTOR.value | ||||
| MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value | MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value | ||||
| TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value | TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value | ||||
| ANY = CommonParameterType.ANY.value | |||||
| DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value | DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value | ||||
| # deprecated, should not use. | # deprecated, should not use. | ||||
| if value and not isinstance(value, list): | if value and not isinstance(value, list): | ||||
| raise ValueError("The tools selector must be a list.") | raise ValueError("The tools selector must be a list.") | ||||
| return value | return value | ||||
| case PluginParameterType.ANY: | |||||
| if value and not isinstance(value, str | dict | list | NumberType): | |||||
| raise ValueError("The var selector must be a string, dictionary, list or number.") | |||||
| return value | |||||
| case PluginParameterType.ARRAY: | case PluginParameterType.ARRAY: | ||||
| if not isinstance(value, list): | if not isinstance(value, list): | ||||
| # Try to parse JSON string for arrays | # Try to parse JSON string for arrays |
| return self | return self | ||||
| class GithubPackage(BaseModel): | |||||
| repo: str | |||||
| version: str | |||||
| package: str | |||||
| class GithubVersion(BaseModel): | |||||
| repo: str | |||||
| version: str | |||||
| class GenericProviderID: | class GenericProviderID: | ||||
| organization: str | organization: str | ||||
| plugin_name: str | plugin_name: str |
| "GET", | "GET", | ||||
| f"plugin/{tenant_id}/management/list", | f"plugin/{tenant_id}/management/list", | ||||
| PluginListResponse, | PluginListResponse, | ||||
| params={"page": 1, "page_size": 256}, | |||||
| params={"page": 1, "page_size": 256, "response_type": "paged"}, | |||||
| ) | ) | ||||
| return result.list | return result.list | ||||
| "GET", | "GET", | ||||
| f"plugin/{tenant_id}/management/list", | f"plugin/{tenant_id}/management/list", | ||||
| PluginListResponse, | PluginListResponse, | ||||
| params={"page": page, "page_size": page_size}, | |||||
| params={"page": page, "page_size": page_size, "response_type": "paged"}, | |||||
| ) | ) | ||||
| def upload_pkg( | def upload_pkg( |
| if prompt_item.edition_type == "basic" or not prompt_item.edition_type: | if prompt_item.edition_type == "basic" or not prompt_item.edition_type: | ||||
| if self.with_variable_tmpl: | if self.with_variable_tmpl: | ||||
| vp = VariablePool() | |||||
| vp = VariablePool.empty() | |||||
| for k, v in inputs.items(): | for k, v in inputs.items(): | ||||
| if k.startswith("#"): | if k.startswith("#"): | ||||
| vp.add(k[1:-1].split("."), v) | vp.add(k[1:-1].split("."), v) |
| from typing import Any | |||||
| from collections.abc import Sequence | |||||
| from constants import UUID_NIL | from constants import UUID_NIL | ||||
| from models import Message | |||||
| def extract_thread_messages(messages: list[Any]): | |||||
| thread_messages = [] | |||||
| def extract_thread_messages(messages: Sequence[Message]): | |||||
| thread_messages: list[Message] = [] | |||||
| next_message = None | next_message = None | ||||
| for message in messages: | for message in messages: |
| from sqlalchemy import select | |||||
| from core.prompt.utils.extract_thread_messages import extract_thread_messages | from core.prompt.utils.extract_thread_messages import extract_thread_messages | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from models.model import Message | from models.model import Message | ||||
| Get the number of thread messages based on the parent message id. | Get the number of thread messages based on the parent message id. | ||||
| """ | """ | ||||
| # Fetch all messages related to the conversation | # Fetch all messages related to the conversation | ||||
| query = ( | |||||
| db.session.query( | |||||
| Message.id, | |||||
| Message.parent_message_id, | |||||
| Message.answer, | |||||
| ) | |||||
| .filter( | |||||
| Message.conversation_id == conversation_id, | |||||
| ) | |||||
| .order_by(Message.created_at.desc()) | |||||
| ) | |||||
| stmt = select(Message).where(Message.conversation_id == conversation_id).order_by(Message.created_at.desc()) | |||||
| messages = query.all() | |||||
| messages = db.session.scalars(stmt).all() | |||||
| # Extract thread messages | # Extract thread messages | ||||
| thread_messages = extract_thread_messages(messages) | thread_messages = extract_thread_messages(messages) |
| """Abstract interface for document clean implementations.""" | |||||
| from core.rag.cleaner.cleaner_base import BaseCleaner | |||||
| class UnstructuredNonAsciiCharsCleaner(BaseCleaner): | |||||
| def clean(self, content) -> str: | |||||
| """clean document content.""" | |||||
| from unstructured.cleaners.core import clean_extra_whitespace | |||||
| # Returns "ITEM 1A: RISK FACTORS" | |||||
| return clean_extra_whitespace(content) |
| """Abstract interface for document clean implementations.""" | |||||
| from core.rag.cleaner.cleaner_base import BaseCleaner | |||||
| class UnstructuredGroupBrokenParagraphsCleaner(BaseCleaner): | |||||
| def clean(self, content) -> str: | |||||
| """clean document content.""" | |||||
| import re | |||||
| from unstructured.cleaners.core import group_broken_paragraphs | |||||
| para_split_re = re.compile(r"(\s*\n\s*){3}") | |||||
| return group_broken_paragraphs(content, paragraph_split=para_split_re) |
| """Abstract interface for document clean implementations.""" | |||||
| from core.rag.cleaner.cleaner_base import BaseCleaner | |||||
| class UnstructuredNonAsciiCharsCleaner(BaseCleaner): | |||||
| def clean(self, content) -> str: | |||||
| """clean document content.""" | |||||
| from unstructured.cleaners.core import clean_non_ascii_chars | |||||
| # Returns "This text contains non-ascii characters!" | |||||
| return clean_non_ascii_chars(content) |
| """Abstract interface for document clean implementations.""" | |||||
| from core.rag.cleaner.cleaner_base import BaseCleaner | |||||
| class UnstructuredNonAsciiCharsCleaner(BaseCleaner): | |||||
| def clean(self, content) -> str: | |||||
| """Replaces unicode quote characters, such as the \x91 character in a string.""" | |||||
| from unstructured.cleaners.core import replace_unicode_quotes | |||||
| return replace_unicode_quotes(content) |
| """Abstract interface for document clean implementations.""" | |||||
| from core.rag.cleaner.cleaner_base import BaseCleaner | |||||
| class UnstructuredTranslateTextCleaner(BaseCleaner): | |||||
| def clean(self, content) -> str: | |||||
| """clean document content.""" | |||||
| from unstructured.cleaners.translate import translate_text | |||||
| return translate_text(content) |
| from typing import Optional | from typing import Optional | ||||
| from flask import Flask, current_app | from flask import Flask, current_app | ||||
| from sqlalchemy.orm import load_only | |||||
| from sqlalchemy.orm import Session, load_only | |||||
| from configs import dify_config | from configs import dify_config | ||||
| from core.rag.data_post_processor.data_post_processor import DataPostProcessor | from core.rag.data_post_processor.data_post_processor import DataPostProcessor | ||||
| @classmethod | @classmethod | ||||
| def _get_dataset(cls, dataset_id: str) -> Optional[Dataset]: | def _get_dataset(cls, dataset_id: str) -> Optional[Dataset]: | ||||
| return db.session.query(Dataset).filter(Dataset.id == dataset_id).first() | |||||
| with Session(db.engine) as session: | |||||
| return session.query(Dataset).filter(Dataset.id == dataset_id).first() | |||||
| @classmethod | @classmethod | ||||
| def keyword_search( | def keyword_search( |
| import tablestore # type: ignore | import tablestore # type: ignore | ||||
| from pydantic import BaseModel, model_validator | from pydantic import BaseModel, model_validator | ||||
| from tablestore import BatchGetRowRequest, TableInBatchGetRowItem | |||||
| from configs import dify_config | from configs import dify_config | ||||
| from core.rag.datasource.vdb.field import Field | from core.rag.datasource.vdb.field import Field | ||||
| self._index_name = f"{collection_name}_idx" | self._index_name = f"{collection_name}_idx" | ||||
| self._tags_field = f"{Field.METADATA_KEY.value}_tags" | self._tags_field = f"{Field.METADATA_KEY.value}_tags" | ||||
| def create_collection(self, embeddings: list[list[float]], **kwargs): | |||||
| dimension = len(embeddings[0]) | |||||
| self._create_collection(dimension) | |||||
| def get_by_ids(self, ids: list[str]) -> list[Document]: | |||||
| docs = [] | |||||
| request = BatchGetRowRequest() | |||||
| columns_to_get = [Field.METADATA_KEY.value, Field.CONTENT_KEY.value] | |||||
| rows_to_get = [[("id", _id)] for _id in ids] | |||||
| request.add(TableInBatchGetRowItem(self._table_name, rows_to_get, columns_to_get, None, 1)) | |||||
| result = self._tablestore_client.batch_get_row(request) | |||||
| table_result = result.get_result_by_table(self._table_name) | |||||
| for item in table_result: | |||||
| if item.is_ok and item.row: | |||||
| kv = {k: v for k, v, t in item.row.attribute_columns} | |||||
| docs.append( | |||||
| Document( | |||||
| page_content=kv[Field.CONTENT_KEY.value], metadata=json.loads(kv[Field.METADATA_KEY.value]) | |||||
| ) | |||||
| ) | |||||
| return docs | |||||
| def get_type(self) -> str: | def get_type(self) -> str: | ||||
| return VectorType.TABLESTORE | return VectorType.TABLESTORE | ||||
| from typing import Optional | |||||
| from pydantic import BaseModel | |||||
| class ClusterEntity(BaseModel): | |||||
| """ | |||||
| Model Config Entity. | |||||
| """ | |||||
| name: str | |||||
| cluster_id: str | |||||
| displayName: str | |||||
| region: str | |||||
| spendingLimit: Optional[int] = 1000 | |||||
| version: str | |||||
| createdBy: str |
| import contextlib | import contextlib | ||||
| import mimetypes | import mimetypes | ||||
| from abc import ABC, abstractmethod | |||||
| from collections.abc import Generator, Iterable, Mapping | |||||
| from collections.abc import Generator, Mapping | |||||
| from io import BufferedReader, BytesIO | from io import BufferedReader, BytesIO | ||||
| from pathlib import Path, PurePath | from pathlib import Path, PurePath | ||||
| from typing import Any, Optional, Union | from typing import Any, Optional, Union | ||||
| if self.source: | if self.source: | ||||
| str_repr += f" {self.source}" | str_repr += f" {self.source}" | ||||
| return str_repr | return str_repr | ||||
| class BlobLoader(ABC): | |||||
| """Abstract interface for blob loaders implementation. | |||||
| Implementer should be able to load raw content from a datasource system according | |||||
| to some criteria and return the raw content lazily as a stream of blobs. | |||||
| """ | |||||
| @abstractmethod | |||||
| def yield_blobs( | |||||
| self, | |||||
| ) -> Iterable[Blob]: | |||||
| """A lazy loader for raw data represented by Blob object. | |||||
| Returns: | |||||
| A generator over blobs | |||||
| """ |
| import logging | |||||
| from core.rag.extractor.extractor_base import BaseExtractor | |||||
| from core.rag.models.document import Document | |||||
| logger = logging.getLogger(__name__) | |||||
| class UnstructuredPDFExtractor(BaseExtractor): | |||||
| """Load pdf files. | |||||
| Args: | |||||
| file_path: Path to the file to load. | |||||
| api_url: Unstructured API URL | |||||
| api_key: Unstructured API Key | |||||
| """ | |||||
| def __init__(self, file_path: str, api_url: str, api_key: str): | |||||
| """Initialize with file path.""" | |||||
| self._file_path = file_path | |||||
| self._api_url = api_url | |||||
| self._api_key = api_key | |||||
| def extract(self) -> list[Document]: | |||||
| if self._api_url: | |||||
| from unstructured.partition.api import partition_via_api | |||||
| elements = partition_via_api( | |||||
| filename=self._file_path, api_url=self._api_url, api_key=self._api_key, strategy="auto" | |||||
| ) | |||||
| else: | |||||
| from unstructured.partition.pdf import partition_pdf | |||||
| elements = partition_pdf(filename=self._file_path, strategy="auto") | |||||
| from unstructured.chunking.title import chunk_by_title | |||||
| chunks = chunk_by_title(elements, max_characters=2000, combine_text_under_n_chars=2000) | |||||
| documents = [] | |||||
| for chunk in chunks: | |||||
| text = chunk.text.strip() | |||||
| documents.append(Document(page_content=text)) | |||||
| return documents |
| import logging | |||||
| from core.rag.extractor.extractor_base import BaseExtractor | |||||
| from core.rag.models.document import Document | |||||
| logger = logging.getLogger(__name__) | |||||
| class UnstructuredTextExtractor(BaseExtractor): | |||||
| """Load msg files. | |||||
| Args: | |||||
| file_path: Path to the file to load. | |||||
| """ | |||||
| def __init__(self, file_path: str, api_url: str): | |||||
| """Initialize with file path.""" | |||||
| self._file_path = file_path | |||||
| self._api_url = api_url | |||||
| def extract(self) -> list[Document]: | |||||
| from unstructured.partition.text import partition_text | |||||
| elements = partition_text(filename=self._file_path) | |||||
| from unstructured.chunking.title import chunk_by_title | |||||
| chunks = chunk_by_title(elements, max_characters=2000, combine_text_under_n_chars=2000) | |||||
| documents = [] | |||||
| for chunk in chunks: | |||||
| text = chunk.text.strip() | |||||
| documents.append(Document(page_content=text)) | |||||
| return documents |
| from flask import Flask, current_app | from flask import Flask, current_app | ||||
| from sqlalchemy import Float, and_, or_, text | from sqlalchemy import Float, and_, or_, text | ||||
| from sqlalchemy import cast as sqlalchemy_cast | from sqlalchemy import cast as sqlalchemy_cast | ||||
| from sqlalchemy.orm import Session | |||||
| from core.app.app_config.entities import ( | from core.app.app_config.entities import ( | ||||
| DatasetEntity, | DatasetEntity, | ||||
| metadata_condition: Optional[MetadataCondition] = None, | metadata_condition: Optional[MetadataCondition] = None, | ||||
| ): | ): | ||||
| with flask_app.app_context(): | with flask_app.app_context(): | ||||
| dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first() | |||||
| with Session(db.engine) as session: | |||||
| dataset = session.query(Dataset).filter(Dataset.id == dataset_id).first() | |||||
| if not dataset: | if not dataset: | ||||
| return [] | return [] |
| Any, | Any, | ||||
| Literal, | Literal, | ||||
| Optional, | Optional, | ||||
| TypedDict, | |||||
| TypeVar, | TypeVar, | ||||
| Union, | Union, | ||||
| ) | ) | ||||
| raise NotImplementedError | raise NotImplementedError | ||||
| class CharacterTextSplitter(TextSplitter): | |||||
| """Splitting text that looks at characters.""" | |||||
| def __init__(self, separator: str = "\n\n", **kwargs: Any) -> None: | |||||
| """Create a new TextSplitter.""" | |||||
| super().__init__(**kwargs) | |||||
| self._separator = separator | |||||
| def split_text(self, text: str) -> list[str]: | |||||
| """Split incoming text and return chunks.""" | |||||
| # First we naively split the large input into a bunch of smaller ones. | |||||
| splits = _split_text_with_regex(text, self._separator, self._keep_separator) | |||||
| _separator = "" if self._keep_separator else self._separator | |||||
| _good_splits_lengths = [] # cache the lengths of the splits | |||||
| if splits: | |||||
| _good_splits_lengths.extend(self._length_function(splits)) | |||||
| return self._merge_splits(splits, _separator, _good_splits_lengths) | |||||
| class LineType(TypedDict): | |||||
| """Line type as typed dict.""" | |||||
| metadata: dict[str, str] | |||||
| content: str | |||||
| class HeaderType(TypedDict): | |||||
| """Header type as typed dict.""" | |||||
| level: int | |||||
| name: str | |||||
| data: str | |||||
| class MarkdownHeaderTextSplitter: | |||||
| """Splitting markdown files based on specified headers.""" | |||||
| def __init__(self, headers_to_split_on: list[tuple[str, str]], return_each_line: bool = False): | |||||
| """Create a new MarkdownHeaderTextSplitter. | |||||
| Args: | |||||
| headers_to_split_on: Headers we want to track | |||||
| return_each_line: Return each line w/ associated headers | |||||
| """ | |||||
| # Output line-by-line or aggregated into chunks w/ common headers | |||||
| self.return_each_line = return_each_line | |||||
| # Given the headers we want to split on, | |||||
| # (e.g., "#, ##, etc") order by length | |||||
| self.headers_to_split_on = sorted(headers_to_split_on, key=lambda split: len(split[0]), reverse=True) | |||||
| def aggregate_lines_to_chunks(self, lines: list[LineType]) -> list[Document]: | |||||
| """Combine lines with common metadata into chunks | |||||
| Args: | |||||
| lines: Line of text / associated header metadata | |||||
| """ | |||||
| aggregated_chunks: list[LineType] = [] | |||||
| for line in lines: | |||||
| if aggregated_chunks and aggregated_chunks[-1]["metadata"] == line["metadata"]: | |||||
| # If the last line in the aggregated list | |||||
| # has the same metadata as the current line, | |||||
| # append the current content to the last lines's content | |||||
| aggregated_chunks[-1]["content"] += " \n" + line["content"] | |||||
| else: | |||||
| # Otherwise, append the current line to the aggregated list | |||||
| aggregated_chunks.append(line) | |||||
| return [Document(page_content=chunk["content"], metadata=chunk["metadata"]) for chunk in aggregated_chunks] | |||||
| def split_text(self, text: str) -> list[Document]: | |||||
| """Split markdown file | |||||
| Args: | |||||
| text: Markdown file""" | |||||
| # Split the input text by newline character ("\n"). | |||||
| lines = text.split("\n") | |||||
| # Final output | |||||
| lines_with_metadata: list[LineType] = [] | |||||
| # Content and metadata of the chunk currently being processed | |||||
| current_content: list[str] = [] | |||||
| current_metadata: dict[str, str] = {} | |||||
| # Keep track of the nested header structure | |||||
| # header_stack: List[Dict[str, Union[int, str]]] = [] | |||||
| header_stack: list[HeaderType] = [] | |||||
| initial_metadata: dict[str, str] = {} | |||||
| for line in lines: | |||||
| stripped_line = line.strip() | |||||
| # Check each line against each of the header types (e.g., #, ##) | |||||
| for sep, name in self.headers_to_split_on: | |||||
| # Check if line starts with a header that we intend to split on | |||||
| if stripped_line.startswith(sep) and ( | |||||
| # Header with no text OR header is followed by space | |||||
| # Both are valid conditions that sep is being used a header | |||||
| len(stripped_line) == len(sep) or stripped_line[len(sep)] == " " | |||||
| ): | |||||
| # Ensure we are tracking the header as metadata | |||||
| if name is not None: | |||||
| # Get the current header level | |||||
| current_header_level = sep.count("#") | |||||
| # Pop out headers of lower or same level from the stack | |||||
| while header_stack and header_stack[-1]["level"] >= current_header_level: | |||||
| # We have encountered a new header | |||||
| # at the same or higher level | |||||
| popped_header = header_stack.pop() | |||||
| # Clear the metadata for the | |||||
| # popped header in initial_metadata | |||||
| if popped_header["name"] in initial_metadata: | |||||
| initial_metadata.pop(popped_header["name"]) | |||||
| # Push the current header to the stack | |||||
| header: HeaderType = { | |||||
| "level": current_header_level, | |||||
| "name": name, | |||||
| "data": stripped_line[len(sep) :].strip(), | |||||
| } | |||||
| header_stack.append(header) | |||||
| # Update initial_metadata with the current header | |||||
| initial_metadata[name] = header["data"] | |||||
| # Add the previous line to the lines_with_metadata | |||||
| # only if current_content is not empty | |||||
| if current_content: | |||||
| lines_with_metadata.append( | |||||
| { | |||||
| "content": "\n".join(current_content), | |||||
| "metadata": current_metadata.copy(), | |||||
| } | |||||
| ) | |||||
| current_content.clear() | |||||
| break | |||||
| else: | |||||
| if stripped_line: | |||||
| current_content.append(stripped_line) | |||||
| elif current_content: | |||||
| lines_with_metadata.append( | |||||
| { | |||||
| "content": "\n".join(current_content), | |||||
| "metadata": current_metadata.copy(), | |||||
| } | |||||
| ) | |||||
| current_content.clear() | |||||
| current_metadata = initial_metadata.copy() | |||||
| if current_content: | |||||
| lines_with_metadata.append({"content": "\n".join(current_content), "metadata": current_metadata}) | |||||
| # lines_with_metadata has each line with associated header metadata | |||||
| # aggregate these into chunks based on common metadata | |||||
| if not self.return_each_line: | |||||
| return self.aggregate_lines_to_chunks(lines_with_metadata) | |||||
| else: | |||||
| return [ | |||||
| Document(page_content=chunk["content"], metadata=chunk["metadata"]) for chunk in lines_with_metadata | |||||
| ] | |||||
| # should be in newer Python versions (3.10+) | |||||
| # @dataclass(frozen=True, kw_only=True, slots=True) | # @dataclass(frozen=True, kw_only=True, slots=True) | ||||
| @dataclass(frozen=True) | @dataclass(frozen=True) | ||||
| class Tokenizer: | class Tokenizer: |
| defined in the core.workflow.repository package. | defined in the core.workflow.repository package. | ||||
| """ | """ | ||||
| from core.repositories.factory import DifyCoreRepositoryFactory, RepositoryImportError | |||||
| from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository | from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository | ||||
| __all__ = [ | __all__ = [ | ||||
| "DifyCoreRepositoryFactory", | |||||
| "RepositoryImportError", | |||||
| "SQLAlchemyWorkflowNodeExecutionRepository", | "SQLAlchemyWorkflowNodeExecutionRepository", | ||||
| ] | ] |
| """ | |||||
| Repository factory for dynamically creating repository instances based on configuration. | |||||
| This module provides a Django-like settings system for repository implementations, | |||||
| allowing users to configure different repository backends through string paths. | |||||
| """ | |||||
| import importlib | |||||
| import inspect | |||||
| import logging | |||||
| from typing import Protocol, Union | |||||
| from sqlalchemy.engine import Engine | |||||
| from sqlalchemy.orm import sessionmaker | |||||
| from configs import dify_config | |||||
| from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository | |||||
| from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository | |||||
| from models import Account, EndUser | |||||
| from models.enums import WorkflowRunTriggeredFrom | |||||
| from models.workflow import WorkflowNodeExecutionTriggeredFrom | |||||
| logger = logging.getLogger(__name__) | |||||
| class RepositoryImportError(Exception): | |||||
| """Raised when a repository implementation cannot be imported or instantiated.""" | |||||
| pass | |||||
| class DifyCoreRepositoryFactory: | |||||
| """ | |||||
| Factory for creating repository instances based on configuration. | |||||
| This factory supports Django-like settings where repository implementations | |||||
| are specified as module paths (e.g., 'module.submodule.ClassName'). | |||||
| """ | |||||
| @staticmethod | |||||
| def _import_class(class_path: str) -> type: | |||||
| """ | |||||
| Import a class from a module path string. | |||||
| Args: | |||||
| class_path: Full module path to the class (e.g., 'module.submodule.ClassName') | |||||
| Returns: | |||||
| The imported class | |||||
| Raises: | |||||
| RepositoryImportError: If the class cannot be imported | |||||
| """ | |||||
| try: | |||||
| module_path, class_name = class_path.rsplit(".", 1) | |||||
| module = importlib.import_module(module_path) | |||||
| repo_class = getattr(module, class_name) | |||||
| assert isinstance(repo_class, type) | |||||
| return repo_class | |||||
| except (ValueError, ImportError, AttributeError) as e: | |||||
| raise RepositoryImportError(f"Cannot import repository class '{class_path}': {e}") from e | |||||
| @staticmethod | |||||
| def _validate_repository_interface(repository_class: type, expected_interface: type[Protocol]) -> None: # type: ignore | |||||
| """ | |||||
| Validate that a class implements the expected repository interface. | |||||
| Args: | |||||
| repository_class: The class to validate | |||||
| expected_interface: The expected interface/protocol | |||||
| Raises: | |||||
| RepositoryImportError: If the class doesn't implement the interface | |||||
| """ | |||||
| # Check if the class has all required methods from the protocol | |||||
| required_methods = [ | |||||
| method | |||||
| for method in dir(expected_interface) | |||||
| if not method.startswith("_") and callable(getattr(expected_interface, method, None)) | |||||
| ] | |||||
| missing_methods = [] | |||||
| for method_name in required_methods: | |||||
| if not hasattr(repository_class, method_name): | |||||
| missing_methods.append(method_name) | |||||
| if missing_methods: | |||||
| raise RepositoryImportError( | |||||
| f"Repository class '{repository_class.__name__}' does not implement required methods " | |||||
| f"{missing_methods} from interface '{expected_interface.__name__}'" | |||||
| ) | |||||
| @staticmethod | |||||
| def _validate_constructor_signature(repository_class: type, required_params: list[str]) -> None: | |||||
| """ | |||||
| Validate that a repository class constructor accepts required parameters. | |||||
| Args: | |||||
| repository_class: The class to validate | |||||
| required_params: List of required parameter names | |||||
| Raises: | |||||
| RepositoryImportError: If the constructor doesn't accept required parameters | |||||
| """ | |||||
| try: | |||||
| # MyPy may flag the line below with the following error: | |||||
| # | |||||
| # > Accessing "__init__" on an instance is unsound, since | |||||
| # > instance.__init__ could be from an incompatible subclass. | |||||
| # | |||||
| # Despite this, we need to ensure that the constructor of `repository_class` | |||||
| # has a compatible signature. | |||||
| signature = inspect.signature(repository_class.__init__) # type: ignore[misc] | |||||
| param_names = list(signature.parameters.keys()) | |||||
| # Remove 'self' parameter | |||||
| if "self" in param_names: | |||||
| param_names.remove("self") | |||||
| missing_params = [param for param in required_params if param not in param_names] | |||||
| if missing_params: | |||||
| raise RepositoryImportError( | |||||
| f"Repository class '{repository_class.__name__}' constructor does not accept required parameters: " | |||||
| f"{missing_params}. Expected parameters: {required_params}" | |||||
| ) | |||||
| except Exception as e: | |||||
| raise RepositoryImportError( | |||||
| f"Failed to validate constructor signature for '{repository_class.__name__}': {e}" | |||||
| ) from e | |||||
| @classmethod | |||||
| def create_workflow_execution_repository( | |||||
| cls, | |||||
| session_factory: Union[sessionmaker, Engine], | |||||
| user: Union[Account, EndUser], | |||||
| app_id: str, | |||||
| triggered_from: WorkflowRunTriggeredFrom, | |||||
| ) -> WorkflowExecutionRepository: | |||||
| """ | |||||
| Create a WorkflowExecutionRepository instance based on configuration. | |||||
| Args: | |||||
| session_factory: SQLAlchemy sessionmaker or engine | |||||
| user: Account or EndUser object | |||||
| app_id: Application ID | |||||
| triggered_from: Source of the execution trigger | |||||
| Returns: | |||||
| Configured WorkflowExecutionRepository instance | |||||
| Raises: | |||||
| RepositoryImportError: If the configured repository cannot be created | |||||
| """ | |||||
| class_path = dify_config.CORE_WORKFLOW_EXECUTION_REPOSITORY | |||||
| logger.debug(f"Creating WorkflowExecutionRepository from: {class_path}") | |||||
| try: | |||||
| repository_class = cls._import_class(class_path) | |||||
| cls._validate_repository_interface(repository_class, WorkflowExecutionRepository) | |||||
| cls._validate_constructor_signature( | |||||
| repository_class, ["session_factory", "user", "app_id", "triggered_from"] | |||||
| ) | |||||
| return repository_class( # type: ignore[no-any-return] | |||||
| session_factory=session_factory, | |||||
| user=user, | |||||
| app_id=app_id, | |||||
| triggered_from=triggered_from, | |||||
| ) | |||||
| except RepositoryImportError: | |||||
| # Re-raise our custom errors as-is | |||||
| raise | |||||
| except Exception as e: | |||||
| logger.exception("Failed to create WorkflowExecutionRepository") | |||||
| raise RepositoryImportError(f"Failed to create WorkflowExecutionRepository from '{class_path}': {e}") from e | |||||
| @classmethod | |||||
| def create_workflow_node_execution_repository( | |||||
| cls, | |||||
| session_factory: Union[sessionmaker, Engine], | |||||
| user: Union[Account, EndUser], | |||||
| app_id: str, | |||||
| triggered_from: WorkflowNodeExecutionTriggeredFrom, | |||||
| ) -> WorkflowNodeExecutionRepository: | |||||
| """ | |||||
| Create a WorkflowNodeExecutionRepository instance based on configuration. | |||||
| Args: | |||||
| session_factory: SQLAlchemy sessionmaker or engine | |||||
| user: Account or EndUser object | |||||
| app_id: Application ID | |||||
| triggered_from: Source of the execution trigger | |||||
| Returns: | |||||
| Configured WorkflowNodeExecutionRepository instance | |||||
| Raises: | |||||
| RepositoryImportError: If the configured repository cannot be created | |||||
| """ | |||||
| class_path = dify_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY | |||||
| logger.debug(f"Creating WorkflowNodeExecutionRepository from: {class_path}") | |||||
| try: | |||||
| repository_class = cls._import_class(class_path) | |||||
| cls._validate_repository_interface(repository_class, WorkflowNodeExecutionRepository) | |||||
| cls._validate_constructor_signature( | |||||
| repository_class, ["session_factory", "user", "app_id", "triggered_from"] | |||||
| ) | |||||
| return repository_class( # type: ignore[no-any-return] | |||||
| session_factory=session_factory, | |||||
| user=user, | |||||
| app_id=app_id, | |||||
| triggered_from=triggered_from, | |||||
| ) | |||||
| except RepositoryImportError: | |||||
| # Re-raise our custom errors as-is | |||||
| raise | |||||
| except Exception as e: | |||||
| logger.exception("Failed to create WorkflowNodeExecutionRepository") | |||||
| raise RepositoryImportError( | |||||
| f"Failed to create WorkflowNodeExecutionRepository from '{class_path}': {e}" | |||||
| ) from e |
| cast_parameter_value, | cast_parameter_value, | ||||
| init_frontend_parameter, | init_frontend_parameter, | ||||
| ) | ) | ||||
| from core.rag.entities.citation_metadata import RetrievalSourceMetadata | |||||
| from core.tools.entities.common_entities import I18nObject | from core.tools.entities.common_entities import I18nObject | ||||
| from core.tools.entities.constants import TOOL_SELECTOR_MODEL_IDENTITY | from core.tools.entities.constants import TOOL_SELECTOR_MODEL_IDENTITY | ||||
| data: Mapping[str, Any] = Field(..., description="Detailed log data") | data: Mapping[str, Any] = Field(..., description="Detailed log data") | ||||
| metadata: Optional[Mapping[str, Any]] = Field(default=None, description="The metadata of the log") | metadata: Optional[Mapping[str, Any]] = Field(default=None, description="The metadata of the log") | ||||
| class RetrieverResourceMessage(BaseModel): | |||||
| retriever_resources: list[RetrievalSourceMetadata] = Field(..., description="retriever resources") | |||||
| context: str = Field(..., description="context") | |||||
| class MessageType(Enum): | class MessageType(Enum): | ||||
| TEXT = "text" | TEXT = "text" | ||||
| IMAGE = "image" | IMAGE = "image" | ||||
| FILE = "file" | FILE = "file" | ||||
| LOG = "log" | LOG = "log" | ||||
| BLOB_CHUNK = "blob_chunk" | BLOB_CHUNK = "blob_chunk" | ||||
| RETRIEVER_RESOURCES = "retriever_resources" | |||||
| type: MessageType = MessageType.TEXT | type: MessageType = MessageType.TEXT | ||||
| """ | """ | ||||
| plain text, image url or link url | plain text, image url or link url | ||||
| """ | """ | ||||
| message: ( | message: ( | ||||
| JsonMessage | TextMessage | BlobChunkMessage | BlobMessage | LogMessage | FileMessage | None | VariableMessage | |||||
| JsonMessage | |||||
| | TextMessage | |||||
| | BlobChunkMessage | |||||
| | BlobMessage | |||||
| | LogMessage | |||||
| | FileMessage | |||||
| | None | |||||
| | VariableMessage | |||||
| | RetrieverResourceMessage | |||||
| ) | ) | ||||
| meta: dict[str, Any] | None = None | meta: dict[str, Any] | None = None | ||||
| FILES = PluginParameterType.FILES.value | FILES = PluginParameterType.FILES.value | ||||
| APP_SELECTOR = PluginParameterType.APP_SELECTOR.value | APP_SELECTOR = PluginParameterType.APP_SELECTOR.value | ||||
| MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value | MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value | ||||
| ANY = PluginParameterType.ANY.value | |||||
| DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value | DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value | ||||
| # MCP object and array type parameters | # MCP object and array type parameters |
| import re | import re | ||||
| import uuid | |||||
| from json import dumps as json_dumps | from json import dumps as json_dumps | ||||
| from json import loads as json_loads | from json import loads as json_loads | ||||
| from json.decoder import JSONDecodeError | from json.decoder import JSONDecodeError | ||||
| # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$ | # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$ | ||||
| path = re.sub(r"[^a-zA-Z0-9_-]", "", path) | path = re.sub(r"[^a-zA-Z0-9_-]", "", path) | ||||
| if not path: | if not path: | ||||
| path = str(uuid.uuid4()) | |||||
| path = "<root>" | |||||
| interface["operation"]["operationId"] = f"{path}_{interface['method']}" | interface["operation"]["operationId"] = f"{path}_{interface['method']}" | ||||
| import json | import json | ||||
| import sys | import sys | ||||
| from collections.abc import Mapping, Sequence | from collections.abc import Mapping, Sequence | ||||
| from typing import Any | |||||
| from typing import Annotated, Any, TypeAlias | |||||
| from pydantic import BaseModel, ConfigDict, field_validator | |||||
| from pydantic import BaseModel, ConfigDict, Discriminator, Tag, field_validator | |||||
| from core.file import File | from core.file import File | ||||
| class Segment(BaseModel): | class Segment(BaseModel): | ||||
| """Segment is runtime type used during the execution of workflow. | |||||
| Note: this class is abstract, you should use subclasses of this class instead. | |||||
| """ | |||||
| model_config = ConfigDict(frozen=True) | model_config = ConfigDict(frozen=True) | ||||
| value_type: SegmentType | value_type: SegmentType | ||||
| class FloatSegment(Segment): | class FloatSegment(Segment): | ||||
| value_type: SegmentType = SegmentType.NUMBER | |||||
| value_type: SegmentType = SegmentType.FLOAT | |||||
| value: float | value: float | ||||
| # NOTE(QuantumGhost): seems that the equality for FloatSegment with `NaN` value has some problems. | # NOTE(QuantumGhost): seems that the equality for FloatSegment with `NaN` value has some problems. | ||||
| # The following tests cannot pass. | # The following tests cannot pass. | ||||
| class IntegerSegment(Segment): | class IntegerSegment(Segment): | ||||
| value_type: SegmentType = SegmentType.NUMBER | |||||
| value_type: SegmentType = SegmentType.INTEGER | |||||
| value: int | value: int | ||||
| @property | @property | ||||
| def text(self) -> str: | def text(self) -> str: | ||||
| return "" | return "" | ||||
| def get_segment_discriminator(v: Any) -> SegmentType | None: | |||||
| if isinstance(v, Segment): | |||||
| return v.value_type | |||||
| elif isinstance(v, dict): | |||||
| value_type = v.get("value_type") | |||||
| if value_type is None: | |||||
| return None | |||||
| try: | |||||
| seg_type = SegmentType(value_type) | |||||
| except ValueError: | |||||
| return None | |||||
| return seg_type | |||||
| else: | |||||
| # return None if the discriminator value isn't found | |||||
| return None | |||||
| # The `SegmentUnion`` type is used to enable serialization and deserialization with Pydantic. | |||||
| # Use `Segment` for type hinting when serialization is not required. | |||||
| # | |||||
| # Note: | |||||
| # - All variants in `SegmentUnion` must inherit from the `Segment` class. | |||||
| # - The union must include all non-abstract subclasses of `Segment`, except: | |||||
| # - `SegmentGroup`, which is not added to the variable pool. | |||||
| # - `Variable` and its subclasses, which are handled by `VariableUnion`. | |||||
| SegmentUnion: TypeAlias = Annotated[ | |||||
| ( | |||||
| Annotated[NoneSegment, Tag(SegmentType.NONE)] | |||||
| | Annotated[StringSegment, Tag(SegmentType.STRING)] | |||||
| | Annotated[FloatSegment, Tag(SegmentType.FLOAT)] | |||||
| | Annotated[IntegerSegment, Tag(SegmentType.INTEGER)] | |||||
| | Annotated[ObjectSegment, Tag(SegmentType.OBJECT)] | |||||
| | Annotated[FileSegment, Tag(SegmentType.FILE)] | |||||
| | Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)] | |||||
| | Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)] | |||||
| | Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)] | |||||
| | Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)] | |||||
| | Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)] | |||||
| ), | |||||
| Discriminator(get_segment_discriminator), | |||||
| ] |
| from collections.abc import Mapping | |||||
| from enum import StrEnum | from enum import StrEnum | ||||
| from typing import Any, Optional | |||||
| from core.file.models import File | |||||
| class ArrayValidation(StrEnum): | |||||
| """Strategy for validating array elements""" | |||||
| # Skip element validation (only check array container) | |||||
| NONE = "none" | |||||
| # Validate the first element (if array is non-empty) | |||||
| FIRST = "first" | |||||
| # Validate all elements in the array. | |||||
| ALL = "all" | |||||
| class SegmentType(StrEnum): | class SegmentType(StrEnum): | ||||
| NUMBER = "number" | NUMBER = "number" | ||||
| INTEGER = "integer" | |||||
| FLOAT = "float" | |||||
| STRING = "string" | STRING = "string" | ||||
| OBJECT = "object" | OBJECT = "object" | ||||
| SECRET = "secret" | SECRET = "secret" | ||||
| GROUP = "group" | GROUP = "group" | ||||
| def is_array_type(self): | |||||
| def is_array_type(self) -> bool: | |||||
| return self in _ARRAY_TYPES | return self in _ARRAY_TYPES | ||||
| @classmethod | |||||
| def infer_segment_type(cls, value: Any) -> Optional["SegmentType"]: | |||||
| """ | |||||
| Attempt to infer the `SegmentType` based on the Python type of the `value` parameter. | |||||
| Returns `None` if no appropriate `SegmentType` can be determined for the given `value`. | |||||
| For example, this may occur if the input is a generic Python object of type `object`. | |||||
| """ | |||||
| if isinstance(value, list): | |||||
| elem_types: set[SegmentType] = set() | |||||
| for i in value: | |||||
| segment_type = cls.infer_segment_type(i) | |||||
| if segment_type is None: | |||||
| return None | |||||
| elem_types.add(segment_type) | |||||
| if len(elem_types) != 1: | |||||
| if elem_types.issubset(_NUMERICAL_TYPES): | |||||
| return SegmentType.ARRAY_NUMBER | |||||
| return SegmentType.ARRAY_ANY | |||||
| elif all(i.is_array_type() for i in elem_types): | |||||
| return SegmentType.ARRAY_ANY | |||||
| match elem_types.pop(): | |||||
| case SegmentType.STRING: | |||||
| return SegmentType.ARRAY_STRING | |||||
| case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: | |||||
| return SegmentType.ARRAY_NUMBER | |||||
| case SegmentType.OBJECT: | |||||
| return SegmentType.ARRAY_OBJECT | |||||
| case SegmentType.FILE: | |||||
| return SegmentType.ARRAY_FILE | |||||
| case SegmentType.NONE: | |||||
| return SegmentType.ARRAY_ANY | |||||
| case _: | |||||
| # This should be unreachable. | |||||
| raise ValueError(f"not supported value {value}") | |||||
| if value is None: | |||||
| return SegmentType.NONE | |||||
| elif isinstance(value, int) and not isinstance(value, bool): | |||||
| return SegmentType.INTEGER | |||||
| elif isinstance(value, float): | |||||
| return SegmentType.FLOAT | |||||
| elif isinstance(value, str): | |||||
| return SegmentType.STRING | |||||
| elif isinstance(value, dict): | |||||
| return SegmentType.OBJECT | |||||
| elif isinstance(value, File): | |||||
| return SegmentType.FILE | |||||
| else: | |||||
| return None | |||||
| def _validate_array(self, value: Any, array_validation: ArrayValidation) -> bool: | |||||
| if not isinstance(value, list): | |||||
| return False | |||||
| # Skip element validation if array is empty | |||||
| if len(value) == 0: | |||||
| return True | |||||
| if self == SegmentType.ARRAY_ANY: | |||||
| return True | |||||
| element_type = _ARRAY_ELEMENT_TYPES_MAPPING[self] | |||||
| if array_validation == ArrayValidation.NONE: | |||||
| return True | |||||
| elif array_validation == ArrayValidation.FIRST: | |||||
| return element_type.is_valid(value[0]) | |||||
| else: | |||||
| return all([element_type.is_valid(i, array_validation=ArrayValidation.NONE)] for i in value) | |||||
| def is_valid(self, value: Any, array_validation: ArrayValidation = ArrayValidation.FIRST) -> bool: | |||||
| """ | |||||
| Check if a value matches the segment type. | |||||
| Users of `SegmentType` should call this method, instead of using | |||||
| `isinstance` manually. | |||||
| Args: | |||||
| value: The value to validate | |||||
| array_validation: Validation strategy for array types (ignored for non-array types) | |||||
| Returns: | |||||
| True if the value matches the type under the given validation strategy | |||||
| """ | |||||
| if self.is_array_type(): | |||||
| return self._validate_array(value, array_validation) | |||||
| elif self == SegmentType.NUMBER: | |||||
| return isinstance(value, (int, float)) | |||||
| elif self == SegmentType.STRING: | |||||
| return isinstance(value, str) | |||||
| elif self == SegmentType.OBJECT: | |||||
| return isinstance(value, dict) | |||||
| elif self == SegmentType.SECRET: | |||||
| return isinstance(value, str) | |||||
| elif self == SegmentType.FILE: | |||||
| return isinstance(value, File) | |||||
| elif self == SegmentType.NONE: | |||||
| return value is None | |||||
| else: | |||||
| raise AssertionError("this statement should be unreachable.") | |||||
| def exposed_type(self) -> "SegmentType": | |||||
| """Returns the type exposed to the frontend. | |||||
| The frontend treats `INTEGER` and `FLOAT` as `NUMBER`, so these are returned as `NUMBER` here. | |||||
| """ | |||||
| if self in (SegmentType.INTEGER, SegmentType.FLOAT): | |||||
| return SegmentType.NUMBER | |||||
| return self | |||||
| _ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = { | |||||
| # ARRAY_ANY does not have correpond element type. | |||||
| SegmentType.ARRAY_STRING: SegmentType.STRING, | |||||
| SegmentType.ARRAY_NUMBER: SegmentType.NUMBER, | |||||
| SegmentType.ARRAY_OBJECT: SegmentType.OBJECT, | |||||
| SegmentType.ARRAY_FILE: SegmentType.FILE, | |||||
| } | |||||
| _ARRAY_TYPES = frozenset( | _ARRAY_TYPES = frozenset( | ||||
| [ | |||||
| list(_ARRAY_ELEMENT_TYPES_MAPPING.keys()) | |||||
| + [ | |||||
| SegmentType.ARRAY_ANY, | SegmentType.ARRAY_ANY, | ||||
| SegmentType.ARRAY_STRING, | |||||
| SegmentType.ARRAY_NUMBER, | |||||
| SegmentType.ARRAY_OBJECT, | |||||
| SegmentType.ARRAY_FILE, | |||||
| ] | |||||
| ) | |||||
| _NUMERICAL_TYPES = frozenset( | |||||
| [ | |||||
| SegmentType.NUMBER, | |||||
| SegmentType.INTEGER, | |||||
| SegmentType.FLOAT, | |||||
| ] | ] | ||||
| ) | ) |
| from uuid import uuid4 | from uuid import uuid4 | ||||
| from pydantic import BaseModel, Field | from pydantic import BaseModel, Field | ||||
| from typing import Annotated, TypeAlias, cast | |||||
| from uuid import uuid4 | |||||
| from pydantic import Discriminator, Field, Tag | |||||
| from core.helper import encrypter | from core.helper import encrypter | ||||
| ObjectSegment, | ObjectSegment, | ||||
| Segment, | Segment, | ||||
| StringSegment, | StringSegment, | ||||
| get_segment_discriminator, | |||||
| ) | ) | ||||
| from .types import SegmentType | from .types import SegmentType | ||||
| class Variable(Segment): | class Variable(Segment): | ||||
| """ | """ | ||||
| A variable is a segment that has a name. | A variable is a segment that has a name. | ||||
| It is mainly used to store segments and their selector in VariablePool. | |||||
| Note: this class is abstract, you should use subclasses of this class instead. | |||||
| """ | """ | ||||
| id: str = Field( | id: str = Field( | ||||
| class RAGPipelineVariableInput(BaseModel): | class RAGPipelineVariableInput(BaseModel): | ||||
| variable: RAGPipelineVariable | variable: RAGPipelineVariable | ||||
| value: Any | value: Any | ||||
| # The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic. | |||||
| # Use `Variable` for type hinting when serialization is not required. | |||||
| # | |||||
| # Note: | |||||
| # - All variants in `VariableUnion` must inherit from the `Variable` class. | |||||
| # - The union must include all non-abstract subclasses of `Segment`, except: | |||||
| VariableUnion: TypeAlias = Annotated[ | |||||
| ( | |||||
| Annotated[NoneVariable, Tag(SegmentType.NONE)] | |||||
| | Annotated[StringVariable, Tag(SegmentType.STRING)] | |||||
| | Annotated[FloatVariable, Tag(SegmentType.FLOAT)] | |||||
| | Annotated[IntegerVariable, Tag(SegmentType.INTEGER)] | |||||
| | Annotated[ObjectVariable, Tag(SegmentType.OBJECT)] | |||||
| | Annotated[FileVariable, Tag(SegmentType.FILE)] | |||||
| | Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)] | |||||
| | Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)] | |||||
| | Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)] | |||||
| | Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)] | |||||
| | Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)] | |||||
| | Annotated[SecretVariable, Tag(SegmentType.SECRET)] | |||||
| ), | |||||
| Discriminator(get_segment_discriminator), | |||||
| ] |
| import re | import re | ||||
| from collections import defaultdict | from collections import defaultdict | ||||
| from collections.abc import Mapping, Sequence | from collections.abc import Mapping, Sequence | ||||
| from typing import Any, Union | |||||
| from typing import Annotated, Any, Union, cast | |||||
| from pydantic import BaseModel, Field | from pydantic import BaseModel, Field | ||||
| SYSTEM_VARIABLE_NODE_ID, | SYSTEM_VARIABLE_NODE_ID, | ||||
| ) | ) | ||||
| from core.workflow.enums import SystemVariableKey | from core.workflow.enums import SystemVariableKey | ||||
| from core.variables.variables import VariableUnion | |||||
| from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID | |||||
| from core.workflow.system_variable import SystemVariable | |||||
| from factories import variable_factory | from factories import variable_factory | ||||
| VariableValue = Union[str, int, float, dict, list, File] | VariableValue = Union[str, int, float, dict, list, File] | ||||
| # The first element of the selector is the node id, it's the first-level key in the dictionary. | # The first element of the selector is the node id, it's the first-level key in the dictionary. | ||||
| # Other elements of the selector are the keys in the second-level dictionary. To get the key, we hash the | # Other elements of the selector are the keys in the second-level dictionary. To get the key, we hash the | ||||
| # elements of the selector except the first one. | # elements of the selector except the first one. | ||||
| variable_dictionary: dict[str, dict[int, Segment]] = Field( | |||||
| variable_dictionary: defaultdict[str, Annotated[dict[int, VariableUnion], Field(default_factory=dict)]] = Field( | |||||
| description="Variables mapping", | description="Variables mapping", | ||||
| default=defaultdict(dict), | default=defaultdict(dict), | ||||
| ) | ) | ||||
| # TODO: This user inputs is not used for pool. | |||||
| # The `user_inputs` is used only when constructing the inputs for the `StartNode`. It's not used elsewhere. | |||||
| user_inputs: Mapping[str, Any] = Field( | user_inputs: Mapping[str, Any] = Field( | ||||
| description="User inputs", | description="User inputs", | ||||
| default_factory=dict, | default_factory=dict, | ||||
| ) | ) | ||||
| system_variables: Mapping[SystemVariableKey, Any] = Field( | |||||
| system_variables: SystemVariable = Field( | |||||
| description="System variables", | description="System variables", | ||||
| default_factory=dict, | |||||
| ) | ) | ||||
| environment_variables: Sequence[Variable] = Field( | |||||
| environment_variables: Sequence[VariableUnion] = Field( | |||||
| description="Environment variables.", | description="Environment variables.", | ||||
| default_factory=list, | default_factory=list, | ||||
| ) | ) | ||||
| conversation_variables: Sequence[Variable] = Field( | |||||
| conversation_variables: Sequence[VariableUnion] = Field( | |||||
| description="Conversation variables.", | description="Conversation variables.", | ||||
| default_factory=list, | default_factory=list, | ||||
| ) | ) | ||||
| ) | ) | ||||
| def model_post_init(self, context: Any, /) -> None: | def model_post_init(self, context: Any, /) -> None: | ||||
| for key, value in self.system_variables.items(): | |||||
| self.add((SYSTEM_VARIABLE_NODE_ID, key.value), value) | |||||
| # Create a mapping from field names to SystemVariableKey enum values | |||||
| self._add_system_variables(self.system_variables) | |||||
| # Add environment variables to the variable pool | # Add environment variables to the variable pool | ||||
| for var in self.environment_variables: | for var in self.environment_variables: | ||||
| self.add((ENVIRONMENT_VARIABLE_NODE_ID, var.name), var) | self.add((ENVIRONMENT_VARIABLE_NODE_ID, var.name), var) | ||||
| segment = variable_factory.build_segment(value) | segment = variable_factory.build_segment(value) | ||||
| variable = variable_factory.segment_to_variable(segment=segment, selector=selector) | variable = variable_factory.segment_to_variable(segment=segment, selector=selector) | ||||
| hash_key = hash(tuple(selector[1:])) | |||||
| self.variable_dictionary[selector[0]][hash_key] = variable | |||||
| key, hash_key = self._selector_to_keys(selector) | |||||
| # Based on the definition of `VariableUnion`, | |||||
| # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. | |||||
| self.variable_dictionary[key][hash_key] = cast(VariableUnion, variable) | |||||
| @classmethod | |||||
| def _selector_to_keys(cls, selector: Sequence[str]) -> tuple[str, int]: | |||||
| return selector[0], hash(tuple(selector[1:])) | |||||
| def _has(self, selector: Sequence[str]) -> bool: | |||||
| key, hash_key = self._selector_to_keys(selector) | |||||
| if key not in self.variable_dictionary: | |||||
| return False | |||||
| if hash_key not in self.variable_dictionary[key]: | |||||
| return False | |||||
| return True | |||||
| def get(self, selector: Sequence[str], /) -> Segment | None: | def get(self, selector: Sequence[str], /) -> Segment | None: | ||||
| """ | """ | ||||
| if len(selector) < MIN_SELECTORS_LENGTH: | if len(selector) < MIN_SELECTORS_LENGTH: | ||||
| return None | return None | ||||
| hash_key = hash(tuple(selector[1:])) | |||||
| value = self.variable_dictionary[selector[0]].get(hash_key) | |||||
| key, hash_key = self._selector_to_keys(selector) | |||||
| value: Segment | None = self.variable_dictionary[key].get(hash_key) | |||||
| if value is None: | if value is None: | ||||
| selector, attr = selector[:-1], selector[-1] | selector, attr = selector[:-1], selector[-1] | ||||
| if len(selector) == 1: | if len(selector) == 1: | ||||
| self.variable_dictionary[selector[0]] = {} | self.variable_dictionary[selector[0]] = {} | ||||
| return | return | ||||
| hash_key = hash(tuple(selector[1:])) | |||||
| self.variable_dictionary[selector[0]].pop(hash_key, None) | |||||
| key, hash_key = self._selector_to_keys(selector) | |||||
| self.variable_dictionary[key].pop(hash_key, None) | |||||
| def convert_template(self, template: str, /): | def convert_template(self, template: str, /): | ||||
| parts = VARIABLE_PATTERN.split(template) | parts = VARIABLE_PATTERN.split(template) | ||||
| if isinstance(segment, FileSegment): | if isinstance(segment, FileSegment): | ||||
| return segment | return segment | ||||
| return None | return None | ||||
| def _add_system_variables(self, system_variable: SystemVariable): | |||||
| sys_var_mapping = system_variable.to_dict() | |||||
| for key, value in sys_var_mapping.items(): | |||||
| if value is None: | |||||
| continue | |||||
| selector = (SYSTEM_VARIABLE_NODE_ID, key) | |||||
| # If the system variable already exists, do not add it again. | |||||
| # This ensures that we can keep the id of the system variables intact. | |||||
| if self._has(selector): | |||||
| continue | |||||
| self.add(selector, value) # type: ignore | |||||
| @classmethod | |||||
| def empty(cls) -> "VariablePool": | |||||
| """Create an empty variable pool.""" | |||||
| return cls(system_variables=SystemVariable.empty()) |
| from typing import Optional | |||||
| from pydantic import BaseModel | |||||
| from core.app.entities.app_invoke_entities import InvokeFrom | |||||
| from core.workflow.nodes.base import BaseIterationState, BaseLoopState, BaseNode | |||||
| from models.enums import UserFrom | |||||
| from models.workflow import Workflow, WorkflowType | |||||
| from .node_entities import NodeRunResult | |||||
| from .variable_pool import VariablePool | |||||
| class WorkflowNodeAndResult: | |||||
| node: BaseNode | |||||
| result: Optional[NodeRunResult] = None | |||||
| def __init__(self, node: BaseNode, result: Optional[NodeRunResult] = None): | |||||
| self.node = node | |||||
| self.result = result | |||||
| class WorkflowRunState: | |||||
| tenant_id: str | |||||
| app_id: str | |||||
| workflow_id: str | |||||
| workflow_type: WorkflowType | |||||
| user_id: str | |||||
| user_from: UserFrom | |||||
| invoke_from: InvokeFrom | |||||
| workflow_call_depth: int | |||||
| start_at: float | |||||
| variable_pool: VariablePool | |||||
| total_tokens: int = 0 | |||||
| workflow_nodes_and_results: list[WorkflowNodeAndResult] | |||||
| class NodeRun(BaseModel): | |||||
| node_id: str | |||||
| iteration_node_id: str | |||||
| loop_node_id: str | |||||
| workflow_node_runs: list[NodeRun] | |||||
| workflow_node_steps: int | |||||
| current_iteration_state: Optional[BaseIterationState] | |||||
| current_loop_state: Optional[BaseLoopState] | |||||
| def __init__( | |||||
| self, | |||||
| workflow: Workflow, | |||||
| start_at: float, | |||||
| variable_pool: VariablePool, | |||||
| user_id: str, | |||||
| user_from: UserFrom, | |||||
| invoke_from: InvokeFrom, | |||||
| workflow_call_depth: int, | |||||
| ): | |||||
| self.workflow_id = workflow.id | |||||
| self.tenant_id = workflow.tenant_id | |||||
| self.app_id = workflow.app_id | |||||
| self.workflow_type = WorkflowType.value_of(workflow.type) | |||||
| self.user_id = user_id | |||||
| self.user_from = user_from | |||||
| self.invoke_from = invoke_from | |||||
| self.workflow_call_depth = workflow_call_depth | |||||
| self.start_at = start_at | |||||
| self.variable_pool = variable_pool | |||||
| self.total_tokens = 0 | |||||
| self.workflow_node_steps = 1 | |||||
| self.workflow_node_runs = [] | |||||
| self.current_iteration_state = None | |||||
| self.current_loop_state = None |
| """total tokens""" | """total tokens""" | ||||
| llm_usage: LLMUsage = LLMUsage.empty_usage() | llm_usage: LLMUsage = LLMUsage.empty_usage() | ||||
| """llm usage info""" | """llm usage info""" | ||||
| # The `outputs` field stores the final output values generated by executing workflows or chatflows. | |||||
| # | |||||
| # Note: Since the type of this field is `dict[str, Any]`, its values may not remain consistent | |||||
| # after a serialization and deserialization round trip. | |||||
| outputs: dict[str, Any] = {} | outputs: dict[str, Any] = {} | ||||
| """outputs""" | |||||
| node_run_steps: int = 0 | node_run_steps: int = 0 | ||||
| """node run steps""" | """node run steps""" |
| from collections.abc import Mapping, Sequence | from collections.abc import Mapping, Sequence | ||||
| from decimal import Decimal | |||||
| from typing import Any, Optional | from typing import Any, Optional | ||||
| from configs import dify_config | from configs import dify_config | ||||
| ) | ) | ||||
| if isinstance(value, float): | if isinstance(value, float): | ||||
| decimal_value = Decimal(str(value)).normalize() | |||||
| precision = -decimal_value.as_tuple().exponent if decimal_value.as_tuple().exponent < 0 else 0 # type: ignore[operator] | |||||
| # raise error if precision is too high | # raise error if precision is too high | ||||
| if len(str(value).split(".")[1]) > dify_config.CODE_MAX_PRECISION: | |||||
| if precision > dify_config.CODE_MAX_PRECISION: | |||||
| raise OutputValidationError( | raise OutputValidationError( | ||||
| f"Output variable `{variable}` has too high precision," | f"Output variable `{variable}` has too high precision," | ||||
| f" it must be less than {dify_config.CODE_MAX_PRECISION} digits." | f" it must be less than {dify_config.CODE_MAX_PRECISION} digits." |
| ) | ) | ||||
| return | return | ||||
| elif self.node_data.error_handle_mode == ErrorHandleMode.TERMINATED: | elif self.node_data.error_handle_mode == ErrorHandleMode.TERMINATED: | ||||
| yield IterationRunFailedEvent( | |||||
| iteration_id=self.id, | |||||
| iteration_node_id=self.node_id, | |||||
| iteration_node_type=self.node_type, | |||||
| iteration_node_data=self.node_data, | |||||
| start_at=start_at, | |||||
| inputs=inputs, | |||||
| outputs={"output": None}, | |||||
| steps=len(iterator_list_value), | |||||
| metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, | |||||
| error=event.error, | |||||
| yield NodeInIterationFailedEvent( | |||||
| **metadata_event.model_dump(), | |||||
| ) | ) | ||||
| outputs[current_index] = None | |||||
| # clean nodes resources | |||||
| for node_id in iteration_graph.node_ids: | |||||
| variable_pool.remove([node_id]) | |||||
| # iteration run failed | |||||
| if self.node_data.is_parallel: | |||||
| yield IterationRunFailedEvent( | |||||
| iteration_id=self.id, | |||||
| iteration_node_id=self.node_id, | |||||
| iteration_node_type=self.node_type, | |||||
| iteration_node_data=self.node_data, | |||||
| parallel_mode_run_id=parallel_mode_run_id, | |||||
| start_at=start_at, | |||||
| inputs=inputs, | |||||
| outputs={"output": outputs}, | |||||
| steps=len(iterator_list_value), | |||||
| metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, | |||||
| error=event.error, | |||||
| ) | |||||
| else: | |||||
| yield IterationRunFailedEvent( | |||||
| iteration_id=self.id, | |||||
| iteration_node_id=self.node_id, | |||||
| iteration_node_type=self.node_type, | |||||
| iteration_node_data=self.node_data, | |||||
| start_at=start_at, | |||||
| inputs=inputs, | |||||
| outputs={"output": outputs}, | |||||
| steps=len(iterator_list_value), | |||||
| metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, | |||||
| error=event.error, | |||||
| ) | |||||
| # stop the iterator | |||||
| yield RunCompletedEvent( | |||||
| run_result=NodeRunResult( | |||||
| status=WorkflowNodeExecutionStatus.FAILED, | |||||
| error=event.error, | |||||
| ) | |||||
| ) | |||||
| return | |||||
| yield metadata_event | yield metadata_event | ||||
| current_output_segment = variable_pool.get(self.node_data.output_selector) | current_output_segment = variable_pool.get(self.node_data.output_selector) |
| error=str(e), | error=str(e), | ||||
| error_type=type(e).__name__, | error_type=type(e).__name__, | ||||
| ) | ) | ||||
| finally: | |||||
| db.session.close() | |||||
| def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[dict[str, Any]]: | def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[dict[str, Any]]: | ||||
| available_datasets = [] | available_datasets = [] | ||||
| .all() | .all() | ||||
| ) | ) | ||||
| # avoid blocking at retrieval | |||||
| db.session.close() | |||||
| for dataset in results: | for dataset in results: | ||||
| # pass if dataset is not available | # pass if dataset is not available | ||||
| if not dataset: | if not dataset: |
| from collections.abc import Mapping | from collections.abc import Mapping | ||||
| from typing import Any, Literal, Optional | |||||
| from typing import Annotated, Any, Literal, Optional | |||||
| from pydantic import BaseModel, Field | |||||
| from pydantic import AfterValidator, BaseModel, Field | |||||
| from core.variables.types import SegmentType | |||||
| from core.workflow.nodes.base import BaseLoopNodeData, BaseLoopState, BaseNodeData | from core.workflow.nodes.base import BaseLoopNodeData, BaseLoopState, BaseNodeData | ||||
| from core.workflow.utils.condition.entities import Condition | from core.workflow.utils.condition.entities import Condition | ||||
| _VALID_VAR_TYPE = frozenset( | |||||
| [ | |||||
| SegmentType.STRING, | |||||
| SegmentType.NUMBER, | |||||
| SegmentType.OBJECT, | |||||
| SegmentType.ARRAY_STRING, | |||||
| SegmentType.ARRAY_NUMBER, | |||||
| SegmentType.ARRAY_OBJECT, | |||||
| ] | |||||
| ) | |||||
| def _is_valid_var_type(seg_type: SegmentType) -> SegmentType: | |||||
| if seg_type not in _VALID_VAR_TYPE: | |||||
| raise ValueError(...) | |||||
| return seg_type | |||||
| class LoopVariableData(BaseModel): | class LoopVariableData(BaseModel): | ||||
| """ | """ | ||||
| """ | """ | ||||
| label: str | label: str | ||||
| var_type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"] | |||||
| var_type: Annotated[SegmentType, AfterValidator(_is_valid_var_type)] | |||||
| value_type: Literal["variable", "constant"] | value_type: Literal["variable", "constant"] | ||||
| value: Optional[Any | list[str]] = None | value: Optional[Any | list[str]] = None | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from core.variables import ( | from core.variables import ( | ||||
| ArrayNumberSegment, | |||||
| ArrayObjectSegment, | |||||
| ArrayStringSegment, | |||||
| IntegerSegment, | IntegerSegment, | ||||
| ObjectSegment, | |||||
| Segment, | Segment, | ||||
| SegmentType, | SegmentType, | ||||
| StringSegment, | |||||
| ) | ) | ||||
| from core.workflow.entities.node_entities import NodeRunResult | from core.workflow.entities.node_entities import NodeRunResult | ||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus | from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus | ||||
| from core.workflow.nodes.event import NodeEvent, RunCompletedEvent | from core.workflow.nodes.event import NodeEvent, RunCompletedEvent | ||||
| from core.workflow.nodes.loop.entities import LoopNodeData | from core.workflow.nodes.loop.entities import LoopNodeData | ||||
| from core.workflow.utils.condition.processor import ConditionProcessor | from core.workflow.utils.condition.processor import ConditionProcessor | ||||
| from factories.variable_factory import TypeMismatchError, build_segment_with_type | |||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||
| from core.workflow.entities.variable_pool import VariablePool | from core.workflow.entities.variable_pool import VariablePool | ||||
| return variable_mapping | return variable_mapping | ||||
| @staticmethod | @staticmethod | ||||
| def _get_segment_for_constant(var_type: str, value: Any) -> Segment: | |||||
| def _get_segment_for_constant(var_type: SegmentType, value: Any) -> Segment: | |||||
| """Get the appropriate segment type for a constant value.""" | """Get the appropriate segment type for a constant value.""" | ||||
| segment_mapping: dict[str, tuple[type[Segment], SegmentType]] = { | |||||
| "string": (StringSegment, SegmentType.STRING), | |||||
| "number": (IntegerSegment, SegmentType.NUMBER), | |||||
| "object": (ObjectSegment, SegmentType.OBJECT), | |||||
| "array[string]": (ArrayStringSegment, SegmentType.ARRAY_STRING), | |||||
| "array[number]": (ArrayNumberSegment, SegmentType.ARRAY_NUMBER), | |||||
| "array[object]": (ArrayObjectSegment, SegmentType.ARRAY_OBJECT), | |||||
| } | |||||
| if var_type in ["array[string]", "array[number]", "array[object]"]: | if var_type in ["array[string]", "array[number]", "array[object]"]: | ||||
| if value: | |||||
| if value and isinstance(value, str): | |||||
| value = json.loads(value) | value = json.loads(value) | ||||
| else: | else: | ||||
| value = [] | value = [] | ||||
| segment_info = segment_mapping.get(var_type) | |||||
| if not segment_info: | |||||
| raise ValueError(f"Invalid variable type: {var_type}") | |||||
| segment_class, value_type = segment_info | |||||
| return segment_class(value=value, value_type=value_type) | |||||
| try: | |||||
| return build_segment_with_type(var_type, value) | |||||
| except TypeMismatchError as type_exc: | |||||
| # Attempt to parse the value as a JSON-encoded string, if applicable. | |||||
| if not isinstance(value, str): | |||||
| raise | |||||
| try: | |||||
| value = json.loads(value) | |||||
| except ValueError: | |||||
| raise type_exc | |||||
| return build_segment_with_type(var_type, value) |
| def _run(self) -> NodeRunResult: | def _run(self) -> NodeRunResult: | ||||
| node_inputs = dict(self.graph_runtime_state.variable_pool.user_inputs) | node_inputs = dict(self.graph_runtime_state.variable_pool.user_inputs) | ||||
| system_inputs = self.graph_runtime_state.variable_pool.system_variables | |||||
| system_inputs = self.graph_runtime_state.variable_pool.system_variables.to_dict() | |||||
| # TODO: System variables should be directly accessible, no need for special handling | # TODO: System variables should be directly accessible, no need for special handling | ||||
| # Set system variables as node outputs. | # Set system variables as node outputs. |
| from core.workflow.graph_engine.entities.event import AgentLogEvent | from core.workflow.graph_engine.entities.event import AgentLogEvent | ||||
| from core.workflow.nodes.base import BaseNode | from core.workflow.nodes.base import BaseNode | ||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from core.workflow.nodes.event import RunCompletedEvent, RunStreamChunkEvent | |||||
| from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent | |||||
| from core.workflow.utils.variable_template_parser import VariableTemplateParser | from core.workflow.utils.variable_template_parser import VariableTemplateParser | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from factories import file_factory | from factories import file_factory | ||||
| agent_logs.append(agent_log) | agent_logs.append(agent_log) | ||||
| yield agent_log | yield agent_log | ||||
| elif message.type == ToolInvokeMessage.MessageType.RETRIEVER_RESOURCES: | |||||
| assert isinstance(message.message, ToolInvokeMessage.RetrieverResourceMessage) | |||||
| yield RunRetrieverResourceEvent( | |||||
| retriever_resources=message.message.retriever_resources, | |||||
| context=message.message.context, | |||||
| ) | |||||
| # Add agent_logs to outputs['json'] to ensure frontend can access thinking process | # Add agent_logs to outputs['json'] to ensure frontend can access thinking process | ||||
| json_output: list[dict[str, Any]] = [] | json_output: list[dict[str, Any]] = [] |
| def get_zero_value(t: SegmentType): | def get_zero_value(t: SegmentType): | ||||
| # TODO(QuantumGhost): this should be a method of `SegmentType`. | |||||
| match t: | match t: | ||||
| case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER: | case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER: | ||||
| return variable_factory.build_segment([]) | return variable_factory.build_segment([]) | ||||
| return variable_factory.build_segment({}) | return variable_factory.build_segment({}) | ||||
| case SegmentType.STRING: | case SegmentType.STRING: | ||||
| return variable_factory.build_segment("") | return variable_factory.build_segment("") | ||||
| case SegmentType.INTEGER: | |||||
| return variable_factory.build_segment(0) | |||||
| case SegmentType.FLOAT: | |||||
| return variable_factory.build_segment(0.0) | |||||
| case SegmentType.NUMBER: | case SegmentType.NUMBER: | ||||
| return variable_factory.build_segment(0) | return variable_factory.build_segment(0) | ||||
| case _: | case _: |