From 239e40c8d53f04267da914876e6bef97072aca18 Mon Sep 17 00:00:00 2001
From: "Junjie.M" <118170653@qq.com>
Date: Tue, 22 Apr 2025 14:46:49 +0800
Subject: [PATCH 01/19] chore: remove useless frontend code file (#18532)
---
web/app/components/tools/provider/card.tsx | 83 -------------------
.../components/tools/provider/contribute.tsx | 40 ---------
web/app/components/tools/provider/grid_bg.svg | 14 ----
3 files changed, 137 deletions(-)
delete mode 100644 web/app/components/tools/provider/card.tsx
delete mode 100644 web/app/components/tools/provider/contribute.tsx
delete mode 100644 web/app/components/tools/provider/grid_bg.svg
diff --git a/web/app/components/tools/provider/card.tsx b/web/app/components/tools/provider/card.tsx
deleted file mode 100644
index a3d93820d2..0000000000
--- a/web/app/components/tools/provider/card.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-'use client'
-import { useMemo } from 'react'
-import { useContext } from 'use-context-selector'
-import { useTranslation } from 'react-i18next'
-import type { Collection } from '../types'
-import cn from '@/utils/classnames'
-import AppIcon from '@/app/components/base/app-icon'
-import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
-import I18n from '@/context/i18n'
-import { getLanguage } from '@/i18n/language'
-import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
-
-type Props = {
- active: boolean
- collection: Collection
- onSelect: () => void
-}
-
-const ProviderCard = ({
- active,
- collection,
- onSelect,
-}: Props) => {
- const { t } = useTranslation()
- const { locale } = useContext(I18n)
- const language = getLanguage(locale)
- const labelList = useLabelStore(s => s.labelList)
-
- const labelContent = useMemo(() => {
- if (!collection.labels)
- return ''
- return collection.labels.map((name) => {
- const label = labelList.find(item => item.name === name)
- return label?.label[language]
- }).filter(Boolean).join(', ')
- }, [collection.labels, labelList, language])
-
- return (
-
Date: Tue, 22 Apr 2025 14:54:21 +0800
Subject: [PATCH 02/19] docs: replace outdated Enterprise inquiry link with a
new one (#18528)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 87ebc9bafc..65e8001dd2 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Self-hosting ·
Documentation ·
- Enterprise inquiry
+ Dify edition overview
From 6b7dfee88b468580312d0f69cc855f7ffc463f26 Mon Sep 17 00:00:00 2001
From: -LAN-
Date: Tue, 22 Apr 2025 17:04:06 +0900
Subject: [PATCH 03/19] fix: Validates session factory type in repository
(#18497)
Signed-off-by: -LAN-
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../workflow_node_execution/sqlalchemy_repository.py | 6 +++++-
api/services/workflow_run_service.py | 2 +-
api/tasks/remove_app_and_related_data_task.py | 2 +-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/api/repositories/workflow_node_execution/sqlalchemy_repository.py b/api/repositories/workflow_node_execution/sqlalchemy_repository.py
index 0594d816a2..e0ad384be6 100644
--- a/api/repositories/workflow_node_execution/sqlalchemy_repository.py
+++ b/api/repositories/workflow_node_execution/sqlalchemy_repository.py
@@ -37,8 +37,12 @@ class SQLAlchemyWorkflowNodeExecutionRepository:
# If an engine is provided, create a sessionmaker from it
if isinstance(session_factory, Engine):
self._session_factory = sessionmaker(bind=session_factory, expire_on_commit=False)
- else:
+ elif isinstance(session_factory, sessionmaker):
self._session_factory = session_factory
+ else:
+ raise ValueError(
+ f"Invalid session_factory type {type(session_factory).__name__}; expected sessionmaker or Engine"
+ )
self._tenant_id = tenant_id
self._app_id = app_id
diff --git a/api/services/workflow_run_service.py b/api/services/workflow_run_service.py
index ff3b33eecd..8b7213eefb 100644
--- a/api/services/workflow_run_service.py
+++ b/api/services/workflow_run_service.py
@@ -133,7 +133,7 @@ class WorkflowRunService:
params={
"tenant_id": app_model.tenant_id,
"app_id": app_model.id,
- "session_factory": db.session.get_bind,
+ "session_factory": db.session.get_bind(),
}
)
diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py
index 4542b1b923..cd8981abf6 100644
--- a/api/tasks/remove_app_and_related_data_task.py
+++ b/api/tasks/remove_app_and_related_data_task.py
@@ -193,7 +193,7 @@ def _delete_app_workflow_node_executions(tenant_id: str, app_id: str):
params={
"tenant_id": tenant_id,
"app_id": app_id,
- "session_factory": db.session.get_bind,
+ "session_factory": db.session.get_bind(),
}
)
From 61e39bccdf31e85575c1f836439b69c29266bcd8 Mon Sep 17 00:00:00 2001
From: -LAN-
Date: Tue, 22 Apr 2025 17:04:20 +0900
Subject: [PATCH 04/19] fix: Patch OpenTelemetry to handle None tokens (#18498)
Signed-off-by: -LAN-
---
api/app_factory.py | 2 +
api/extensions/ext_otel_patch.py | 63 ++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+)
create mode 100644 api/extensions/ext_otel_patch.py
diff --git a/api/app_factory.py b/api/app_factory.py
index 586f2ded9e..9648d770ab 100644
--- a/api/app_factory.py
+++ b/api/app_factory.py
@@ -52,6 +52,7 @@ def initialize_extensions(app: DifyApp):
ext_mail,
ext_migrate,
ext_otel,
+ ext_otel_patch,
ext_proxy_fix,
ext_redis,
ext_repositories,
@@ -84,6 +85,7 @@ def initialize_extensions(app: DifyApp):
ext_proxy_fix,
ext_blueprints,
ext_commands,
+ ext_otel_patch, # Apply patch before initializing OpenTelemetry
ext_otel,
]
for ext in extensions:
diff --git a/api/extensions/ext_otel_patch.py b/api/extensions/ext_otel_patch.py
new file mode 100644
index 0000000000..58309fe4d1
--- /dev/null
+++ b/api/extensions/ext_otel_patch.py
@@ -0,0 +1,63 @@
+"""
+Patch for OpenTelemetry context detach method to handle None tokens gracefully.
+
+This patch addresses the issue where OpenTelemetry's context.detach() method raises a TypeError
+when called with a None token. The error occurs in the contextvars_context.py file where it tries
+to call reset() on a None token.
+
+Related GitHub issue: https://github.com/langgenius/dify/issues/18496
+
+Error being fixed:
+```
+Traceback (most recent call last):
+ File "opentelemetry/context/__init__.py", line 154, in detach
+ _RUNTIME_CONTEXT.detach(token)
+ File "opentelemetry/context/contextvars_context.py", line 50, in detach
+ self._current_context.reset(token) # type: ignore
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+TypeError: expected an instance of Token, got None
+```
+
+Instead of modifying the third-party package directly, this patch monkey-patches the
+context.detach method to gracefully handle None tokens.
+"""
+
+import logging
+from functools import wraps
+
+from opentelemetry import context
+
+logger = logging.getLogger(__name__)
+
+# Store the original detach method
+original_detach = context.detach
+
+
+# Create a patched version that handles None tokens
+@wraps(original_detach)
+def patched_detach(token):
+ """
+ A patched version of context.detach that handles None tokens gracefully.
+ """
+ if token is None:
+ logger.debug("Attempted to detach a None token, skipping")
+ return
+
+ return original_detach(token)
+
+
+def is_enabled():
+ """
+ Check if the extension is enabled.
+ Always enable this patch to prevent errors even when OpenTelemetry is disabled.
+ """
+ return True
+
+
+def init_app(app):
+ """
+ Initialize the OpenTelemetry context patch.
+ """
+ # Replace the original detach method with our patched version
+ context.detach = patched_detach
+ logger.info("OpenTelemetry context.detach patched to handle None tokens")
From 79bf590576b19b8962d953800c45395a970141d3 Mon Sep 17 00:00:00 2001
From: elsie_else
Date: Tue, 22 Apr 2025 16:07:26 +0800
Subject: [PATCH 05/19] docs: update enterprise inquiry links across all README
language variants (#18541)
---
README_AR.md | 2 +-
README_BN.md | 2 +-
README_CN.md | 2 +-
README_DE.md | 2 +-
README_ES.md | 2 +-
README_FR.md | 2 +-
README_JA.md | 2 +-
README_KL.md | 2 +-
README_KR.md | 2 +-
README_PT.md | 2 +-
README_SI.md | 2 +-
README_TR.md | 2 +-
README_TW.md | 2 +-
README_VI.md | 2 +-
14 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/README_AR.md b/README_AR.md
index e58f59da5d..4f93802fda 100644
--- a/README_AR.md
+++ b/README_AR.md
@@ -4,7 +4,7 @@
Dify Cloud ·
الاستضافة الذاتية ·
التوثيق ·
- استفسار الشركات (للإنجليزية فقط)
+ نظرة عامة على منتجات Dify
diff --git a/README_BN.md b/README_BN.md
index 3ebc81af5d..7599fae9ff 100644
--- a/README_BN.md
+++ b/README_BN.md
@@ -8,7 +8,7 @@
ডিফাই ক্লাউড ·
সেল্ফ-হোস্টিং ·
ডকুমেন্টেশন ·
- ব্যাবসায়িক অনুসন্ধান
+ Dify পণ্যের রূপভেদ
diff --git a/README_CN.md b/README_CN.md
index 6d3c601100..973629f459 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -4,7 +4,7 @@
Dify 云服务 ·
自托管 ·
文档 ·
- (需用英文)常见问题解答 / 联系团队
+ Dify 产品形态总览
diff --git a/README_DE.md b/README_DE.md
index b3b9bf3221..738c0e3b67 100644
--- a/README_DE.md
+++ b/README_DE.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Selbstgehostetes ·
Dokumentation ·
- Anfrage an Unternehmen
+ Überblick über die Dify-Produkte
diff --git a/README_ES.md b/README_ES.md
index d14afdd2eb..212268b73d 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Auto-alojamiento ·
Documentación ·
- Consultas empresariales (en inglés)
+ Resumen de las ediciones de Dify
diff --git a/README_FR.md b/README_FR.md
index 031196303e..89eea7d058 100644
--- a/README_FR.md
+++ b/README_FR.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Auto-hébergement ·
Documentation ·
- Demande d’entreprise (en anglais seulement)
+ Présentation des différentes offres Dify
diff --git a/README_JA.md b/README_JA.md
index 3b7a6f50db..adca219753 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -4,7 +4,7 @@
Dify Cloud ·
セルフホスティング ·
ドキュメント ·
- 企業のお問い合わせ(英語のみ)
+ Difyの各種エディションについて
diff --git a/README_KL.md b/README_KL.md
index ccadb77274..17e6c9d509 100644
--- a/README_KL.md
+++ b/README_KL.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Self-hosting ·
Documentation ·
- Commercial enquiries
+ Dify product editions
diff --git a/README_KR.md b/README_KR.md
index c1a98f8b68..d44723f9b6 100644
--- a/README_KR.md
+++ b/README_KR.md
@@ -4,7 +4,7 @@
Dify 클라우드 ·
셀프-호스팅 ·
문서 ·
- 기업 문의 (영어만 가능)
+ Dify 제품 에디션 안내
diff --git a/README_PT.md b/README_PT.md
index 5b3c782645..9dc2207279 100644
--- a/README_PT.md
+++ b/README_PT.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Auto-hospedagem ·
Documentação ·
- Consultas empresariais
+ Visão geral das edições do Dify
diff --git a/README_SI.md b/README_SI.md
index 7c0867c776..caa5975973 100644
--- a/README_SI.md
+++ b/README_SI.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Samostojno gostovanje ·
Dokumentacija ·
- Povpraševanje za podjetja
+ Pregled ponudb izdelkov Dify
diff --git a/README_TR.md b/README_TR.md
index f8890b00ef..ab2853a019 100644
--- a/README_TR.md
+++ b/README_TR.md
@@ -4,7 +4,7 @@
Dify Bulut ·
Kendi Sunucunuzda Barındırma ·
Dokümantasyon ·
- Yalnızca İngilizce: Kurumsal Sorgulama
+ Dify ürün seçeneklerine genel bakış
diff --git a/README_TW.md b/README_TW.md
index 260f1e80ac..8263a22b64 100644
--- a/README_TW.md
+++ b/README_TW.md
@@ -8,7 +8,7 @@
Dify 雲端服務 ·
自行託管 ·
說明文件 ·
- 企業諮詢
+ 產品方案概覽
diff --git a/README_VI.md b/README_VI.md
index 15d2d5ae80..852ed7aaa0 100644
--- a/README_VI.md
+++ b/README_VI.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Tự triển khai ·
Tài liệu ·
- Yêu cầu doanh nghiệp
+ Tổng quan các lựa chọn sản phẩm Dify
From 35a008af18c80ecc55193730634f823d3157cc33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?=
Date: Tue, 22 Apr 2025 16:07:51 +0800
Subject: [PATCH 06/19] fix can't resize workflow run panel (#18538)
---
.../workflow/panel/workflow-preview.tsx | 40 ++++++++++++++++++-
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx
index 228a376535..34b0ec6395 100644
--- a/web/app/components/workflow/panel/workflow-preview.tsx
+++ b/web/app/components/workflow/panel/workflow-preview.tsx
@@ -1,5 +1,6 @@
import {
memo,
+ useCallback,
useEffect,
useState,
} from 'react'
@@ -47,10 +48,45 @@ const WorkflowPreview = () => {
switchTab('DETAIL')
}, [workflowRunningData])
+ const [panelWidth, setPanelWidth] = useState(420)
+ const [isResizing, setIsResizing] = useState(false)
+
+ const startResizing = useCallback((e: React.MouseEvent) => {
+ e.preventDefault()
+ setIsResizing(true)
+ }, [])
+
+ const stopResizing = useCallback(() => {
+ setIsResizing(false)
+ }, [])
+
+ const resize = useCallback((e: MouseEvent) => {
+ if (isResizing) {
+ const newWidth = window.innerWidth - e.clientX
+ if (newWidth > 420 && newWidth < 1024)
+ setPanelWidth(newWidth)
+ }
+ }, [isResizing])
+
+ useEffect(() => {
+ window.addEventListener('mousemove', resize)
+ window.addEventListener('mouseup', stopResizing)
+ return () => {
+ window.removeEventListener('mousemove', resize)
+ window.removeEventListener('mouseup', stopResizing)
+ }
+ }, [resize, stopResizing])
+
return (
+ relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl
+ `}
+ style={{ width: `${panelWidth}px` }}
+ >
+
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`}
handleCancelDebugAndPreviewPanel()}>
From a1158cc946c92ca83700411e2d995173eabcefc1 Mon Sep 17 00:00:00 2001
From: ZalterCitty
Date: Tue, 22 Apr 2025 16:17:55 +0800
Subject: [PATCH 08/19] fix: Update prompt message content types to use Literal
and add union type for content (#17136)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: 朱庆超
Co-authored-by: crazywoola <427733928@qq.com>
---
api/core/agent/base_agent_runner.py | 5 ++-
api/core/agent/cot_chat_agent_runner.py | 5 ++-
api/core/agent/fc_agent_runner.py | 5 ++-
api/core/file/file_manager.py | 6 ++--
api/core/memory/token_buffer_memory.py | 4 +--
.../entities/message_entities.py | 33 +++++++++++--------
api/core/prompt/advanced_prompt_transform.py | 7 ++--
api/core/prompt/simple_prompt_transform.py | 4 +--
api/core/workflow/nodes/llm/node.py | 16 +++++----
.../core/prompt/test_prompt_message.py | 27 +++++++++++++++
10 files changed, 73 insertions(+), 39 deletions(-)
create mode 100644 api/tests/unit_tests/core/prompt/test_prompt_message.py
diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py
index 48c92ea2db..e648613605 100644
--- a/api/core/agent/base_agent_runner.py
+++ b/api/core/agent/base_agent_runner.py
@@ -21,14 +21,13 @@ from core.model_runtime.entities import (
AssistantPromptMessage,
LLMUsage,
PromptMessage,
- PromptMessageContent,
PromptMessageTool,
SystemPromptMessage,
TextPromptMessageContent,
ToolPromptMessage,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.model_runtime.entities.model_entities import ModelFeature
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.prompt.utils.extract_thread_messages import extract_thread_messages
@@ -501,7 +500,7 @@ class BaseAgentRunner(AppRunner):
)
if not file_objs:
return UserPromptMessage(content=message.query)
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
for file in file_objs:
prompt_message_contents.append(
diff --git a/api/core/agent/cot_chat_agent_runner.py b/api/core/agent/cot_chat_agent_runner.py
index 7d407a4976..5ff89bdacb 100644
--- a/api/core/agent/cot_chat_agent_runner.py
+++ b/api/core/agent/cot_chat_agent_runner.py
@@ -5,12 +5,11 @@ from core.file import file_manager
from core.model_runtime.entities import (
AssistantPromptMessage,
PromptMessage,
- PromptMessageContent,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.model_runtime.utils.encoders import jsonable_encoder
@@ -40,7 +39,7 @@ class CotChatAgentRunner(CotAgentRunner):
Organize user query
"""
if self.files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
# get image detail config
diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py
index f45fa5c66e..a1110e7709 100644
--- a/api/core/agent/fc_agent_runner.py
+++ b/api/core/agent/fc_agent_runner.py
@@ -15,14 +15,13 @@ from core.model_runtime.entities import (
LLMResultChunkDelta,
LLMUsage,
PromptMessage,
- PromptMessageContent,
PromptMessageContentType,
SystemPromptMessage,
TextPromptMessageContent,
ToolPromptMessage,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform
from core.tools.entities.tool_entities import ToolInvokeMeta
from core.tools.tool_engine import ToolEngine
@@ -395,7 +394,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
Organize user query
"""
if self.files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
# get image detail config
diff --git a/api/core/file/file_manager.py b/api/core/file/file_manager.py
index 4ebe997ac5..9a204e9ff6 100644
--- a/api/core/file/file_manager.py
+++ b/api/core/file/file_manager.py
@@ -7,9 +7,9 @@ from core.model_runtime.entities import (
AudioPromptMessageContent,
DocumentPromptMessageContent,
ImagePromptMessageContent,
- MultiModalPromptMessageContent,
VideoPromptMessageContent,
)
+from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from extensions.ext_storage import storage
from . import helpers
@@ -43,7 +43,7 @@ def to_prompt_message_content(
/,
*,
image_detail_config: ImagePromptMessageContent.DETAIL | None = None,
-) -> MultiModalPromptMessageContent:
+) -> PromptMessageContentUnionTypes:
if f.extension is None:
raise ValueError("Missing file extension")
if f.mime_type is None:
@@ -58,7 +58,7 @@ def to_prompt_message_content(
if f.type == FileType.IMAGE:
params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
- prompt_class_map: Mapping[FileType, type[MultiModalPromptMessageContent]] = {
+ prompt_class_map: Mapping[FileType, type[PromptMessageContentUnionTypes]] = {
FileType.IMAGE: ImagePromptMessageContent,
FileType.AUDIO: AudioPromptMessageContent,
FileType.VIDEO: VideoPromptMessageContent,
diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py
index 3c90dd22a2..2254b3d4d5 100644
--- a/api/core/memory/token_buffer_memory.py
+++ b/api/core/memory/token_buffer_memory.py
@@ -8,11 +8,11 @@ from core.model_runtime.entities import (
AssistantPromptMessage,
ImagePromptMessageContent,
PromptMessage,
- PromptMessageContent,
PromptMessageRole,
TextPromptMessageContent,
UserPromptMessage,
)
+from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from core.prompt.utils.extract_thread_messages import extract_thread_messages
from extensions.ext_database import db
from factories import file_factory
@@ -100,7 +100,7 @@ class TokenBufferMemory:
if not file_objs:
prompt_messages.append(UserPromptMessage(content=message.query))
else:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
for file in file_objs:
prompt_message = file_manager.to_prompt_message_content(
diff --git a/api/core/model_runtime/entities/message_entities.py b/api/core/model_runtime/entities/message_entities.py
index 3bed2460dd..b1c43d1455 100644
--- a/api/core/model_runtime/entities/message_entities.py
+++ b/api/core/model_runtime/entities/message_entities.py
@@ -1,6 +1,6 @@
from collections.abc import Sequence
from enum import Enum, StrEnum
-from typing import Any, Optional, Union
+from typing import Annotated, Any, Literal, Optional, Union
from pydantic import BaseModel, Field, field_serializer, field_validator
@@ -61,11 +61,7 @@ class PromptMessageContentType(StrEnum):
class PromptMessageContent(BaseModel):
- """
- Model class for prompt message content.
- """
-
- type: PromptMessageContentType
+ pass
class TextPromptMessageContent(PromptMessageContent):
@@ -73,7 +69,7 @@ class TextPromptMessageContent(PromptMessageContent):
Model class for text prompt message content.
"""
- type: PromptMessageContentType = PromptMessageContentType.TEXT
+ type: Literal[PromptMessageContentType.TEXT] = PromptMessageContentType.TEXT
data: str
@@ -82,7 +78,6 @@ class MultiModalPromptMessageContent(PromptMessageContent):
Model class for multi-modal prompt message content.
"""
- type: PromptMessageContentType
format: str = Field(default=..., description="the format of multi-modal file")
base64_data: str = Field(default="", description="the base64 data of multi-modal file")
url: str = Field(default="", description="the url of multi-modal file")
@@ -94,11 +89,11 @@ class MultiModalPromptMessageContent(PromptMessageContent):
class VideoPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.VIDEO
+ type: Literal[PromptMessageContentType.VIDEO] = PromptMessageContentType.VIDEO
class AudioPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.AUDIO
+ type: Literal[PromptMessageContentType.AUDIO] = PromptMessageContentType.AUDIO
class ImagePromptMessageContent(MultiModalPromptMessageContent):
@@ -110,12 +105,24 @@ class ImagePromptMessageContent(MultiModalPromptMessageContent):
LOW = "low"
HIGH = "high"
- type: PromptMessageContentType = PromptMessageContentType.IMAGE
+ type: Literal[PromptMessageContentType.IMAGE] = PromptMessageContentType.IMAGE
detail: DETAIL = DETAIL.LOW
class DocumentPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.DOCUMENT
+ type: Literal[PromptMessageContentType.DOCUMENT] = PromptMessageContentType.DOCUMENT
+
+
+PromptMessageContentUnionTypes = Annotated[
+ Union[
+ TextPromptMessageContent,
+ ImagePromptMessageContent,
+ DocumentPromptMessageContent,
+ AudioPromptMessageContent,
+ VideoPromptMessageContent,
+ ],
+ Field(discriminator="type"),
+]
class PromptMessage(BaseModel):
@@ -124,7 +131,7 @@ class PromptMessage(BaseModel):
"""
role: PromptMessageRole
- content: Optional[str | Sequence[PromptMessageContent]] = None
+ content: Optional[str | list[PromptMessageContentUnionTypes]] = None
name: Optional[str] = None
def is_empty(self) -> bool:
diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py
index c7427f797e..25964ae063 100644
--- a/api/core/prompt/advanced_prompt_transform.py
+++ b/api/core/prompt/advanced_prompt_transform.py
@@ -9,13 +9,12 @@ from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities import (
AssistantPromptMessage,
PromptMessage,
- PromptMessageContent,
PromptMessageRole,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig
from core.prompt.prompt_transform import PromptTransform
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
@@ -125,7 +124,7 @@ class AdvancedPromptTransform(PromptTransform):
prompt = Jinja2Formatter.format(prompt, prompt_inputs)
if files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
for file in files:
prompt_message_contents.append(
@@ -201,7 +200,7 @@ class AdvancedPromptTransform(PromptTransform):
prompt_messages = self._append_chat_histories(memory, memory_config, prompt_messages, model_config)
if files and query is not None:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
for file in files:
prompt_message_contents.append(
diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py
index ad56d84cb6..47808928f7 100644
--- a/api/core/prompt/simple_prompt_transform.py
+++ b/api/core/prompt/simple_prompt_transform.py
@@ -11,7 +11,7 @@ from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities.message_entities import (
ImagePromptMessageContent,
PromptMessage,
- PromptMessageContent,
+ PromptMessageContentUnionTypes,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
@@ -277,7 +277,7 @@ class SimplePromptTransform(PromptTransform):
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
) -> UserPromptMessage:
if files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
for file in files:
prompt_message_contents.append(
diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py
index 8db7394e54..1089e7168e 100644
--- a/api/core/workflow/nodes/llm/node.py
+++ b/api/core/workflow/nodes/llm/node.py
@@ -24,7 +24,7 @@ from core.model_runtime.entities import (
from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from core.model_runtime.entities.message_entities import (
AssistantPromptMessage,
- PromptMessageContent,
+ PromptMessageContentUnionTypes,
PromptMessageRole,
SystemPromptMessage,
UserPromptMessage,
@@ -594,8 +594,7 @@ class LLMNode(BaseNode[LLMNodeData]):
variable_pool: VariablePool,
jinja2_variables: Sequence[VariableSelector],
) -> tuple[Sequence[PromptMessage], Optional[Sequence[str]]]:
- # FIXME: fix the type error cause prompt_messages is type quick a few times
- prompt_messages: list[Any] = []
+ prompt_messages: list[PromptMessage] = []
if isinstance(prompt_template, list):
# For chat model
@@ -657,12 +656,14 @@ class LLMNode(BaseNode[LLMNodeData]):
# For issue #11247 - Check if prompt content is a string or a list
prompt_content_type = type(prompt_content)
if prompt_content_type == str:
+ prompt_content = str(prompt_content)
if "#histories#" in prompt_content:
prompt_content = prompt_content.replace("#histories#", memory_text)
else:
prompt_content = memory_text + "\n" + prompt_content
prompt_messages[0].content = prompt_content
elif prompt_content_type == list:
+ prompt_content = prompt_content if isinstance(prompt_content, list) else []
for content_item in prompt_content:
if content_item.type == PromptMessageContentType.TEXT:
if "#histories#" in content_item.data:
@@ -675,9 +676,10 @@ class LLMNode(BaseNode[LLMNodeData]):
# Add current query to the prompt message
if sys_query:
if prompt_content_type == str:
- prompt_content = prompt_messages[0].content.replace("#sys.query#", sys_query)
+ prompt_content = str(prompt_messages[0].content).replace("#sys.query#", sys_query)
prompt_messages[0].content = prompt_content
elif prompt_content_type == list:
+ prompt_content = prompt_content if isinstance(prompt_content, list) else []
for content_item in prompt_content:
if content_item.type == PromptMessageContentType.TEXT:
content_item.data = sys_query + "\n" + content_item.data
@@ -707,7 +709,7 @@ class LLMNode(BaseNode[LLMNodeData]):
filtered_prompt_messages = []
for prompt_message in prompt_messages:
if isinstance(prompt_message.content, list):
- prompt_message_content = []
+ prompt_message_content: list[PromptMessageContentUnionTypes] = []
for content_item in prompt_message.content:
# Skip content if features are not defined
if not model_config.model_schema.features:
@@ -1132,7 +1134,9 @@ class LLMNode(BaseNode[LLMNodeData]):
)
-def _combine_message_content_with_role(*, contents: Sequence[PromptMessageContent], role: PromptMessageRole):
+def _combine_message_content_with_role(
+ *, contents: Optional[str | list[PromptMessageContentUnionTypes]] = None, role: PromptMessageRole
+):
match role:
case PromptMessageRole.USER:
return UserPromptMessage(content=contents)
diff --git a/api/tests/unit_tests/core/prompt/test_prompt_message.py b/api/tests/unit_tests/core/prompt/test_prompt_message.py
new file mode 100644
index 0000000000..e5da51d733
--- /dev/null
+++ b/api/tests/unit_tests/core/prompt/test_prompt_message.py
@@ -0,0 +1,27 @@
+from core.model_runtime.entities.message_entities import (
+ ImagePromptMessageContent,
+ TextPromptMessageContent,
+ UserPromptMessage,
+)
+
+
+def test_build_prompt_message_with_prompt_message_contents():
+ prompt = UserPromptMessage(content=[TextPromptMessageContent(data="Hello, World!")])
+ assert isinstance(prompt.content, list)
+ assert isinstance(prompt.content[0], TextPromptMessageContent)
+ assert prompt.content[0].data == "Hello, World!"
+
+
+def test_dump_prompt_message():
+ example_url = "https://example.com/image.jpg"
+ prompt = UserPromptMessage(
+ content=[
+ ImagePromptMessageContent(
+ url=example_url,
+ format="jpeg",
+ mime_type="image/jpeg",
+ )
+ ]
+ )
+ data = prompt.model_dump()
+ assert data["content"][0].get("url") == example_url
From 3737e0b087d8bc96b24d8f157de3407d880d232c Mon Sep 17 00:00:00 2001
From: Joel
Date: Tue, 22 Apr 2025 16:48:45 +0800
Subject: [PATCH 09/19] fix: clickjacking (#18516)
Signed-off-by: -LAN-
Co-authored-by: -LAN-
---
api/.env.example | 5 +++-
docker/.env.example | 3 +++
docker/docker-compose-template.yaml | 3 ++-
docker/docker-compose.yaml | 4 ++-
web/.env.example | 2 ++
.../app/overview/embedded/index.tsx | 8 +++---
web/docker/entrypoint.sh | 1 +
web/middleware.ts | 26 +++++++++++++------
8 files changed, 37 insertions(+), 15 deletions(-)
diff --git a/api/.env.example b/api/.env.example
index 01ddb4adfd..b5820fcdc2 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -482,4 +482,7 @@ OTEL_MAX_QUEUE_SIZE=2048
OTEL_MAX_EXPORT_BATCH_SIZE=512
OTEL_METRIC_EXPORT_INTERVAL=60000
OTEL_BATCH_EXPORT_TIMEOUT=10000
-OTEL_METRIC_EXPORT_TIMEOUT=30000
\ No newline at end of file
+OTEL_METRIC_EXPORT_TIMEOUT=30000
+
+# Prevent Clickjacking
+ALLOW_EMBED=false
diff --git a/docker/.env.example b/docker/.env.example
index f8310a10f1..0b80dccb37 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -1068,3 +1068,6 @@ OTEL_MAX_EXPORT_BATCH_SIZE=512
OTEL_METRIC_EXPORT_INTERVAL=60000
OTEL_BATCH_EXPORT_TIMEOUT=10000
OTEL_METRIC_EXPORT_TIMEOUT=30000
+
+# Prevent Clickjacking
+ALLOW_EMBED=false
diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml
index c6d41849ef..377ff9c117 100644
--- a/docker/docker-compose-template.yaml
+++ b/docker/docker-compose-template.yaml
@@ -66,6 +66,7 @@ services:
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
@@ -552,7 +553,7 @@ services:
volumes:
- ./volumes/opengauss/data:/var/lib/opengauss/data
healthcheck:
- test: ["CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1"]
+ test: [ "CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1" ]
interval: 10s
timeout: 10s
retries: 10
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index d8ff7d841a..81fa651ed9 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -474,6 +474,7 @@ x-shared-env: &shared-api-worker-env
OTEL_METRIC_EXPORT_INTERVAL: ${OTEL_METRIC_EXPORT_INTERVAL:-60000}
OTEL_BATCH_EXPORT_TIMEOUT: ${OTEL_BATCH_EXPORT_TIMEOUT:-10000}
OTEL_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
services:
# API service
@@ -542,6 +543,7 @@ services:
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
@@ -1028,7 +1030,7 @@ services:
volumes:
- ./volumes/opengauss/data:/var/lib/opengauss/data
healthcheck:
- test: ["CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1"]
+ test: [ "CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1" ]
interval: 10s
timeout: 10s
retries: 10
diff --git a/web/.env.example b/web/.env.example
index 1c3f42ddfc..51631c2437 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -29,6 +29,8 @@ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
NEXT_PUBLIC_CSP_WHITELIST=
+# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
+NEXT_PUBLIC_ALLOW_EMBED=
# Github Access Token, used for invoking Github API
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=
diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx
index 37fbd5e291..d4e5dd8898 100644
--- a/web/app/components/app/overview/embedded/index.tsx
+++ b/web/app/components/app/overview/embedded/index.tsx
@@ -29,7 +29,7 @@ const OPTION_MAP = {
iframe: {
getContent: (url: string, token: string) =>
`