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
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
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/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/configs/app_config.py b/api/configs/app_config.py
index cb0adb751c..3a3ad35ee7 100644
--- a/api/configs/app_config.py
+++ b/api/configs/app_config.py
@@ -13,6 +13,7 @@ from .observability import ObservabilityConfig
from .packaging import PackagingInfo
from .remote_settings_sources import RemoteSettingsSource, RemoteSettingsSourceConfig, RemoteSettingsSourceName
from .remote_settings_sources.apollo import ApolloSettingsSource
+from .remote_settings_sources.nacos import NacosSettingsSource
logger = logging.getLogger(__name__)
@@ -34,6 +35,8 @@ class RemoteSettingsSourceFactory(PydanticBaseSettingsSource):
match remote_source_name:
case RemoteSettingsSourceName.APOLLO:
remote_source = ApolloSettingsSource(current_state)
+ case RemoteSettingsSourceName.NACOS:
+ remote_source = NacosSettingsSource(current_state)
case _:
logger.warning(f"Unsupported remote source: {remote_source_name}")
return {}
diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py
index 15dfe0063b..c2ad24094a 100644
--- a/api/configs/middleware/__init__.py
+++ b/api/configs/middleware/__init__.py
@@ -22,6 +22,7 @@ from .vdb.baidu_vector_config import BaiduVectorDBConfig
from .vdb.chroma_config import ChromaConfig
from .vdb.couchbase_config import CouchbaseConfig
from .vdb.elasticsearch_config import ElasticsearchConfig
+from .vdb.huawei_cloud_config import HuaweiCloudConfig
from .vdb.lindorm_config import LindormConfig
from .vdb.milvus_config import MilvusConfig
from .vdb.myscale_config import MyScaleConfig
@@ -263,6 +264,7 @@ class MiddlewareConfig(
VectorStoreConfig,
AnalyticdbConfig,
ChromaConfig,
+ HuaweiCloudConfig,
MilvusConfig,
MyScaleConfig,
OpenSearchConfig,
diff --git a/api/configs/middleware/vdb/huawei_cloud_config.py b/api/configs/middleware/vdb/huawei_cloud_config.py
new file mode 100644
index 0000000000..2290c60499
--- /dev/null
+++ b/api/configs/middleware/vdb/huawei_cloud_config.py
@@ -0,0 +1,25 @@
+from typing import Optional
+
+from pydantic import Field
+from pydantic_settings import BaseSettings
+
+
+class HuaweiCloudConfig(BaseSettings):
+ """
+ Configuration settings for Huawei cloud search service
+ """
+
+ HUAWEI_CLOUD_HOSTS: Optional[str] = Field(
+ description="Hostname or IP address of the Huawei cloud search service instance",
+ default=None,
+ )
+
+ HUAWEI_CLOUD_USER: Optional[str] = Field(
+ description="Username for authenticating with Huawei cloud search service",
+ default=None,
+ )
+
+ HUAWEI_CLOUD_PASSWORD: Optional[str] = Field(
+ description="Password for authenticating with Huawei cloud search service",
+ default=None,
+ )
diff --git a/api/configs/remote_settings_sources/enums.py b/api/configs/remote_settings_sources/enums.py
index 3081f2950f..dd998cac64 100644
--- a/api/configs/remote_settings_sources/enums.py
+++ b/api/configs/remote_settings_sources/enums.py
@@ -3,3 +3,4 @@ from enum import StrEnum
class RemoteSettingsSourceName(StrEnum):
APOLLO = "apollo"
+ NACOS = "nacos"
diff --git a/api/configs/remote_settings_sources/nacos/__init__.py b/api/configs/remote_settings_sources/nacos/__init__.py
new file mode 100644
index 0000000000..b1ce8e87bc
--- /dev/null
+++ b/api/configs/remote_settings_sources/nacos/__init__.py
@@ -0,0 +1,52 @@
+import logging
+import os
+from collections.abc import Mapping
+from typing import Any
+
+from pydantic.fields import FieldInfo
+
+from .http_request import NacosHttpClient
+
+logger = logging.getLogger(__name__)
+
+from configs.remote_settings_sources.base import RemoteSettingsSource
+
+from .utils import _parse_config
+
+
+class NacosSettingsSource(RemoteSettingsSource):
+ def __init__(self, configs: Mapping[str, Any]):
+ self.configs = configs
+ self.remote_configs: dict[str, Any] = {}
+ self.async_init()
+
+ def async_init(self):
+ data_id = os.getenv("DIFY_ENV_NACOS_DATA_ID", "dify-api-env.properties")
+ group = os.getenv("DIFY_ENV_NACOS_GROUP", "nacos-dify")
+ tenant = os.getenv("DIFY_ENV_NACOS_NAMESPACE", "")
+
+ params = {"dataId": data_id, "group": group, "tenant": tenant}
+ try:
+ content = NacosHttpClient().http_request("/nacos/v1/cs/configs", method="GET", headers={}, params=params)
+ self.remote_configs = self._parse_config(content)
+ except Exception as e:
+ logger.exception("[get-access-token] exception occurred")
+ raise
+
+ def _parse_config(self, content: str) -> dict:
+ if not content:
+ return {}
+ try:
+ return _parse_config(self, content)
+ except Exception as e:
+ raise RuntimeError(f"Failed to parse config: {e}")
+
+ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
+ if not isinstance(self.remote_configs, dict):
+ raise ValueError(f"remote configs is not dict, but {type(self.remote_configs)}")
+
+ field_value = self.remote_configs.get(field_name)
+ if field_value is None:
+ return None, field_name, False
+
+ return field_value, field_name, False
diff --git a/api/configs/remote_settings_sources/nacos/http_request.py b/api/configs/remote_settings_sources/nacos/http_request.py
new file mode 100644
index 0000000000..2785bd955b
--- /dev/null
+++ b/api/configs/remote_settings_sources/nacos/http_request.py
@@ -0,0 +1,83 @@
+import base64
+import hashlib
+import hmac
+import logging
+import os
+import time
+
+import requests
+
+logger = logging.getLogger(__name__)
+
+
+class NacosHttpClient:
+ def __init__(self):
+ self.username = os.getenv("DIFY_ENV_NACOS_USERNAME")
+ self.password = os.getenv("DIFY_ENV_NACOS_PASSWORD")
+ self.ak = os.getenv("DIFY_ENV_NACOS_ACCESS_KEY")
+ self.sk = os.getenv("DIFY_ENV_NACOS_SECRET_KEY")
+ self.server = os.getenv("DIFY_ENV_NACOS_SERVER_ADDR", "localhost:8848")
+ self.token = None
+ self.token_ttl = 18000
+ self.token_expire_time: float = 0
+
+ def http_request(self, url, method="GET", headers=None, params=None):
+ try:
+ self._inject_auth_info(headers, params)
+ response = requests.request(method, url="http://" + self.server + url, headers=headers, params=params)
+ response.raise_for_status()
+ return response.text
+ except requests.exceptions.RequestException as e:
+ return f"Request to Nacos failed: {e}"
+
+ def _inject_auth_info(self, headers, params, module="config"):
+ headers.update({"User-Agent": "Nacos-Http-Client-In-Dify:v0.0.1"})
+
+ if module == "login":
+ return
+
+ ts = str(int(time.time() * 1000))
+
+ if self.ak and self.sk:
+ sign_str = self.get_sign_str(params["group"], params["tenant"], ts)
+ headers["Spas-AccessKey"] = self.ak
+ headers["Spas-Signature"] = self.__do_sign(sign_str, self.sk)
+ headers["timeStamp"] = ts
+ if self.username and self.password:
+ self.get_access_token(force_refresh=False)
+ params["accessToken"] = self.token
+
+ def __do_sign(self, sign_str, sk):
+ return (
+ base64.encodebytes(hmac.new(sk.encode(), sign_str.encode(), digestmod=hashlib.sha1).digest())
+ .decode()
+ .strip()
+ )
+
+ def get_sign_str(self, group, tenant, ts):
+ sign_str = ""
+ if tenant:
+ sign_str = tenant + "+"
+ if group:
+ sign_str = sign_str + group + "+"
+ if sign_str:
+ sign_str += ts
+ return sign_str
+
+ def get_access_token(self, force_refresh=False):
+ current_time = time.time()
+ if self.token and not force_refresh and self.token_expire_time > current_time:
+ return self.token
+
+ params = {"username": self.username, "password": self.password}
+ url = "http://" + self.server + "/nacos/v1/auth/login"
+ try:
+ resp = requests.request("POST", url, headers=None, params=params)
+ resp.raise_for_status()
+ response_data = resp.json()
+ self.token = response_data.get("accessToken")
+ self.token_ttl = response_data.get("tokenTtl", 18000)
+ self.token_expire_time = current_time + self.token_ttl - 10
+ except Exception as e:
+ logger.exception("[get-access-token] exception occur")
+ raise
diff --git a/api/configs/remote_settings_sources/nacos/utils.py b/api/configs/remote_settings_sources/nacos/utils.py
new file mode 100644
index 0000000000..f3372563b1
--- /dev/null
+++ b/api/configs/remote_settings_sources/nacos/utils.py
@@ -0,0 +1,31 @@
+def _parse_config(self, content: str) -> dict[str, str]:
+ config: dict[str, str] = {}
+ if not content:
+ return config
+
+ for line in content.splitlines():
+ cleaned_line = line.strip()
+ if not cleaned_line or cleaned_line.startswith(("#", "!")):
+ continue
+
+ separator_index = -1
+ for i, c in enumerate(cleaned_line):
+ if c in ("=", ":") and (i == 0 or cleaned_line[i - 1] != "\\"):
+ separator_index = i
+ break
+
+ if separator_index == -1:
+ continue
+
+ key = cleaned_line[:separator_index].strip()
+ raw_value = cleaned_line[separator_index + 1 :].strip()
+
+ try:
+ decoded_value = bytes(raw_value, "utf-8").decode("unicode_escape")
+ decoded_value = decoded_value.replace(r"\=", "=").replace(r"\:", ":")
+ except UnicodeDecodeError:
+ decoded_value = raw_value
+
+ config[key] = decoded_value
+
+ return config
diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py
index 4644ac6299..752d124735 100644
--- a/api/controllers/console/datasets/datasets.py
+++ b/api/controllers/console/datasets/datasets.py
@@ -664,6 +664,7 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.OPENGAUSS
| VectorType.OCEANBASE
| VectorType.TABLESTORE
+ | VectorType.HUAWEI_CLOUD
| VectorType.TENCENT
):
return {
@@ -710,6 +711,7 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.OCEANBASE
| VectorType.TABLESTORE
| VectorType.TENCENT
+ | VectorType.HUAWEI_CLOUD
):
return {
"retrieval_method": [
diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py
index a5bd2a4bcf..46dee20f8b 100644
--- a/api/controllers/console/workspace/endpoint.py
+++ b/api/controllers/console/workspace/endpoint.py
@@ -5,6 +5,7 @@ from werkzeug.exceptions import Forbidden
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
from core.model_runtime.utils.encoders import jsonable_encoder
+from core.plugin.manager.exc import PluginPermissionDeniedError
from libs.login import login_required
from services.plugin.endpoint_service import EndpointService
@@ -28,15 +29,18 @@ class EndpointCreateApi(Resource):
settings = args["settings"]
name = args["name"]
- return {
- "success": EndpointService.create_endpoint(
- tenant_id=user.current_tenant_id,
- user_id=user.id,
- plugin_unique_identifier=plugin_unique_identifier,
- name=name,
- settings=settings,
- )
- }
+ try:
+ return {
+ "success": EndpointService.create_endpoint(
+ tenant_id=user.current_tenant_id,
+ user_id=user.id,
+ plugin_unique_identifier=plugin_unique_identifier,
+ name=name,
+ settings=settings,
+ )
+ }
+ except PluginPermissionDeniedError as e:
+ raise ValueError(e.description) from e
class EndpointListApi(Resource):
diff --git a/api/controllers/files/upload.py b/api/controllers/files/upload.py
index ca5ea54435..28ee0eecf4 100644
--- a/api/controllers/files/upload.py
+++ b/api/controllers/files/upload.py
@@ -1,3 +1,5 @@
+from mimetypes import guess_extension
+
from flask import request
from flask_restful import Resource, marshal_with # type: ignore
from werkzeug.exceptions import Forbidden
@@ -9,8 +11,8 @@ from controllers.files.error import UnsupportedFileTypeError
from controllers.inner_api.plugin.wraps import get_user
from controllers.service_api.app.error import FileTooLargeError
from core.file.helpers import verify_plugin_file_signature
+from core.tools.tool_file_manager import ToolFileManager
from fields.file_fields import file_fields
-from services.file_service import FileService
class PluginUploadFileApi(Resource):
@@ -51,19 +53,26 @@ class PluginUploadFileApi(Resource):
raise Forbidden("Invalid request.")
try:
- upload_file = FileService.upload_file(
- filename=filename,
- content=file.read(),
+ tool_file = ToolFileManager.create_file_by_raw(
+ user_id=user.id,
+ tenant_id=tenant_id,
+ file_binary=file.read(),
mimetype=mimetype,
- user=user,
- source=None,
+ filename=filename,
+ conversation_id=None,
)
+
+ extension = guess_extension(tool_file.mimetype) or ".bin"
+ preview_url = ToolFileManager.sign_file(tool_file_id=tool_file.id, extension=extension)
+ tool_file.mime_type = mimetype
+ tool_file.extension = extension
+ tool_file.preview_url = preview_url
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
- return upload_file, 201
+ return tool_file, 201
api.add_resource(PluginUploadFileApi, "/files/upload/for-plugin")
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/rag/datasource/vdb/huawei/__init__.py b/api/core/rag/datasource/vdb/huawei/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/core/rag/datasource/vdb/huawei/huawei_cloud_vector.py b/api/core/rag/datasource/vdb/huawei/huawei_cloud_vector.py
new file mode 100644
index 0000000000..89423eb160
--- /dev/null
+++ b/api/core/rag/datasource/vdb/huawei/huawei_cloud_vector.py
@@ -0,0 +1,215 @@
+import json
+import logging
+import ssl
+from typing import Any, Optional
+
+from elasticsearch import Elasticsearch
+from pydantic import BaseModel, model_validator
+
+from configs import dify_config
+from core.rag.datasource.vdb.field import Field
+from core.rag.datasource.vdb.vector_base import BaseVector
+from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
+from core.rag.datasource.vdb.vector_type import VectorType
+from core.rag.embedding.embedding_base import Embeddings
+from core.rag.models.document import Document
+from extensions.ext_redis import redis_client
+from models.dataset import Dataset
+
+logger = logging.getLogger(__name__)
+
+
+def create_ssl_context() -> ssl.SSLContext:
+ ssl_context = ssl.create_default_context()
+ ssl_context.check_hostname = False
+ ssl_context.verify_mode = ssl.CERT_NONE
+ return ssl_context
+
+
+class HuaweiCloudVectorConfig(BaseModel):
+ hosts: str
+ username: str | None
+ password: str | None
+
+ @model_validator(mode="before")
+ @classmethod
+ def validate_config(cls, values: dict) -> dict:
+ if not values["hosts"]:
+ raise ValueError("config HOSTS is required")
+ return values
+
+ def to_elasticsearch_params(self) -> dict[str, Any]:
+ params = {
+ "hosts": self.hosts.split(","),
+ "verify_certs": False,
+ "ssl_show_warn": False,
+ "request_timeout": 30000,
+ "retry_on_timeout": True,
+ "max_retries": 10,
+ }
+ if self.username and self.password:
+ params["basic_auth"] = (self.username, self.password)
+ return params
+
+
+class HuaweiCloudVector(BaseVector):
+ def __init__(self, index_name: str, config: HuaweiCloudVectorConfig):
+ super().__init__(index_name.lower())
+ self._client = Elasticsearch(**config.to_elasticsearch_params())
+
+ def get_type(self) -> str:
+ return VectorType.HUAWEI_CLOUD
+
+ def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
+ uuids = self._get_uuids(documents)
+ for i in range(len(documents)):
+ self._client.index(
+ index=self._collection_name,
+ id=uuids[i],
+ document={
+ Field.CONTENT_KEY.value: documents[i].page_content,
+ Field.VECTOR.value: embeddings[i] or None,
+ Field.METADATA_KEY.value: documents[i].metadata or {},
+ },
+ )
+ self._client.indices.refresh(index=self._collection_name)
+ return uuids
+
+ def text_exists(self, id: str) -> bool:
+ return bool(self._client.exists(index=self._collection_name, id=id))
+
+ def delete_by_ids(self, ids: list[str]) -> None:
+ if not ids:
+ return
+ for id in ids:
+ self._client.delete(index=self._collection_name, id=id)
+
+ def delete_by_metadata_field(self, key: str, value: str) -> None:
+ query_str = {"query": {"match": {f"metadata.{key}": f"{value}"}}}
+ results = self._client.search(index=self._collection_name, body=query_str)
+ ids = [hit["_id"] for hit in results["hits"]["hits"]]
+ if ids:
+ self.delete_by_ids(ids)
+
+ def delete(self) -> None:
+ self._client.indices.delete(index=self._collection_name)
+
+ def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
+ top_k = kwargs.get("top_k", 4)
+
+ query = {
+ "size": top_k,
+ "query": {
+ "vector": {
+ Field.VECTOR.value: {
+ "vector": query_vector,
+ "topk": top_k,
+ }
+ }
+ },
+ }
+
+ results = self._client.search(index=self._collection_name, body=query)
+
+ docs_and_scores = []
+ for hit in results["hits"]["hits"]:
+ docs_and_scores.append(
+ (
+ Document(
+ page_content=hit["_source"][Field.CONTENT_KEY.value],
+ vector=hit["_source"][Field.VECTOR.value],
+ metadata=hit["_source"][Field.METADATA_KEY.value],
+ ),
+ hit["_score"],
+ )
+ )
+
+ docs = []
+ for doc, score in docs_and_scores:
+ score_threshold = float(kwargs.get("score_threshold") or 0.0)
+ if score > score_threshold:
+ if doc.metadata is not None:
+ doc.metadata["score"] = score
+ docs.append(doc)
+
+ return docs
+
+ def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
+ query_str = {"match": {Field.CONTENT_KEY.value: query}}
+ results = self._client.search(index=self._collection_name, query=query_str, size=kwargs.get("top_k", 4))
+ docs = []
+ for hit in results["hits"]["hits"]:
+ docs.append(
+ Document(
+ page_content=hit["_source"][Field.CONTENT_KEY.value],
+ vector=hit["_source"][Field.VECTOR.value],
+ metadata=hit["_source"][Field.METADATA_KEY.value],
+ )
+ )
+
+ return docs
+
+ def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
+ metadatas = [d.metadata if d.metadata is not None else {} for d in texts]
+ self.create_collection(embeddings, metadatas)
+ self.add_texts(texts, embeddings, **kwargs)
+
+ def create_collection(
+ self,
+ embeddings: list[list[float]],
+ metadatas: Optional[list[dict[Any, Any]]] = None,
+ index_params: Optional[dict] = None,
+ ):
+ lock_name = f"vector_indexing_lock_{self._collection_name}"
+ with redis_client.lock(lock_name, timeout=20):
+ collection_exist_cache_key = f"vector_indexing_{self._collection_name}"
+ if redis_client.get(collection_exist_cache_key):
+ logger.info(f"Collection {self._collection_name} already exists.")
+ return
+
+ if not self._client.indices.exists(index=self._collection_name):
+ dim = len(embeddings[0])
+ mappings = {
+ "properties": {
+ Field.CONTENT_KEY.value: {"type": "text"},
+ Field.VECTOR.value: { # Make sure the dimension is correct here
+ "type": "vector",
+ "dimension": dim,
+ "indexing": True,
+ "algorithm": "GRAPH",
+ "metric": "cosine",
+ "neighbors": 32,
+ "efc": 128,
+ },
+ Field.METADATA_KEY.value: {
+ "type": "object",
+ "properties": {
+ "doc_id": {"type": "keyword"} # Map doc_id to keyword type
+ },
+ },
+ }
+ }
+ settings = {"index.vector": True}
+ self._client.indices.create(index=self._collection_name, mappings=mappings, settings=settings)
+
+ redis_client.set(collection_exist_cache_key, 1, ex=3600)
+
+
+class HuaweiCloudVectorFactory(AbstractVectorFactory):
+ def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> HuaweiCloudVector:
+ if dataset.index_struct_dict:
+ class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
+ collection_name = class_prefix.lower()
+ else:
+ dataset_id = dataset.id
+ collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
+ dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.HUAWEI_CLOUD, collection_name))
+
+ return HuaweiCloudVector(
+ index_name=collection_name,
+ config=HuaweiCloudVectorConfig(
+ hosts=dify_config.HUAWEI_CLOUD_HOSTS or "http://localhost:9200",
+ username=dify_config.HUAWEI_CLOUD_USER,
+ password=dify_config.HUAWEI_CLOUD_PASSWORD,
+ ),
+ )
diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py
index 00601c38a1..05158cc7ca 100644
--- a/api/core/rag/datasource/vdb/vector_factory.py
+++ b/api/core/rag/datasource/vdb/vector_factory.py
@@ -156,6 +156,10 @@ class Vector:
from core.rag.datasource.vdb.tablestore.tablestore_vector import TableStoreVectorFactory
return TableStoreVectorFactory
+ case VectorType.HUAWEI_CLOUD:
+ from core.rag.datasource.vdb.huawei.huawei_cloud_vector import HuaweiCloudVectorFactory
+
+ return HuaweiCloudVectorFactory
case _:
raise ValueError(f"Vector store {vector_type} is not supported.")
diff --git a/api/core/rag/datasource/vdb/vector_type.py b/api/core/rag/datasource/vdb/vector_type.py
index 940f12caef..0421be3458 100644
--- a/api/core/rag/datasource/vdb/vector_type.py
+++ b/api/core/rag/datasource/vdb/vector_type.py
@@ -26,3 +26,4 @@ class VectorType(StrEnum):
OCEANBASE = "oceanbase"
OPENGAUSS = "opengauss"
TABLESTORE = "tablestore"
+ HUAWEI_CLOUD = "huawei_cloud"
diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py
index 23ea775dec..4869a21e80 100644
--- a/api/core/rag/retrieval/dataset_retrieval.py
+++ b/api/core/rag/retrieval/dataset_retrieval.py
@@ -869,7 +869,9 @@ class DatasetRetrieval:
)
)
metadata_condition = MetadataCondition(
- logical_operator=metadata_filtering_conditions.logical_operator, # type: ignore
+ logical_operator=metadata_filtering_conditions.logical_operator
+ if metadata_filtering_conditions
+ else "or", # type: ignore
conditions=conditions,
)
elif metadata_filtering_mode == "manual":
@@ -891,10 +893,10 @@ class DatasetRetrieval:
else:
raise ValueError("Invalid metadata filtering mode")
if filters:
- if metadata_filtering_conditions.logical_operator == "or": # type: ignore
- document_query = document_query.filter(or_(*filters))
- else:
+ if metadata_filtering_conditions and metadata_filtering_conditions.logical_operator == "and": # type: ignore
document_query = document_query.filter(and_(*filters))
+ else:
+ document_query = document_query.filter(or_(*filters))
documents = document_query.all()
# group by dataset_id
metadata_filter_document_ids = defaultdict(list) if documents else None # type: ignore
diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
index 07a711cc4e..4ec033572c 100644
--- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
+++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
@@ -349,7 +349,9 @@ class KnowledgeRetrievalNode(LLMNode):
)
)
metadata_condition = MetadataCondition(
- logical_operator=node_data.metadata_filtering_conditions.logical_operator, # type: ignore
+ logical_operator=node_data.metadata_filtering_conditions.logical_operator
+ if node_data.metadata_filtering_conditions
+ else "or", # type: ignore
conditions=conditions,
)
elif node_data.metadata_filtering_mode == "manual":
@@ -380,7 +382,10 @@ class KnowledgeRetrievalNode(LLMNode):
else:
raise ValueError("Invalid metadata filtering mode")
if filters:
- if node_data.metadata_filtering_conditions.logical_operator == "and": # type: ignore
+ if (
+ node_data.metadata_filtering_conditions
+ and node_data.metadata_filtering_conditions.logical_operator == "and"
+ ): # type: ignore
document_query = document_query.filter(and_(*filters))
else:
document_query = document_query.filter(or_(*filters))
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/extensions/ext_otel.py b/api/extensions/ext_otel.py
index 59ec0d0686..a2edd832ec 100644
--- a/api/extensions/ext_otel.py
+++ b/api/extensions/ext_otel.py
@@ -14,7 +14,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExport
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
-from opentelemetry.metrics import get_meter_provider, set_meter_provider
+from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator
@@ -112,6 +112,11 @@ def is_celery_worker():
def init_flask_instrumentor(app: DifyApp):
+ meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
+ _http_response_counter = meter.create_counter(
+ "http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}"
+ )
+
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
@@ -119,6 +124,11 @@ def init_flask_instrumentor(app: DifyApp):
else:
span.set_status(StatusCode.ERROR, status)
+ status = status.split(" ")[0]
+ status_code = int(status)
+ status_class = f"{status_code // 100}xx"
+ _http_response_counter.add(1, {"status_code": status_code, "status_class": status_class})
+
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
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")
diff --git a/api/fields/file_fields.py b/api/fields/file_fields.py
index f896c15f0f..dfc1b623d5 100644
--- a/api/fields/file_fields.py
+++ b/api/fields/file_fields.py
@@ -19,6 +19,7 @@ file_fields = {
"mime_type": fields.String,
"created_by": fields.String,
"created_at": TimestampField,
+ "preview_url": fields.String,
}
remote_file_info_fields = {
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/services/workflow_service.py b/api/services/workflow_service.py
index 5cd5c55746..63e3791147 100644
--- a/api/services/workflow_service.py
+++ b/api/services/workflow_service.py
@@ -289,7 +289,7 @@ class WorkflowService:
params={
"tenant_id": app_model.tenant_id,
"app_id": app_model.id,
- "session_factory": db.session.get_bind,
+ "session_factory": db.session.get_bind(),
}
)
repository.save(workflow_node_execution)
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(),
}
)
diff --git a/api/tests/integration_tests/vdb/__mock/huaweicloudvectordb.py b/api/tests/integration_tests/vdb/__mock/huaweicloudvectordb.py
new file mode 100644
index 0000000000..e1aba4e2c1
--- /dev/null
+++ b/api/tests/integration_tests/vdb/__mock/huaweicloudvectordb.py
@@ -0,0 +1,88 @@
+import os
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+from api.core.rag.datasource.vdb.field import Field
+from elasticsearch import Elasticsearch
+
+
+class MockIndicesClient:
+ def __init__(self):
+ pass
+
+ def create(self, index, mappings, settings):
+ return {"acknowledge": True}
+
+ def refresh(self, index):
+ return {"acknowledge": True}
+
+ def delete(self, index):
+ return {"acknowledge": True}
+
+ def exists(self, index):
+ return True
+
+
+class MockClient:
+ def __init__(self, **kwargs):
+ self.indices = MockIndicesClient()
+
+ def index(self, **kwargs):
+ return {"acknowledge": True}
+
+ def exists(self, **kwargs):
+ return True
+
+ def delete(self, **kwargs):
+ return {"acknowledge": True}
+
+ def search(self, **kwargs):
+ return {
+ "took": 1,
+ "hits": {
+ "hits": [
+ {
+ "_source": {
+ Field.CONTENT_KEY.value: "abcdef",
+ Field.VECTOR.value: [1, 2],
+ Field.METADATA_KEY.value: {},
+ },
+ "_score": 1.0,
+ },
+ {
+ "_source": {
+ Field.CONTENT_KEY.value: "123456",
+ Field.VECTOR.value: [2, 2],
+ Field.METADATA_KEY.value: {},
+ },
+ "_score": 0.9,
+ },
+ {
+ "_source": {
+ Field.CONTENT_KEY.value: "a1b2c3",
+ Field.VECTOR.value: [3, 2],
+ Field.METADATA_KEY.value: {},
+ },
+ "_score": 0.8,
+ },
+ ]
+ },
+ }
+
+
+MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true"
+
+
+@pytest.fixture
+def setup_client_mock(request, monkeypatch: MonkeyPatch):
+ if MOCK:
+ monkeypatch.setattr(Elasticsearch, "__init__", MockClient.__init__)
+ monkeypatch.setattr(Elasticsearch, "index", MockClient.index)
+ monkeypatch.setattr(Elasticsearch, "exists", MockClient.exists)
+ monkeypatch.setattr(Elasticsearch, "delete", MockClient.delete)
+ monkeypatch.setattr(Elasticsearch, "search", MockClient.search)
+
+ yield
+
+ if MOCK:
+ monkeypatch.undo()
diff --git a/api/tests/integration_tests/vdb/huawei/__init__.py b/api/tests/integration_tests/vdb/huawei/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/tests/integration_tests/vdb/huawei/test_huawei_cloud.py b/api/tests/integration_tests/vdb/huawei/test_huawei_cloud.py
new file mode 100644
index 0000000000..943b2bc877
--- /dev/null
+++ b/api/tests/integration_tests/vdb/huawei/test_huawei_cloud.py
@@ -0,0 +1,28 @@
+from core.rag.datasource.vdb.huawei.huawei_cloud_vector import HuaweiCloudVector, HuaweiCloudVectorConfig
+from tests.integration_tests.vdb.__mock.huaweicloudvectordb import setup_client_mock
+from tests.integration_tests.vdb.test_vector_store import AbstractVectorTest, get_example_text, setup_mock_redis
+
+
+class HuaweiCloudVectorTest(AbstractVectorTest):
+ def __init__(self):
+ super().__init__()
+ self.vector = HuaweiCloudVector(
+ "dify",
+ HuaweiCloudVectorConfig(
+ hosts="https://127.0.0.1:9200",
+ username="dify",
+ password="dify",
+ ),
+ )
+
+ def search_by_vector(self):
+ hits_by_vector = self.vector.search_by_vector(query_vector=self.example_embedding)
+ assert len(hits_by_vector) == 3
+
+ def search_by_full_text(self):
+ hits_by_full_text = self.vector.search_by_full_text(query=get_example_text())
+ assert len(hits_by_full_text) == 3
+
+
+def test_huawei_cloud_vector(setup_mock_redis, setup_client_mock):
+ HuaweiCloudVectorTest().run_all_tests()
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
diff --git a/dev/pytest/pytest_vdb.sh b/dev/pytest/pytest_vdb.sh
index c68a94c79b..dd03ca3514 100755
--- a/dev/pytest/pytest_vdb.sh
+++ b/dev/pytest/pytest_vdb.sh
@@ -15,3 +15,4 @@ pytest api/tests/integration_tests/vdb/chroma \
api/tests/integration_tests/vdb/couchbase \
api/tests/integration_tests/vdb/oceanbase \
api/tests/integration_tests/vdb/tidb_vector \
+ api/tests/integration_tests/vdb/huawei \
diff --git a/docker/.env.example b/docker/.env.example
index 82ef4174c2..0b80dccb37 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -574,6 +574,11 @@ OPENGAUSS_MIN_CONNECTION=1
OPENGAUSS_MAX_CONNECTION=5
OPENGAUSS_ENABLE_PQ=false
+# huawei cloud search service vector configurations, only available when VECTOR_STORE is `huawei_cloud`
+HUAWEI_CLOUD_HOSTS=https://127.0.0.1:9200
+HUAWEI_CLOUD_USER=admin
+HUAWEI_CLOUD_PASSWORD=admin
+
# Upstash Vector configuration, only available when VECTOR_STORE is `upstash`
UPSTASH_VECTOR_URL=https://xxx-vector.upstash.io
UPSTASH_VECTOR_TOKEN=dify
@@ -1063,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 def4b77c65..81fa651ed9 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -266,6 +266,9 @@ x-shared-env: &shared-api-worker-env
OPENGAUSS_MIN_CONNECTION: ${OPENGAUSS_MIN_CONNECTION:-1}
OPENGAUSS_MAX_CONNECTION: ${OPENGAUSS_MAX_CONNECTION:-5}
OPENGAUSS_ENABLE_PQ: ${OPENGAUSS_ENABLE_PQ:-false}
+ HUAWEI_CLOUD_HOSTS: ${HUAWEI_CLOUD_HOSTS:-https://127.0.0.1:9200}
+ HUAWEI_CLOUD_USER: ${HUAWEI_CLOUD_USER:-admin}
+ HUAWEI_CLOUD_PASSWORD: ${HUAWEI_CLOUD_PASSWORD:-admin}
UPSTASH_VECTOR_URL: ${UPSTASH_VECTOR_URL:-https://xxx-vector.upstash.io}
UPSTASH_VECTOR_TOKEN: ${UPSTASH_VECTOR_TOKEN:-dify}
TABLESTORE_ENDPOINT: ${TABLESTORE_ENDPOINT:-https://instance-name.cn-hangzhou.ots.aliyuncs.com}
@@ -471,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
@@ -539,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:-}
@@ -1025,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/sdks/php-client/.gitignore b/sdks/php-client/.gitignore
new file mode 100644
index 0000000000..61ead86667
--- /dev/null
+++ b/sdks/php-client/.gitignore
@@ -0,0 +1 @@
+/vendor
diff --git a/sdks/php-client/README.md b/sdks/php-client/README.md
index b0a435bbaf..812980d834 100644
--- a/sdks/php-client/README.md
+++ b/sdks/php-client/README.md
@@ -9,6 +9,21 @@ This is the PHP SDK for the Dify API, which allows you to easily integrate Dify
## Usage
+If you want to try the example, you can run `composer install` in this directory.
+
+In exist project, copy the `dify-client.php` to you project, and merge the following to your `composer.json` file, then run `composer install && composer dump-autoload` to install. Guzzle does not require 7.9, other versions have not been tested, but you can try.
+
+```json
+{
+ "require": {
+ "guzzlehttp/guzzle": "^7.9"
+ },
+ "autoload": {
+ "files": ["path/to/dify-client.php"]
+ }
+}
+```
+
After installing the SDK, you can use it in your project like this:
```php
@@ -16,10 +31,6 @@ After installing the SDK, you can use it in your project like this:
require 'vendor/autoload.php';
-use YourVendorName\DifyPHP\DifyClient;
-use YourVendorName\DifyPHP\CompletionClient;
-use YourVendorName\DifyPHP\ChatClient;
-
$apiKey = 'your-api-key-here';
$difyClient = new DifyClient($apiKey);
diff --git a/sdks/php-client/composer.json b/sdks/php-client/composer.json
new file mode 100644
index 0000000000..6e49e44075
--- /dev/null
+++ b/sdks/php-client/composer.json
@@ -0,0 +1,9 @@
+{
+ "require": {
+ "php": ">=7.2",
+ "guzzlehttp/guzzle": "^7.9"
+ },
+ "autoload": {
+ "files": ["dify-client.php"]
+ }
+}
diff --git a/sdks/php-client/composer.lock b/sdks/php-client/composer.lock
new file mode 100644
index 0000000000..aa07dc03ef
--- /dev/null
+++ b/sdks/php-client/composer.lock
@@ -0,0 +1,663 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "7827c548fdcc7e87cb0ae341dd2c6b1b",
+ "packages": [
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-24T11:22:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T13:27:01+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T12:30:47+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:10:41+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:20:29+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.6.0"
+}
diff --git a/sdks/php-client/dify-client.php b/sdks/php-client/dify-client.php
index ccd61f091a..acb862093a 100644
--- a/sdks/php-client/dify-client.php
+++ b/sdks/php-client/dify-client.php
@@ -1,7 +1,5 @@
api_key = $api_key;
- $this->base_url = $base_url ?? "https://api.dify.ai/v1/";
+ $this->base_url = $base_url ?? 'https://api.dify.ai/v1/';
$this->client = new Client([
'base_uri' => $this->base_url,
'headers' => [
@@ -19,13 +17,6 @@ class DifyClient {
'Content-Type' => 'application/json',
],
]);
- $this->file_client = new Client([
- 'base_uri' => $this->base_url,
- 'headers' => [
- 'Authorization' => 'Bearer ' . $this->api_key,
- 'Content-Type' => 'multipart/form-data',
- ],
- ]);
}
protected function send_request($method, $endpoint, $data = null, $params = null, $stream = false) {
@@ -58,7 +49,7 @@ class DifyClient {
'multipart' => $this->prepareMultipart($data, $files)
];
- return $this->file_client->request('POST', 'files/upload', $options);
+ return $this->client->request('POST', 'files/upload', $options);
}
protected function prepareMultipart($data, $files) {
@@ -132,7 +123,7 @@ class ChatClient extends DifyClient {
public function get_suggestions($message_id, $user) {
$params = [
'user' => $user
- ]
+ ];
return $this->send_request('GET', "messages/{$message_id}/suggested", null, $params);
}
@@ -188,10 +179,9 @@ class ChatClient extends DifyClient {
'user' => $user,
];
$options = [
- 'multipart' => $this->prepareMultipart($data, $files)
+ 'multipart' => $this->prepareMultipart($data, $audio_file)
];
- return $this->file_client->request('POST', 'audio-to-text', $options);
-
+ return $this->client->request('POST', 'audio-to-text', $options);
}
}
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/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
index 90885dacc8..645f6045f0 100644
--- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
+++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
@@ -62,13 +62,13 @@ const SettingsModal: FC