From a575fbca9414bc0d684f92d73dcc8b027dc7eb07 Mon Sep 17 00:00:00 2001 From: NFish Date: Fri, 25 Apr 2025 14:37:04 +0800 Subject: [PATCH 1/9] fix: update api rate limit on Pricing page (#18755) --- web/app/components/billing/config.ts | 8 ++++++-- .../components/billing/pricing/plan-item.tsx | 19 ++++++++++++++----- web/app/components/billing/type.ts | 6 +++++- web/context/provider-context.tsx | 3 ++- web/i18n/en-US/billing.ts | 4 ++++ web/i18n/ja-JP/billing.ts | 10 +++++++--- web/i18n/zh-Hans/billing.ts | 4 ++++ 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index 52651259ef..1d5fbc7491 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -1,3 +1,4 @@ +import type { BasicPlan } from '@/app/components/billing/type' import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type' const supportModelProviders = 'OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replicate' @@ -10,7 +11,7 @@ export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/dify-business' export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify' export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6' -export const ALL_PLANS: Record = { +export const ALL_PLANS: Record = { sandbox: { level: 1, price: 0, @@ -22,6 +23,7 @@ export const ALL_PLANS: Record = { vectorSpace: '50MB', documentsUploadQuota: 0, documentsRequestQuota: 10, + apiRateLimit: 5000, documentProcessingPriority: Priority.standard, messageRequest: 200, annotatedResponse: 10, @@ -38,6 +40,7 @@ export const ALL_PLANS: Record = { vectorSpace: '5GB', documentsUploadQuota: 0, documentsRequestQuota: 100, + apiRateLimit: NUM_INFINITE, documentProcessingPriority: Priority.priority, messageRequest: 5000, annotatedResponse: 2000, @@ -54,6 +57,7 @@ export const ALL_PLANS: Record = { vectorSpace: '20GB', documentsUploadQuota: 0, documentsRequestQuota: 1000, + apiRateLimit: NUM_INFINITE, documentProcessingPriority: Priority.topPriority, messageRequest: 10000, annotatedResponse: 5000, @@ -62,7 +66,7 @@ export const ALL_PLANS: Record = { } export const defaultPlan = { - type: Plan.sandbox, + type: Plan.sandbox as BasicPlan, usage: { documents: 50, vectorSpace: 1, diff --git a/web/app/components/billing/pricing/plan-item.tsx b/web/app/components/billing/pricing/plan-item.tsx index a0b8685989..07af0ffec8 100644 --- a/web/app/components/billing/pricing/plan-item.tsx +++ b/web/app/components/billing/pricing/plan-item.tsx @@ -2,7 +2,8 @@ import type { FC, ReactNode } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine } from '@remixicon/react' +import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine, RiTerminalBoxLine } from '@remixicon/react' +import type { BasicPlan } from '../type' import { Plan } from '../type' import { ALL_PLANS, NUM_INFINITE } from '../config' import Toast from '../../base/toast' @@ -15,8 +16,8 @@ import { useAppContext } from '@/context/app-context' import { fetchSubscriptionUrls } from '@/service/billing' type Props = { - currentPlan: Plan - plan: Plan + currentPlan: BasicPlan + plan: BasicPlan planRange: PlanRange canPay: boolean } @@ -127,8 +128,8 @@ const PlanItem: FC = ({
{style[plan].icon}
-
{t(`${i18nPrefix}.name`)}
- {isMostPopularPlan &&
+
{t(`${i18nPrefix}.name`)}
+ {isMostPopularPlan &&
@@ -205,6 +206,14 @@ const PlanItem: FC = ({ label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })} tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')} /> + } + label={ + planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}` + : `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}` + } + tooltip={planInfo.apiRateLimit === NUM_INFINITE ? null : t('billing.plansCommon.apiRateLimitTooltip') as string} + /> } label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')} diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 28bce37098..2f5728ceef 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -9,6 +9,9 @@ export enum Priority { priority = 'priority', topPriority = 'top-priority', } + +export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team + export type PlanInfo = { level: number price: number @@ -20,6 +23,7 @@ export type PlanInfo = { vectorSpace: string documentsUploadQuota: number documentsRequestQuota: number + apiRateLimit: number documentProcessingPriority: Priority logHistory: number messageRequest: number @@ -60,7 +64,7 @@ export type CurrentPlanInfoBackend = { billing: { enabled: boolean subscription: { - plan: Plan + plan: BasicPlan } } members: { diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index bd997380e7..90af9aae0c 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -17,6 +17,7 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { RETRIEVE_METHOD } from '@/types/app' +import type { BasicPlan } from '@/app/components/billing/type' import { Plan, type UsagePlanInfo } from '@/app/components/billing/type' import { fetchCurrentPlanInfo } from '@/service/billing' import { parseCurrentPlan } from '@/app/components/billing/utils' @@ -34,7 +35,7 @@ type ProviderContextState = { supportRetrievalMethods: RETRIEVE_METHOD[] isAPIKeySet: boolean plan: { - type: Plan + type: BasicPlan usage: UsagePlanInfo total: UsagePlanInfo } diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index 893e730842..57358dcf36 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -55,6 +55,10 @@ const translation = { vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.', documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit', documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ', + apiRateLimit: 'API Rate Limit', + apiRateLimitUnit: '{{count,number}}/day', + unlimitedApiRate: 'No API Rate Limit', + apiRateLimitTooltip: 'API Rate Limit applies to all requests made through the Dify API, including text generation, chat conversations, workflow executions, and document processing.', documentProcessingPriority: ' Document Processing', documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.', priority: { diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts index 891779d0b6..bcb509b85b 100644 --- a/web/i18n/ja-JP/billing.ts +++ b/web/i18n/ja-JP/billing.ts @@ -54,6 +54,10 @@ const translation = { vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、知識データストレージのリソースを消費します。知識データストレージの上限に達すると、新しいドキュメントはアップロードされません。', documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限', documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが1分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが1分間に10回連続でヒットテストを実行した場合、そのワークスペースは次の1分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。', + apiRateLimit: 'APIレート制限', + apiRateLimitUnit: '{{count,number}}/日', + unlimitedApiRate: '無制限のAPIコール', + apiRateLimitTooltip: 'APIレート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API経由のすべてのリクエストに適用されます。', documentProcessingPriority: '文書処理', documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。', priority: { @@ -100,17 +104,17 @@ const translation = { }, plans: { sandbox: { - name: 'Sandbox(サンドボックス)', + name: 'Sandbox', for: '主要機能の無料体験', description: '主要機能を無料で体験', }, professional: { - name: 'Professional(プロフェッショナル)', + name: 'Professional', for: '個人開発者/小規模チーム向け', description: '個人開発者・小規模チームに最適', }, team: { - name: 'Team(チーム)', + name: 'Team', for: '中規模チーム向け', description: '成長期のチームに必要な機能を備えたプラン', }, diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts index 8bddbfc2ba..e5fbff77b0 100644 --- a/web/i18n/zh-Hans/billing.ts +++ b/web/i18n/zh-Hans/billing.ts @@ -54,6 +54,10 @@ const translation = { vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。', documentsRequestQuota: '{{count,number}}/分钟 知识库请求频率限制', documentsRequestQuotaTooltip: '指每分钟内,一个空间在知识库中可执行的操作总数,包括数据集的创建、删除、更新,文档的上传、修改、归档,以及知识库查询等,用于评估知识库请求的性能。例如,Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。', + apiRateLimit: 'API 请求频率限制', + apiRateLimitUnit: '{{count,number}} 次/天', + unlimitedApiRate: 'API 请求频率无限制', + apiRateLimitTooltip: 'API 请求频率限制涵盖所有通过 Dify API 发起的调用,例如文本生成、聊天对话、工作流执行和文档处理等。', documentProcessingPriority: '文档处理', documentProcessingPriorityUpgrade: '以更快的速度、更高的精度处理更多的数据。', priority: { From a5e6a0dc0c1946b59fe26a5a23c8e1b01cec5e47 Mon Sep 17 00:00:00 2001 From: crazywoola <100913391+crazywoola@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:36:54 +0800 Subject: [PATCH 2/9] enable pan by fingers (#18775) --- web/app/components/workflow/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 9a3e13822a..64aefc4dd3 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -316,6 +316,7 @@ export const Workflow: FC = memo(({ nodesConnectable={!nodesReadOnly} nodesFocusable={!nodesReadOnly} edgesFocusable={!nodesReadOnly} + panOnScroll panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly} zoomOnPinch={!workflowReadOnly} zoomOnScroll={!workflowReadOnly} From a94454285809d77c8b9f725630e8500f8a1a3ff7 Mon Sep 17 00:00:00 2001 From: minglu7 <1347866672@qq.com> Date: Fri, 25 Apr 2025 16:43:44 +0800 Subject: [PATCH 3/9] fix(web): add missing 'clsx' dependency for pagination component (#18769) --- web/package.json | 1 + web/pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/web/package.json b/web/package.json index b232f908a3..960812f5a3 100644 --- a/web/package.json +++ b/web/package.json @@ -61,6 +61,7 @@ "ahooks": "^3.8.4", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", + "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index bf39194eaa..4d48af8742 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -115,6 +115,9 @@ importers: classnames: specifier: ^2.5.1 version: 2.5.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 From 9bcc8041e9d4b6537e5ad16ee05f4da3c2de5f6b Mon Sep 17 00:00:00 2001 From: bravomark <62681807+bravomark@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:45:48 +0800 Subject: [PATCH 4/9] fix: #18744 The model order defined in position.yaml in the Model Plugin is not taking effect. (#18756) --- api/core/entities/provider_configuration.py | 20 ++++++++++++++++++- .../entities/provider_entities.py | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index b3affc91a6..86887c9b4a 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -798,7 +798,25 @@ class ProviderConfiguration(BaseModel): provider_models = [m for m in provider_models if m.status == ModelStatus.ACTIVE] # resort provider_models - return sorted(provider_models, key=lambda x: x.model_type.value) + # Optimize sorting logic: first sort by provider.position order, then by model_type.value + # Get the position list for model types (retrieve only once for better performance) + model_type_positions = {} + if hasattr(self.provider, "position") and self.provider.position: + model_type_positions = self.provider.position + + def get_sort_key(model: ModelWithProviderEntity): + # Get the position list for the current model type + positions = model_type_positions.get(model.model_type.value, []) + + # If the model name is in the position list, use its index for sorting + # Otherwise use a large value (list length) to place undefined models at the end + position_index = positions.index(model.model) if model.model in positions else len(positions) + + # Return composite sort key: (model_type value, model position index) + return (model.model_type.value, position_index) + + # Sort using the composite sort key + return sorted(provider_models, key=get_sort_key) def _get_system_provider_models( self, diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index 85321bed94..d0f9ee13e5 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -134,6 +134,9 @@ class ProviderEntity(BaseModel): # pydantic configs model_config = ConfigDict(protected_namespaces=()) + # position from plugin _position.yaml + position: Optional[dict[str, list[str]]] = {} + @field_validator("models", mode="before") @classmethod def validate_models(cls, v): From ec82534a1ea69c8ff056713a57df369d343d742d Mon Sep 17 00:00:00 2001 From: gsmini Date: Fri, 25 Apr 2025 16:47:03 +0800 Subject: [PATCH 5/9] optimize account status field hard coded (#18771) Co-authored-by: crazywoola <427733928@qq.com> --- api/controllers/console/wraps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index e5e8038ad7..6911181d82 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -10,6 +10,7 @@ from configs import dify_config from controllers.console.workspace.error import AccountNotInitializedError from extensions.ext_database import db from extensions.ext_redis import redis_client +from models.account import AccountStatus from models.dataset import RateLimitLog from models.model import DifySetup from services.feature_service import FeatureService, LicenseStatus @@ -24,7 +25,7 @@ def account_initialization_required(view): # check account initialization account = current_user - if account.status == "uninitialized": + if account.status == AccountStatus.UNINITIALIZED: raise AccountNotInitializedError() return view(*args, **kwargs) From 9a3ecc1ac8643338402967ddbc0714d209ec7ca1 Mon Sep 17 00:00:00 2001 From: 1betatsu <44730472+1betatsu@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:48:38 +0900 Subject: [PATCH 6/9] fix: Allow advanced chat app to get workflow run detail (#18753) (#18758) --- api/controllers/service_api/app/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 8b10a028f3..ca3e35aab8 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -59,7 +59,7 @@ class WorkflowRunDetailApi(Resource): Get a workflow task running detail """ app_mode = AppMode.value_of(app_model.mode) - if app_mode != AppMode.WORKFLOW: + if app_mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]: raise NotWorkflowAppError() workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).first() From 0e68e8b40a4c4e5c2b033cdd5d5a89c94ab04bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Fri, 25 Apr 2025 17:26:16 +0800 Subject: [PATCH 7/9] fix: embed chatbot can't drag when use mouse (#18781) --- web/public/embed.js | 8 +++++--- web/public/embed.min.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/public/embed.js b/web/public/embed.js index 8f13a7178c..8aaf94f955 100644 --- a/web/public/embed.js +++ b/web/public/embed.js @@ -25,7 +25,7 @@ // Main function to embed the chatbot async function embedChatbot() { let isDragging = false - + if (!config || !config.token) { console.error(`${configKey} is empty or token is not provided`); return; @@ -81,7 +81,7 @@ // 3) APPEND it to the document body right away: document.body.appendChild(preloadedIframe); // ─── End Fix Snippet - if(iframeUrl.length > 2048) { + if (iframeUrl.length > 2048) { console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"); } @@ -252,6 +252,8 @@ } else { startX = e.clientX - element.offsetLeft; startY = e.clientY - element.offsetTop; + startClientX = e.clientX; + startClientY = e.clientY; } document.addEventListener("mousemove", drag); document.addEventListener("touchmove", drag, { passive: false }); @@ -264,7 +266,7 @@ const touch = e.type === "touchmove" ? e.touches[0] : e; const deltaX = touch.clientX - startClientX; const deltaY = touch.clientY - startClientY; - + // Determine whether it is a drag operation if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8) { isDragging = true; diff --git a/web/public/embed.min.js b/web/public/embed.min.js index 13837183c0..c0aeac4487 100644 --- a/web/public/embed.min.js +++ b/web/public/embed.min.js @@ -35,4 +35,4 @@ - `,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),p.draggable){var r=n;var a=p.dragAxis||"both";let s,d,t,l;function o(e){u=!1,"touchstart"===e.type?(s=e.touches[0].clientX-r.offsetLeft,d=e.touches[0].clientY-r.offsetTop,t=e.touches[0].clientX,l=e.touches[0].clientY):(s=e.clientX-r.offsetLeft,d=e.clientY-r.offsetTop),document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-l;if(u=8{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048 Date: Fri, 25 Apr 2025 19:05:36 +0900 Subject: [PATCH 8/9] refactor: Apply DI to WorkflowNodeExecutionRepository. (#18794) Signed-off-by: -LAN- --- .../app/apps/advanced_chat/app_generator.py | 42 +++++++++++++++++++ .../advanced_chat/generate_task_pipeline.py | 3 ++ api/core/app/apps/workflow/app_generator.py | 42 +++++++++++++++++++ .../apps/workflow/generate_task_pipeline.py | 3 ++ .../task_pipeline/workflow_cycle_manage.py | 22 ++-------- 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index ef582d28e0..6079b51daa 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -7,6 +7,7 @@ from typing import Any, Literal, Optional, Union, overload from flask import Flask, current_app from pydantic import ValidationError +from sqlalchemy.orm import sessionmaker import contexts from configs import dify_config @@ -24,6 +25,8 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from core.prompt.utils.get_thread_messages_length import get_thread_messages_length +from core.repository import RepositoryFactory +from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory from models.account import Account @@ -158,11 +161,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( workflow=workflow, user=user, invoke_from=invoke_from, application_generate_entity=application_generate_entity, + workflow_node_execution_repository=workflow_node_execution_repository, conversation=conversation, stream=streaming, ) @@ -215,11 +229,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( workflow=workflow, user=user, invoke_from=InvokeFrom.DEBUGGER, application_generate_entity=application_generate_entity, + workflow_node_execution_repository=workflow_node_execution_repository, conversation=None, stream=streaming, ) @@ -270,11 +295,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( workflow=workflow, user=user, invoke_from=InvokeFrom.DEBUGGER, application_generate_entity=application_generate_entity, + workflow_node_execution_repository=workflow_node_execution_repository, conversation=None, stream=streaming, ) @@ -286,6 +322,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): user: Union[Account, EndUser], invoke_from: InvokeFrom, application_generate_entity: AdvancedChatAppGenerateEntity, + workflow_node_execution_repository: WorkflowNodeExecutionRepository, conversation: Optional[Conversation] = None, stream: bool = True, ) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], Any, None]: @@ -296,6 +333,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param user: account or end user :param invoke_from: invoke from source :param application_generate_entity: application generate entity + :param workflow_node_execution_repository: repository for workflow node execution :param conversation: conversation :param stream: is stream """ @@ -348,6 +386,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, user=user, + workflow_node_execution_repository=workflow_node_execution_repository, stream=stream, ) @@ -419,6 +458,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation: Conversation, message: Message, user: Union[Account, EndUser], + workflow_node_execution_repository: WorkflowNodeExecutionRepository, stream: bool = False, ) -> Union[ChatbotAppBlockingResponse, Generator[ChatbotAppStreamResponse, None, None]]: """ @@ -430,6 +470,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param message: message :param user: account or end user :param stream: is stream + :param workflow_node_execution_repository: optional repository for workflow node execution :return: """ # init generate task pipeline @@ -442,6 +483,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): user=user, stream=stream, dialogue_count=self._dialogue_count, + workflow_node_execution_repository=workflow_node_execution_repository, ) try: diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index baefca0c3f..43ccaea9c0 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -62,6 +62,7 @@ from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.model_runtime.entities.llm_entities import LLMUsage from core.model_runtime.utils.encoders import jsonable_encoder from core.ops.ops_trace_manager import TraceQueueManager +from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes import NodeType @@ -93,6 +94,7 @@ class AdvancedChatAppGenerateTaskPipeline: user: Union[Account, EndUser], stream: bool, dialogue_count: int, + workflow_node_execution_repository: WorkflowNodeExecutionRepository, ) -> None: self._base_task_pipeline = BasedGenerateTaskPipeline( application_generate_entity=application_generate_entity, @@ -123,6 +125,7 @@ class AdvancedChatAppGenerateTaskPipeline: SystemVariableKey.WORKFLOW_ID: workflow.id, SystemVariableKey.WORKFLOW_RUN_ID: application_generate_entity.workflow_run_id, }, + workflow_node_execution_repository=workflow_node_execution_repository, ) self._task_state = WorkflowTaskState() diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 08986b16f0..6be3a7331d 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -7,6 +7,7 @@ from typing import Any, Literal, Optional, Union, overload from flask import Flask, current_app from pydantic import ValidationError +from sqlalchemy.orm import sessionmaker import contexts from configs import dify_config @@ -22,6 +23,8 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager +from core.repository import RepositoryFactory +from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory from models import Account, App, EndUser, Workflow @@ -133,12 +136,23 @@ class WorkflowAppGenerator(BaseAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( app_model=app_model, workflow=workflow, user=user, application_generate_entity=application_generate_entity, invoke_from=invoke_from, + workflow_node_execution_repository=workflow_node_execution_repository, streaming=streaming, workflow_thread_pool_id=workflow_thread_pool_id, ) @@ -151,6 +165,7 @@ class WorkflowAppGenerator(BaseAppGenerator): user: Union[Account, EndUser], application_generate_entity: WorkflowAppGenerateEntity, invoke_from: InvokeFrom, + workflow_node_execution_repository: WorkflowNodeExecutionRepository, streaming: bool = True, workflow_thread_pool_id: Optional[str] = None, ) -> Union[Mapping[str, Any], Generator[str | Mapping[str, Any], None, None]]: @@ -162,6 +177,7 @@ class WorkflowAppGenerator(BaseAppGenerator): :param user: account or end user :param application_generate_entity: application generate entity :param invoke_from: invoke from source + :param workflow_node_execution_repository: repository for workflow node execution :param streaming: is stream :param workflow_thread_pool_id: workflow thread pool id """ @@ -193,6 +209,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow=workflow, queue_manager=queue_manager, user=user, + workflow_node_execution_repository=workflow_node_execution_repository, stream=streaming, ) @@ -245,12 +262,23 @@ class WorkflowAppGenerator(BaseAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( app_model=app_model, workflow=workflow, user=user, invoke_from=InvokeFrom.DEBUGGER, application_generate_entity=application_generate_entity, + workflow_node_execution_repository=workflow_node_execution_repository, streaming=streaming, ) @@ -299,12 +327,23 @@ class WorkflowAppGenerator(BaseAppGenerator): contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) + # Create workflow node execution repository + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( + params={ + "tenant_id": application_generate_entity.app_config.tenant_id, + "app_id": application_generate_entity.app_config.app_id, + "session_factory": session_factory, + } + ) + return self._generate( app_model=app_model, workflow=workflow, user=user, invoke_from=InvokeFrom.DEBUGGER, application_generate_entity=application_generate_entity, + workflow_node_execution_repository=workflow_node_execution_repository, streaming=streaming, ) @@ -361,6 +400,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow: Workflow, queue_manager: AppQueueManager, user: Union[Account, EndUser], + workflow_node_execution_repository: WorkflowNodeExecutionRepository, stream: bool = False, ) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]: """ @@ -370,6 +410,7 @@ class WorkflowAppGenerator(BaseAppGenerator): :param queue_manager: queue manager :param user: account or end user :param stream: is stream + :param workflow_node_execution_repository: optional repository for workflow node execution :return: """ # init generate task pipeline @@ -379,6 +420,7 @@ class WorkflowAppGenerator(BaseAppGenerator): queue_manager=queue_manager, user=user, stream=stream, + workflow_node_execution_repository=workflow_node_execution_repository, ) try: diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 1f998edb6a..68131a7463 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -54,6 +54,7 @@ from core.app.entities.task_entities import ( from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.ops.ops_trace_manager import TraceQueueManager +from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.enums import SystemVariableKey from extensions.ext_database import db from models.account import Account @@ -82,6 +83,7 @@ class WorkflowAppGenerateTaskPipeline: queue_manager: AppQueueManager, user: Union[Account, EndUser], stream: bool, + workflow_node_execution_repository: WorkflowNodeExecutionRepository, ) -> None: self._base_task_pipeline = BasedGenerateTaskPipeline( application_generate_entity=application_generate_entity, @@ -109,6 +111,7 @@ class WorkflowAppGenerateTaskPipeline: SystemVariableKey.WORKFLOW_ID: workflow.id, SystemVariableKey.WORKFLOW_RUN_ID: application_generate_entity.workflow_run_id, }, + workflow_node_execution_repository=workflow_node_execution_repository, ) self._application_generate_entity = application_generate_entity diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 5ce9f737d1..38e7c9eb12 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -6,7 +6,7 @@ from typing import Any, Optional, Union, cast from uuid import uuid4 from sqlalchemy import func, select -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import Session from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.queue_entities import ( @@ -49,14 +49,13 @@ from core.file import FILE_MODEL_IDENTITY, File from core.model_runtime.utils.encoders import jsonable_encoder from core.ops.entities.trace_entity import TraceTaskName from core.ops.ops_trace_manager import TraceQueueManager, TraceTask -from core.repository import RepositoryFactory +from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.tools.tool_manager import ToolManager from core.workflow.entities.node_entities import NodeRunMetadataKey from core.workflow.enums import SystemVariableKey from core.workflow.nodes import NodeType from core.workflow.nodes.tool.entities import ToolNodeData from core.workflow.workflow_entry import WorkflowEntry -from extensions.ext_database import db from models.account import Account from models.enums import CreatedByRole, WorkflowRunTriggeredFrom from models.model import EndUser @@ -76,26 +75,13 @@ class WorkflowCycleManage: *, application_generate_entity: Union[AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity], workflow_system_variables: dict[SystemVariableKey, Any], + workflow_node_execution_repository: WorkflowNodeExecutionRepository, ) -> None: self._workflow_run: WorkflowRun | None = None self._workflow_node_executions: dict[str, WorkflowNodeExecution] = {} self._application_generate_entity = application_generate_entity self._workflow_system_variables = workflow_system_variables - - # Initialize the session factory and repository - # We use the global db engine instead of the session passed to methods - # Disable expire_on_commit to avoid the need for merging objects - self._session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) - self._workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository( - params={ - "tenant_id": self._application_generate_entity.app_config.tenant_id, - "app_id": self._application_generate_entity.app_config.app_id, - "session_factory": self._session_factory, - } - ) - - # We'll still keep the cache for backward compatibility and performance - # but use the repository for database operations + self._workflow_node_execution_repository = workflow_node_execution_repository def _handle_workflow_run_start( self, From 09a5f8da1da215ebc9b89589ae7ea110362a5dc6 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 25 Apr 2025 19:42:39 +0900 Subject: [PATCH 9/9] feat(app_dsl_service): Refines version compatibility logic (#18798) Signed-off-by: -LAN- --- api/services/app_dsl_service.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 936101c78c..a2775fe6ad 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -77,13 +77,19 @@ def _check_version_compatibility(imported_version: str) -> ImportStatus: except version.InvalidVersion: return ImportStatus.FAILED - # Compare major version and minor version - if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor: + # If imported version is newer than current, always return PENDING + if imported_ver > current_ver: return ImportStatus.PENDING - if current_ver.micro != imported_ver.micro: + # If imported version is older than current's major, return PENDING + if imported_ver.major < current_ver.major: + return ImportStatus.PENDING + + # If imported version is older than current's minor, return COMPLETED_WITH_WARNINGS + if imported_ver.minor < current_ver.minor: return ImportStatus.COMPLETED_WITH_WARNINGS + # If imported version equals or is older than current's micro, return COMPLETED return ImportStatus.COMPLETED