Merge branch 'main' into feat/plugin

pull/9184/head
Yeuoly 2 years ago
commit ed7fcc5f7d
No known key found for this signature in database
GPG Key ID: A66E7E320FB19F61

@ -3,8 +3,8 @@
cd web && npm install cd web && npm install
pipx install poetry pipx install poetry
echo 'alias start-api="cd /workspaces/dify/api && flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc

@ -0,0 +1,24 @@
title: "General Discussion"
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: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:
label: Content
placeholder: Please describe the content you would like to discuss.
validations:
required: true
- type: markdown
attributes:
value: Please limit one request per issue.

@ -0,0 +1,30 @@
title: "Help"
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: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:
label: 1. Is this request related to a challenge you're experiencing? Tell me about your story.
placeholder: Please describe the specific scenario or problem you're facing as clearly as possible. For instance "I was trying to use [feature] for [specific task], and [what happened]... It was frustrating because...."
validations:
required: true
- type: textarea
attributes:
label: 2. Additional context or comments
placeholder: (Any other information, comments, documentations, links, or screenshots that would provide more clarity. This is the place to add anything else not covered above.)
validations:
required: false
- type: markdown
attributes:
value: Please limit one request per issue.

@ -0,0 +1,37 @@
title: Suggestions for New Features
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: "请务必使用英文提交 Issue否则会被关闭。谢谢:"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:
label: 1. Is this request related to a challenge you're experiencing? Tell me about your story.
placeholder: Please describe the specific scenario or problem you're facing as clearly as possible. For instance "I was trying to use [feature] for [specific task], and [what happened]... It was frustrating because...."
validations:
required: true
- type: textarea
attributes:
label: 2. Additional context or comments
placeholder: (Any other information, comments, documentations, links, or screenshots that would provide more clarity. This is the place to add anything else not covered above.)
validations:
required: false
- type: checkboxes
attributes:
label: 3. Can you help us with this feature?
description: Let us know! This is not a commitment, but a starting point for collaboration.
options:
- label: I am interested in contributing to this feature.
required: false
- type: markdown
attributes:
value: Please limit one request per issue.

@ -22,7 +22,7 @@ body:
- type: input - type: input
attributes: attributes:
label: Dify version label: Dify version
placeholder: 0.6.11 placeholder: 0.6.15
description: See about section in Dify console description: See about section in Dify console
validations: validations:
required: true required: true

@ -89,6 +89,5 @@ jobs:
pgvecto-rs pgvecto-rs
pgvector pgvector
chroma chroma
myscale
- name: Test Vector Stores - name: Test Vector Stores
run: poetry run -C api bash dev/pytest/pytest_vdb.sh run: poetry run -C api bash dev/pytest/pytest_vdb.sh

1
.gitignore vendored

@ -174,5 +174,6 @@ sdks/python-client/dify_client.egg-info
.vscode/* .vscode/*
!.vscode/launch.json !.vscode/launch.json
pyrightconfig.json pyrightconfig.json
api/.vscode
.idea/ .idea/

@ -81,7 +81,7 @@ Dify requires the following dependencies to build, make sure they're installed o
Dify is composed of a backend and a frontend. Navigate to the backend directory by `cd api/`, then follow the [Backend README](api/README.md) to install it. In a separate terminal, navigate to the frontend directory by `cd web/`, then follow the [Frontend README](web/README.md) to install. Dify is composed of a backend and a frontend. Navigate to the backend directory by `cd api/`, then follow the [Backend README](api/README.md) to install it. In a separate terminal, navigate to the frontend directory by `cd web/`, then follow the [Frontend README](web/README.md) to install.
Check the [installation FAQ](https://docs.dify.ai/getting-started/faq/install-faq) for a list of common issues and steps to troubleshoot. Check the [installation FAQ](https://docs.dify.ai/learn-more/faq/self-host-faq) for a list of common issues and steps to troubleshoot.
### 5. Visit dify in your browser ### 5. Visit dify in your browser

@ -2,17 +2,17 @@
考虑到我们的现状,我们需要灵活快速地交付,但我们也希望确保像你这样的贡献者在贡献过程中获得尽可能顺畅的体验。我们为此编写了这份贡献指南,旨在让你熟悉代码库和我们与贡献者的合作方式,以便你能快速进入有趣的部分。 考虑到我们的现状,我们需要灵活快速地交付,但我们也希望确保像你这样的贡献者在贡献过程中获得尽可能顺畅的体验。我们为此编写了这份贡献指南,旨在让你熟悉代码库和我们与贡献者的合作方式,以便你能快速进入有趣的部分。
这份指南,就像 Dify 本身一样,是一个不断改进的工作。如果有时它落后于实际项目,我们非常感谢你的理解,并欢迎任何反馈以供我们改进。 这份指南,就像 Dify 本身一样,是一个不断改进的工作。如果有时它落后于实际项目,我们非常感谢你的理解,并欢迎提供任何反馈以供我们改进。
在许可方面,请花一分钟阅读我们简短的[许可证和贡献者协议](./LICENSE)。社区还遵守[行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。 在许可方面,请花一分钟阅读我们简短的 [许可证和贡献者协议](./LICENSE)。社区还遵守 [行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
## 在开始之前 ## 在开始之前
[查找](https://github.com/langgenius/dify/issues?q=is:issue+is:closed)现有问题,或[创建](https://github.com/langgenius/dify/issues/new/choose)一个新问题。我们将问题分为两类: [查找](https://github.com/langgenius/dify/issues?q=is:issue+is:closed)现有问题,或 [创建](https://github.com/langgenius/dify/issues/new/choose) 一个新问题。我们将问题分为两类:
### 功能请求: ### 功能请求:
* 如果您要提出新的功能请求,请解释所提议的功能的目标,并尽可能提供详细的上下文。[@perzeusss](https://github.com/perzeuss)制作了一个很好的[功能请求助手](https://udify.app/chat/MK2kVSnw1gakVwMX),可以帮助您起草需求。随时尝试一下。 * 如果您要提出新的功能请求,请解释所提议的功能的目标,并尽可能提供详细的上下文。[@perzeusss](https://github.com/perzeuss) 制作了一个很好的 [功能请求助手](https://udify.app/chat/MK2kVSnw1gakVwMX),可以帮助您起草需求。随时尝试一下。
* 如果您想从现有问题中选择一个,请在其下方留下评论表示您的意愿。 * 如果您想从现有问题中选择一个,请在其下方留下评论表示您的意愿。
@ -20,45 +20,44 @@
根据所提议的功能所属的领域不同,您可能需要与不同的团队成员交流。以下是我们团队成员目前正在从事的各个领域的概述: 根据所提议的功能所属的领域不同,您可能需要与不同的团队成员交流。以下是我们团队成员目前正在从事的各个领域的概述:
| Member | Scope | | 团队成员 | 工作范围 |
| ------------------------------------------------------------ | ---------------------------------------------------- | | ------------------------------------------------------------ | ---------------------------------------------------- |
| [@yeuoly](https://github.com/Yeuoly) | Architecting Agents | | [@yeuoly](https://github.com/Yeuoly) | 架构 Agents |
| [@jyong](https://github.com/JohnJyong) | RAG pipeline design | | [@jyong](https://github.com/JohnJyong) | RAG 流水线设计 |
| [@GarfieldDai](https://github.com/GarfieldDai) | Building workflow orchestrations | | [@GarfieldDai](https://github.com/GarfieldDai) | 构建 workflow 编排 |
| [@iamjoel](https://github.com/iamjoel) & [@zxhlyh](https://github.com/zxhlyh) | Making our frontend a breeze to use | | [@iamjoel](https://github.com/iamjoel) & [@zxhlyh](https://github.com/zxhlyh) | 让我们的前端更易用 |
| [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | Developer experience, points of contact for anything | | [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | 开发人员体验, 综合事项联系人 |
| [@takatost](https://github.com/takatost) | Overall product direction and architecture | | [@takatost](https://github.com/takatost) | 产品整体方向和架构 |
How we prioritize: 事项优先级:
| Feature Type | Priority | | 功能类型 | 优先级 |
| ------------------------------------------------------------ | --------------- | | ------------------------------------------------------------ | --------------- |
| High-Priority Features as being labeled by a team member | High Priority | | 被团队成员标记为高优先级的功能 | 高优先级 |
| Popular feature requests from our [community feedback board](https://github.com/langgenius/dify/discussions/categories/feedbacks) | Medium Priority | | [community feedback board](https://github.com/langgenius/dify/discussions/categories/feedbacks) 内反馈的常见功能请求 | 中等优先级 |
| Non-core features and minor enhancements | Low Priority | | 非核心功能和小幅改进 | 低优先级 |
| Valuable but not immediate | Future-Feature | | 有价值当不紧急 | 未来功能 |
### 其他任何事情例如bug报告、性能优化、拼写错误更正 ### 其他任何事情(例如 bug 报告、性能优化、拼写错误更正):
* 立即开始编码。 * 立即开始编码。
How we prioritize: 事项优先级:
| Issue Type | Priority | | Issue 类型 | 优先级 |
| ------------------------------------------------------------ | --------------- | | ------------------------------------------------------------ | --------------- |
| Bugs in core functions (cannot login, applications not working, security loopholes) | Critical | | 核心功能的 Bugs例如无法登录、应用无法工作、安全漏洞 | 紧急 |
| Non-critical bugs, performance boosts | Medium Priority | | 非紧急 bugs, 性能提升 | 中等优先级 |
| Minor fixes (typos, confusing but working UI) | Low Priority | | 小幅修复(错别字, 能正常工作但存在误导的 UI) | 低优先级 |
## 安装 ## 安装
以下是设置Dify进行开发的步骤 以下是设置 Dify 进行开发的步骤:
### 1. Fork该仓库 ### 1. Fork 该仓库
### 2. 克隆仓库 ### 2. 克隆仓库
从终端克隆fork的仓库: 从终端克隆代码仓库:
``` ```
git clone git@github.com:<github_username>/dify.git git clone git@github.com:<github_username>/dify.git
@ -76,72 +75,72 @@ Dify 依赖以下工具和库:
### 4. 安装 ### 4. 安装
Dify由后端和前端组成。通过`cd api/`导航到后端目录,然后按照[后端README](api/README.md)进行安装。在另一个终端中,通过`cd web/`导航到前端目录,然后按照[前端README](web/README.md)进行安装。 Dify 由后端和前端组成。通过 `cd api/` 导航到后端目录,然后按照 [后端 README](api/README.md) 进行安装。在另一个终端中,通过 `cd web/` 导航到前端目录,然后按照 [前端 README](web/README.md) 进行安装。
查看[安装常见问题解答](https://docs.dify.ai/getting-started/faq/install-faq)以获取常见问题列表和故障排除步骤。 查看 [安装常见问题解答](https://docs.dify.ai/v/zh-hans/learn-more/faq/install-faq) 以获取常见问题列表和故障排除步骤。
### 5. 在浏览器中访问Dify ### 5. 在浏览器中访问 Dify
为了验证您的设置,打开浏览器并访问[http://localhost:3000](http://localhost:3000)默认或您自定义的URL和端口。现在您应该看到Dify正在运行。 为了验证您的设置,打开浏览器并访问 [http://localhost:3000](http://localhost:3000)(默认或您自定义的 URL 和端口)。现在您应该看到 Dify 正在运行。
## 开发 ## 开发
如果您要添加模型提供程序,请参考[此指南](https://github.com/langgenius/dify/blob/main/api/core/model_runtime/README.md)。 如果您要添加模型提供程序,请参考 [此指南](https://github.com/langgenius/dify/blob/main/api/core/model_runtime/README.md)。
如果您要向Agent或Workflow添加工具提供程序请参考[此指南](./api/core/tools/README.md)。 如果您要向 Agent Workflow 添加工具提供程序,请参考 [此指南](./api/core/tools/README.md)。
为了帮助您快速了解您的贡献在哪个部分以下是Dify后端和前端的简要注释大纲 为了帮助您快速了解您的贡献在哪个部分,以下是 Dify 后端和前端的简要注释大纲:
### 后端 ### 后端
Dify的后端使用Python编写使用[Flask](https://flask.palletsprojects.com/en/3.0.x/)框架。它使用[SQLAlchemy](https://www.sqlalchemy.org/)作为ORM使用[Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html)作为任务队列。授权逻辑通过Flask-login进行处理。 Dify 的后端使用 Python 编写,使用 [Flask](https://flask.palletsprojects.com/en/3.0.x/) 框架。它使用 [SQLAlchemy](https://www.sqlalchemy.org/) 作为 ORM使用 [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) 作为任务队列。授权逻辑通过 Flask-login 进行处理。
``` ```
[api/] [api/]
├── constants // Constant settings used throughout code base. ├── constants // 用于整个代码库的常量设置。
├── controllers // API route definitions and request handling logic. ├── controllers // API 路由定义和请求处理逻辑。
├── core // Core application orchestration, model integrations, and tools. ├── core // 核心应用编排、模型集成和工具。
├── docker // Docker & containerization related configurations. ├── docker // Docker 和容器化相关配置。
├── events // Event handling and processing ├── events // 事件处理和处理。
├── extensions // Extensions with 3rd party frameworks/platforms. ├── extensions // 与第三方框架/平台的扩展。
├── fields // field definitions for serialization/marshalling. ├── fields // 用于序列化/封装的字段定义。
├── libs // Reusable libraries and helpers. ├── libs // 可重用的库和助手。
├── migrations // Scripts for database migration. ├── migrations // 数据库迁移脚本。
├── models // Database models & schema definitions. ├── models // 数据库模型和架构定义。
├── services // Specifies business logic. ├── services // 指定业务逻辑。
├── storage // Private key storage. ├── storage // 私钥存储。
├── tasks // Handling of async tasks and background jobs. ├── tasks // 异步任务和后台作业的处理。
└── tests └── tests
``` ```
### 前端 ### 前端
该网站使用基于Typescript的[Next.js](https://nextjs.org/)模板进行引导,并使用[Tailwind CSS](https://tailwindcss.com/)进行样式设计。[React-i18next](https://react.i18next.com/)用于国际化。 该网站使用基于 Typescript [Next.js](https://nextjs.org/) 模板进行引导,并使用 [Tailwind CSS](https://tailwindcss.com/) 进行样式设计。[React-i18next](https://react.i18next.com/) 用于国际化。
``` ```
[web/] [web/]
├── app // layouts, pages, and components ├── app // 布局、页面和组件
│ ├── (commonLayout) // common layout used throughout the app │ ├── (commonLayout) // 整个应用通用的布局
│ ├── (shareLayout) // layouts specifically shared across token-specific sessions │ ├── (shareLayout) // 在特定会话中共享的布局
│ ├── activate // activate page │ ├── activate // 激活页面
│ ├── components // shared by pages and layouts │ ├── components // 页面和布局共享的组件
│ ├── install // install page │ ├── install // 安装页面
│ ├── signin // signin page │ ├── signin // 登录页面
│ └── styles // globally shared styles │ └── styles // 全局共享的样式
├── assets // Static assets ├── assets // 静态资源
├── bin // scripts ran at build step ├── bin // 构建步骤运行的脚本
├── config // adjustable settings and options ├── config // 可调整的设置和选项
├── context // shared contexts used by different portions of the app ├── context // 应用中不同部分使用的共享上下文
├── dictionaries // Language-specific translate files ├── dictionaries // 语言特定的翻译文件
├── docker // container configurations ├── docker // 容器配置
├── hooks // Reusable hooks ├── hooks // 可重用的钩子
├── i18n // Internationalization configuration ├── i18n // 国际化配置
├── models // describes data models & shapes of API responses ├── models // 描述数据模型和 API 响应的形状
├── public // meta assets like favicon ├── public // 如 favicon 等元资源
├── service // specifies shapes of API actions ├── service // 定义 API 操作的形状
├── test ├── test
├── types // descriptions of function params and return values ├── types // 函数参数和返回值的描述
└── utils // Shared utility functions └── utils // 共享的实用函数
``` ```
## 提交你的 PR ## 提交你的 PR

@ -1,7 +1,7 @@
Dify にコントリビュートしたいとお考えなのですね。それは素晴らしいことです。 Dify にコントリビュートしたいとお考えなのですね。それは素晴らしいことです。
私たちは、LLM アプリケーションの構築と管理のための最も直感的なワークフローを設計するという壮大な野望を持っています。人数も資金も限られている新興企業として、コミュニティからの支援は本当に重要です。 私たちは、LLM アプリケーションの構築と管理のための最も直感的なワークフローを設計するという壮大な野望を持っています。人数も資金も限られている新興企業として、コミュニティからの支援は本当に重要です。
私たちは現状を鑑み、機敏かつ迅速に開発をする必要がありますが、同時にあなたのようなコントリビューターの方々に、可能な限りスムーズな貢献体験をしていただきたいと思っています。そのためにこのコントリビュートガイドを作成しました。 私たちは現状を鑑み、機敏かつ迅速に開発をする必要がありますが、同時にあなたのようなコントリビューターの方々に、可能な限りスムーズな貢献体験をしていただきたいと思っています。そのためにこのコントリビュートガイドを作成しました。
コードベースやコントリビュータの方々と私たちがどのように仕事をしているのかに慣れていただき、楽しいパートにすぐに飛び込めるようにすることが目的です。 コードベースやコントリビュータの方々と私たちがどのように仕事をしているのかに慣れていただき、楽しいパートにすぐに飛び込めるようにすることが目的です。
このガイドは Dify そのものと同様に、継続的に改善されています。実際のプロジェクトに遅れをとることがあるかもしれませんが、ご理解のほどよろしくお願いいたします。 このガイドは Dify そのものと同様に、継続的に改善されています。実際のプロジェクトに遅れをとることがあるかもしれませんが、ご理解のほどよろしくお願いいたします。
@ -14,13 +14,13 @@ Dify にコントリビュートしたいとお考えなのですね。それは
### 機能リクエスト ### 機能リクエスト
* 新しい機能要望を出す場合は、提案する機能が何を実現するものなのかを説明し、可能な限り多くのコンテキストを含めてください。[@perzeusss](https://github.com/perzeuss)は、あなたの要望を書き出すのに役立つ [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) を作ってくれました。気軽に試してみてください。 * 新しい機能要望を出す場合は、提案する機能が何を実現するものなのかを説明し、可能な限り多くのコンテキストを含めてください。[@perzeusss](https://github.com/perzeuss)は、あなたの要望を書き出すのに役立つ [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) を作ってくれました。気軽に試してみてください。
* 既存の課題から 1 つ選びたい場合は、その下にコメントを書いてください。 * 既存の課題から 1 つ選びたい場合は、その下にコメントを書いてください。
関連する方向で作業しているチームメンバーが参加します。すべてが良好であれば、コーディングを開始する許可が与えられます。私たちが変更を提案した場合にあなたの作業が無駄になることがないよう、それまでこの機能の作業を控えていただくようお願いいたします。 関連する方向で作業しているチームメンバーが参加します。すべてが良好であれば、コーディングを開始する許可が与えられます。私たちが変更を提案した場合にあなたの作業が無駄になることがないよう、それまでこの機能の作業を控えていただくようお願いいたします。
提案された機能がどの分野に属するかによって、あなたは異なるチーム・メンバーと話をするかもしれません。以下は、各チームメンバーが現在取り組んでいる分野の概要です。 提案された機能がどの分野に属するかによって、あなたは異なるチーム・メンバーと話をするかもしれません。以下は、各チームメンバーが現在取り組んでいる分野の概要です。
| Member | Scope | | Member | Scope |
| --------------------------------------------------------------------------------------- | ------------------------------------ | | --------------------------------------------------------------------------------------- | ------------------------------------ |
@ -82,7 +82,7 @@ Dify はバックエンドとフロントエンドから構成されています
まず`cd api/`でバックエンドのディレクトリに移動し、[Backend README](api/README.md)に従ってインストールします。 まず`cd api/`でバックエンドのディレクトリに移動し、[Backend README](api/README.md)に従ってインストールします。
次に別のターミナルで、`cd web/`でフロントエンドのディレクトリに移動し、[Frontend README](web/README.md)に従ってインストールしてください。 次に別のターミナルで、`cd web/`でフロントエンドのディレクトリに移動し、[Frontend README](web/README.md)に従ってインストールしてください。
よくある問題とトラブルシューティングの手順については、[installation FAQ](https://docs.dify.ai/getting-started/faq/install-faq) を確認してください。 よくある問題とトラブルシューティングの手順については、[installation FAQ](https://docs.dify.ai/v/japanese/learn-more/faq/install-faq) を確認してください。
### 5. ブラウザで dify にアクセスする ### 5. ブラウザで dify にアクセスする
@ -153,7 +153,7 @@ Dify のバックエンドは[Flask](https://flask.palletsprojects.com/en/3.0.x/
いよいよ、私たちのリポジトリにプルリクエスト (PR) を提出する時が来ました。主要な機能については、まず `deploy/dev` ブランチにマージしてテストしてから `main` ブランチにマージします。 いよいよ、私たちのリポジトリにプルリクエスト (PR) を提出する時が来ました。主要な機能については、まず `deploy/dev` ブランチにマージしてテストしてから `main` ブランチにマージします。
マージ競合などの問題が発生した場合、またはプル リクエストを開く方法がわからない場合は、[GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) をチェックしてみてください。 マージ競合などの問題が発生した場合、またはプル リクエストを開く方法がわからない場合は、[GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) をチェックしてみてください。
これで完了です!あなたの PR がマージされると、[README](https://github.com/langgenius/dify/blob/main/README.md) にコントリビューターとして紹介されます。 これで完了です!あなたの PR がマージされると、[README](https://github.com/langgenius/dify/blob/main/README.md) にコントリビューターとして紹介されます。
## ヘルプを得る ## ヘルプを得る

@ -216,7 +216,6 @@ At the same time, please consider supporting Dify by sharing it on social media
* [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. * [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Email](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). Best for: questions you have about using Dify.AI.
* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. * [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
* [Twitter](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. * [Twitter](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.

@ -199,7 +199,6 @@ docker compose up -d
## المجتمع والاتصال ## المجتمع والاتصال
* [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة. * [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
* [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [البريد الإلكتروني](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). الأفضل لـ: الأسئلة التي تتعلق باستخدام Dify.AI.
* [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. * [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
* [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. * [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.

@ -224,7 +224,6 @@ Al mismo tiempo, considera apoyar a Dify compartiéndolo en redes sociales y en
* [Discusión en GitHub](https://github.com/langgenius/dify/discussions). Lo mejor para: compartir comentarios y hacer preguntas. * [Discusión en GitHub](https://github.com/langgenius/dify/discussions). Lo mejor para: compartir comentarios y hacer preguntas.
* [Reporte de problemas en GitHub](https://github.com/langgenius/dify/issues). Lo mejor para: errores que encuentres usando Dify.AI y propuestas de características. Consulta nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [Reporte de problemas en GitHub](https://github.com/langgenius/dify/issues). Lo mejor para: errores que encuentres usando Dify.AI y propuestas de características. Consulta nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Correo electrónico](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). Lo mejor para: preguntas que tengas sobre el uso de Dify.AI.
* [Discord](https://discord.gg/FngNHpbcY7). Lo mejor para: compartir tus aplicaciones y pasar el rato con la comunidad. * [Discord](https://discord.gg/FngNHpbcY7). Lo mejor para: compartir tus aplicaciones y pasar el rato con la comunidad.
* [Twitter](https://twitter.com/dify_ai). Lo mejor para: compartir tus aplicaciones y pasar el rato con la comunidad. * [Twitter](https://twitter.com/dify_ai). Lo mejor para: compartir tus aplicaciones y pasar el rato con la comunidad.

@ -222,7 +222,6 @@ Dans le même temps, veuillez envisager de soutenir Dify en le partageant sur le
* [Discussion GitHub](https://github.com/langgenius/dify/discussions). Meilleur pour: partager des commentaires et poser des questions. * [Discussion GitHub](https://github.com/langgenius/dify/discussions). Meilleur pour: partager des commentaires et poser des questions.
* [Problèmes GitHub](https://github.com/langgenius/dify/issues). Meilleur pour: les bogues que vous rencontrez en utilisant Dify.AI et les propositions de fonctionnalités. Consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [Problèmes GitHub](https://github.com/langgenius/dify/issues). Meilleur pour: les bogues que vous rencontrez en utilisant Dify.AI et les propositions de fonctionnalités. Consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [E-mail](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). Meilleur pour: les questions que vous avez sur l'utilisation de Dify.AI.
* [Discord](https://discord.gg/FngNHpbcY7). Meilleur pour: partager vos applications et passer du temps avec la communauté. * [Discord](https://discord.gg/FngNHpbcY7). Meilleur pour: partager vos applications et passer du temps avec la communauté.
* [Twitter](https://twitter.com/dify_ai). Meilleur pour: partager vos applications et passer du temps avec la communauté. * [Twitter](https://twitter.com/dify_ai). Meilleur pour: partager vos applications et passer du temps avec la communauté.

@ -221,7 +221,6 @@ docker compose up -d
* [Github Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。 * [Github Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。
* [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください * [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください
* [Email](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). 主に: Dify.AIの使用に関する質問。
* [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。 * [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。
* [Twitter](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。 * [Twitter](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。
@ -239,7 +238,7 @@ docker compose up -d
<td>無料の30分間のミーティングをスケジュール</td> <td>無料の30分間のミーティングをスケジュール</td>
</tr> </tr>
<tr> <tr>
<td><a href='mailto:support@dify.ai?subject=[GitHub]Technical%20Support'>技術サポート</a></td> <td><a href='https://github.com/langgenius/dify/issues'>技術サポート</a></td>
<td>技術的な問題やサポートに関する質問</td> <td>技術的な問題やサポートに関する質問</td>
</tr> </tr>
<tr> <tr>

@ -224,7 +224,6 @@ At the same time, please consider supporting Dify by sharing it on social media
). Best for: sharing feedback and asking questions. ). Best for: sharing feedback and asking questions.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Email](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). Best for: questions you have about using Dify.AI.
* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. * [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
* [Twitter](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. * [Twitter](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.

@ -214,7 +214,6 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
* [Github 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다. * [Github 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다.
* [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. * [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
* [이메일](mailto:support@dify.ai?subject=[GitHub]Questions%20About%20Dify). Dify.AI 사용에 대한 질문하기에 적합합니다.
* [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. * [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.
* [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. * [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.

@ -183,6 +183,7 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
# Model Configuration # Model Configuration
MULTIMODAL_SEND_IMAGE_FORMAT=base64 MULTIMODAL_SEND_IMAGE_FORMAT=base64
PROMPT_GENERATION_MAX_TOKENS=512
# Mail configuration, support: resend, smtp # Mail configuration, support: resend, smtp
MAIL_TYPE= MAIL_TYPE=
@ -216,6 +217,7 @@ UNSTRUCTURED_API_KEY=
SSRF_PROXY_HTTP_URL= SSRF_PROXY_HTTP_URL=
SSRF_PROXY_HTTPS_URL= SSRF_PROXY_HTTPS_URL=
SSRF_DEFAULT_MAX_RETRIES=3
BATCH_UPLOAD_LIMIT=10 BATCH_UPLOAD_LIMIT=10
KEYWORD_DATA_SOURCE_TYPE=database KEYWORD_DATA_SOURCE_TYPE=database
@ -260,3 +262,6 @@ APP_MAX_ACTIVE_REQUESTS=0
# Plugin configuration # Plugin configuration
PLUGIN_INNER_API_URL=http://127.0.0.1:5002 PLUGIN_INNER_API_URL=http://127.0.0.1:5002
PLUGIN_INNER_API_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi PLUGIN_INNER_API_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi
# Celery beat configuration
CELERY_BEAT_SCHEDULER_TIME=1

@ -12,7 +12,8 @@
```bash ```bash
cd ../docker cd ../docker
cp middleware.env.example middleware.env cp middleware.env.example middleware.env
docker compose -f docker-compose.middleware.yaml -p dify up -d # change the profile to other vector database if you are not using weaviate
docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d
cd ../api cd ../api
``` ```

@ -1,7 +1,5 @@
import os import os
from configs import dify_config
if os.environ.get("DEBUG", "false").lower() != 'true': if os.environ.get("DEBUG", "false").lower() != 'true':
from gevent import monkey from gevent import monkey
@ -23,7 +21,9 @@ from flask import Flask, Response, request
from flask_cors import CORS from flask_cors import CORS
from werkzeug.exceptions import Unauthorized from werkzeug.exceptions import Unauthorized
import contexts
from commands import register_commands from commands import register_commands
from configs import dify_config
# DO NOT REMOVE BELOW # DO NOT REMOVE BELOW
from events import event_handlers from events import event_handlers
@ -181,7 +181,10 @@ def load_user_from_request(request_from_flask_login):
decoded = PassportService().verify(auth_token) decoded = PassportService().verify(auth_token)
user_id = decoded.get('user_id') user_id = decoded.get('user_id')
return AccountService.load_logged_in_account(account_id=user_id, token=auth_token) account = AccountService.load_logged_in_account(account_id=user_id, token=auth_token)
if account:
contexts.tenant_id.set(account.current_tenant_id)
return account
@login_manager.unauthorized_handler @login_manager.unauthorized_handler

@ -23,6 +23,7 @@ class SecurityConfig(BaseSettings):
default=24, default=24,
) )
class AppExecutionConfig(BaseSettings): class AppExecutionConfig(BaseSettings):
""" """
App Execution configs App Execution configs
@ -418,7 +419,6 @@ class DataSetConfig(BaseSettings):
default=False, default=False,
) )
class WorkspaceConfig(BaseSettings): class WorkspaceConfig(BaseSettings):
""" """
Workspace configs Workspace configs
@ -448,6 +448,13 @@ class ImageFormatConfig(BaseSettings):
) )
class CeleryBeatConfig(BaseSettings):
CELERY_BEAT_SCHEDULER_TIME: int = Field(
description='the time of the celery scheduler, default to 1 day',
default=1,
)
class FeatureConfig( class FeatureConfig(
# place the configs in alphabet order # place the configs in alphabet order
AppExecutionConfig, AppExecutionConfig,
@ -476,5 +483,6 @@ class FeatureConfig(
# hosted services config # hosted services config
HostedServiceConfig, HostedServiceConfig,
CeleryBeatConfig,
): ):
pass pass

@ -79,7 +79,7 @@ class HostedAzureOpenAiConfig(BaseSettings):
default=False, default=False,
) )
HOSTED_OPENAI_API_KEY: Optional[str] = Field( HOSTED_AZURE_OPENAI_API_KEY: Optional[str] = Field(
description='', description='',
default=None, default=None,
) )

@ -1,4 +1,5 @@
from typing import Any, Optional from typing import Any, Optional
from urllib.parse import quote_plus
from pydantic import Field, NonNegativeInt, PositiveInt, computed_field from pydantic import Field, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -104,7 +105,7 @@ class DatabaseConfig:
).strip("&") ).strip("&")
db_extras = f"?{db_extras}" if db_extras else "" db_extras = f"?{db_extras}" if db_extras else ""
return (f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://" return (f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://"
f"{self.DB_USERNAME}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}" f"{quote_plus(self.DB_USERNAME)}:{quote_plus(self.DB_PASSWORD)}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
f"{db_extras}") f"{db_extras}")
SQLALCHEMY_POOL_SIZE: NonNegativeInt = Field( SQLALCHEMY_POOL_SIZE: NonNegativeInt = Field(

@ -1,4 +1,3 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt from pydantic import BaseModel, Field, PositiveInt
@ -8,32 +7,32 @@ class MyScaleConfig(BaseModel):
MyScale configs MyScale configs
""" """
MYSCALE_HOST: Optional[str] = Field( MYSCALE_HOST: str = Field(
description='MyScale host', description='MyScale host',
default=None, default='localhost',
) )
MYSCALE_PORT: Optional[PositiveInt] = Field( MYSCALE_PORT: PositiveInt = Field(
description='MyScale port', description='MyScale port',
default=8123, default=8123,
) )
MYSCALE_USER: Optional[str] = Field( MYSCALE_USER: str = Field(
description='MyScale user', description='MyScale user',
default=None, default='default',
) )
MYSCALE_PASSWORD: Optional[str] = Field( MYSCALE_PASSWORD: str = Field(
description='MyScale password', description='MyScale password',
default=None, default='',
) )
MYSCALE_DATABASE: Optional[str] = Field( MYSCALE_DATABASE: str = Field(
description='MyScale database name', description='MyScale database name',
default=None, default='default',
) )
MYSCALE_FTS_PARAMS: Optional[str] = Field( MYSCALE_FTS_PARAMS: str = Field(
description='MyScale fts index parameters', description='MyScale fts index parameters',
default=None, default='',
) )

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description='Dify version', description='Dify version',
default='0.6.13', default='0.6.15',
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

@ -0,0 +1,2 @@
# TODO: Update all string in code to use this constant
HIDDEN_VALUE = '[__HIDDEN__]'

@ -0,0 +1,3 @@
from contextvars import ContextVar
tenant_id: ContextVar[str] = ContextVar('tenant_id')

@ -15,6 +15,7 @@ from fields.app_fields import (
app_pagination_fields, app_pagination_fields,
) )
from libs.login import login_required from libs.login import login_required
from services.app_dsl_service import AppDslService
from services.app_service import AppService from services.app_service import AppService
ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion'] ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion']
@ -97,8 +98,42 @@ class AppImportApi(Resource):
parser.add_argument('icon_background', type=str, location='json') parser.add_argument('icon_background', type=str, location='json')
args = parser.parse_args() args = parser.parse_args()
app_service = AppService() app = AppDslService.import_and_create_new_app(
app = app_service.import_app(current_user.current_tenant_id, args['data'], args, current_user) tenant_id=current_user.current_tenant_id,
data=args['data'],
args=args,
account=current_user
)
return app, 201
class AppImportFromUrlApi(Resource):
@setup_required
@login_required
@account_initialization_required
@marshal_with(app_detail_fields_with_site)
@cloud_edition_billing_resource_check('apps')
def post(self):
"""Import app from url"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument('url', type=str, required=True, nullable=False, location='json')
parser.add_argument('name', type=str, location='json')
parser.add_argument('description', type=str, location='json')
parser.add_argument('icon', type=str, location='json')
parser.add_argument('icon_background', type=str, location='json')
args = parser.parse_args()
app = AppDslService.import_and_create_new_app_from_url(
tenant_id=current_user.current_tenant_id,
url=args['url'],
args=args,
account=current_user
)
return app, 201 return app, 201
@ -177,9 +212,13 @@ class AppCopyApi(Resource):
parser.add_argument('icon_background', type=str, location='json') parser.add_argument('icon_background', type=str, location='json')
args = parser.parse_args() args = parser.parse_args()
app_service = AppService() data = AppDslService.export_dsl(app_model=app_model, include_secret=True)
data = app_service.export_app(app_model) app = AppDslService.import_and_create_new_app(
app = app_service.import_app(current_user.current_tenant_id, data, args, current_user) tenant_id=current_user.current_tenant_id,
data=data,
args=args,
account=current_user
)
return app, 201 return app, 201
@ -195,10 +234,13 @@ class AppExportApi(Resource):
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()
app_service = AppService() # Add include_secret params
parser = reqparse.RequestParser()
parser.add_argument('include_secret', type=inputs.boolean, default=False, location='args')
args = parser.parse_args()
return { return {
"data": app_service.export_app(app_model) "data": AppDslService.export_dsl(app_model=app_model, include_secret=args['include_secret'])
} }
@ -322,6 +364,7 @@ class AppTraceApi(Resource):
api.add_resource(AppListApi, '/apps') api.add_resource(AppListApi, '/apps')
api.add_resource(AppImportApi, '/apps/import') api.add_resource(AppImportApi, '/apps/import')
api.add_resource(AppImportFromUrlApi, '/apps/import/url')
api.add_resource(AppApi, '/apps/<uuid:app_id>') api.add_resource(AppApi, '/apps/<uuid:app_id>')
api.add_resource(AppCopyApi, '/apps/<uuid:app_id>/copy') api.add_resource(AppCopyApi, '/apps/<uuid:app_id>/copy')
api.add_resource(AppExportApi, '/apps/<uuid:app_id>/export') api.add_resource(AppExportApi, '/apps/<uuid:app_id>/export')

@ -22,7 +22,7 @@ from fields.conversation_fields import (
) )
from libs.helper import datetime_string from libs.helper import datetime_string
from libs.login import login_required from libs.login import login_required
from models.model import AppMode, Conversation, Message, MessageAnnotation from models.model import AppMode, Conversation, EndUser, Message, MessageAnnotation
class CompletionConversationApi(Resource): class CompletionConversationApi(Resource):
@ -156,19 +156,31 @@ class ChatConversationApi(Resource):
parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args') parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
args = parser.parse_args() args = parser.parse_args()
subquery = (
db.session.query(
Conversation.id.label('conversation_id'),
EndUser.session_id.label('from_end_user_session_id')
)
.outerjoin(EndUser, Conversation.from_end_user_id == EndUser.id)
.subquery()
)
query = db.select(Conversation).where(Conversation.app_id == app_model.id) query = db.select(Conversation).where(Conversation.app_id == app_model.id)
if args['keyword']: if args['keyword']:
keyword_filter = '%{}%'.format(args['keyword'])
query = query.join( query = query.join(
Message, Message.conversation_id == Conversation.id Message, Message.conversation_id == Conversation.id,
).join(
subquery, subquery.c.conversation_id == Conversation.id
).filter( ).filter(
or_( or_(
Message.query.ilike('%{}%'.format(args['keyword'])), Message.query.ilike(keyword_filter),
Message.answer.ilike('%{}%'.format(args['keyword'])), Message.answer.ilike(keyword_filter),
Conversation.name.ilike('%{}%'.format(args['keyword'])), Conversation.name.ilike(keyword_filter),
Conversation.introduction.ilike('%{}%'.format(args['keyword'])), Conversation.introduction.ilike(keyword_filter),
subquery.c.from_end_user_session_id.ilike(keyword_filter)
), ),
) )
account = current_user account = current_user

@ -1,3 +1,5 @@
import os
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
@ -22,17 +24,21 @@ class RuleGenerateApi(Resource):
@account_initialization_required @account_initialization_required
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('audiences', type=str, required=True, nullable=False, location='json') parser.add_argument('instruction', type=str, required=True, nullable=False, location='json')
parser.add_argument('hoping_to_solve', type=str, required=True, nullable=False, location='json') parser.add_argument('model_config', type=dict, required=True, nullable=False, location='json')
parser.add_argument('no_variable', type=bool, required=True, default=False, location='json')
args = parser.parse_args() args = parser.parse_args()
account = current_user account = current_user
PROMPT_GENERATION_MAX_TOKENS = int(os.getenv('PROMPT_GENERATION_MAX_TOKENS', '512'))
try: try:
rules = LLMGenerator.generate_rule_config( rules = LLMGenerator.generate_rule_config(
account.current_tenant_id, tenant_id=account.current_tenant_id,
args['audiences'], instruction=args['instruction'],
args['hoping_to_solve'] model_config=args['model_config'],
no_variable=args['no_variable'],
rule_config_max_tokens=PROMPT_GENERATION_MAX_TOKENS
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)

@ -281,7 +281,7 @@ class UserSatisfactionRateStatistic(Resource):
SELECT date(DATE_TRUNC('day', m.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, SELECT date(DATE_TRUNC('day', m.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
COUNT(m.id) as message_count, COUNT(mf.id) as feedback_count COUNT(m.id) as message_count, COUNT(mf.id) as feedback_count
FROM messages m FROM messages m
LEFT JOIN message_feedbacks mf on mf.message_id=m.id LEFT JOIN message_feedbacks mf on mf.message_id=m.id and mf.rating='like'
WHERE m.app_id = :app_id WHERE m.app_id = :app_id
''' '''
arg_dict = {'tz': account.timezone, 'app_id': app_model.id} arg_dict = {'tz': account.timezone, 'app_id': app_model.id}

@ -13,6 +13,7 @@ from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.segments import factory
from core.errors.error import AppInvokeQuotaExceededError from core.errors.error import AppInvokeQuotaExceededError
from fields.workflow_fields import workflow_fields from fields.workflow_fields import workflow_fields
from fields.workflow_run_fields import workflow_run_node_execution_fields from fields.workflow_run_fields import workflow_run_node_execution_fields
@ -20,6 +21,7 @@ from libs import helper
from libs.helper import TimestampField, uuid_value from libs.helper import TimestampField, uuid_value
from libs.login import current_user, login_required from libs.login import current_user, login_required
from models.model import App, AppMode from models.model import App, AppMode
from services.app_dsl_service import AppDslService
from services.app_generate_service import AppGenerateService from services.app_generate_service import AppGenerateService
from services.errors.app import WorkflowHashNotEqualError from services.errors.app import WorkflowHashNotEqualError
from services.workflow_service import WorkflowService from services.workflow_service import WorkflowService
@ -40,7 +42,7 @@ class DraftWorkflowApi(Resource):
# The role of the current user in the ta table must be admin, owner, or editor # The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()
# fetch draft workflow by app_model # fetch draft workflow by app_model
workflow_service = WorkflowService() workflow_service = WorkflowService()
workflow = workflow_service.get_draft_workflow(app_model=app_model) workflow = workflow_service.get_draft_workflow(app_model=app_model)
@ -63,13 +65,15 @@ class DraftWorkflowApi(Resource):
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()
content_type = request.headers.get('Content-Type') content_type = request.headers.get('Content-Type', '')
if 'application/json' in content_type: if 'application/json' in content_type:
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('graph', type=dict, required=True, nullable=False, location='json') parser.add_argument('graph', type=dict, required=True, nullable=False, location='json')
parser.add_argument('features', type=dict, required=True, nullable=False, location='json') parser.add_argument('features', type=dict, required=True, nullable=False, location='json')
parser.add_argument('hash', type=str, required=False, location='json') parser.add_argument('hash', type=str, required=False, location='json')
# TODO: set this to required=True after frontend is updated
parser.add_argument('environment_variables', type=list, required=False, location='json')
args = parser.parse_args() args = parser.parse_args()
elif 'text/plain' in content_type: elif 'text/plain' in content_type:
try: try:
@ -83,7 +87,8 @@ class DraftWorkflowApi(Resource):
args = { args = {
'graph': data.get('graph'), 'graph': data.get('graph'),
'features': data.get('features'), 'features': data.get('features'),
'hash': data.get('hash') 'hash': data.get('hash'),
'environment_variables': data.get('environment_variables')
} }
except json.JSONDecodeError: except json.JSONDecodeError:
return {'message': 'Invalid JSON data'}, 400 return {'message': 'Invalid JSON data'}, 400
@ -93,12 +98,15 @@ class DraftWorkflowApi(Resource):
workflow_service = WorkflowService() workflow_service = WorkflowService()
try: try:
environment_variables_list = args.get('environment_variables') or []
environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
workflow = workflow_service.sync_draft_workflow( workflow = workflow_service.sync_draft_workflow(
app_model=app_model, app_model=app_model,
graph=args.get('graph'), graph=args['graph'],
features=args.get('features'), features=args['features'],
unique_hash=args.get('hash'), unique_hash=args.get('hash'),
account=current_user account=current_user,
environment_variables=environment_variables,
) )
except WorkflowHashNotEqualError: except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync() raise DraftWorkflowNotSync()
@ -128,8 +136,7 @@ class DraftWorkflowImportApi(Resource):
parser.add_argument('data', type=str, required=True, nullable=False, location='json') parser.add_argument('data', type=str, required=True, nullable=False, location='json')
args = parser.parse_args() args = parser.parse_args()
workflow_service = WorkflowService() workflow = AppDslService.import_and_overwrite_workflow(
workflow = workflow_service.import_draft_workflow(
app_model=app_model, app_model=app_model,
data=args['data'], data=args['data'],
account=current_user account=current_user

@ -1,10 +1,11 @@
import flask_restful import flask_restful
from flask import current_app, request from flask import request
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
import services import services
from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.apikey import api_key_fields, api_key_list from controllers.console.apikey import api_key_fields, api_key_list
from controllers.console.app.error import ProviderNotInitializeError from controllers.console.app.error import ProviderNotInitializeError
@ -530,7 +531,7 @@ class DatasetApiBaseUrlApi(Resource):
@account_initialization_required @account_initialization_required
def get(self): def get(self):
return { return {
'api_base_url': (current_app.config['SERVICE_API_URL'] if current_app.config['SERVICE_API_URL'] 'api_base_url': (dify_config.SERVICE_API_URL if dify_config.SERVICE_API_URL
else request.host_url.rstrip('/')) + '/v1' else request.host_url.rstrip('/')) + '/v1'
} }
@ -540,20 +541,20 @@ class DatasetRetrievalSettingApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
def get(self): def get(self):
vector_type = current_app.config['VECTOR_STORE'] vector_type = dify_config.VECTOR_STORE
match vector_type: match vector_type:
case VectorType.MILVUS | VectorType.RELYT | VectorType.PGVECTOR | VectorType.TIDB_VECTOR | VectorType.CHROMA | VectorType.TENCENT | VectorType.ORACLE: case VectorType.MILVUS | VectorType.RELYT | VectorType.PGVECTOR | VectorType.TIDB_VECTOR | VectorType.CHROMA | VectorType.TENCENT:
return { return {
'retrieval_method': [ 'retrieval_method': [
RetrievalMethod.SEMANTIC_SEARCH RetrievalMethod.SEMANTIC_SEARCH.value
] ]
} }
case VectorType.QDRANT | VectorType.WEAVIATE | VectorType.OPENSEARCH | VectorType.ANALYTICDB | VectorType.MYSCALE: case VectorType.QDRANT | VectorType.WEAVIATE | VectorType.OPENSEARCH | VectorType.ANALYTICDB | VectorType.MYSCALE | VectorType.ORACLE:
return { return {
'retrieval_method': [ 'retrieval_method': [
RetrievalMethod.SEMANTIC_SEARCH, RetrievalMethod.SEMANTIC_SEARCH.value,
RetrievalMethod.FULL_TEXT_SEARCH, RetrievalMethod.FULL_TEXT_SEARCH.value,
RetrievalMethod.HYBRID_SEARCH, RetrievalMethod.HYBRID_SEARCH.value,
] ]
} }
case _: case _:
@ -566,18 +567,18 @@ class DatasetRetrievalSettingMockApi(Resource):
@account_initialization_required @account_initialization_required
def get(self, vector_type): def get(self, vector_type):
match vector_type: match vector_type:
case VectorType.MILVUS | VectorType.RELYT | VectorType.PGVECTOR | VectorType.TIDB_VECTOR | VectorType.CHROMA | VectorType.TENCENT | VectorType.ORACLE: case VectorType.MILVUS | VectorType.RELYT | VectorType.PGVECTOR | VectorType.TIDB_VECTOR | VectorType.CHROMA | VectorType.TENCENT:
return { return {
'retrieval_method': [ 'retrieval_method': [
RetrievalMethod.SEMANTIC_SEARCH RetrievalMethod.SEMANTIC_SEARCH.value
] ]
} }
case VectorType.QDRANT | VectorType.WEAVIATE | VectorType.OPENSEARCH| VectorType.ANALYTICDB | VectorType.MYSCALE: case VectorType.QDRANT | VectorType.WEAVIATE | VectorType.OPENSEARCH| VectorType.ANALYTICDB | VectorType.MYSCALE | VectorType.ORACLE:
return { return {
'retrieval_method': [ 'retrieval_method': [
RetrievalMethod.SEMANTIC_SEARCH, RetrievalMethod.SEMANTIC_SEARCH.value,
RetrievalMethod.FULL_TEXT_SEARCH, RetrievalMethod.FULL_TEXT_SEARCH.value,
RetrievalMethod.HYBRID_SEARCH, RetrievalMethod.HYBRID_SEARCH.value,
] ]
} }
case _: case _:

@ -75,7 +75,7 @@ class DatasetDocumentSegmentListApi(Resource):
) )
if last_id is not None: if last_id is not None:
last_segment = DocumentSegment.query.get(str(last_id)) last_segment = db.session.get(DocumentSegment, str(last_id))
if last_segment: if last_segment:
query = query.filter( query = query.filter(
DocumentSegment.position > last_segment.position) DocumentSegment.position > last_segment.position)

@ -1,8 +1,9 @@
from flask import current_app, request from flask import request
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, marshal_with from flask_restful import Resource, marshal_with
import services import services
from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.datasets.error import ( from controllers.console.datasets.error import (
FileTooLargeError, FileTooLargeError,
@ -26,9 +27,9 @@ class FileApi(Resource):
@account_initialization_required @account_initialization_required
@marshal_with(upload_config_fields) @marshal_with(upload_config_fields)
def get(self): def get(self):
file_size_limit = current_app.config.get("UPLOAD_FILE_SIZE_LIMIT") file_size_limit = dify_config.UPLOAD_FILE_SIZE_LIMIT
batch_count_limit = current_app.config.get("UPLOAD_FILE_BATCH_LIMIT") batch_count_limit = dify_config.UPLOAD_FILE_BATCH_LIMIT
image_file_size_limit = current_app.config.get("UPLOAD_IMAGE_FILE_SIZE_LIMIT") image_file_size_limit = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT
return { return {
'file_size_limit': file_size_limit, 'file_size_limit': file_size_limit,
'batch_count_limit': batch_count_limit, 'batch_count_limit': batch_count_limit,
@ -76,7 +77,7 @@ class FileSupportTypeApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
def get(self): def get(self):
etl_type = current_app.config['ETL_TYPE'] etl_type = dify_config.ETL_TYPE
allowed_extensions = UNSTRUCTURED_ALLOWED_EXTENSIONS if etl_type == 'Unstructured' else ALLOWED_EXTENSIONS allowed_extensions = UNSTRUCTURED_ALLOWED_EXTENSIONS if etl_type == 'Unstructured' else ALLOWED_EXTENSIONS
return {'allowed_extensions': allowed_extensions} return {'allowed_extensions': allowed_extensions}

@ -78,10 +78,12 @@ class ChatTextApi(InstalledAppResource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('message_id', type=str, required=False, location='json') parser.add_argument('message_id', type=str, required=False, location='json')
parser.add_argument('voice', type=str, location='json') parser.add_argument('voice', type=str, location='json')
parser.add_argument('text', type=str, location='json')
parser.add_argument('streaming', type=bool, location='json') parser.add_argument('streaming', type=bool, location='json')
args = parser.parse_args() args = parser.parse_args()
message_id = args.get('message_id') message_id = args.get('message_id', None)
text = args.get('text', None)
if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value] if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value]
and app_model.workflow and app_model.workflow
and app_model.workflow.features_dict): and app_model.workflow.features_dict):
@ -95,7 +97,8 @@ class ChatTextApi(InstalledAppResource):
response = AudioService.transcript_tts( response = AudioService.transcript_tts(
app_model=app_model, app_model=app_model,
message_id=message_id, message_id=message_id,
voice=voice voice=voice,
text=text
) )
return response return response
except services.errors.app_model_config.AppModelConfigBrokenError: except services.errors.app_model_config.AppModelConfigBrokenError:

@ -1,7 +1,7 @@
from flask import current_app
from flask_restful import fields, marshal_with from flask_restful import fields, marshal_with
from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.app.error import AppUnavailableError from controllers.console.app.error import AppUnavailableError
from controllers.console.explore.wraps import InstalledAppResource from controllers.console.explore.wraps import InstalledAppResource
@ -78,7 +78,7 @@ class AppParameterApi(InstalledAppResource):
"transfer_methods": ["remote_url", "local_file"] "transfer_methods": ["remote_url", "local_file"]
}}), }}),
'system_parameters': { 'system_parameters': {
'image_file_size_limit': current_app.config.get('UPLOAD_IMAGE_FILE_SIZE_LIMIT') 'image_file_size_limit': dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT
} }
} }

@ -1,8 +1,9 @@
import os import os
from flask import current_app, session from flask import session
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from configs import dify_config
from libs.helper import str_len from libs.helper import str_len
from models.model import DifySetup from models.model import DifySetup
from services.account_service import TenantService from services.account_service import TenantService
@ -40,7 +41,7 @@ class InitValidateAPI(Resource):
return {'result': 'success'}, 201 return {'result': 'success'}, 201
def get_init_validate_status(): def get_init_validate_status():
if current_app.config['EDITION'] == 'SELF_HOSTED': if dify_config.EDITION == 'SELF_HOSTED':
if os.environ.get('INIT_PASSWORD'): if os.environ.get('INIT_PASSWORD'):
return session.get('is_init_validated') or DifySetup.query.first() return session.get('is_init_validated') or DifySetup.query.first()

@ -1,8 +1,9 @@
from functools import wraps from functools import wraps
from flask import current_app, request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from configs import dify_config
from libs.helper import email, get_remote_ip, str_len from libs.helper import email, get_remote_ip, str_len
from libs.password import valid_password from libs.password import valid_password
from models.model import DifySetup from models.model import DifySetup
@ -17,7 +18,7 @@ from .wraps import only_edition_self_hosted
class SetupApi(Resource): class SetupApi(Resource):
def get(self): def get(self):
if current_app.config['EDITION'] == 'SELF_HOSTED': if dify_config.EDITION == 'SELF_HOSTED':
setup_status = get_setup_status() setup_status = get_setup_status()
if setup_status: if setup_status:
return { return {
@ -77,7 +78,7 @@ def setup_required(view):
def get_setup_status(): def get_setup_status():
if current_app.config['EDITION'] == 'SELF_HOSTED': if dify_config.EDITION == 'SELF_HOSTED':
return DifySetup.query.first() return DifySetup.query.first()
else: else:
return True return True

@ -3,9 +3,10 @@ import json
import logging import logging
import requests import requests
from flask import current_app
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from configs import dify_config
from . import api from . import api
@ -15,16 +16,16 @@ class VersionApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('current_version', type=str, required=True, location='args') parser.add_argument('current_version', type=str, required=True, location='args')
args = parser.parse_args() args = parser.parse_args()
check_update_url = current_app.config['CHECK_UPDATE_URL'] check_update_url = dify_config.CHECK_UPDATE_URL
result = { result = {
'version': current_app.config['CURRENT_VERSION'], 'version': dify_config.CURRENT_VERSION,
'release_date': '', 'release_date': '',
'release_notes': '', 'release_notes': '',
'can_auto_update': False, 'can_auto_update': False,
'features': { 'features': {
'can_replace_logo': current_app.config['CAN_REPLACE_LOGO'], 'can_replace_logo': dify_config.CAN_REPLACE_LOGO,
'model_load_balancing_enabled': current_app.config['MODEL_LB_ENABLED'] 'model_load_balancing_enabled': dify_config.MODEL_LB_ENABLED
} }
} }

@ -1,10 +1,11 @@
import datetime import datetime
import pytz import pytz
from flask import current_app, 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 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.setup import setup_required from controllers.console.setup import setup_required
@ -36,7 +37,7 @@ class AccountInitApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
if current_app.config['EDITION'] == 'CLOUD': if dify_config.EDITION == 'CLOUD':
parser.add_argument('invitation_code', type=str, location='json') parser.add_argument('invitation_code', type=str, location='json')
parser.add_argument( parser.add_argument(
@ -45,7 +46,7 @@ class AccountInitApi(Resource):
required=True, location='json') required=True, location='json')
args = parser.parse_args() args = parser.parse_args()
if current_app.config['EDITION'] == 'CLOUD': if dify_config.EDITION == 'CLOUD':
if not args['invitation_code']: if not args['invitation_code']:
raise ValueError('invitation_code is required') raise ValueError('invitation_code is required')

@ -1,8 +1,8 @@
from flask import current_app
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 controllers.console import api from controllers.console import api
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
@ -48,7 +48,7 @@ class MemberInviteEmailApi(Resource):
inviter = current_user inviter = current_user
invitation_results = [] invitation_results = []
console_web_url = current_app.config.get("CONSOLE_WEB_URL") console_web_url = dify_config.CONSOLE_WEB_URL
for invitee_email in invitee_emails: for invitee_email in invitee_emails:
try: try:
token = RegisterService.invite_new_member(inviter.current_tenant, invitee_email, interface_language, role=invitee_role, inviter=inviter) token = RegisterService.invite_new_member(inviter.current_tenant, invitee_email, interface_language, role=invitee_role, inviter=inviter)
@ -117,7 +117,7 @@ class MemberUpdateRoleApi(Resource):
if not TenantAccountRole.is_valid_role(new_role): if not TenantAccountRole.is_valid_role(new_role):
return {'code': 'invalid-role', 'message': 'Invalid role'}, 400 return {'code': 'invalid-role', 'message': 'Invalid role'}, 400
member = Account.query.get(str(member_id)) member = db.session.get(Account, str(member_id))
if not member: if not member:
abort(404) abort(404)

@ -1,10 +1,11 @@
import io import io
from flask import current_app, send_file from flask import send_file
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 werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
@ -104,7 +105,7 @@ class ToolBuiltinProviderIconApi(Resource):
@setup_required @setup_required
def get(self, provider): def get(self, provider):
icon_bytes, mimetype = BuiltinToolManageService.get_builtin_tool_provider_icon(provider) icon_bytes, mimetype = BuiltinToolManageService.get_builtin_tool_provider_icon(provider)
icon_cache_max_age = current_app.config.get('TOOL_ICON_CACHE_MAX_AGE') icon_cache_max_age = dify_config.TOOL_ICON_CACHE_MAX_AGE
return send_file(io.BytesIO(icon_bytes), mimetype=mimetype, max_age=icon_cache_max_age) return send_file(io.BytesIO(icon_bytes), mimetype=mimetype, max_age=icon_cache_max_age)
class ToolApiProviderAddApi(Resource): class ToolApiProviderAddApi(Resource):

@ -1,9 +1,10 @@
import json import json
from functools import wraps from functools import wraps
from flask import abort, current_app, request from flask import abort, request
from flask_login import current_user from flask_login import current_user
from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError from controllers.console.workspace.error import AccountNotInitializedError
from services.feature_service import FeatureService from services.feature_service import FeatureService
from services.operation_service import OperationService from services.operation_service import OperationService
@ -26,7 +27,7 @@ def account_initialization_required(view):
def only_edition_cloud(view): def only_edition_cloud(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if current_app.config['EDITION'] != 'CLOUD': if dify_config.EDITION != 'CLOUD':
abort(404) abort(404)
return view(*args, **kwargs) return view(*args, **kwargs)
@ -37,7 +38,7 @@ def only_edition_cloud(view):
def only_edition_self_hosted(view): def only_edition_self_hosted(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if current_app.config['EDITION'] != 'SELF_HOSTED': if dify_config.EDITION != 'SELF_HOSTED':
abort(404) abort(404)
return view(*args, **kwargs) return view(*args, **kwargs)

@ -76,10 +76,12 @@ class TextApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('message_id', type=str, required=False, location='json') parser.add_argument('message_id', type=str, required=False, location='json')
parser.add_argument('voice', type=str, location='json') parser.add_argument('voice', type=str, location='json')
parser.add_argument('text', type=str, location='json')
parser.add_argument('streaming', type=bool, location='json') parser.add_argument('streaming', type=bool, location='json')
args = parser.parse_args() args = parser.parse_args()
message_id = args.get('message_id') message_id = args.get('message_id', None)
text = args.get('text', None)
if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value] if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value]
and app_model.workflow and app_model.workflow
and app_model.workflow.features_dict): and app_model.workflow.features_dict):
@ -87,15 +89,15 @@ class TextApi(Resource):
voice = args.get('voice') if args.get('voice') else text_to_speech.get('voice') voice = args.get('voice') if args.get('voice') else text_to_speech.get('voice')
else: else:
try: try:
voice = args.get('voice') if args.get('voice') else app_model.app_model_config.text_to_speech_dict.get( voice = args.get('voice') if args.get('voice') else app_model.app_model_config.text_to_speech_dict.get('voice')
'voice')
except Exception: except Exception:
voice = None voice = None
response = AudioService.transcript_tts( response = AudioService.transcript_tts(
app_model=app_model, app_model=app_model,
message_id=message_id, message_id=message_id,
end_user=end_user.external_user_id, end_user=end_user.external_user_id,
voice=voice voice=voice,
text=text
) )
return response return response

@ -1,6 +1,6 @@
import logging import logging
from flask_restful import Resource, reqparse from flask_restful import Resource, fields, marshal_with, reqparse
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
from controllers.service_api import api from controllers.service_api import api
@ -21,14 +21,43 @@ from core.errors.error import (
QuotaExceededError, QuotaExceededError,
) )
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db
from libs import helper from libs import helper
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from models.workflow import WorkflowRun
from services.app_generate_service import AppGenerateService from services.app_generate_service import AppGenerateService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WorkflowRunApi(Resource): class WorkflowRunApi(Resource):
workflow_run_fields = {
'id': fields.String,
'workflow_id': fields.String,
'status': fields.String,
'inputs': fields.Raw,
'outputs': fields.Raw,
'error': fields.String,
'total_steps': fields.Integer,
'total_tokens': fields.Integer,
'created_at': fields.DateTime,
'finished_at': fields.DateTime,
'elapsed_time': fields.Float,
}
@validate_app_token
@marshal_with(workflow_run_fields)
def get(self, app_model: App, workflow_id: str):
"""
Get a workflow task running detail
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_id).first()
return workflow_run
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
""" """
@ -88,5 +117,5 @@ class WorkflowTaskStopApi(Resource):
} }
api.add_resource(WorkflowRunApi, '/workflows/run') api.add_resource(WorkflowRunApi, '/workflows/run/<string:workflow_id>', '/workflows/run')
api.add_resource(WorkflowTaskStopApi, '/workflows/tasks/<string:task_id>/stop') api.add_resource(WorkflowTaskStopApi, '/workflows/tasks/<string:task_id>/stop')

@ -74,10 +74,12 @@ class TextApi(WebApiResource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('message_id', type=str, required=False, location='json') parser.add_argument('message_id', type=str, required=False, location='json')
parser.add_argument('voice', type=str, location='json') parser.add_argument('voice', type=str, location='json')
parser.add_argument('text', type=str, location='json')
parser.add_argument('streaming', type=bool, location='json') parser.add_argument('streaming', type=bool, location='json')
args = parser.parse_args() args = parser.parse_args()
message_id = args.get('message_id') message_id = args.get('message_id', None)
text = args.get('text', None)
if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value] if (app_model.mode in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value]
and app_model.workflow and app_model.workflow
and app_model.workflow.features_dict): and app_model.workflow.features_dict):
@ -94,7 +96,8 @@ class TextApi(WebApiResource):
app_model=app_model, app_model=app_model,
message_id=message_id, message_id=message_id,
end_user=end_user.external_user_id, end_user=end_user.external_user_id,
voice=voice voice=voice,
text=text
) )
return response return response

@ -342,10 +342,14 @@ class FunctionCallAgentRunner(BaseAgentRunner):
""" """
tool_calls = [] tool_calls = []
for prompt_message in llm_result_chunk.delta.message.tool_calls: for prompt_message in llm_result_chunk.delta.message.tool_calls:
args = {}
if prompt_message.function.arguments != '':
args = json.loads(prompt_message.function.arguments)
tool_calls.append(( tool_calls.append((
prompt_message.id, prompt_message.id,
prompt_message.function.name, prompt_message.function.name,
json.loads(prompt_message.function.arguments), args,
)) ))
return tool_calls return tool_calls
@ -359,10 +363,14 @@ class FunctionCallAgentRunner(BaseAgentRunner):
""" """
tool_calls = [] tool_calls = []
for prompt_message in llm_result.message.tool_calls: for prompt_message in llm_result.message.tool_calls:
args = {}
if prompt_message.function.arguments != '':
args = json.loads(prompt_message.function.arguments)
tool_calls.append(( tool_calls.append((
prompt_message.id, prompt_message.id,
prompt_message.function.name, prompt_message.function.name,
json.loads(prompt_message.function.arguments), args,
)) ))
return tool_calls return tool_calls

@ -1,6 +1,7 @@
from typing import Optional, Union from collections.abc import Mapping
from typing import Any
from core.app.app_config.entities import AppAdditionalFeatures, EasyUIBasedAppModelConfigFrom from core.app.app_config.entities import AppAdditionalFeatures
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.app_config.features.more_like_this.manager import MoreLikeThisConfigManager from core.app.app_config.features.more_like_this.manager import MoreLikeThisConfigManager
from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager from core.app.app_config.features.opening_statement.manager import OpeningStatementConfigManager
@ -10,37 +11,19 @@ from core.app.app_config.features.suggested_questions_after_answer.manager impor
SuggestedQuestionsAfterAnswerConfigManager, SuggestedQuestionsAfterAnswerConfigManager,
) )
from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager
from models.model import AppMode, AppModelConfig from models.model import AppMode
class BaseAppConfigManager: class BaseAppConfigManager:
@classmethod
def convert_to_config_dict(cls, config_from: EasyUIBasedAppModelConfigFrom,
app_model_config: Union[AppModelConfig, dict],
config_dict: Optional[dict] = None) -> dict:
"""
Convert app model config to config dict
:param config_from: app model config from
:param app_model_config: app model config
:param config_dict: app model config dict
:return:
"""
if config_from != EasyUIBasedAppModelConfigFrom.ARGS:
app_model_config_dict = app_model_config.to_dict()
config_dict = app_model_config_dict.copy()
return config_dict
@classmethod @classmethod
def convert_features(cls, config_dict: dict, app_mode: AppMode) -> AppAdditionalFeatures: def convert_features(cls, config_dict: Mapping[str, Any], app_mode: AppMode) -> AppAdditionalFeatures:
""" """
Convert app config to app model config Convert app config to app model config
:param config_dict: app config :param config_dict: app config
:param app_mode: app mode :param app_mode: app mode
""" """
config_dict = config_dict.copy() config_dict = dict(config_dict.items())
additional_features = AppAdditionalFeatures() additional_features = AppAdditionalFeatures()
additional_features.show_retrieve_source = RetrievalResourceConfigManager.convert( additional_features.show_retrieve_source = RetrievalResourceConfigManager.convert(

@ -62,7 +62,12 @@ class DatasetConfigManager:
return None return None
# dataset configs # dataset configs
dataset_configs = config.get('dataset_configs', {'retrieval_model': 'single'}) if 'dataset_configs' in config and config.get('dataset_configs'):
dataset_configs = config.get('dataset_configs')
else:
dataset_configs = {
'retrieval_model': 'multiple'
}
query_variable = config.get('dataset_query_variable') query_variable = config.get('dataset_query_variable')
if dataset_configs['retrieval_model'] == 'single': if dataset_configs['retrieval_model'] == 'single':
@ -83,9 +88,10 @@ class DatasetConfigManager:
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of(
dataset_configs['retrieval_model'] dataset_configs['retrieval_model']
), ),
top_k=dataset_configs.get('top_k'), top_k=dataset_configs.get('top_k', 4),
score_threshold=dataset_configs.get('score_threshold'), score_threshold=dataset_configs.get('score_threshold'),
reranking_model=dataset_configs.get('reranking_model') reranking_model=dataset_configs.get('reranking_model'),
weights=dataset_configs.get('weights')
) )
) )
@ -114,12 +120,6 @@ class DatasetConfigManager:
if not isinstance(config["dataset_configs"], dict): if not isinstance(config["dataset_configs"], dict):
raise ValueError("dataset_configs must be of object type") raise ValueError("dataset_configs must be of object type")
if config["dataset_configs"]['retrieval_model'] == 'multiple':
if not config["dataset_configs"]['reranking_model']:
raise ValueError("reranking_model has not been set")
if not isinstance(config["dataset_configs"]['reranking_model'], dict):
raise ValueError("reranking_model must be of object type")
if not isinstance(config["dataset_configs"], dict): if not isinstance(config["dataset_configs"], dict):
raise ValueError("dataset_configs must be of object type") raise ValueError("dataset_configs must be of object type")

@ -159,7 +159,11 @@ class DatasetRetrieveConfigEntity(BaseModel):
retrieve_strategy: RetrieveStrategy retrieve_strategy: RetrieveStrategy
top_k: Optional[int] = None top_k: Optional[int] = None
score_threshold: Optional[float] = None score_threshold: Optional[float] = None
rerank_mode: Optional[str] = 'reranking_model'
reranking_model: Optional[dict] = None reranking_model: Optional[dict] = None
weights: Optional[dict] = None
class DatasetEntity(BaseModel): class DatasetEntity(BaseModel):

@ -1,11 +1,12 @@
from typing import Optional from collections.abc import Mapping
from typing import Any, Optional
from core.app.app_config.entities import FileExtraConfig from core.app.app_config.entities import FileExtraConfig
class FileUploadConfigManager: class FileUploadConfigManager:
@classmethod @classmethod
def convert(cls, config: dict, is_vision: bool = True) -> Optional[FileExtraConfig]: def convert(cls, config: Mapping[str, Any], is_vision: bool = True) -> Optional[FileExtraConfig]:
""" """
Convert model config to model config Convert model config to model config

@ -3,13 +3,13 @@ from core.app.app_config.entities import TextToSpeechEntity
class TextToSpeechConfigManager: class TextToSpeechConfigManager:
@classmethod @classmethod
def convert(cls, config: dict) -> bool: def convert(cls, config: dict):
""" """
Convert model config to model config Convert model config to model config
:param config: model config args :param config: model config args
""" """
text_to_speech = False text_to_speech = None
text_to_speech_dict = config.get('text_to_speech') text_to_speech_dict = config.get('text_to_speech')
if text_to_speech_dict: if text_to_speech_dict:
if text_to_speech_dict.get('enabled'): if text_to_speech_dict.get('enabled'):

@ -1,3 +1,4 @@
import contextvars
import logging import logging
import os import os
import threading import threading
@ -8,6 +9,7 @@ from typing import Union
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
import contexts
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.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner
@ -107,6 +109,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
extras=extras, extras=extras,
trace_manager=trace_manager trace_manager=trace_manager
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
return self._generate( return self._generate(
app_model=app_model, app_model=app_model,
@ -173,6 +176,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
inputs=args['inputs'] inputs=args['inputs']
) )
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
return self._generate( return self._generate(
app_model=app_model, app_model=app_model,
@ -225,6 +229,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
'queue_manager': queue_manager, 'queue_manager': queue_manager,
'conversation_id': conversation.id, 'conversation_id': conversation.id,
'message_id': message.id, 'message_id': message.id,
'user': user,
'context': contextvars.copy_context()
}) })
worker_thread.start() worker_thread.start()
@ -249,7 +255,9 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
application_generate_entity: AdvancedChatAppGenerateEntity, application_generate_entity: AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager, queue_manager: AppQueueManager,
conversation_id: str, conversation_id: str,
message_id: str) -> None: message_id: str,
user: Account,
context: contextvars.Context) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
@ -259,6 +267,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param message_id: message ID :param message_id: message ID
:return: :return:
""" """
for var, val in context.items():
var.set(val)
with flask_app.app_context(): with flask_app.app_context():
try: try:
runner = AdvancedChatAppRunner() runner = AdvancedChatAppRunner()

@ -1,7 +1,8 @@
import logging import logging
import os import os
import time import time
from typing import Optional, cast from collections.abc import Mapping
from typing import Any, Optional, cast
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig
from core.app.apps.advanced_chat.workflow_event_trigger_callback import WorkflowEventTriggerCallback from core.app.apps.advanced_chat.workflow_event_trigger_callback import WorkflowEventTriggerCallback
@ -14,6 +15,7 @@ from core.app.entities.app_invoke_entities import (
) )
from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueStopEvent, QueueTextChunkEvent from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueStopEvent, QueueTextChunkEvent
from core.moderation.base import ModerationException from core.moderation.base import ModerationException
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.node_entities import SystemVariable from core.workflow.entities.node_entities import SystemVariable
from core.workflow.nodes.base_node import UserFrom from core.workflow.nodes.base_node import UserFrom
from core.workflow.workflow_engine_manager import WorkflowEngineManager from core.workflow.workflow_engine_manager import WorkflowEngineManager
@ -87,7 +89,7 @@ class AdvancedChatAppRunner(AppRunner):
db.session.close() db.session.close()
workflow_callbacks = [WorkflowEventTriggerCallback( workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
queue_manager=queue_manager, queue_manager=queue_manager,
workflow=workflow workflow=workflow
)] )]
@ -161,7 +163,7 @@ class AdvancedChatAppRunner(AppRunner):
self, queue_manager: AppQueueManager, self, queue_manager: AppQueueManager,
app_record: App, app_record: App,
app_generate_entity: AdvancedChatAppGenerateEntity, app_generate_entity: AdvancedChatAppGenerateEntity,
inputs: dict, inputs: Mapping[str, Any],
query: str, query: str,
message_id: str message_id: str
) -> bool: ) -> bool:

@ -1,9 +1,11 @@
import json import json
from collections.abc import Generator from collections.abc import Generator
from typing import cast from typing import Any, cast
from core.app.apps.base_app_generate_response_converter import AppGenerateResponseConverter from core.app.apps.base_app_generate_response_converter import AppGenerateResponseConverter
from core.app.entities.task_entities import ( from core.app.entities.task_entities import (
AppBlockingResponse,
AppStreamResponse,
ChatbotAppBlockingResponse, ChatbotAppBlockingResponse,
ChatbotAppStreamResponse, ChatbotAppStreamResponse,
ErrorStreamResponse, ErrorStreamResponse,
@ -18,12 +20,13 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
_blocking_response_type = ChatbotAppBlockingResponse _blocking_response_type = ChatbotAppBlockingResponse
@classmethod @classmethod
def convert_blocking_full_response(cls, blocking_response: ChatbotAppBlockingResponse) -> dict: def convert_blocking_full_response(cls, blocking_response: AppBlockingResponse) -> dict[str, Any]:
""" """
Convert blocking full response. Convert blocking full response.
:param blocking_response: blocking response :param blocking_response: blocking response
:return: :return:
""" """
blocking_response = cast(ChatbotAppBlockingResponse, blocking_response)
response = { response = {
'event': 'message', 'event': 'message',
'task_id': blocking_response.task_id, 'task_id': blocking_response.task_id,
@ -39,7 +42,7 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
return response return response
@classmethod @classmethod
def convert_blocking_simple_response(cls, blocking_response: ChatbotAppBlockingResponse) -> dict: def convert_blocking_simple_response(cls, blocking_response: AppBlockingResponse) -> dict[str, Any]:
""" """
Convert blocking simple response. Convert blocking simple response.
:param blocking_response: blocking response :param blocking_response: blocking response
@ -53,8 +56,7 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
return response return response
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_full_response(cls, stream_response: Generator[AppStreamResponse, None, None]) -> Generator[str, Any, None]:
-> Generator[str, None, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -83,8 +85,7 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_simple_response(cls, stream_response: Generator[AppStreamResponse, None, None]) -> Generator[str, Any, None]:
-> Generator[str, None, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response

@ -118,7 +118,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
self._stream_generate_routes = self._get_stream_generate_routes() self._stream_generate_routes = self._get_stream_generate_routes()
self._conversation_name_generate_thread = None self._conversation_name_generate_thread = None
def process(self) -> Union[ChatbotAppBlockingResponse, Generator[ChatbotAppStreamResponse, None, None]]: def process(self):
""" """
Process generate task pipeline. Process generate task pipeline.
:return: :return:
@ -141,8 +141,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
else: else:
return self._to_blocking_response(generator) return self._to_blocking_response(generator)
def _to_blocking_response(self, generator: Generator[StreamResponse, None, None]) \ def _to_blocking_response(self, generator: Generator[StreamResponse, None, None]) -> ChatbotAppBlockingResponse:
-> ChatbotAppBlockingResponse:
""" """
Process blocking response. Process blocking response.
:return: :return:
@ -172,8 +171,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
raise Exception('Queue listening stopped unexpectedly.') raise Exception('Queue listening stopped unexpectedly.')
def _to_stream_response(self, generator: Generator[StreamResponse, None, None]) \ def _to_stream_response(self, generator: Generator[StreamResponse, None, None]) -> Generator[ChatbotAppStreamResponse, Any, None]:
-> Generator[ChatbotAppStreamResponse, None, None]:
""" """
To stream response. To stream response.
:return: :return:

@ -14,13 +14,13 @@ from core.app.entities.queue_entities import (
QueueWorkflowStartedEvent, QueueWorkflowStartedEvent,
QueueWorkflowSucceededEvent, QueueWorkflowSucceededEvent,
) )
from core.workflow.callbacks.base_workflow_callback import BaseWorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType from core.workflow.entities.node_entities import NodeType
from models.workflow import Workflow from models.workflow import Workflow
class WorkflowEventTriggerCallback(BaseWorkflowCallback): class WorkflowEventTriggerCallback(WorkflowCallback):
def __init__(self, queue_manager: AppQueueManager, workflow: Workflow): def __init__(self, queue_manager: AppQueueManager, workflow: Workflow):
self._queue_manager = queue_manager self._queue_manager = queue_manager

@ -1,7 +1,7 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Generator from collections.abc import Generator
from typing import Union from typing import Any, Union
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.task_entities import AppBlockingResponse, AppStreamResponse from core.app.entities.task_entities import AppBlockingResponse, AppStreamResponse
@ -15,44 +15,41 @@ class AppGenerateResponseConverter(ABC):
@classmethod @classmethod
def convert(cls, response: Union[ def convert(cls, response: Union[
AppBlockingResponse, AppBlockingResponse,
Generator[AppStreamResponse, None, None] Generator[AppStreamResponse, Any, None]
], invoke_from: InvokeFrom) -> Union[ ], invoke_from: InvokeFrom):
dict,
Generator[str, None, None]
]:
if invoke_from in [InvokeFrom.DEBUGGER, InvokeFrom.SERVICE_API]: if invoke_from in [InvokeFrom.DEBUGGER, InvokeFrom.SERVICE_API]:
if isinstance(response, cls._blocking_response_type): if isinstance(response, AppBlockingResponse):
return cls.convert_blocking_full_response(response) return cls.convert_blocking_full_response(response)
else: else:
def _generate(): def _generate_full_response() -> Generator[str, Any, None]:
for chunk in cls.convert_stream_full_response(response): for chunk in cls.convert_stream_full_response(response):
if chunk == 'ping': if chunk == 'ping':
yield f'event: {chunk}\n\n' yield f'event: {chunk}\n\n'
else: else:
yield f'data: {chunk}\n\n' yield f'data: {chunk}\n\n'
return _generate() return _generate_full_response()
else: else:
if isinstance(response, cls._blocking_response_type): if isinstance(response, AppBlockingResponse):
return cls.convert_blocking_simple_response(response) return cls.convert_blocking_simple_response(response)
else: else:
def _generate(): def _generate_simple_response() -> Generator[str, Any, None]:
for chunk in cls.convert_stream_simple_response(response): for chunk in cls.convert_stream_simple_response(response):
if chunk == 'ping': if chunk == 'ping':
yield f'event: {chunk}\n\n' yield f'event: {chunk}\n\n'
else: else:
yield f'data: {chunk}\n\n' yield f'data: {chunk}\n\n'
return _generate() return _generate_simple_response()
@classmethod @classmethod
@abstractmethod @abstractmethod
def convert_blocking_full_response(cls, blocking_response: AppBlockingResponse) -> dict: def convert_blocking_full_response(cls, blocking_response: AppBlockingResponse) -> dict[str, Any]:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@abstractmethod @abstractmethod
def convert_blocking_simple_response(cls, blocking_response: AppBlockingResponse) -> dict: def convert_blocking_simple_response(cls, blocking_response: AppBlockingResponse) -> dict[str, Any]:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@ -68,7 +65,7 @@ class AppGenerateResponseConverter(ABC):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def _get_simple_metadata(cls, metadata: dict) -> dict: def _get_simple_metadata(cls, metadata: dict[str, Any]):
""" """
Get simple metadata. Get simple metadata.
:param metadata: metadata :param metadata: metadata

@ -5,9 +5,9 @@ from collections.abc import Generator
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from flask import current_app
from sqlalchemy.orm import DeclarativeMeta from sqlalchemy.orm import DeclarativeMeta
from configs import dify_config
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import ( from core.app.entities.queue_entities import (
AppQueueEvent, AppQueueEvent,
@ -48,7 +48,7 @@ class AppQueueManager:
:return: :return:
""" """
# wait for APP_MAX_EXECUTION_TIME seconds to stop listen # wait for APP_MAX_EXECUTION_TIME seconds to stop listen
listen_timeout = current_app.config.get("APP_MAX_EXECUTION_TIME") listen_timeout = dify_config.APP_MAX_EXECUTION_TIME
start_time = time.time() start_time = time.time()
last_ping_time = 0 last_ping_time = 0
while True: while True:

@ -1,3 +1,4 @@
import contextvars
import logging import logging
import os import os
import threading import threading
@ -8,6 +9,7 @@ from typing import Union
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
import contexts
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.base_app_generator import BaseAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom
@ -38,7 +40,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
invoke_from: InvokeFrom, invoke_from: InvokeFrom,
stream: bool = True, stream: bool = True,
call_depth: int = 0, call_depth: int = 0,
) -> Union[dict, Generator[dict, None, None]]: ):
""" """
Generate App response. Generate App response.
@ -86,6 +88,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
call_depth=call_depth, call_depth=call_depth,
trace_manager=trace_manager trace_manager=trace_manager
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
return self._generate( return self._generate(
app_model=app_model, app_model=app_model,
@ -126,7 +129,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(target=self._generate_worker, kwargs={
'flask_app': current_app._get_current_object(), 'flask_app': current_app._get_current_object(),
'application_generate_entity': application_generate_entity, 'application_generate_entity': application_generate_entity,
'queue_manager': queue_manager 'queue_manager': queue_manager,
'context': contextvars.copy_context()
}) })
worker_thread.start() worker_thread.start()
@ -150,8 +154,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
node_id: str, node_id: str,
user: Account, user: Account,
args: dict, args: dict,
stream: bool = True) \ stream: bool = True):
-> Union[dict, Generator[dict, None, None]]:
""" """
Generate App response. Generate App response.
@ -193,6 +196,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
inputs=args['inputs'] inputs=args['inputs']
) )
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
return self._generate( return self._generate(
app_model=app_model, app_model=app_model,
@ -205,7 +209,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
def _generate_worker(self, flask_app: Flask, def _generate_worker(self, flask_app: Flask,
application_generate_entity: WorkflowAppGenerateEntity, application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager) -> None: queue_manager: AppQueueManager,
context: contextvars.Context) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
@ -213,6 +218,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param queue_manager: queue manager :param queue_manager: queue manager
:return: :return:
""" """
for var, val in context.items():
var.set(val)
with flask_app.app_context(): with flask_app.app_context():
try: try:
# workflow app # workflow app

@ -10,6 +10,7 @@ from core.app.entities.app_invoke_entities import (
InvokeFrom, InvokeFrom,
WorkflowAppGenerateEntity, WorkflowAppGenerateEntity,
) )
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.node_entities import SystemVariable from core.workflow.entities.node_entities import SystemVariable
from core.workflow.nodes.base_node import UserFrom from core.workflow.nodes.base_node import UserFrom
from core.workflow.workflow_engine_manager import WorkflowEngineManager from core.workflow.workflow_engine_manager import WorkflowEngineManager
@ -57,7 +58,7 @@ class WorkflowAppRunner:
db.session.close() db.session.close()
workflow_callbacks = [WorkflowEventTriggerCallback( workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
queue_manager=queue_manager, queue_manager=queue_manager,
workflow=workflow workflow=workflow
)] )]

@ -14,13 +14,13 @@ from core.app.entities.queue_entities import (
QueueWorkflowStartedEvent, QueueWorkflowStartedEvent,
QueueWorkflowSucceededEvent, QueueWorkflowSucceededEvent,
) )
from core.workflow.callbacks.base_workflow_callback import BaseWorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType from core.workflow.entities.node_entities import NodeType
from models.workflow import Workflow from models.workflow import Workflow
class WorkflowEventTriggerCallback(BaseWorkflowCallback): class WorkflowEventTriggerCallback(WorkflowCallback):
def __init__(self, queue_manager: AppQueueManager, workflow: Workflow): def __init__(self, queue_manager: AppQueueManager, workflow: Workflow):
self._queue_manager = queue_manager self._queue_manager = queue_manager

@ -2,7 +2,7 @@ from typing import Optional
from core.app.entities.queue_entities import AppQueueEvent from core.app.entities.queue_entities import AppQueueEvent
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.workflow.callbacks.base_workflow_callback import BaseWorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType from core.workflow.entities.node_entities import NodeType
@ -15,7 +15,7 @@ _TEXT_COLOR_MAPPING = {
} }
class WorkflowLoggingCallback(BaseWorkflowCallback): class WorkflowLoggingCallback(WorkflowCallback):
def __init__(self) -> None: def __init__(self) -> None:
self.current_node_id = None self.current_node_id = None

@ -1,3 +1,4 @@
from collections.abc import Mapping
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
@ -76,7 +77,7 @@ class AppGenerateEntity(BaseModel):
# app config # app config
app_config: AppConfig app_config: AppConfig
inputs: dict[str, Any] inputs: Mapping[str, Any]
files: list[FileVar] = [] files: list[FileVar] = []
user_id: str user_id: str
@ -140,7 +141,7 @@ class AdvancedChatAppGenerateEntity(AppGenerateEntity):
app_config: WorkflowUIBasedAppConfig app_config: WorkflowUIBasedAppConfig
conversation_id: Optional[str] = None conversation_id: Optional[str] = None
query: Optional[str] = None query: str
class SingleIterationRunEntity(BaseModel): class SingleIterationRunEntity(BaseModel):
""" """

@ -0,0 +1,53 @@
from .segment_group import SegmentGroup
from .segments import (
ArrayAnySegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
from .variables import (
ArrayAnyVariable,
ArrayFileVariable,
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileVariable,
FloatVariable,
IntegerVariable,
NoneVariable,
ObjectVariable,
SecretVariable,
StringVariable,
Variable,
)
__all__ = [
'IntegerVariable',
'FloatVariable',
'ObjectVariable',
'SecretVariable',
'FileVariable',
'StringVariable',
'ArrayAnyVariable',
'Variable',
'SegmentType',
'SegmentGroup',
'Segment',
'NoneSegment',
'NoneVariable',
'IntegerSegment',
'FloatSegment',
'ObjectSegment',
'ArrayAnySegment',
'FileSegment',
'StringSegment',
'ArrayStringVariable',
'ArrayNumberVariable',
'ArrayObjectVariable',
'ArrayFileVariable',
]

@ -0,0 +1,86 @@
from collections.abc import Mapping
from typing import Any
from core.file.file_obj import FileVar
from .segments import (
ArrayAnySegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
from .variables import (
ArrayFileVariable,
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileVariable,
FloatVariable,
IntegerVariable,
ObjectVariable,
SecretVariable,
StringVariable,
Variable,
)
def build_variable_from_mapping(m: Mapping[str, Any], /) -> Variable:
if (value_type := m.get('value_type')) is None:
raise ValueError('missing value type')
if not m.get('name'):
raise ValueError('missing name')
if (value := m.get('value')) is None:
raise ValueError('missing value')
match value_type:
case SegmentType.STRING:
return StringVariable.model_validate(m)
case SegmentType.SECRET:
return SecretVariable.model_validate(m)
case SegmentType.NUMBER if isinstance(value, int):
return IntegerVariable.model_validate(m)
case SegmentType.NUMBER if isinstance(value, float):
return FloatVariable.model_validate(m)
case SegmentType.NUMBER if not isinstance(value, float | int):
raise ValueError(f'invalid number value {value}')
case SegmentType.FILE:
return FileVariable.model_validate(m)
case SegmentType.OBJECT if isinstance(value, dict):
return ObjectVariable.model_validate(
{**m, 'value': {k: build_variable_from_mapping(v) for k, v in value.items()}}
)
case SegmentType.ARRAY_STRING if isinstance(value, list):
return ArrayStringVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_NUMBER if isinstance(value, list):
return ArrayNumberVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_OBJECT if isinstance(value, list):
return ArrayObjectVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
case SegmentType.ARRAY_FILE if isinstance(value, list):
return ArrayFileVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
raise ValueError(f'not supported value type {value_type}')
def build_segment(value: Any, /) -> Segment:
if value is None:
return NoneSegment()
if isinstance(value, str):
return StringSegment(value=value)
if isinstance(value, int):
return IntegerSegment(value=value)
if isinstance(value, float):
return FloatSegment(value=value)
if isinstance(value, dict):
# TODO: Limit the depth of the object
obj = {k: build_segment(v) for k, v in value.items()}
return ObjectSegment(value=obj)
if isinstance(value, list):
# TODO: Limit the depth of the array
elements = [build_segment(v) for v in value]
return ArrayAnySegment(value=elements)
if isinstance(value, FileVar):
return FileSegment(value=value)
raise ValueError(f'not supported value {value}')

@ -0,0 +1,18 @@
import re
from core.workflow.entities.variable_pool import VariablePool
from . import SegmentGroup, factory
VARIABLE_PATTERN = re.compile(r'\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}')
def convert_template(*, template: str, variable_pool: VariablePool):
parts = re.split(VARIABLE_PATTERN, template)
segments = []
for part in filter(lambda x: x, parts):
if '.' in part and (value := variable_pool.get(part.split('.'))):
segments.append(value)
else:
segments.append(factory.build_segment(part))
return SegmentGroup(value=segments)

@ -0,0 +1,22 @@
from .segments import Segment
from .types import SegmentType
class SegmentGroup(Segment):
value_type: SegmentType = SegmentType.GROUP
value: list[Segment]
@property
def text(self):
return ''.join([segment.text for segment in self.value])
@property
def log(self):
return ''.join([segment.log for segment in self.value])
@property
def markdown(self):
return ''.join([segment.markdown for segment in self.value])
def to_object(self):
return [segment.to_object() for segment in self.value]

@ -0,0 +1,140 @@
import json
from collections.abc import Mapping, Sequence
from typing import Any
from pydantic import BaseModel, ConfigDict, field_validator
from core.file.file_obj import FileVar
from .types import SegmentType
class Segment(BaseModel):
model_config = ConfigDict(frozen=True)
value_type: SegmentType
value: Any
@field_validator('value_type')
def validate_value_type(cls, value):
"""
This validator checks if the provided value is equal to the default value of the 'value_type' field.
If the value is different, a ValueError is raised.
"""
if value != cls.model_fields['value_type'].default:
raise ValueError("Cannot modify 'value_type'")
return value
@property
def text(self) -> str:
return str(self.value)
@property
def log(self) -> str:
return str(self.value)
@property
def markdown(self) -> str:
return str(self.value)
def to_object(self) -> Any:
return self.value
class NoneSegment(Segment):
value_type: SegmentType = SegmentType.NONE
value: None = None
@property
def text(self) -> str:
return 'null'
@property
def log(self) -> str:
return 'null'
@property
def markdown(self) -> str:
return 'null'
class StringSegment(Segment):
value_type: SegmentType = SegmentType.STRING
value: str
class FloatSegment(Segment):
value_type: SegmentType = SegmentType.NUMBER
value: float
class IntegerSegment(Segment):
value_type: SegmentType = SegmentType.NUMBER
value: int
class FileSegment(Segment):
value_type: SegmentType = SegmentType.FILE
# TODO: embed FileVar in this model.
value: FileVar
@property
def markdown(self) -> str:
return self.value.to_markdown()
class ObjectSegment(Segment):
value_type: SegmentType = SegmentType.OBJECT
value: Mapping[str, Segment]
@property
def text(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False)
@property
def log(self) -> str:
# TODO: Process variables.
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
@property
def markdown(self) -> str:
# TODO: Use markdown code block
return json.dumps(self.model_dump()['value'], ensure_ascii=False, indent=2)
def to_object(self):
return {k: v.to_object() for k, v in self.value.items()}
class ArraySegment(Segment):
@property
def markdown(self) -> str:
return '\n'.join(['- ' + item.markdown for item in self.value])
def to_object(self):
return [v.to_object() for v in self.value]
class ArrayAnySegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Segment]
class ArrayStringSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_STRING
value: Sequence[StringSegment]
class ArrayNumberSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_NUMBER
value: Sequence[FloatSegment | IntegerSegment]
class ArrayObjectSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_OBJECT
value: Sequence[ObjectSegment]
class ArrayFileSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_FILE
value: Sequence[FileSegment]

@ -0,0 +1,17 @@
from enum import Enum
class SegmentType(str, Enum):
NONE = 'none'
NUMBER = 'number'
STRING = 'string'
SECRET = 'secret'
ARRAY_ANY = 'array[any]'
ARRAY_STRING = 'array[string]'
ARRAY_NUMBER = 'array[number]'
ARRAY_OBJECT = 'array[object]'
ARRAY_FILE = 'array[file]'
OBJECT = 'object'
FILE = 'file'
GROUP = 'group'

@ -0,0 +1,85 @@
from pydantic import Field
from core.helper import encrypter
from .segments import (
ArrayAnySegment,
ArrayFileSegment,
ArrayNumberSegment,
ArrayObjectSegment,
ArrayStringSegment,
FileSegment,
FloatSegment,
IntegerSegment,
NoneSegment,
ObjectSegment,
Segment,
StringSegment,
)
from .types import SegmentType
class Variable(Segment):
"""
A variable is a segment that has a name.
"""
id: str = Field(
default='',
description="Unique identity for variable. It's only used by environment variables now.",
)
name: str
description: str = Field(default='', description='Description of the variable.')
class StringVariable(StringSegment, Variable):
pass
class FloatVariable(FloatSegment, Variable):
pass
class IntegerVariable(IntegerSegment, Variable):
pass
class FileVariable(FileSegment, Variable):
pass
class ObjectVariable(ObjectSegment, Variable):
pass
class ArrayAnyVariable(ArrayAnySegment, Variable):
pass
class ArrayStringVariable(ArrayStringSegment, Variable):
pass
class ArrayNumberVariable(ArrayNumberSegment, Variable):
pass
class ArrayObjectVariable(ArrayObjectSegment, Variable):
pass
class ArrayFileVariable(ArrayFileSegment, Variable):
pass
class SecretVariable(StringVariable):
value_type: SegmentType = SegmentType.SECRET
@property
def log(self) -> str:
return encrypter.obfuscated_token(self.value)
class NoneVariable(NoneSegment, Variable):
value_type: SegmentType = SegmentType.NONE
value: None = None

@ -1,9 +1,11 @@
import os import os
from collections.abc import Mapping, Sequence
from typing import Any, Optional, TextIO, Union from typing import Any, Optional, TextIO, Union
from pydantic import BaseModel from pydantic import BaseModel
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName from core.ops.ops_trace_manager import TraceQueueManager, TraceTask, TraceTaskName
from core.tools.entities.tool_entities import ToolInvokeMessage
_TEXT_COLOR_MAPPING = { _TEXT_COLOR_MAPPING = {
"blue": "36;1", "blue": "36;1",
@ -43,7 +45,7 @@ class DifyAgentCallbackHandler(BaseModel):
def on_tool_start( def on_tool_start(
self, self,
tool_name: str, tool_name: str,
tool_inputs: dict[str, Any], tool_inputs: Mapping[str, Any],
) -> None: ) -> None:
"""Do nothing.""" """Do nothing."""
print_text("\n[on_tool_start] ToolCall:" + tool_name + "\n" + str(tool_inputs) + "\n", color=self.color) print_text("\n[on_tool_start] ToolCall:" + tool_name + "\n" + str(tool_inputs) + "\n", color=self.color)
@ -51,8 +53,8 @@ class DifyAgentCallbackHandler(BaseModel):
def on_tool_end( def on_tool_end(
self, self,
tool_name: str, tool_name: str,
tool_inputs: dict[str, Any], tool_inputs: Mapping[str, Any],
tool_outputs: str, tool_outputs: Sequence[ToolInvokeMessage],
message_id: Optional[str] = None, message_id: Optional[str] = None,
timer: Optional[Any] = None, timer: Optional[Any] = None,
trace_manager: Optional[TraceQueueManager] = None trace_manager: Optional[TraceQueueManager] = None

@ -1,4 +1,5 @@
from typing import Union from collections.abc import Mapping, Sequence
from typing import Any, Union
import requests import requests
@ -16,7 +17,7 @@ class MessageFileParser:
self.tenant_id = tenant_id self.tenant_id = tenant_id
self.app_id = app_id self.app_id = app_id
def validate_and_transform_files_arg(self, files: list[dict], file_extra_config: FileExtraConfig, def validate_and_transform_files_arg(self, files: Sequence[Mapping[str, Any]], file_extra_config: FileExtraConfig,
user: Union[Account, EndUser]) -> list[FileVar]: user: Union[Account, EndUser]) -> list[FileVar]:
""" """
validate and transform files arg validate and transform files arg

@ -6,8 +6,7 @@ import os
import time import time
from typing import Optional from typing import Optional
from flask import current_app from configs import dify_config
from extensions.ext_storage import storage from extensions.ext_storage import storage
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'] IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
@ -23,7 +22,7 @@ class UploadFileParser:
if upload_file.extension not in IMAGE_EXTENSIONS: if upload_file.extension not in IMAGE_EXTENSIONS:
return None return None
if current_app.config['MULTIMODAL_SEND_IMAGE_FORMAT'] == 'url' or force_url: if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == 'url' or force_url:
return cls.get_signed_temp_image_url(upload_file.id) return cls.get_signed_temp_image_url(upload_file.id)
else: else:
# get image file base64 # get image file base64
@ -44,13 +43,13 @@ class UploadFileParser:
:param upload_file: UploadFile object :param upload_file: UploadFile object
:return: :return:
""" """
base_url = current_app.config.get('FILES_URL') base_url = dify_config.FILES_URL
image_preview_url = f'{base_url}/files/{upload_file_id}/image-preview' image_preview_url = f'{base_url}/files/{upload_file_id}/image-preview'
timestamp = str(int(time.time())) timestamp = str(int(time.time()))
nonce = os.urandom(16).hex() nonce = os.urandom(16).hex()
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}" data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
secret_key = current_app.config['SECRET_KEY'].encode() secret_key = dify_config.SECRET_KEY.encode()
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest() sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode() encoded_sign = base64.urlsafe_b64encode(sign).decode()
@ -68,7 +67,7 @@ class UploadFileParser:
:return: :return:
""" """
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}" data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
secret_key = current_app.config['SECRET_KEY'].encode() secret_key = dify_config.SECRET_KEY.encode()
recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest() recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode() recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()
@ -77,4 +76,4 @@ class UploadFileParser:
return False return False
current_time = int(time.time()) current_time = int(time.time())
return current_time - int(timestamp) <= current_app.config.get('FILES_ACCESS_TIMEOUT') return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
CODE_EXECUTION_ENDPOINT = dify_config.CODE_EXECUTION_ENDPOINT CODE_EXECUTION_ENDPOINT = dify_config.CODE_EXECUTION_ENDPOINT
CODE_EXECUTION_API_KEY = dify_config.CODE_EXECUTION_API_KEY CODE_EXECUTION_API_KEY = dify_config.CODE_EXECUTION_API_KEY
CODE_EXECUTION_TIMEOUT= (10, 60) CODE_EXECUTION_TIMEOUT = (10, 60)
class CodeExecutionException(Exception): class CodeExecutionException(Exception):
pass pass
@ -64,7 +64,7 @@ class CodeExecutor:
@classmethod @classmethod
def execute_code(cls, def execute_code(cls,
language: Literal['python3', 'javascript', 'jinja2'], language: CodeLanguage,
preload: str, preload: str,
code: str, code: str,
dependencies: Optional[list[CodeDependency]] = None) -> str: dependencies: Optional[list[CodeDependency]] = None) -> str:
@ -119,7 +119,7 @@ class CodeExecutor:
return response.data.stdout return response.data.stdout
@classmethod @classmethod
def execute_workflow_code_template(cls, language: Literal['python3', 'javascript', 'jinja2'], code: str, inputs: dict, dependencies: Optional[list[CodeDependency]] = None) -> dict: def execute_workflow_code_template(cls, language: CodeLanguage, code: str, inputs: dict, dependencies: Optional[list[CodeDependency]] = None) -> dict:
""" """
Execute code Execute code
:param language: code language :param language: code language

@ -6,11 +6,16 @@ from models.account import Tenant
def obfuscated_token(token: str): def obfuscated_token(token: str):
return token[:6] + '*' * (len(token) - 8) + token[-2:] if not token:
return token
if len(token) <= 8:
return '*' * 20
return token[:6] + '*' * 12 + token[-2:]
def encrypt_token(tenant_id: str, token: str): def encrypt_token(tenant_id: str, token: str):
tenant = db.session.query(Tenant).filter(Tenant.id == tenant_id).first() if not (tenant := db.session.query(Tenant).filter(Tenant.id == tenant_id).first()):
raise ValueError(f'Tenant with id {tenant_id} not found')
encrypted_token = rsa.encrypt(token, tenant.encrypt_public_key) encrypted_token = rsa.encrypt(token, tenant.encrypt_public_key)
return base64.b64encode(encrypted_token).decode() return base64.b64encode(encrypted_token).decode()

@ -13,15 +13,10 @@ def get_position_map(folder_path: str, *, file_name: str = "_position.yaml") ->
:param file_name: the YAML file name, default to '_position.yaml' :param file_name: the YAML file name, default to '_position.yaml'
:return: a dict with name as key and index as value :return: a dict with name as key and index as value
""" """
position_file_name = os.path.join(folder_path, file_name) position_file_path = os.path.join(folder_path, file_name)
positions = load_yaml_file(position_file_name, ignore_error=True) yaml_content = load_yaml_file(file_path=position_file_path, default_value=[])
position_map = {} positions = [item.strip() for item in yaml_content if item and isinstance(item, str) and item.strip()]
index = 0 return {name: index for index, name in enumerate(positions)}
for _, name in enumerate(positions):
if name and isinstance(name, str):
position_map[name.strip()] = index
index += 1
return position_map
def sort_by_position_map( def sort_by_position_map(

@ -1,48 +1,75 @@
""" """
Proxy requests to avoid SSRF Proxy requests to avoid SSRF
""" """
import logging
import os import os
import time
import httpx import httpx
SSRF_PROXY_ALL_URL = os.getenv('SSRF_PROXY_ALL_URL', '') SSRF_PROXY_ALL_URL = os.getenv('SSRF_PROXY_ALL_URL', '')
SSRF_PROXY_HTTP_URL = os.getenv('SSRF_PROXY_HTTP_URL', '') SSRF_PROXY_HTTP_URL = os.getenv('SSRF_PROXY_HTTP_URL', '')
SSRF_PROXY_HTTPS_URL = os.getenv('SSRF_PROXY_HTTPS_URL', '') SSRF_PROXY_HTTPS_URL = os.getenv('SSRF_PROXY_HTTPS_URL', '')
SSRF_DEFAULT_MAX_RETRIES = int(os.getenv('SSRF_DEFAULT_MAX_RETRIES', '3'))
proxies = { proxies = {
'http://': SSRF_PROXY_HTTP_URL, 'http://': SSRF_PROXY_HTTP_URL,
'https://': SSRF_PROXY_HTTPS_URL 'https://': SSRF_PROXY_HTTPS_URL
} if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None } if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None
BACKOFF_FACTOR = 0.5
STATUS_FORCELIST = [429, 500, 502, 503, 504]
def make_request(method, url, **kwargs): def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
if SSRF_PROXY_ALL_URL: if "allow_redirects" in kwargs:
return httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs) allow_redirects = kwargs.pop("allow_redirects")
elif proxies: if "follow_redirects" not in kwargs:
return httpx.request(method=method, url=url, proxies=proxies, **kwargs) kwargs["follow_redirects"] = allow_redirects
else:
return httpx.request(method=method, url=url, **kwargs) retries = 0
while retries <= max_retries:
try:
if SSRF_PROXY_ALL_URL:
response = httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs)
elif proxies:
response = httpx.request(method=method, url=url, proxies=proxies, **kwargs)
else:
response = httpx.request(method=method, url=url, **kwargs)
if response.status_code not in STATUS_FORCELIST:
return response
else:
logging.warning(f"Received status code {response.status_code} for URL {url} which is in the force list")
def get(url, **kwargs): except httpx.RequestError as e:
return make_request('GET', url, **kwargs) logging.warning(f"Request to URL {url} failed on attempt {retries + 1}: {e}")
retries += 1
if retries <= max_retries:
time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
def post(url, **kwargs): raise Exception(f"Reached maximum retries ({max_retries}) for URL {url}")
return make_request('POST', url, **kwargs)
def put(url, **kwargs): def get(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('PUT', url, **kwargs) return make_request('GET', url, max_retries=max_retries, **kwargs)
def patch(url, **kwargs): def post(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('PATCH', url, **kwargs) return make_request('POST', url, max_retries=max_retries, **kwargs)
def delete(url, **kwargs): def put(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('DELETE', url, **kwargs) return make_request('PUT', url, max_retries=max_retries, **kwargs)
def head(url, **kwargs): def patch(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('HEAD', url, **kwargs) return make_request('PATCH', url, max_retries=max_retries, **kwargs)
def delete(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('DELETE', url, max_retries=max_retries, **kwargs)
def head(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
return make_request('HEAD', url, max_retries=max_retries, **kwargs)

@ -3,10 +3,13 @@ import logging
import re import re
from typing import Optional from typing import Optional
from core.llm_generator.output_parser.errors import OutputParserException
from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
from core.llm_generator.prompts import CONVERSATION_TITLE_PROMPT, GENERATOR_QA_PROMPT from core.llm_generator.prompts import (
CONVERSATION_TITLE_PROMPT,
GENERATOR_QA_PROMPT,
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
)
from core.model_manager import ModelManager from core.model_manager import ModelManager
from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage
from core.model_runtime.entities.model_entities import ModelType from core.model_runtime.entities.model_entities import ModelType
@ -115,55 +118,158 @@ class LLMGenerator:
return questions return questions
@classmethod @classmethod
def generate_rule_config(cls, tenant_id: str, audiences: str, hoping_to_solve: str) -> dict: def generate_rule_config(cls, tenant_id: str, instruction: str, model_config: dict, no_variable: bool, rule_config_max_tokens: int = 512) -> dict:
output_parser = RuleConfigGeneratorOutputParser() output_parser = RuleConfigGeneratorOutputParser()
error = ""
error_step = ""
rule_config = {
"prompt": "",
"variables": [],
"opening_statement": "",
"error": ""
}
model_parameters = {
"max_tokens": rule_config_max_tokens,
"temperature": 0.01
}
if no_variable:
prompt_template = PromptTemplateParser(
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE
)
prompt_generate = prompt_template.format(
inputs={
"TASK_DESCRIPTION": instruction,
},
remove_template_variables=False
)
prompt_messages = [UserPromptMessage(content=prompt_generate)]
model_manager = ModelManager()
model_instance = model_manager.get_default_model_instance(
tenant_id=tenant_id,
model_type=ModelType.LLM,
)
try:
response = model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters=model_parameters,
stream=False
)
rule_config["prompt"] = response.message.content
except InvokeError as e:
error = str(e)
error_step = "generate rule config"
except Exception as e:
logging.exception(e)
rule_config["error"] = str(e)
rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""
return rule_config
# get rule config prompt, parameter and statement
prompt_generate, parameter_generate, statement_generate = output_parser.get_format_instructions()
prompt_template = PromptTemplateParser( prompt_template = PromptTemplateParser(
template=output_parser.get_format_instructions() prompt_generate
)
parameter_template = PromptTemplateParser(
parameter_generate
)
statement_template = PromptTemplateParser(
statement_generate
) )
prompt = prompt_template.format( # format the prompt_generate_prompt
prompt_generate_prompt = prompt_template.format(
inputs={ inputs={
"audiences": audiences, "TASK_DESCRIPTION": instruction,
"hoping_to_solve": hoping_to_solve,
"variable": "{{variable}}",
"lanA": "{{lanA}}",
"lanB": "{{lanB}}",
"topic": "{{topic}}"
}, },
remove_template_variables=False remove_template_variables=False
) )
prompt_messages = [UserPromptMessage(content=prompt_generate_prompt)]
# get model instance
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") if model_config else None,
model=model_config.get("name") if model_config else None,
) )
prompt_messages = [UserPromptMessage(content=prompt)]
try: try:
response = model_instance.invoke_llm( try:
prompt_messages=prompt_messages, # the first step to generate the task prompt
model_parameters={ prompt_content = model_instance.invoke_llm(
"max_tokens": 512, prompt_messages=prompt_messages,
"temperature": 0 model_parameters=model_parameters,
stream=False
)
except InvokeError as e:
error = str(e)
error_step = "generate prefix prompt"
rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""
return rule_config
rule_config["prompt"] = prompt_content.message.content
parameter_generate_prompt = parameter_template.format(
inputs={
"INPUT_TEXT": prompt_content.message.content,
}, },
stream=False remove_template_variables=False
) )
parameter_messages = [UserPromptMessage(content=parameter_generate_prompt)]
# the second step to generate the task_parameter and task_statement
statement_generate_prompt = statement_template.format(
inputs={
"TASK_DESCRIPTION": instruction,
"INPUT_TEXT": prompt_content.message.content,
},
remove_template_variables=False
)
statement_messages = [UserPromptMessage(content=statement_generate_prompt)]
try:
parameter_content = model_instance.invoke_llm(
prompt_messages=parameter_messages,
model_parameters=model_parameters,
stream=False
)
rule_config["variables"] = re.findall(r'"\s*([^"]+)\s*"', parameter_content.message.content)
except InvokeError as e:
error = str(e)
error_step = "generate variables"
try:
statement_content = model_instance.invoke_llm(
prompt_messages=statement_messages,
model_parameters=model_parameters,
stream=False
)
rule_config["opening_statement"] = statement_content.message.content
except InvokeError as e:
error = str(e)
error_step = "generate conversation opener"
rule_config = output_parser.parse(response.message.content)
except InvokeError as e:
raise e
except OutputParserException:
raise ValueError('Please give a valid input for intended audience or hoping to solve problems.')
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
rule_config = { rule_config["error"] = str(e)
"prompt": "",
"variables": [], rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""
"opening_statement": ""
}
return rule_config return rule_config

@ -1,14 +1,18 @@
from typing import Any from typing import Any
from core.llm_generator.output_parser.errors import OutputParserException from core.llm_generator.output_parser.errors import OutputParserException
from core.llm_generator.prompts import RULE_CONFIG_GENERATE_TEMPLATE from core.llm_generator.prompts import (
RULE_CONFIG_PARAMETER_GENERATE_TEMPLATE,
RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
RULE_CONFIG_STATEMENT_GENERATE_TEMPLATE,
)
from libs.json_in_md_parser import parse_and_check_json_markdown from libs.json_in_md_parser import parse_and_check_json_markdown
class RuleConfigGeneratorOutputParser: class RuleConfigGeneratorOutputParser:
def get_format_instructions(self) -> str: def get_format_instructions(self) -> tuple[str, str, str]:
return RULE_CONFIG_GENERATE_TEMPLATE return RULE_CONFIG_PROMPT_GENERATE_TEMPLATE, RULE_CONFIG_PARAMETER_GENERATE_TEMPLATE, RULE_CONFIG_STATEMENT_GENERATE_TEMPLATE
def parse(self, text: str) -> Any: def parse(self, text: str) -> Any:
try: try:

@ -64,6 +64,7 @@ User Input:
SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT = ( SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT = (
"Please help me predict the three most likely questions that human would ask, " "Please help me predict the three most likely questions that human would ask, "
"and keeping each question under 20 characters.\n" "and keeping each question under 20 characters.\n"
"MAKE SURE your output is the SAME language as the Assistant's latest response(if the main response is written in Chinese, then the language of your output must be using Chinese.)!\n"
"The output must be an array in JSON format following the specified schema:\n" "The output must be an array in JSON format following the specified schema:\n"
"[\"question1\",\"question2\",\"question3\"]\n" "[\"question1\",\"question2\",\"question3\"]\n"
) )
@ -80,65 +81,73 @@ GENERATOR_QA_PROMPT = (
'<QA Pairs>' '<QA Pairs>'
) )
RULE_CONFIG_GENERATE_TEMPLATE = """Given MY INTENDED AUDIENCES and HOPING TO SOLVE using a language model, please select \ WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE = """
the model prompt that best suits the input. Here is a task description for which I would like you to create a high-quality prompt template for:
You will be provided with the prompt, variables, and an opening statement. <task_description>
Only the content enclosed in double curly braces, such as {{variable}}, in the prompt can be considered as a variable; \ {{TASK_DESCRIPTION}}
otherwise, it cannot exist as a variable in the variables. </task_description>
If you believe revising the original input will result in a better response from the language model, you may \ Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
suggest revisions. - Do not inlcude <input> or <output> section and variables in the prompt, assume user will add them at their own will.
- Clear instructions for the AI that will be using this prompt, demarcated with <instructions> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
<<PRINCIPLES OF GOOD PROMPT>> - Relevant examples if needed to clarify the task further, demarcated with <example> tags. Do not include variables in the prompt. Give three pairs of input and output examples.
Integrate the intended audience in the prompt e.g. the audience is an expert in the field. - Include other relevant sections demarcated with appropriate XML tags like <examples>, <instructions>.
Break down complex tasks into a sequence of simpler prompts in an interactive conversation. - Use the same language as task description.
Implement example-driven prompting (Use few-shot prompting). - Output in ``` xml ``` and start with <instruction>
When formatting your prompt start with Instruction followed by either Example if relevant. \ Please generate the full prompt template with at least 300 words and output only the prompt template.
Subsequently present your content. Use one or more line breaks to separate instructions examples questions context and input data. """
Incorporate the following phrases: Your task is and You MUST.
Incorporate the following phrases: You will be penalized.
Use leading words like writing think step by step.
Add to your prompt the following phrase Ensure that your answer is unbiased and does not rely on stereotypes.
Assign a role to the large language models.
Use Delimiters.
To write an essay /text /paragraph /article or any type of text that should be detailed: Write a detailed [essay/text/paragraph] for me on [topic] in detail by adding all the information necessary.
Clearly state the requirements that the model must follow in order to produce content in the form of the keywords regulations hint or instructions
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like, \
no any other string out of markdown code snippet:
```json
{{{{
"prompt": string \\ generated prompt
"variables": list of string \\ variables
"opening_statement": string \\ an opening statement to guide users on how to ask questions with generated prompt \
and fill in variables, with a welcome sentence, and keep TLDR.
}}}}
```
<< EXAMPLES >>
[EXAMPLE A]
```json
{
"prompt": "I need your help to translate the following {{Input_language}}paper paragraph into {{Target_language}}, in a style similar to a popular science magazine in {{Target_language}}. #### Rules Ensure accurate conveyance of the original text's facts and context during translation. Maintain the original paragraph format and retain technical terms and company abbreviations ",
"variables": ["Input_language", "Target_language"],
"opening_statement": " Hi. I am your translation assistant. I can help you with any translation and ensure accurate conveyance of information. "
}
```
[EXAMPLE B]
```json
{
"prompt": "Your task is to review the provided meeting notes and create a concise summary that captures the essential information, focusing on key takeaways and action items assigned to specific individuals or departments during the meeting. Use clear and professional language, and organize the summary in a logical manner using appropriate formatting such as headings, subheadings, and bullet points. Ensure that the summary is easy to understand and provides a comprehensive but succinct overview of the meeting's content, with a particular focus on clearly indicating who is responsible for each action item.",
"variables": ["meeting_notes"],
"opening_statement": "Hi! I'm your meeting notes summarizer AI. I can help you with any meeting notes and ensure accurate conveyance of information."
}
```
<< MY INTENDED AUDIENCES >> RULE_CONFIG_PROMPT_GENERATE_TEMPLATE = """
{{audiences}} Here is a task description for which I would like you to create a high-quality prompt template for:
<task_description>
{{TASK_DESCRIPTION}}
</task_description>
Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
- Descriptive variable names surrounded by {{ }} (two curly brackets) to indicate where the actual values will be substituted in. Choose variable names that clearly indicate the type of value expected. Variable names have to be composed of number, english alphabets and underline and nothing else.
- Clear instructions for the AI that will be using this prompt, demarcated with <instructions> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Relevant examples if needed to clarify the task further, demarcated with <example> tags. Do not use curly brackets any other than in <instruction> section.
- Any other relevant sections demarcated with appropriate XML tags like <input>, <output>, etc.
- Use the same language as task description.
- Output in ``` xml ``` and start with <instruction>
Please generate the full prompt template and output only the prompt template.
"""
<< HOPING TO SOLVE >> RULE_CONFIG_PARAMETER_GENERATE_TEMPLATE = """
{{hoping_to_solve}} I need to extract the following information from the input text. The <information to be extracted> tag specifies the 'type', 'description' and 'required' of the information to be extracted.
<information to be extracted>
variables name bounded two double curly brackets. Variable name has to be composed of number, english alphabets and underline and nothing else.
</information to be extracted>
Step 1: Carefully read the input and understand the structure of the expected output.
Step 2: Extract relevant parameters from the provided text based on the name and description of object.
Step 3: Structure the extracted parameters to JSON object as specified in <structure>.
Step 4: Ensure that the list of variable_names is properly formatted and valid. The output should not contain any XML tags. Output an empty list if there is no valid variable name in input text.
### Structure
Here is the structure of the expected output, I should always follow the output structure.
["variable_name_1", "variable_name_2"]
### Input Text
Inside <text></text> XML tags, there is a text that I should extract parameters and convert to a JSON object.
<text>
{{INPUT_TEXT}}
</text>
### Answer
I should always output a valid list. Output nothing other than the list of variable_name. Output an empty list if there is no variable name in input text.
"""
<< OUTPUT >> RULE_CONFIG_STATEMENT_GENERATE_TEMPLATE = """
""" <instruction>
Step 1: Identify the purpose of the chatbot from the variable {{TASK_DESCRIPTION}} and infer chatbot's tone (e.g., friendly, professional, etc.) to add personality traits.
Step 2: Create a coherent and engaging opening statement.
Step 3: Ensure the output is welcoming and clearly explains what the chatbot is designed to do. Do not include any XML tags in the output.
Please use the same language as the user's input language. If user uses chinese then generate opening statement in chinese, if user uses english then generate opening statement in english.
Example Input:
Provide customer support for an e-commerce website
Example Output:
Welcome! I'm here to assist you with any questions or issues you might have with your shopping experience. Whether you're looking for product information, need help with your order, or have any other inquiries, feel free to ask. I'm friendly, helpful, and ready to support you in any way I can.
<Task>
Here is the task description: {{INPUT_TEXT}}
You just need to generate the output
"""

@ -103,7 +103,7 @@ class TokenBufferMemory:
if curr_message_tokens > max_token_limit: if curr_message_tokens > max_token_limit:
pruned_memory = [] pruned_memory = []
while curr_message_tokens > max_token_limit and prompt_messages: while curr_message_tokens > max_token_limit and len(prompt_messages)>1:
pruned_memory.append(prompt_messages.pop(0)) pruned_memory.append(prompt_messages.pop(0))
curr_message_tokens = self.model_instance.get_llm_num_tokens( curr_message_tokens = self.model_instance.get_llm_num_tokens(
prompt_messages prompt_messages

@ -410,7 +410,7 @@ class LBModelManager:
self._model = model self._model = model
self._load_balancing_configs = load_balancing_configs self._load_balancing_configs = load_balancing_configs
for load_balancing_config in self._load_balancing_configs: for load_balancing_config in self._load_balancing_configs[:]: # Iterate over a shallow copy of the list
if load_balancing_config.name == "__inherit__": if load_balancing_config.name == "__inherit__":
if not managed_credentials: if not managed_credentials:
# remove __inherit__ if managed credentials is not provided # remove __inherit__ if managed credentials is not provided

@ -86,6 +86,9 @@
- `agent-thought` Agent reasoning, generally over 70B with thought chain capability. - `agent-thought` Agent reasoning, generally over 70B with thought chain capability.
- `vision` Vision, i.e., image understanding. - `vision` Vision, i.e., image understanding.
- `tool-call`
- `multi-tool-call`
- `stream-tool-call`
### FetchFrom ### FetchFrom

@ -87,6 +87,9 @@
- `agent-thought` Agent 推理,一般超过 70B 有思维链能力。 - `agent-thought` Agent 推理,一般超过 70B 有思维链能力。
- `vision` 视觉,即:图像理解。 - `vision` 视觉,即:图像理解。
- `tool-call` 工具调用
- `multi-tool-call` 多工具调用
- `stream-tool-call` 流式工具调用
### FetchFrom ### FetchFrom

@ -162,7 +162,7 @@ class AIModel(ABC):
# traverse all model_schema_yaml_paths # traverse all model_schema_yaml_paths
for model_schema_yaml_path in model_schema_yaml_paths: for model_schema_yaml_path in model_schema_yaml_paths:
# read yaml data from yaml file # read yaml data from yaml file
yaml_data = load_yaml_file(model_schema_yaml_path, ignore_error=True) yaml_data = load_yaml_file(model_schema_yaml_path)
new_parameter_rules = [] new_parameter_rules = []
for parameter_rule in yaml_data.get('parameter_rules', []): for parameter_rule in yaml_data.get('parameter_rules', []):

@ -44,7 +44,7 @@ class ModelProvider(ABC):
# read provider schema from yaml file # read provider schema from yaml file
yaml_path = os.path.join(current_path, f'{provider_name}.yaml') yaml_path = os.path.join(current_path, f'{provider_name}.yaml')
yaml_data = load_yaml_file(yaml_path, ignore_error=True) yaml_data = load_yaml_file(yaml_path)
try: try:
# yaml_data to entity # yaml_data to entity

@ -23,6 +23,7 @@
- tongyi - tongyi
- wenxin - wenxin
- moonshot - moonshot
- tencent
- jina - jina
- chatglm - chatglm
- yi - yi

@ -27,9 +27,9 @@ parameter_rules:
- name: max_tokens - name: max_tokens
use_template: max_tokens use_template: max_tokens
required: true required: true
default: 4096 default: 8192
min: 1 min: 1
max: 4096 max: 8192
- name: response_format - name: response_format
use_template: response_format use_template: response_format
pricing: pricing:

@ -113,6 +113,11 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
if system: if system:
extra_model_kwargs['system'] = system extra_model_kwargs['system'] = system
# Add the new header for claude-3-5-sonnet-20240620 model
extra_headers = {}
if model == "claude-3-5-sonnet-20240620":
extra_headers["anthropic-beta"] = "max-tokens-3-5-sonnet-2024-07-15"
if tools: if tools:
extra_model_kwargs['tools'] = [ extra_model_kwargs['tools'] = [
self._transform_tool_prompt(tool) for tool in tools self._transform_tool_prompt(tool) for tool in tools
@ -121,6 +126,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
model=model, model=model,
messages=prompt_message_dicts, messages=prompt_message_dicts,
stream=stream, stream=stream,
extra_headers=extra_headers,
**model_parameters, **model_parameters,
**extra_model_kwargs **extra_model_kwargs
) )
@ -130,6 +136,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
model=model, model=model,
messages=prompt_message_dicts, messages=prompt_message_dicts,
stream=stream, stream=stream,
extra_headers=extra_headers,
**model_parameters, **model_parameters,
**extra_model_kwargs **extra_model_kwargs
) )
@ -138,7 +145,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel):
return self._handle_chat_generate_stream_response(model, credentials, response, prompt_messages) return self._handle_chat_generate_stream_response(model, credentials, response, prompt_messages)
return self._handle_chat_generate_response(model, credentials, response, prompt_messages) return self._handle_chat_generate_response(model, credentials, response, prompt_messages)
def _code_block_mode_wrapper(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], def _code_block_mode_wrapper(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None, model_parameters: dict, tools: Optional[list[PromptMessageTool]] = None,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None,

@ -71,6 +71,9 @@ model_credential_schema:
- label: - label:
en_US: '2024-02-01' en_US: '2024-02-01'
value: '2024-02-01' value: '2024-02-01'
- label:
en_US: '2024-06-01'
value: '2024-06-01'
placeholder: placeholder:
zh_Hans: 在此选择您的 API 版本 zh_Hans: 在此选择您的 API 版本
en_US: Select your API Version here en_US: Select your API Version here

@ -375,6 +375,10 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
continue continue
delta = chunk.choices[0] delta = chunk.choices[0]
# NOTE: For fix https://github.com/langgenius/dify/issues/5790
if delta.delta is None:
continue
# extract tool calls from response # extract tool calls from response
self._update_tool_calls(tool_calls=tool_calls, tool_calls_response=delta.delta.tool_calls) self._update_tool_calls(tool_calls=tool_calls, tool_calls_response=delta.delta.tool_calls)
@ -501,7 +505,7 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
sub_messages.append(sub_message_dict) sub_messages.append(sub_message_dict)
message_dict = {"role": "user", "content": sub_messages} message_dict = {"role": "user", "content": sub_messages}
elif isinstance(message, AssistantPromptMessage): elif isinstance(message, AssistantPromptMessage):
message = cast(AssistantPromptMessage, message) # message = cast(AssistantPromptMessage, message)
message_dict = {"role": "assistant", "content": message.content} message_dict = {"role": "assistant", "content": message.content}
if message.tool_calls: if message.tool_calls:
message_dict["tool_calls"] = [helper.dump_model(tool_call) for tool_call in message.tool_calls] message_dict["tool_calls"] = [helper.dump_model(tool_call) for tool_call in message.tool_calls]

@ -66,6 +66,10 @@ provider_credential_schema:
label: label:
en_US: Europe (Frankfurt) en_US: Europe (Frankfurt)
zh_Hans: 欧洲 (法兰克福) zh_Hans: 欧洲 (法兰克福)
- value: eu-west-2
label:
en_US: Eu west London (London)
zh_Hans: 欧洲西部 (伦敦)
- value: us-gov-west-1 - value: us-gov-west-1
label: label:
en_US: AWS GovCloud (US-West) en_US: AWS GovCloud (US-West)

@ -10,10 +10,13 @@
- cohere.command-text-v14 - cohere.command-text-v14
- cohere.command-r-plus-v1.0 - cohere.command-r-plus-v1.0
- cohere.command-r-v1.0 - cohere.command-r-v1.0
- meta.llama3-1-8b-instruct-v1:0
- meta.llama3-1-70b-instruct-v1:0
- meta.llama3-8b-instruct-v1:0 - meta.llama3-8b-instruct-v1:0
- meta.llama3-70b-instruct-v1:0 - meta.llama3-70b-instruct-v1:0
- meta.llama2-13b-chat-v1 - meta.llama2-13b-chat-v1
- meta.llama2-70b-chat-v1 - meta.llama2-70b-chat-v1
- mistral.mistral-large-2407-v1:0
- mistral.mistral-small-2402-v1:0 - mistral.mistral-small-2402-v1:0
- mistral.mistral-large-2402-v1:0 - mistral.mistral-large-2402-v1:0
- mistral.mixtral-8x7b-instruct-v0:1 - mistral.mixtral-8x7b-instruct-v0:1

@ -48,6 +48,28 @@ logger = logging.getLogger(__name__)
class BedrockLargeLanguageModel(LargeLanguageModel): class BedrockLargeLanguageModel(LargeLanguageModel):
# please refer to the documentation: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
# TODO There is invoke issue: context limit on Cohere Model, will add them after fixed.
CONVERSE_API_ENABLED_MODEL_INFO=[
{'prefix': 'anthropic.claude-v2', 'support_system_prompts': True, 'support_tool_use': False},
{'prefix': 'anthropic.claude-v1', 'support_system_prompts': True, 'support_tool_use': False},
{'prefix': 'anthropic.claude-3', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'meta.llama', 'support_system_prompts': True, 'support_tool_use': False},
{'prefix': 'mistral.mistral-7b-instruct', 'support_system_prompts': False, 'support_tool_use': False},
{'prefix': 'mistral.mixtral-8x7b-instruct', 'support_system_prompts': False, 'support_tool_use': False},
{'prefix': 'mistral.mistral-large', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'mistral.mistral-small', 'support_system_prompts': True, 'support_tool_use': True},
{'prefix': 'amazon.titan', 'support_system_prompts': False, 'support_tool_use': False}
]
@staticmethod
def _find_model_info(model_id):
for model in BedrockLargeLanguageModel.CONVERSE_API_ENABLED_MODEL_INFO:
if model_id.startswith(model['prefix']):
return model
logger.info(f"current model id: {model_id} did not support by Converse API")
return None
def _invoke(self, model: str, credentials: dict, def _invoke(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage], model_parameters: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None,
@ -66,10 +88,12 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param user: unique user id :param user: unique user id
:return: full response or stream response chunk generator result :return: full response or stream response chunk generator result
""" """
# TODO: consolidate different invocation methods for models based on base model capabilities
# invoke anthropic models via boto3 client model_info= BedrockLargeLanguageModel._find_model_info(model)
if "anthropic" in model: if model_info:
return self._generate_anthropic(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools) model_info['model'] = model
# invoke models via boto3 converse API
return self._generate_with_converse(model_info, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
# invoke Cohere models via boto3 client # invoke Cohere models via boto3 client
if "cohere.command-r" in model: if "cohere.command-r" in model:
return self._generate_cohere_chat(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools) return self._generate_cohere_chat(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
@ -151,12 +175,12 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
return self._handle_generate_response(model, credentials, response, prompt_messages) return self._handle_generate_response(model, credentials, response, prompt_messages)
def _generate_anthropic(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, def _generate_with_converse(self, model_info: dict, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]: stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]:
""" """
Invoke Anthropic large language model Invoke large language model with converse API
:param model: model name :param model_info: model information
:param credentials: model credentials :param credentials: model credentials
:param prompt_messages: prompt messages :param prompt_messages: prompt messages
:param model_parameters: model parameters :param model_parameters: model parameters
@ -173,25 +197,36 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
inference_config, additional_model_fields = self._convert_converse_api_model_parameters(model_parameters, stop) inference_config, additional_model_fields = self._convert_converse_api_model_parameters(model_parameters, stop)
parameters = { parameters = {
'modelId': model, 'modelId': model_info['model'],
'messages': prompt_message_dicts, 'messages': prompt_message_dicts,
'inferenceConfig': inference_config, 'inferenceConfig': inference_config,
'additionalModelRequestFields': additional_model_fields, 'additionalModelRequestFields': additional_model_fields,
} }
if system and len(system) > 0: if model_info['support_system_prompts'] and system and len(system) > 0:
parameters['system'] = system parameters['system'] = system
if tools: if model_info['support_tool_use'] and tools:
parameters['toolConfig'] = self._convert_converse_tool_config(tools=tools) parameters['toolConfig'] = self._convert_converse_tool_config(tools=tools)
try:
if stream:
response = bedrock_client.converse_stream(**parameters)
return self._handle_converse_stream_response(model_info['model'], credentials, response, prompt_messages)
else:
response = bedrock_client.converse(**parameters)
return self._handle_converse_response(model_info['model'], credentials, response, prompt_messages)
except ClientError as ex:
error_code = ex.response['Error']['Code']
full_error_msg = f"{error_code}: {ex.response['Error']['Message']}"
raise self._map_client_to_invoke_error(error_code, full_error_msg)
except (EndpointConnectionError, NoRegionError, ServiceNotInRegionError) as ex:
raise InvokeConnectionError(str(ex))
if stream: except UnknownServiceError as ex:
response = bedrock_client.converse_stream(**parameters) raise InvokeServerUnavailableError(str(ex))
return self._handle_converse_stream_response(model, credentials, response, prompt_messages)
else:
response = bedrock_client.converse(**parameters)
return self._handle_converse_response(model, credentials, response, prompt_messages)
except Exception as ex:
raise InvokeError(str(ex))
def _handle_converse_response(self, model: str, credentials: dict, response: dict, def _handle_converse_response(self, model: str, credentials: dict, response: dict,
prompt_messages: list[PromptMessage]) -> LLMResult: prompt_messages: list[PromptMessage]) -> LLMResult:
""" """
@ -203,10 +238,30 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param prompt_messages: prompt messages :param prompt_messages: prompt messages
:return: full response chunk generator result :return: full response chunk generator result
""" """
response_content = response['output']['message']['content']
# transform assistant message to prompt message # transform assistant message to prompt message
assistant_prompt_message = AssistantPromptMessage( if response['stopReason'] == 'tool_use':
content=response['output']['message']['content'][0]['text'] tool_calls = []
) text, tool_use = self._extract_tool_use(response_content)
tool_call = AssistantPromptMessage.ToolCall(
id=tool_use['toolUseId'],
type='function',
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=tool_use['name'],
arguments=json.dumps(tool_use['input'])
)
)
tool_calls.append(tool_call)
assistant_prompt_message = AssistantPromptMessage(
content=text,
tool_calls=tool_calls
)
else:
assistant_prompt_message = AssistantPromptMessage(
content=response_content[0]['text']
)
# calculate num tokens # calculate num tokens
if response['usage']: if response['usage']:
@ -229,6 +284,18 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
) )
return result return result
def _extract_tool_use(self, content:dict)-> tuple[str, dict]:
tool_use = {}
text = ''
for item in content:
if 'toolUse' in item:
tool_use = item['toolUse']
elif 'text' in item:
text = item['text']
else:
raise ValueError(f"Got unknown item: {item}")
return text, tool_use
def _handle_converse_stream_response(self, model: str, credentials: dict, response: dict, def _handle_converse_stream_response(self, model: str, credentials: dict, response: dict,
prompt_messages: list[PromptMessage], ) -> Generator: prompt_messages: list[PromptMessage], ) -> Generator:
""" """
@ -340,14 +407,12 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
""" """
system = [] system = []
prompt_message_dicts = []
for message in prompt_messages: for message in prompt_messages:
if isinstance(message, SystemPromptMessage): if isinstance(message, SystemPromptMessage):
message.content=message.content.strip() message.content=message.content.strip()
system.append({"text": message.content}) system.append({"text": message.content})
else:
prompt_message_dicts = []
for message in prompt_messages:
if not isinstance(message, SystemPromptMessage):
prompt_message_dicts.append(self._convert_prompt_message_to_dict(message)) prompt_message_dicts.append(self._convert_prompt_message_to_dict(message))
return system, prompt_message_dicts return system, prompt_message_dicts
@ -448,7 +513,6 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
} }
else: else:
raise ValueError(f"Got unknown type {message}") raise ValueError(f"Got unknown type {message}")
return message_dict return message_dict
def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage] | str, def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage] | str,
@ -505,7 +569,6 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
except ClientError as ex: except ClientError as ex:
error_code = ex.response['Error']['Code'] error_code = ex.response['Error']['Code']
full_error_msg = f"{error_code}: {ex.response['Error']['Message']}" full_error_msg = f"{error_code}: {ex.response['Error']['Message']}"
raise CredentialsValidateFailedError(str(self._map_client_to_invoke_error(error_code, full_error_msg))) raise CredentialsValidateFailedError(str(self._map_client_to_invoke_error(error_code, full_error_msg)))
except Exception as ex: except Exception as ex:

@ -0,0 +1,25 @@
model: meta.llama3-1-70b-instruct-v1:0
label:
en_US: Llama 3.1 Instruct 70B
model_type: llm
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.5
- name: top_p
use_template: top_p
default: 0.9
- name: max_gen_len
use_template: max_tokens
required: true
default: 512
min: 1
max: 2048
pricing:
input: '0.00265'
output: '0.0035'
unit: '0.001'
currency: USD

@ -0,0 +1,25 @@
model: meta.llama3-1-8b-instruct-v1:0
label:
en_US: Llama 3.1 Instruct 8B
model_type: llm
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.5
- name: top_p
use_template: top_p
default: 0.9
- name: max_gen_len
use_template: max_tokens
required: true
default: 512
min: 1
max: 2048
pricing:
input: '0.0003'
output: '0.0006'
unit: '0.001'
currency: USD

@ -2,6 +2,9 @@ model: mistral.mistral-large-2402-v1:0
label: label:
en_US: Mistral Large en_US: Mistral Large
model_type: llm model_type: llm
features:
- tool-call
- agent-thought
model_properties: model_properties:
mode: completion mode: completion
context_size: 32000 context_size: 32000

@ -0,0 +1,29 @@
model: mistral.mistral-large-2407-v1:0
label:
en_US: Mistral Large 2 (24.07)
model_type: llm
features:
- tool-call
model_properties:
mode: completion
context_size: 128000
parameter_rules:
- name: temperature
use_template: temperature
required: false
default: 0.7
- name: top_p
use_template: top_p
required: false
default: 1
- name: max_tokens
use_template: max_tokens
required: true
default: 512
min: 1
max: 8192
pricing:
input: '0.003'
output: '0.009'
unit: '0.001'
currency: USD

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save