From 37fea072bc116b1661fa98fc124564e72d85e168 Mon Sep 17 00:00:00 2001 From: "Byron.wang" Date: Mon, 21 Oct 2024 19:04:28 +0800 Subject: [PATCH 02/35] enhance: use urllib join instead of fstring (#9549) --- README.md | 2 +- README_CN.md | 2 +- api/services/enterprise/base.py | 4 ++-- .../model_providers/wenxin/test_text_embedding.py | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 75094d39db..f6d14bb840 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Star Dify on GitHub and be instantly notified of new releases. > Before installing Dify, make sure your machine meets the following minimum system requirements: > >- CPU >= 2 Core ->- RAM >= 4GB +>- RAM >= 4 GiB
diff --git a/README_CN.md b/README_CN.md index 4553524ce5..689f98ccf4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -174,7 +174,7 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI 在安装 Dify 之前,请确保您的机器满足以下最低系统要求: - CPU >= 2 Core -- RAM >= 4GB +- RAM >= 4 GiB ### 快速启动 diff --git a/api/services/enterprise/base.py b/api/services/enterprise/base.py index 7d4fdfd2d0..380051b40d 100644 --- a/api/services/enterprise/base.py +++ b/api/services/enterprise/base.py @@ -1,4 +1,5 @@ import os +from urllib.parse import urljoin import requests @@ -15,8 +16,7 @@ class EnterpriseRequest: @classmethod def send_request(cls, method, endpoint, json=None, params=None): headers = {"Content-Type": "application/json", "Enterprise-Api-Secret-Key": cls.secret_key} - - url = f"{cls.base_url}{endpoint}" + url = urljoin(cls.base_url, endpoint) response = requests.request(method, url, json=json, params=params, headers=headers, proxies=cls.proxies) return response.json() diff --git a/api/tests/unit_tests/core/model_runtime/model_providers/wenxin/test_text_embedding.py b/api/tests/unit_tests/core/model_runtime/model_providers/wenxin/test_text_embedding.py index 5b159b49b6..a301f56d75 100644 --- a/api/tests/unit_tests/core/model_runtime/model_providers/wenxin/test_text_embedding.py +++ b/api/tests/unit_tests/core/model_runtime/model_providers/wenxin/test_text_embedding.py @@ -1,3 +1,5 @@ +import string + import numpy as np from core.model_runtime.entities.text_embedding_entities import TextEmbeddingResult @@ -31,7 +33,7 @@ def test_max_chunks(): max_chunks = embedding_model._get_max_chunks(model, credentials) embedding_model._create_text_embedding = _create_text_embedding - texts = ["0123456789" for i in range(0, max_chunks * 2)] + texts = [string.digits for i in range(0, max_chunks * 2)] result: TextEmbeddingResult = embedding_model.invoke(model, credentials, texts, "test") assert len(result.embeddings) == max_chunks * 2 From 9b32bfb3dbdac71f214e612c7b5482549aadf3d9 Mon Sep 17 00:00:00 2001 From: AAEE86 <33052466+AAEE86@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:04:45 +0800 Subject: [PATCH 03/35] feat: Updata tongyi models (#9552) --- .../model_providers/tongyi/llm/qwen-max-longcontext.yaml | 1 + .../model_runtime/model_providers/tongyi/llm/qwen-max.yaml | 4 ++-- .../model_runtime/model_providers/tongyi/llm/qwen-plus.yaml | 2 +- .../model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml index 098494ff95..cc0bb7a117 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max-longcontext.yaml @@ -76,3 +76,4 @@ pricing: output: '0.12' unit: '0.001' currency: RMB +deprecated: true diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml index 9d0d3f8db3..4af4822e86 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-max.yaml @@ -10,7 +10,7 @@ features: - stream-tool-call model_properties: mode: chat - context_size: 8000 + context_size: 32000 parameter_rules: - name: temperature use_template: temperature @@ -26,7 +26,7 @@ parameter_rules: type: int default: 2000 min: 1 - max: 2000 + max: 8192 help: zh_Hans: 用于指定模型在生成内容时token的最大数量,它定义了生成的上限,但不保证每次都会生成到这个数量。 en_US: It is used to specify the maximum number of tokens when the model generates content. It defines the upper limit of generation, but does not guarantee that this number will be generated every time. diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml index 9089e57255..529a29b1b5 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-plus.yaml @@ -10,7 +10,7 @@ features: - stream-tool-call model_properties: mode: chat - context_size: 131072 + context_size: 128000 parameter_rules: - name: temperature use_template: temperature diff --git a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml index 215c9ec5fc..a0c4ba6820 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml +++ b/api/core/model_runtime/model_providers/tongyi/llm/qwen-turbo.yaml @@ -10,7 +10,7 @@ features: - stream-tool-call model_properties: mode: chat - context_size: 8000 + context_size: 128000 parameter_rules: - name: temperature use_template: temperature @@ -26,7 +26,7 @@ parameter_rules: type: int default: 2000 min: 1 - max: 2000 + max: 8192 help: zh_Hans: 用于指定模型在生成内容时token的最大数量,它定义了生成的上限,但不保证每次都会生成到这个数量。 en_US: It is used to specify the maximum number of tokens when the model generates content. It defines the upper limit of generation, but does not guarantee that this number will be generated every time. From 79fe175440c5633bef3939c1e0c573c66ae4856e Mon Sep 17 00:00:00 2001 From: DDDDD12138 <43703884+DDDDD12138@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:04:54 +0800 Subject: [PATCH 04/35] chore: lint code to remove unused imports and variables (#9553) --- web/app/account/account-page/index.tsx | 4 ---- web/app/components/base/audio-btn/index.tsx | 4 +--- .../datasets/create/step-two/language-select/index.tsx | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index b42b481eba..71540ce3b1 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -23,10 +23,6 @@ const titleClassName = ` const descriptionClassName = ` mt-1 text-xs font-normal text-gray-500 ` -const inputClassName = ` - mt-2 w-full px-3 py-2 bg-gray-100 rounded - text-sm font-normal text-gray-800 -` const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ diff --git a/web/app/components/base/audio-btn/index.tsx b/web/app/components/base/audio-btn/index.tsx index d57c79b571..593411ed4d 100644 --- a/web/app/components/base/audio-btn/index.tsx +++ b/web/app/components/base/audio-btn/index.tsx @@ -1,10 +1,9 @@ 'use client' -import { useRef, useState } from 'react' +import { useState } from 'react' import { t } from 'i18next' import { useParams, usePathname } from 'next/navigation' import s from './style.module.css' import Tooltip from '@/app/components/base/tooltip' -import { randomString } from '@/utils' import Loading from '@/app/components/base/loading' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' @@ -28,7 +27,6 @@ const AudioBtn = ({ }: AudioBtnProps) => { const [audioState, setAudioState] = useState('initial') - const selector = useRef(`play-tooltip-${randomString(4)}`) const params = useParams() const pathname = usePathname() const audio_finished_call = (event: string): any => { diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx index fab2bb1c71..41f3e0abb5 100644 --- a/web/app/components/datasets/create/step-two/language-select/index.tsx +++ b/web/app/components/datasets/create/step-two/language-select/index.tsx @@ -24,7 +24,7 @@ const LanguageSelect: FC = ({ disabled={disabled} htmlContent={
- {languages.filter(language => language.supported).map(({ prompt_name, name }) => ( + {languages.filter(language => language.supported).map(({ prompt_name }) => (
Date: Mon, 21 Oct 2024 19:05:44 +0800 Subject: [PATCH 05/35] chore: format get_customizable_model_schema return value (#9335) --- .../model_runtime/docs/en_US/customizable_model_scale_out.md | 2 +- .../docs/zh_Hans/customizable_model_scale_out.md | 2 +- .../model_runtime/model_providers/azure_ai_studio/llm/llm.py | 2 +- .../model_providers/azure_ai_studio/rerank/rerank.py | 2 +- .../model_providers/huggingface_tei/rerank/rerank.py | 2 +- .../huggingface_tei/text_embedding/text_embedding.py | 2 +- api/core/model_runtime/model_providers/localai/llm/llm.py | 4 ++-- .../model_providers/localai/speech2text/speech2text.py | 2 +- .../model_providers/localai/text_embedding/text_embedding.py | 2 +- api/core/model_runtime/model_providers/moonshot/llm/llm.py | 2 +- .../model_providers/openai/speech2text/speech2text.py | 2 +- .../openai_api_compatible/speech2text/speech2text.py | 2 +- api/core/model_runtime/model_providers/openllm/llm/llm.py | 3 ++- api/core/model_runtime/model_providers/sagemaker/llm/llm.py | 2 +- .../model_runtime/model_providers/sagemaker/rerank/rerank.py | 2 +- .../model_providers/sagemaker/speech2text/speech2text.py | 2 +- .../sagemaker/text_embedding/text_embedding.py | 2 +- api/core/model_runtime/model_providers/sagemaker/tts/tts.py | 2 +- api/core/model_runtime/model_providers/siliconflow/llm/llm.py | 2 +- api/core/model_runtime/model_providers/stepfun/llm/llm.py | 2 +- api/core/model_runtime/model_providers/tongyi/llm/llm.py | 2 +- .../model_providers/triton_inference_server/llm/llm.py | 3 ++- .../model_runtime/model_providers/volcengine_maas/llm/llm.py | 3 ++- api/core/model_runtime/model_providers/xinference/llm/llm.py | 4 ++-- .../model_runtime/model_providers/xinference/rerank/rerank.py | 2 +- .../model_providers/xinference/speech2text/speech2text.py | 2 +- .../xinference/text_embedding/text_embedding.py | 2 +- api/core/model_runtime/model_providers/xinference/tts/tts.py | 2 +- 28 files changed, 33 insertions(+), 30 deletions(-) diff --git a/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md b/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md index f5b806ade6..f050919d81 100644 --- a/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md +++ b/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md @@ -218,7 +218,7 @@ For instance, Xinference supports `max_tokens`, `temperature`, and `top_p` param However, some vendors may support different parameters for different models. For example, the `OpenLLM` vendor supports `top_k`, but not all models provided by this vendor support `top_k`. Let's say model A supports `top_k` but model B does not. In such cases, we need to dynamically generate the model parameter schema, as illustrated below: ```python - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md b/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md index 7b3a8edba3..240f65802b 100644 --- a/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md +++ b/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md @@ -205,7 +205,7 @@ provider_credential_schema: 但是有的供应商根据不同的模型支持不同的参数,如供应商`OpenLLM`支持`top_k`,但是并不是这个供应商提供的所有模型都支持`top_k`,我们这里举例A模型支持`top_k`,B模型不支持`top_k`,那么我们需要在这里动态生成模型参数的Schema,如下所示: ```python - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/azure_ai_studio/llm/llm.py b/api/core/model_runtime/model_providers/azure_ai_studio/llm/llm.py index 516ef8b295..53030bad83 100644 --- a/api/core/model_runtime/model_providers/azure_ai_studio/llm/llm.py +++ b/api/core/model_runtime/model_providers/azure_ai_studio/llm/llm.py @@ -294,7 +294,7 @@ class AzureAIStudioLargeLanguageModel(LargeLanguageModel): ], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ Used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/azure_ai_studio/rerank/rerank.py b/api/core/model_runtime/model_providers/azure_ai_studio/rerank/rerank.py index 6ed7ab277c..b6dfb20b66 100644 --- a/api/core/model_runtime/model_providers/azure_ai_studio/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/azure_ai_studio/rerank/rerank.py @@ -148,7 +148,7 @@ class AzureRerankModel(RerankModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError, json.JSONDecodeError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/huggingface_tei/rerank/rerank.py b/api/core/model_runtime/model_providers/huggingface_tei/rerank/rerank.py index 74a1dfc3ff..0bb9a9c8b5 100644 --- a/api/core/model_runtime/model_providers/huggingface_tei/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/huggingface_tei/rerank/rerank.py @@ -118,7 +118,7 @@ class HuggingfaceTeiRerankModel(RerankModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/huggingface_tei/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/huggingface_tei/text_embedding/text_embedding.py index 6b43934538..a0917630a9 100644 --- a/api/core/model_runtime/model_providers/huggingface_tei/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/huggingface_tei/text_embedding/text_embedding.py @@ -189,7 +189,7 @@ class HuggingfaceTeiTextEmbeddingModel(TextEmbeddingModel): return usage - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/localai/llm/llm.py b/api/core/model_runtime/model_providers/localai/llm/llm.py index e7295355f6..756c5571d4 100644 --- a/api/core/model_runtime/model_providers/localai/llm/llm.py +++ b/api/core/model_runtime/model_providers/localai/llm/llm.py @@ -1,5 +1,5 @@ from collections.abc import Generator -from typing import cast +from typing import Optional, cast from httpx import Timeout from openai import ( @@ -212,7 +212,7 @@ class LocalAILanguageModel(LargeLanguageModel): except Exception as ex: raise CredentialsValidateFailedError(f"Invalid credentials {str(ex)}") - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: completion_model = None if credentials["completion_type"] == "chat_completion": completion_model = LLMMode.CHAT.value diff --git a/api/core/model_runtime/model_providers/localai/speech2text/speech2text.py b/api/core/model_runtime/model_providers/localai/speech2text/speech2text.py index 4b9d0f5bfe..260a35d9d4 100644 --- a/api/core/model_runtime/model_providers/localai/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/localai/speech2text/speech2text.py @@ -73,7 +73,7 @@ class LocalAISpeech2text(Speech2TextModel): InvokeBadRequestError: [InvokeBadRequestError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/localai/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/localai/text_embedding/text_embedding.py index b4dfc1a4de..0111c3362a 100644 --- a/api/core/model_runtime/model_providers/localai/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/localai/text_embedding/text_embedding.py @@ -115,7 +115,7 @@ class LocalAITextEmbeddingModel(TextEmbeddingModel): num_tokens += self._get_num_tokens_by_gpt2(text) return num_tokens - def _get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def _get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ Get customizable model schema diff --git a/api/core/model_runtime/model_providers/moonshot/llm/llm.py b/api/core/model_runtime/model_providers/moonshot/llm/llm.py index 3ea46c2967..01a2a07325 100644 --- a/api/core/model_runtime/model_providers/moonshot/llm/llm.py +++ b/api/core/model_runtime/model_providers/moonshot/llm/llm.py @@ -50,7 +50,7 @@ class MoonshotLargeLanguageModel(OAIAPICompatLargeLanguageModel): self._add_custom_parameters(credentials) super().validate_credentials(model, credentials) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: return AIModelEntity( model=model, label=I18nObject(en_US=model, zh_Hans=model), diff --git a/api/core/model_runtime/model_providers/openai/speech2text/speech2text.py b/api/core/model_runtime/model_providers/openai/speech2text/speech2text.py index 0d54d2ea9a..ecf455c6fd 100644 --- a/api/core/model_runtime/model_providers/openai/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/openai/speech2text/speech2text.py @@ -61,7 +61,7 @@ class OpenAISpeech2TextModel(_CommonOpenAI, Speech2TextModel): return response.text - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/speech2text/speech2text.py b/api/core/model_runtime/model_providers/openai_api_compatible/speech2text/speech2text.py index cef77cc941..a490537e51 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/speech2text/speech2text.py @@ -62,7 +62,7 @@ class OAICompatSpeech2TextModel(_CommonOaiApiCompat, Speech2TextModel): except Exception as ex: raise CredentialsValidateFailedError(str(ex)) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/openllm/llm/llm.py b/api/core/model_runtime/model_providers/openllm/llm/llm.py index 34b4de7962..e5ecc748fb 100644 --- a/api/core/model_runtime/model_providers/openllm/llm/llm.py +++ b/api/core/model_runtime/model_providers/openllm/llm/llm.py @@ -1,4 +1,5 @@ from collections.abc import Generator +from typing import Optional from core.model_runtime.entities.common_entities import I18nObject from core.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMResultChunk, LLMResultChunkDelta @@ -193,7 +194,7 @@ class OpenLLMLargeLanguageModel(LargeLanguageModel): ), ) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/sagemaker/llm/llm.py b/api/core/model_runtime/model_providers/sagemaker/llm/llm.py index 97b7692044..dd53914a69 100644 --- a/api/core/model_runtime/model_providers/sagemaker/llm/llm.py +++ b/api/core/model_runtime/model_providers/sagemaker/llm/llm.py @@ -408,7 +408,7 @@ class SageMakerLargeLanguageModel(LargeLanguageModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/sagemaker/rerank/rerank.py b/api/core/model_runtime/model_providers/sagemaker/rerank/rerank.py index 959dff6a21..49c3fa5921 100644 --- a/api/core/model_runtime/model_providers/sagemaker/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/sagemaker/rerank/rerank.py @@ -157,7 +157,7 @@ class SageMakerRerankModel(RerankModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/sagemaker/speech2text/speech2text.py b/api/core/model_runtime/model_providers/sagemaker/speech2text/speech2text.py index 94bae71e53..8fdf68abe1 100644 --- a/api/core/model_runtime/model_providers/sagemaker/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/sagemaker/speech2text/speech2text.py @@ -111,7 +111,7 @@ class SageMakerSpeech2TextModel(Speech2TextModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/sagemaker/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/sagemaker/text_embedding/text_embedding.py index ae7d805b4e..ececfda11a 100644 --- a/api/core/model_runtime/model_providers/sagemaker/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/sagemaker/text_embedding/text_embedding.py @@ -180,7 +180,7 @@ class SageMakerEmbeddingModel(TextEmbeddingModel): InvokeBadRequestError: [KeyError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/sagemaker/tts/tts.py b/api/core/model_runtime/model_providers/sagemaker/tts/tts.py index 1a5afd18f9..6a5946453b 100644 --- a/api/core/model_runtime/model_providers/sagemaker/tts/tts.py +++ b/api/core/model_runtime/model_providers/sagemaker/tts/tts.py @@ -159,7 +159,7 @@ class SageMakerText2SpeechModel(TTSModel): return self._tts_invoke_streaming(model_type, payload, sagemaker_endpoint) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/siliconflow/llm/llm.py b/api/core/model_runtime/model_providers/siliconflow/llm/llm.py index 4f8f4e0f61..6015442c2b 100644 --- a/api/core/model_runtime/model_providers/siliconflow/llm/llm.py +++ b/api/core/model_runtime/model_providers/siliconflow/llm/llm.py @@ -40,7 +40,7 @@ class SiliconflowLargeLanguageModel(OAIAPICompatLargeLanguageModel): credentials["mode"] = "chat" credentials["endpoint_url"] = "https://api.siliconflow.cn/v1" - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: return AIModelEntity( model=model, label=I18nObject(en_US=model, zh_Hans=model), diff --git a/api/core/model_runtime/model_providers/stepfun/llm/llm.py b/api/core/model_runtime/model_providers/stepfun/llm/llm.py index dab666e4d0..43b91a1aec 100644 --- a/api/core/model_runtime/model_providers/stepfun/llm/llm.py +++ b/api/core/model_runtime/model_providers/stepfun/llm/llm.py @@ -50,7 +50,7 @@ class StepfunLargeLanguageModel(OAIAPICompatLargeLanguageModel): self._add_custom_parameters(credentials) super().validate_credentials(model, credentials) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: return AIModelEntity( model=model, label=I18nObject(en_US=model, zh_Hans=model), diff --git a/api/core/model_runtime/model_providers/tongyi/llm/llm.py b/api/core/model_runtime/model_providers/tongyi/llm/llm.py index 3e3585b30a..3a1bb75a59 100644 --- a/api/core/model_runtime/model_providers/tongyi/llm/llm.py +++ b/api/core/model_runtime/model_providers/tongyi/llm/llm.py @@ -535,7 +535,7 @@ class TongyiLargeLanguageModel(LargeLanguageModel): ], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ Architecture for defining customizable models diff --git a/api/core/model_runtime/model_providers/triton_inference_server/llm/llm.py b/api/core/model_runtime/model_providers/triton_inference_server/llm/llm.py index cf7e3f14be..47a4b99214 100644 --- a/api/core/model_runtime/model_providers/triton_inference_server/llm/llm.py +++ b/api/core/model_runtime/model_providers/triton_inference_server/llm/llm.py @@ -1,4 +1,5 @@ from collections.abc import Generator +from typing import Optional from httpx import Response, post from yarl import URL @@ -109,7 +110,7 @@ class TritonInferenceAILargeLanguageModel(LargeLanguageModel): raise NotImplementedError(f"PromptMessage type {type(item)} is not supported") return text - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/volcengine_maas/llm/llm.py b/api/core/model_runtime/model_providers/volcengine_maas/llm/llm.py index dec6c9d789..1c776cec7e 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/llm/llm.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/llm/llm.py @@ -1,5 +1,6 @@ import logging from collections.abc import Generator +from typing import Optional from volcenginesdkarkruntime.types.chat import ChatCompletion, ChatCompletionChunk @@ -298,7 +299,7 @@ class VolcengineMaaSLargeLanguageModel(LargeLanguageModel): chunks = client.stream_chat(prompt_messages, **req_params) return _handle_stream_chat_response(chunks) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/xinference/llm/llm.py b/api/core/model_runtime/model_providers/xinference/llm/llm.py index 0c9d08679a..b82f0430c5 100644 --- a/api/core/model_runtime/model_providers/xinference/llm/llm.py +++ b/api/core/model_runtime/model_providers/xinference/llm/llm.py @@ -1,5 +1,5 @@ from collections.abc import Generator, Iterator -from typing import cast +from typing import Optional, cast from openai import ( APIConnectionError, @@ -321,7 +321,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): return message_dict - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/xinference/rerank/rerank.py b/api/core/model_runtime/model_providers/xinference/rerank/rerank.py index 6368cd76dc..efaf114854 100644 --- a/api/core/model_runtime/model_providers/xinference/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/xinference/rerank/rerank.py @@ -142,7 +142,7 @@ class XinferenceRerankModel(RerankModel): InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError], } - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py b/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py index c5ad383911..3d7aefeb6d 100644 --- a/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py @@ -129,7 +129,7 @@ class XinferenceSpeech2TextModel(Speech2TextModel): return response["text"] - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py index f64b9c50af..e51e6a941c 100644 --- a/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py @@ -184,7 +184,7 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel): return usage - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ diff --git a/api/core/model_runtime/model_providers/xinference/tts/tts.py b/api/core/model_runtime/model_providers/xinference/tts/tts.py index 3f46b50c33..ad7b64efb5 100644 --- a/api/core/model_runtime/model_providers/xinference/tts/tts.py +++ b/api/core/model_runtime/model_providers/xinference/tts/tts.py @@ -116,7 +116,7 @@ class XinferenceText2SpeechModel(TTSModel): """ return self._tts_invoke_streaming(model, credentials, content_text, voice) - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: """ used to define customizable model schema """ From faad247d85b102c7471f872a00961fd503d93bfe Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 21 Oct 2024 19:42:22 +0800 Subject: [PATCH 06/35] fix(upload): correct incorrect dictionary key usage (#9563) --- api/core/app/app_config/features/file_upload/manager.py | 2 +- api/core/prompt/utils/prompt_template_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/app/app_config/features/file_upload/manager.py b/api/core/app/app_config/features/file_upload/manager.py index 6d301f6ea7..12037f8570 100644 --- a/api/core/app/app_config/features/file_upload/manager.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -20,7 +20,7 @@ class FileUploadConfigManager: data = { "image_config": { "number_limits": file_upload_dict["number_limits"], - "transfer_methods": file_upload_dict["allowed_file_upload_methods"], + "transfer_methods": file_upload_dict["allowed_upload_methods"], } } diff --git a/api/core/prompt/utils/prompt_template_parser.py b/api/core/prompt/utils/prompt_template_parser.py index 8111559675..0fd08c5d3c 100644 --- a/api/core/prompt/utils/prompt_template_parser.py +++ b/api/core/prompt/utils/prompt_template_parser.py @@ -33,7 +33,7 @@ class PromptTemplateParser: key = match.group(1) value = inputs.get(key, match.group(0)) # return original matched string if key not found - if remove_template_variables: + if remove_template_variables and isinstance(value, str): return PromptTemplateParser.remove_template_variables(value, self.with_variable_tmpl) return value From 831c2225414cc848bfcd63cad30835e9581c6b9b Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 21 Oct 2024 19:58:57 +0800 Subject: [PATCH 07/35] Fix: file upload support extension .md (#9564) --- web/app/components/base/prompt-editor/constants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx index 4740042135..00360c7bc1 100644 --- a/web/app/components/base/prompt-editor/constants.tsx +++ b/web/app/components/base/prompt-editor/constants.tsx @@ -52,7 +52,7 @@ export const getInputVars = (text: string): ValueSelector[] => { export const FILE_EXTS: Record = { [SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'], - [SupportUploadFileTypes.document]: ['TXT', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'], + [SupportUploadFileTypes.document]: ['TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'], [SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'WEBM', 'AMR'], [SupportUploadFileTypes.video]: ['MP4', 'MOV', 'MPEG', 'MPGA'], } From 5459d812e7bb473ed4849aeee3ed2c8a26c8ea1d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 21 Oct 2024 20:16:46 +0800 Subject: [PATCH 08/35] fix(iteration): handle empty iterator gracefully (#9565) --- api/core/workflow/nodes/iteration/iteration_node.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index b28ae0a85c..7503c13ce8 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -46,6 +46,15 @@ class IterationNode(BaseNode[IterationNodeData]): if not iterator_list_segment: raise ValueError(f"Iterator variable {self.node_data.iterator_selector} not found") + if len(iterator_list_segment.value) == 0: + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + outputs={"output": []}, + ) + ) + return + iterator_list_value = iterator_list_segment.to_object() if not isinstance(iterator_list_value, list): From 4ae0bb83f1d1acfd08173f3e4177e97937441497 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 21 Oct 2024 20:40:47 +0800 Subject: [PATCH 09/35] fix(file upload): correct upload method key for image config (#9568) --- api/core/app/app_config/features/file_upload/manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/core/app/app_config/features/file_upload/manager.py b/api/core/app/app_config/features/file_upload/manager.py index 12037f8570..42beec2535 100644 --- a/api/core/app/app_config/features/file_upload/manager.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -17,10 +17,13 @@ class FileUploadConfigManager: file_upload_dict = config.get("file_upload") if file_upload_dict: if file_upload_dict.get("enabled"): + transform_methods = file_upload_dict.get("allowed_file_upload_methods") or file_upload_dict.get( + "allowed_upload_methods", [] + ) data = { "image_config": { "number_limits": file_upload_dict["number_limits"], - "transfer_methods": file_upload_dict["allowed_upload_methods"], + "transfer_methods": transform_methods, } } From 8e987593594257a0266c205097b68e15c268191a Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 21 Oct 2024 22:52:21 +0800 Subject: [PATCH 10/35] Fix: style of features panel in safari (#9573) --- .../base/features/feature-panel/index.tsx | 119 ------- .../feature-panel/opening-statement/index.tsx | 328 ------------------ .../new-feature-panel/dialog-wrapper.tsx | 2 +- 3 files changed, 1 insertion(+), 448 deletions(-) delete mode 100644 web/app/components/base/features/feature-panel/index.tsx delete mode 100644 web/app/components/base/features/feature-panel/opening-statement/index.tsx diff --git a/web/app/components/base/features/feature-panel/index.tsx b/web/app/components/base/features/feature-panel/index.tsx deleted file mode 100644 index 72799ef2fc..0000000000 --- a/web/app/components/base/features/feature-panel/index.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { - memo, - useMemo, -} from 'react' -import { useTranslation } from 'react-i18next' -import type { OnFeaturesChange } from '../types' -import { useFeatures } from '../hooks' -import FileUpload from './file-upload' -import OpeningStatement from './opening-statement' -import type { OpeningStatementProps } from './opening-statement' -import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer' -import TextToSpeech from './text-to-speech' -import SpeechToText from './speech-to-text' -import Citation from './citation' -import Moderation from './moderation' -import type { InputVar } from '@/app/components/workflow/types' - -export type FeaturePanelProps = { - onChange?: OnFeaturesChange - openingStatementProps: OpeningStatementProps - disabled?: boolean - workflowVariables: InputVar[] -} -const FeaturePanel = ({ - onChange, - openingStatementProps, - disabled, - workflowVariables, -}: FeaturePanelProps) => { - const { t } = useTranslation() - const features = useFeatures(s => s.features) - - const showAdvanceFeature = useMemo(() => { - return features.opening?.enabled || features.suggested?.enabled || features.speech2text?.enabled || features.text2speech?.enabled || features.citation?.enabled - }, [features]) - - const showToolFeature = useMemo(() => { - return features.moderation?.enabled - }, [features]) - - return ( -
- - { - showAdvanceFeature && ( -
-
-
- {t('appDebug.feature.groupChat.title')} -
-
-
-
- { - features.opening?.enabled && ( - - ) - } - { - features.suggested?.enabled && ( - - ) - } - { - features.text2speech?.enabled && ( - - ) - } - { - features.speech2text?.enabled && ( - - ) - } - { - features.citation?.enabled && ( - - ) - } -
-
- ) - } - { - showToolFeature && ( -
-
-
- {t('appDebug.feature.groupChat.title')} -
-
-
-
- { - features.moderation?.enabled && ( - - ) - } -
-
- ) - } -
- ) -} -export default memo(FeaturePanel) diff --git a/web/app/components/base/features/feature-panel/opening-statement/index.tsx b/web/app/components/base/features/feature-panel/opening-statement/index.tsx deleted file mode 100644 index 1f102700ad..0000000000 --- a/web/app/components/base/features/feature-panel/opening-statement/index.tsx +++ /dev/null @@ -1,328 +0,0 @@ -/* eslint-disable multiline-ternary */ -'use client' -import type { FC } from 'react' -import React, { useEffect, useRef, useState } from 'react' -import produce from 'immer' -import { - RiAddLine, - RiDeleteBinLine, -} from '@remixicon/react' -import { useTranslation } from 'react-i18next' -import { useBoolean } from 'ahooks' -import { ReactSortable } from 'react-sortablejs' -import { - useFeatures, - useFeaturesStore, -} from '../../hooks' -import type { OnFeaturesChange } from '../../types' -import cn from '@/utils/classnames' -import Panel from '@/app/components/app/configuration/base/feature-panel' -import Button from '@/app/components/base/button' -import OperationBtn from '@/app/components/app/configuration/base/operation-btn' -import { getInputKeys } from '@/app/components/base/block-input' -import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var' -import { getNewVar } from '@/utils/var' -import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight' -import type { PromptVariable } from '@/models/debug' -import type { InputVar } from '@/app/components/workflow/types' - -const MAX_QUESTION_NUM = 5 - -export type OpeningStatementProps = { - onChange?: OnFeaturesChange - readonly?: boolean - promptVariables?: PromptVariable[] - onAutoAddPromptVariable: (variable: PromptVariable[]) => void - workflowVariables?: InputVar[] -} - -// regex to match the {{}} and replace it with a span -const regex = /\{\{([^}]+)\}\}/g - -const OpeningStatement: FC = ({ - onChange, - readonly, - promptVariables = [], - onAutoAddPromptVariable, - workflowVariables = [], -}) => { - const { t } = useTranslation() - const featureStore = useFeaturesStore() - const openingStatement = useFeatures(s => s.features.opening) - const value = openingStatement?.opening_statement || '' - const suggestedQuestions = openingStatement?.suggested_questions || [] - const [notIncludeKeys, setNotIncludeKeys] = useState([]) - - const hasValue = !!(value || '').trim() - const inputRef = useRef(null) - - const [isFocus, { setTrue: didSetFocus, setFalse: setBlur }] = useBoolean(false) - - const setFocus = () => { - didSetFocus() - setTimeout(() => { - const input = inputRef.current - if (input) { - input.focus() - input.setSelectionRange(input.value.length, input.value.length) - } - }, 0) - } - - const [tempValue, setTempValue] = useState(value) - useEffect(() => { - setTempValue(value || '') - }, [value]) - - const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(suggestedQuestions || []) - const notEmptyQuestions = tempSuggestedQuestions.filter(question => !!question && question.trim()) - const coloredContent = (tempValue || '') - .replace(//g, '>') - .replace(regex, varHighlightHTML({ name: '$1' })) // `{{$1}}` - .replace(/\n/g, '
') - - const handleEdit = () => { - if (readonly) - return - setFocus() - } - - const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false) - - const handleCancel = () => { - setBlur() - setTempValue(value) - setTempSuggestedQuestions(suggestedQuestions) - } - - const handleConfirm = () => { - const keys = getInputKeys(tempValue) - const promptKeys = promptVariables.map(item => item.key) - const workflowVariableKeys = workflowVariables.map(item => item.variable) - let notIncludeKeys: string[] = [] - - if (promptKeys.length === 0 && workflowVariables.length === 0) { - if (keys.length > 0) - notIncludeKeys = keys - } - else { - if (workflowVariables.length > 0) - notIncludeKeys = keys.filter(key => !workflowVariableKeys.includes(key)) - - else notIncludeKeys = keys.filter(key => !promptKeys.includes(key)) - } - - if (notIncludeKeys.length > 0) { - setNotIncludeKeys(notIncludeKeys) - showConfirmAddVar() - return - } - setBlur() - const { getState } = featureStore! - const { - features, - setFeatures, - } = getState() - - const newFeatures = produce(features, (draft) => { - if (draft.opening) { - draft.opening.opening_statement = tempValue - draft.opening.suggested_questions = tempSuggestedQuestions - } - }) - setFeatures(newFeatures) - - if (onChange) - onChange(newFeatures) - } - - const cancelAutoAddVar = () => { - const { getState } = featureStore! - const { - features, - setFeatures, - } = getState() - - const newFeatures = produce(features, (draft) => { - if (draft.opening) - draft.opening.opening_statement = tempValue - }) - setFeatures(newFeatures) - - if (onChange) - onChange(newFeatures) - hideConfirmAddVar() - setBlur() - } - - const autoAddVar = () => { - const { getState } = featureStore! - const { - features, - setFeatures, - } = getState() - - const newFeatures = produce(features, (draft) => { - if (draft.opening) - draft.opening.opening_statement = tempValue - }) - setFeatures(newFeatures) - if (onChange) - onChange(newFeatures) - onAutoAddPromptVariable([...notIncludeKeys.map(key => getNewVar(key, 'string'))]) - hideConfirmAddVar() - setBlur() - } - - const headerRight = !readonly ? ( - isFocus ? ( -
- - -
- ) : ( - - ) - ) : null - - const renderQuestions = () => { - return isFocus ? ( -
-
-
-
{t('appDebug.openingStatement.openingQuestion')}
-
·
-
{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}
-
-
-
- { - return { - id: index, - name, - } - })} - setList={list => setTempSuggestedQuestions(list.map(item => item.name))} - handle='.handle' - ghostClass="opacity-50" - animation={150} - > - {tempSuggestedQuestions.map((question, index) => { - return ( -
-
- - - -
- { - const value = e.target.value - setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => { - if (index === i) - return value - - return item - })) - }} - className={'w-full overflow-x-auto pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer rounded-lg'} - /> - -
{ - setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i)) - }} - > - -
-
- ) - })}
- {tempSuggestedQuestions.length < MAX_QUESTION_NUM && ( -
{ setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }} - className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'> - -
{t('appDebug.variableConfig.addOption')}
-
- )} -
- ) : ( -
- {notEmptyQuestions.map((question, index) => { - return ( -
- {question} -
- ) - })} -
- ) - } - - return ( - - - - } - headerRight={headerRight} - hasHeaderBottomBorder={!hasValue} - isFocus={isFocus} - > -
- {(hasValue || (!hasValue && isFocus)) ? ( - <> - {isFocus - ? ( -
- -
- ) - : ( -
- )} - {renderQuestions()} - ) : ( -
{t('appDebug.openingStatement.noDataPlaceHolder')}
- )} - - {isShowConfirmAddVar && ( - - )} - -
-
- ) -} -export default React.memo(OpeningStatement) diff --git a/web/app/components/base/features/new-feature-panel/dialog-wrapper.tsx b/web/app/components/base/features/new-feature-panel/dialog-wrapper.tsx index 4975a06d15..e2b03faad2 100644 --- a/web/app/components/base/features/new-feature-panel/dialog-wrapper.tsx +++ b/web/app/components/base/features/new-feature-panel/dialog-wrapper.tsx @@ -45,7 +45,7 @@ const DialogWrapper = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - + {children} From 495cf580144fb57664043d9d7ac00a0e801581ea Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Mon, 21 Oct 2024 23:32:09 +0800 Subject: [PATCH 11/35] dep: bump pydantic to 2.9 (#9077) --- api/poetry.lock | 199 +++++++++++++++++++++++---------------------- api/pyproject.toml | 4 +- 2 files changed, 102 insertions(+), 101 deletions(-) diff --git a/api/poetry.lock b/api/poetry.lock index 9239b1f887..384222f2f0 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -6921,119 +6921,120 @@ files = [ [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -7063,13 +7064,13 @@ semver = ["semver (>=3.0.2)"] [[package]] name = "pydantic-settings" -version = "2.4.0" +version = "2.6.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, - {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, + {file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"}, + {file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"}, ] [package.dependencies] @@ -10795,4 +10796,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "642b2dae9e18ee6671d3d2c7129cb9a77327b69dacba996d00de2a9475d5bad3" +content-hash = "5b102e3bc077ed730e9fb7be9015541111ffe7787888372d50a757aecb1d9eff" diff --git a/api/pyproject.toml b/api/pyproject.toml index c2c62ffecd..fdf6e93bf0 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -163,8 +163,8 @@ openpyxl = "~3.1.5" pandas = { version = "~2.2.2", extras = ["performance", "excel"] } psycopg2-binary = "~2.9.6" pycryptodome = "3.19.1" -pydantic = "~2.8.2" -pydantic-settings = "~2.4.0" +pydantic = "~2.9.2" +pydantic-settings = "~2.6.0" pydantic_extra_types = "~2.9.0" pyjwt = "~2.8.0" pypdfium2 = "~4.17.0" From 740a723072902b444519a3fe8512034bc3b1422f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 21 Oct 2024 23:33:16 +0800 Subject: [PATCH 12/35] fix(validation): improve variable handling and validation (#9578) --- .../easy_ui_based_app/variables/manager.py | 4 ++-- api/core/app/app_config/entities.py | 12 +++++++++++- api/core/tools/utils/workflow_configuration_sync.py | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/core/app/app_config/easy_ui_based_app/variables/manager.py b/api/core/app/app_config/easy_ui_based_app/variables/manager.py index 126eb0b41e..2f2445a336 100644 --- a/api/core/app/app_config/easy_ui_based_app/variables/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/variables/manager.py @@ -53,11 +53,11 @@ class BasicVariablesConfigManager: VariableEntity( type=variable_type, variable=variable.get("variable"), - description=variable.get("description", ""), + description=variable.get("description") or "", label=variable.get("label"), required=variable.get("required", False), max_length=variable.get("max_length"), - options=variable.get("options", []), + options=variable.get("options") or [], ) ) diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index d8fa08c0a3..6c6e342a07 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -2,7 +2,7 @@ from collections.abc import Sequence from enum import Enum from typing import Any, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from core.file import FileExtraConfig, FileTransferMethod, FileType from core.model_runtime.entities.message_entities import PromptMessageRole @@ -114,6 +114,16 @@ class VariableEntity(BaseModel): allowed_file_extensions: Sequence[str] = Field(default_factory=list) allowed_file_upload_methods: Sequence[FileTransferMethod] = Field(default_factory=list) + @field_validator("description", mode="before") + @classmethod + def convert_none_description(cls, v: Any) -> str: + return v or "" + + @field_validator("options", mode="before") + @classmethod + def convert_none_options(cls, v: Any) -> Sequence[str]: + return v or [] + class ExternalDataVariableEntity(BaseModel): """ diff --git a/api/core/tools/utils/workflow_configuration_sync.py b/api/core/tools/utils/workflow_configuration_sync.py index 3ea07b75aa..d92bfb9b90 100644 --- a/api/core/tools/utils/workflow_configuration_sync.py +++ b/api/core/tools/utils/workflow_configuration_sync.py @@ -22,7 +22,7 @@ class WorkflowToolConfigurationUtils: if not start_node: return [] - return [VariableEntity(**variable) for variable in start_node.get("data", {}).get("variables", [])] + return [VariableEntity.model_validate(variable) for variable in start_node.get("data", {}).get("variables", [])] @classmethod def check_is_synced( From 38a4f0234ddcb0fe90504763af44562f9116664b Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 21 Oct 2024 23:35:25 +0800 Subject: [PATCH 13/35] fix(http_request): handle empty and string data inputs (#9579) --- api/core/workflow/nodes/http_request/entities.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/http_request/entities.py b/api/core/workflow/nodes/http_request/entities.py index 816ece9577..dec76a277e 100644 --- a/api/core/workflow/nodes/http_request/entities.py +++ b/api/core/workflow/nodes/http_request/entities.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from typing import Literal, Optional +from typing import Any, Literal, Optional import httpx from pydantic import BaseModel, Field, ValidationInfo, field_validator @@ -52,6 +52,16 @@ class HttpRequestNodeBody(BaseModel): type: Literal["none", "form-data", "x-www-form-urlencoded", "raw-text", "json", "binary"] data: Sequence[BodyData] = Field(default_factory=list) + @field_validator("data", mode="before") + @classmethod + def check_data(cls, v: Any): + """For compatibility, if body is not set, return empty list.""" + if not v: + return [] + if isinstance(v, str): + return [BodyData(key="", type="text", value=v)] + return v + class HttpRequestNodeTimeout(BaseModel): connect: int = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT From c0636175539118f1c8afa53fc2b2ae3326dd4a87 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 00:42:40 +0800 Subject: [PATCH 14/35] fix(workflow): improve database session handling and variable management (#9581) --- .../task_pipeline/workflow_cycle_manage.py | 44 ++++++++++--------- api/core/workflow/entities/variable_pool.py | 3 -- api/core/workflow/nodes/llm/node.py | 8 ++-- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 138503d404..2abee5bef5 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -4,6 +4,8 @@ from collections.abc import Mapping, Sequence from datetime import datetime, timezone from typing import Any, Optional, Union, cast +from sqlalchemy.orm import Session + from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.queue_entities import ( QueueIterationCompletedEvent, @@ -232,30 +234,30 @@ class WorkflowCycleManage: self, workflow_run: WorkflowRun, event: QueueNodeStartedEvent ) -> WorkflowNodeExecution: # init workflow node execution - workflow_node_execution = WorkflowNodeExecution() - workflow_node_execution.tenant_id = workflow_run.tenant_id - workflow_node_execution.app_id = workflow_run.app_id - workflow_node_execution.workflow_id = workflow_run.workflow_id - workflow_node_execution.triggered_from = WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value - workflow_node_execution.workflow_run_id = workflow_run.id - workflow_node_execution.predecessor_node_id = event.predecessor_node_id - workflow_node_execution.index = event.node_run_index - workflow_node_execution.node_execution_id = event.node_execution_id - workflow_node_execution.node_id = event.node_id - workflow_node_execution.node_type = event.node_type.value - workflow_node_execution.title = event.node_data.title - workflow_node_execution.status = WorkflowNodeExecutionStatus.RUNNING.value - workflow_node_execution.created_by_role = workflow_run.created_by_role - workflow_node_execution.created_by = workflow_run.created_by - workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None) - db.session.add(workflow_node_execution) - db.session.commit() - db.session.refresh(workflow_node_execution) - db.session.close() + with Session(db.engine, expire_on_commit=False) as session: + workflow_node_execution = WorkflowNodeExecution() + workflow_node_execution.tenant_id = workflow_run.tenant_id + workflow_node_execution.app_id = workflow_run.app_id + workflow_node_execution.workflow_id = workflow_run.workflow_id + workflow_node_execution.triggered_from = WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value + workflow_node_execution.workflow_run_id = workflow_run.id + workflow_node_execution.predecessor_node_id = event.predecessor_node_id + workflow_node_execution.index = event.node_run_index + workflow_node_execution.node_execution_id = event.node_execution_id + workflow_node_execution.node_id = event.node_id + workflow_node_execution.node_type = event.node_type.value + workflow_node_execution.title = event.node_data.title + workflow_node_execution.status = WorkflowNodeExecutionStatus.RUNNING.value + workflow_node_execution.created_by_role = workflow_run.created_by_role + workflow_node_execution.created_by = workflow_run.created_by + workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None) + + session.add(workflow_node_execution) + session.commit() + session.refresh(workflow_node_execution) self._wip_workflow_node_executions[workflow_node_execution.node_execution_id] = workflow_node_execution - return workflow_node_execution def _handle_workflow_node_execution_success(self, event: QueueNodeSucceededEvent) -> WorkflowNodeExecution: diff --git a/api/core/workflow/entities/variable_pool.py b/api/core/workflow/entities/variable_pool.py index 5f932c0a8e..2a7c7234ea 100644 --- a/api/core/workflow/entities/variable_pool.py +++ b/api/core/workflow/entities/variable_pool.py @@ -96,9 +96,6 @@ class VariablePool(BaseModel): if len(selector) < 2: raise ValueError("Invalid selector") - if value is None: - return - if isinstance(value, Segment): v = value else: diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 24e479153e..f310a67b76 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -320,11 +320,11 @@ class LLMNode(BaseNode[LLMNodeData]): variable_selectors = variable_template_parser.extract_variable_selectors() for variable_selector in variable_selectors: - variable_value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector) + variable_value = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) if variable_value is None: raise ValueError(f"Variable {variable_selector.variable} not found") - inputs[variable_selector.variable] = variable_value + inputs[variable_selector.variable] = variable_value.to_object() memory = node_data.memory if memory and memory.query_prompt_template: @@ -332,11 +332,11 @@ class LLMNode(BaseNode[LLMNodeData]): template=memory.query_prompt_template ).extract_variable_selectors() for variable_selector in query_variable_selectors: - variable_value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector) + variable_value = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) if variable_value is None: raise ValueError(f"Variable {variable_selector.variable} not found") - inputs[variable_selector.variable] = variable_value + inputs[variable_selector.variable] = variable_value.to_object() return inputs From 2e657b7b12916eb64c4686285bf66dd1fbf5c160 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 08:59:04 +0800 Subject: [PATCH 15/35] fix(workflow): handle NoneSegments in variable extraction (#9585) --- api/core/workflow/nodes/llm/node.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index f310a67b76..4179d34f9a 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -22,7 +22,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder from core.prompt.advanced_prompt_transform import AdvancedPromptTransform from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig from core.prompt.utils.prompt_message_util import PromptMessageUtil -from core.variables import ArrayAnySegment, ArrayFileSegment, FileSegment +from core.variables import ArrayAnySegment, ArrayFileSegment, FileSegment, NoneSegment from core.workflow.constants import SYSTEM_VARIABLE_NODE_ID from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult from core.workflow.enums import SystemVariableKey @@ -320,11 +320,12 @@ class LLMNode(BaseNode[LLMNodeData]): variable_selectors = variable_template_parser.extract_variable_selectors() for variable_selector in variable_selectors: - variable_value = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) - if variable_value is None: + variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) + if variable is None: raise ValueError(f"Variable {variable_selector.variable} not found") - - inputs[variable_selector.variable] = variable_value.to_object() + if isinstance(variable, NoneSegment): + continue + inputs[variable_selector.variable] = variable.to_object() memory = node_data.memory if memory and memory.query_prompt_template: @@ -332,11 +333,12 @@ class LLMNode(BaseNode[LLMNodeData]): template=memory.query_prompt_template ).extract_variable_selectors() for variable_selector in query_variable_selectors: - variable_value = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) - if variable_value is None: + variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) + if variable is None: raise ValueError(f"Variable {variable_selector.variable} not found") - - inputs[variable_selector.variable] = variable_value.to_object() + if isinstance(variable, NoneSegment): + continue + inputs[variable_selector.variable] = variable.to_object() return inputs From ac24300274ba5e7d304773cc60cf15bd5555396d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 09:00:21 +0800 Subject: [PATCH 16/35] refactor(template_transform): use keyword-only arguments (#9575) --- .../nodes/template_transform/template_transform_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index 857a693c5b..b85271ddc6 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -56,7 +56,7 @@ class TemplateTransformNode(BaseNode[TemplateTransformNodeData]): @classmethod def _extract_variable_selector_to_variable_mapping( - cls, graph_config: Mapping[str, Any], node_id: str, node_data: TemplateTransformNodeData + cls, *, graph_config: Mapping[str, Any], node_id: str, node_data: TemplateTransformNodeData ) -> Mapping[str, Sequence[str]]: """ Extract variable selector to variable mapping From 83b2b8fe60dc5301a9b6c6801e09fd7549cddbd5 Mon Sep 17 00:00:00 2001 From: zhuhao <37029601+hwzhuhao@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:00:44 +0800 Subject: [PATCH 17/35] refactor: add logging extension module for log initialization (#9524) --- api/app_factory.py | 41 ++------------------------------- api/extensions/ext_logging.py | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 api/extensions/ext_logging.py diff --git a/api/app_factory.py b/api/app_factory.py index 04654c2699..b7bfe947f5 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -10,9 +10,6 @@ if os.environ.get("DEBUG", "false").lower() != "true": grpc.experimental.gevent.init_gevent() import json -import logging -import sys -from logging.handlers import RotatingFileHandler from flask import Flask, Response, request from flask_cors import CORS @@ -27,6 +24,7 @@ from extensions import ( ext_compress, ext_database, ext_hosting_provider, + ext_logging, ext_login, ext_mail, ext_migrate, @@ -70,43 +68,7 @@ def create_flask_app_with_configs() -> Flask: def create_app() -> Flask: app = create_flask_app_with_configs() - app.secret_key = app.config["SECRET_KEY"] - - log_handlers = None - log_file = app.config.get("LOG_FILE") - if log_file: - log_dir = os.path.dirname(log_file) - os.makedirs(log_dir, exist_ok=True) - log_handlers = [ - RotatingFileHandler( - filename=log_file, - maxBytes=1024 * 1024 * 1024, - backupCount=5, - ), - logging.StreamHandler(sys.stdout), - ] - - logging.basicConfig( - level=app.config.get("LOG_LEVEL"), - format=app.config.get("LOG_FORMAT"), - datefmt=app.config.get("LOG_DATEFORMAT"), - handlers=log_handlers, - force=True, - ) - log_tz = app.config.get("LOG_TZ") - if log_tz: - from datetime import datetime - - import pytz - - timezone = pytz.timezone(log_tz) - - def time_converter(seconds): - return datetime.utcfromtimestamp(seconds).astimezone(timezone).timetuple() - - for handler in logging.root.handlers: - handler.formatter.converter = time_converter initialize_extensions(app) register_blueprints(app) register_commands(app) @@ -117,6 +79,7 @@ def create_app() -> Flask: def initialize_extensions(app): # Since the application instance is now created, pass it to each Flask # extension instance to bind it to the Flask application instance (app) + ext_logging.init_app(app) ext_compress.init_app(app) ext_code_based_extension.init() ext_database.init_app(app) diff --git a/api/extensions/ext_logging.py b/api/extensions/ext_logging.py new file mode 100644 index 0000000000..22f868881f --- /dev/null +++ b/api/extensions/ext_logging.py @@ -0,0 +1,43 @@ +import logging +import os +import sys +from logging.handlers import RotatingFileHandler + +from flask import Flask + + +def init_app(app: Flask): + log_handlers = None + log_file = app.config.get("LOG_FILE") + if log_file: + log_dir = os.path.dirname(log_file) + os.makedirs(log_dir, exist_ok=True) + log_handlers = [ + RotatingFileHandler( + filename=log_file, + maxBytes=1024 * 1024 * 1024, + backupCount=5, + ), + logging.StreamHandler(sys.stdout), + ] + + logging.basicConfig( + level=app.config.get("LOG_LEVEL"), + format=app.config.get("LOG_FORMAT"), + datefmt=app.config.get("LOG_DATEFORMAT"), + handlers=log_handlers, + force=True, + ) + log_tz = app.config.get("LOG_TZ") + if log_tz: + from datetime import datetime + + import pytz + + timezone = pytz.timezone(log_tz) + + def time_converter(seconds): + return datetime.utcfromtimestamp(seconds).astimezone(timezone).timetuple() + + for handler in logging.root.handlers: + handler.formatter.converter = time_converter From 3f1c84f65a3f0dcb261e0596f4a7ecbeb42cbcde Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Tue, 22 Oct 2024 09:18:31 +0800 Subject: [PATCH 18/35] chore: cleanup ineffective linter rules exclusions (#9580) --- api/pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index fdf6e93bf0..1334b37866 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -87,14 +87,6 @@ ignore = [ "tests/*" = [ "F811", # redefined-while-unused "F401", # unused-import - "PT001", # missing-function-docstring - "PT004", # missing-parameter-docstring -] -"core/rag/extractor/word_extractor.py" = [ - "RUF100", # Unused `noqa` directive -] -"core/tools/provider/builtin/gitlab/tools/gitlab_commits.py" = [ - "PLR1714", # Consider merging multiple comparisons ] [tool.ruff.lint.pyflakes] From 5838345f48c10879168aa80fad8fc7d388ad06db Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 10:49:03 +0800 Subject: [PATCH 19/35] fix(entities): add validator for `VisionConfig` to handle None values (#9598) --- api/core/workflow/nodes/llm/entities.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/llm/entities.py b/api/core/workflow/nodes/llm/entities.py index b4de312461..a25d563fe0 100644 --- a/api/core/workflow/nodes/llm/entities.py +++ b/api/core/workflow/nodes/llm/entities.py @@ -1,7 +1,7 @@ from collections.abc import Sequence from typing import Any, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from core.model_runtime.entities import ImagePromptMessageContent from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig @@ -30,6 +30,13 @@ class VisionConfig(BaseModel): enabled: bool = False configs: VisionConfigOptions = Field(default_factory=VisionConfigOptions) + @field_validator("configs", mode="before") + @classmethod + def convert_none_configs(cls, v: Any): + if v is None: + return VisionConfigOptions() + return v + class PromptConfig(BaseModel): jinja2_variables: Optional[list[VariableSelector]] = None From 8f670f31b87160d3947ab5c13ad7a0c742d0077c Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 10:49:19 +0800 Subject: [PATCH 20/35] refactor(variables): replace deprecated 'get_any' with 'get' method (#9584) --- api/core/workflow/entities/variable_pool.py | 21 ------ api/core/workflow/nodes/code/code_node.py | 13 ++-- .../nodes/iteration/iteration_node.py | 35 +++++++--- .../knowledge_retrieval_node.py | 11 +++- api/core/workflow/nodes/llm/node.py | 65 ++++++++++--------- .../template_transform_node.py | 9 ++- .../variable_aggregator_node.py | 14 ++-- .../workflow/nodes/test_code.py | 4 ++ 8 files changed, 95 insertions(+), 77 deletions(-) diff --git a/api/core/workflow/entities/variable_pool.py b/api/core/workflow/entities/variable_pool.py index 2a7c7234ea..f8968990d4 100644 --- a/api/core/workflow/entities/variable_pool.py +++ b/api/core/workflow/entities/variable_pool.py @@ -4,7 +4,6 @@ from collections.abc import Mapping, Sequence from typing import Any, Union from pydantic import BaseModel, Field -from typing_extensions import deprecated from core.file import File, FileAttribute, file_manager from core.variables import Segment, SegmentGroup, Variable @@ -133,26 +132,6 @@ class VariablePool(BaseModel): return value - @deprecated("This method is deprecated, use `get` instead.") - def get_any(self, selector: Sequence[str], /) -> Any | None: - """ - Retrieves the value from the variable pool based on the given selector. - - Args: - selector (Sequence[str]): The selector used to identify the variable. - - Returns: - Any: The value associated with the given selector. - - Raises: - ValueError: If the selector is invalid. - """ - if len(selector) < 2: - raise ValueError("Invalid selector") - hash_key = hash(tuple(selector[1:])) - value = self.variable_dictionary[selector[0]].get(hash_key) - return value.to_object() if value else None - def remove(self, selector: Sequence[str], /): """ Remove variables from the variable pool based on the given selector. diff --git a/api/core/workflow/nodes/code/code_node.py b/api/core/workflow/nodes/code/code_node.py index dd533ffc4c..9d7d9027c3 100644 --- a/api/core/workflow/nodes/code/code_node.py +++ b/api/core/workflow/nodes/code/code_node.py @@ -41,10 +41,15 @@ class CodeNode(BaseNode[CodeNodeData]): # Get variables variables = {} for variable_selector in self.node_data.variables: - variable = variable_selector.variable - value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector) - - variables[variable] = value + variable_name = variable_selector.variable + variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) + if variable is None: + return NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + inputs=variables, + error=f"Variable `{variable_selector.value_selector}` not found", + ) + variables[variable_name] = variable.to_object() # Run code try: result = CodeExecutor.execute_workflow_code_template( diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 7503c13ce8..af79da9215 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -5,6 +5,7 @@ from typing import Any, cast from configs import dify_config from core.model_runtime.utils.encoders import jsonable_encoder +from core.variables import IntegerSegment from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult from core.workflow.graph_engine.entities.event import ( BaseGraphEvent, @@ -147,9 +148,16 @@ class IterationNode(BaseNode[IterationNodeData]): if NodeRunMetadataKey.ITERATION_ID not in metadata: metadata[NodeRunMetadataKey.ITERATION_ID] = self.node_id - metadata[NodeRunMetadataKey.ITERATION_INDEX] = variable_pool.get_any( - [self.node_id, "index"] - ) + index_variable = variable_pool.get([self.node_id, "index"]) + if not isinstance(index_variable, IntegerSegment): + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=f"Invalid index variable type: {type(index_variable)}", + ) + ) + return + metadata[NodeRunMetadataKey.ITERATION_INDEX] = index_variable.value event.route_node_state.node_run_result.metadata = metadata yield event @@ -181,7 +189,16 @@ class IterationNode(BaseNode[IterationNodeData]): yield event # append to iteration output variable list - current_iteration_output = variable_pool.get_any(self.node_data.output_selector) + current_iteration_output_variable = variable_pool.get(self.node_data.output_selector) + if current_iteration_output_variable is None: + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=f"Iteration output variable {self.node_data.output_selector} not found", + ) + ) + return + current_iteration_output = current_iteration_output_variable.to_object() outputs.append(current_iteration_output) # remove all nodes outputs from variable pool @@ -189,11 +206,11 @@ class IterationNode(BaseNode[IterationNodeData]): variable_pool.remove([node_id]) # move to next iteration - current_index = variable_pool.get([self.node_id, "index"]) - if current_index is None: + current_index_variable = variable_pool.get([self.node_id, "index"]) + if not isinstance(current_index_variable, IntegerSegment): raise ValueError(f"iteration {self.node_id} current index not found") - next_index = int(current_index.to_object()) + 1 + next_index = current_index_variable.value + 1 variable_pool.add([self.node_id, "index"], next_index) if next_index < len(iterator_list_value): @@ -205,9 +222,7 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_type=self.node_type, iteration_node_data=self.node_data, index=next_index, - pre_iteration_output=jsonable_encoder(current_iteration_output) - if current_iteration_output - else None, + pre_iteration_output=jsonable_encoder(current_iteration_output), ) yield IterationRunSucceededEvent( 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 b286f34d7f..2a5795a3ed 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -14,6 +14,7 @@ from core.model_runtime.entities.model_entities import ModelFeature, ModelType from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.retrieval_methods import RetrievalMethod +from core.variables import StringSegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType @@ -39,8 +40,14 @@ class KnowledgeRetrievalNode(BaseNode[KnowledgeRetrievalNodeData]): def _run(self) -> NodeRunResult: # extract variables - variable = self.graph_runtime_state.variable_pool.get_any(self.node_data.query_variable_selector) - query = variable + variable = self.graph_runtime_state.variable_pool.get(self.node_data.query_variable_selector) + if not isinstance(variable, StringSegment): + return NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + inputs={}, + error="Query variable is not string type.", + ) + query = variable.value variables = {"query": query} if not query: return NodeRunResult( diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 4179d34f9a..94aa8c5eab 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -22,7 +22,15 @@ from core.model_runtime.utils.encoders import jsonable_encoder from core.prompt.advanced_prompt_transform import AdvancedPromptTransform from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig from core.prompt.utils.prompt_message_util import PromptMessageUtil -from core.variables import ArrayAnySegment, ArrayFileSegment, FileSegment, NoneSegment +from core.variables import ( + ArrayAnySegment, + ArrayFileSegment, + ArraySegment, + FileSegment, + NoneSegment, + ObjectSegment, + StringSegment, +) from core.workflow.constants import SYSTEM_VARIABLE_NODE_ID from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult from core.workflow.enums import SystemVariableKey @@ -263,50 +271,44 @@ class LLMNode(BaseNode[LLMNodeData]): return variables for variable_selector in node_data.prompt_config.jinja2_variables or []: - variable = variable_selector.variable - value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector) + variable_name = variable_selector.variable + variable = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) + if variable is None: + raise ValueError(f"Variable {variable_selector.variable} not found") - def parse_dict(d: dict) -> str: + def parse_dict(input_dict: Mapping[str, Any]) -> str: """ Parse dict into string """ # check if it's a context structure - if "metadata" in d and "_source" in d["metadata"] and "content" in d: - return d["content"] + if "metadata" in input_dict and "_source" in input_dict["metadata"] and "content" in input_dict: + return input_dict["content"] # else, parse the dict try: - return json.dumps(d, ensure_ascii=False) + return json.dumps(input_dict, ensure_ascii=False) except Exception: - return str(d) + return str(input_dict) - if isinstance(value, str): - value = value - elif isinstance(value, list): + if isinstance(variable, ArraySegment): result = "" - for item in value: + for item in variable.value: if isinstance(item, dict): result += parse_dict(item) - elif isinstance(item, str): - result += item - elif isinstance(item, int | float): - result += str(item) else: result += str(item) result += "\n" value = result.strip() - elif isinstance(value, dict): - value = parse_dict(value) - elif isinstance(value, int | float): - value = str(value) + elif isinstance(variable, ObjectSegment): + value = parse_dict(variable.value) else: - value = str(value) + value = variable.text - variables[variable] = value + variables[variable_name] = value return variables - def _fetch_inputs(self, node_data: LLMNodeData) -> dict[str, str]: + def _fetch_inputs(self, node_data: LLMNodeData) -> dict[str, Any]: inputs = {} prompt_template = node_data.prompt_template @@ -363,14 +365,14 @@ class LLMNode(BaseNode[LLMNodeData]): if not node_data.context.variable_selector: return - context_value = self.graph_runtime_state.variable_pool.get_any(node_data.context.variable_selector) - if context_value: - if isinstance(context_value, str): - yield RunRetrieverResourceEvent(retriever_resources=[], context=context_value) - elif isinstance(context_value, list): + context_value_variable = self.graph_runtime_state.variable_pool.get(node_data.context.variable_selector) + if context_value_variable: + if isinstance(context_value_variable, StringSegment): + yield RunRetrieverResourceEvent(retriever_resources=[], context=context_value_variable.value) + elif isinstance(context_value_variable, ArraySegment): context_str = "" original_retriever_resource = [] - for item in context_value: + for item in context_value_variable.value: if isinstance(item, str): context_str += item + "\n" else: @@ -484,11 +486,12 @@ class LLMNode(BaseNode[LLMNodeData]): return None # get conversation id - conversation_id = self.graph_runtime_state.variable_pool.get_any( + conversation_id_variable = self.graph_runtime_state.variable_pool.get( ["sys", SystemVariableKey.CONVERSATION_ID.value] ) - if conversation_id is None: + if not isinstance(conversation_id_variable, StringSegment): return None + conversation_id = conversation_id_variable.value # get conversation conversation = ( diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index b85271ddc6..0ee66784c5 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -33,8 +33,13 @@ class TemplateTransformNode(BaseNode[TemplateTransformNodeData]): variables = {} for variable_selector in self.node_data.variables: variable_name = variable_selector.variable - value = self.graph_runtime_state.variable_pool.get_any(variable_selector.value_selector) - variables[variable_name] = value + value = self.graph_runtime_state.variable_pool.get(variable_selector.value_selector) + if value is None: + return NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=f"Variable {variable_name} not found in variable pool", + ) + variables[variable_name] = value.to_object() # Run code try: result = CodeExecutor.execute_workflow_code_template( diff --git a/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py b/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py index 05477e2a90..031a7b8309 100644 --- a/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py +++ b/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py @@ -19,27 +19,27 @@ class VariableAggregatorNode(BaseNode[VariableAssignerNodeData]): if not self.node_data.advanced_settings or not self.node_data.advanced_settings.group_enabled: for selector in self.node_data.variables: - variable = self.graph_runtime_state.variable_pool.get_any(selector) + variable = self.graph_runtime_state.variable_pool.get(selector) if variable is not None: - outputs = {"output": variable} + outputs = {"output": variable.to_object()} - inputs = {".".join(selector[1:]): variable} + inputs = {".".join(selector[1:]): variable.to_object()} break else: for group in self.node_data.advanced_settings.groups: for selector in group.variables: - variable = self.graph_runtime_state.variable_pool.get_any(selector) + variable = self.graph_runtime_state.variable_pool.get(selector) if variable is not None: - outputs[group.group_name] = {"output": variable} - inputs[".".join(selector[1:])] = variable + outputs[group.group_name] = {"output": variable.to_object()} + inputs[".".join(selector[1:])] = variable.to_object() break return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs=outputs, inputs=inputs) @classmethod def _extract_variable_selector_to_variable_mapping( - cls, graph_config: Mapping[str, Any], node_id: str, node_data: VariableAssignerNodeData + cls, *, graph_config: Mapping[str, Any], node_id: str, node_data: VariableAssignerNodeData ) -> Mapping[str, Sequence[str]]: """ Extract variable selector to variable mapping diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index fd0f25cf04..4de985ae7c 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -102,6 +102,8 @@ def test_execute_code(setup_code_executor_mock): } node = init_code_node(code_config) + node.graph_runtime_state.variable_pool.add(["1", "123", "args1"], 1) + node.graph_runtime_state.variable_pool.add(["1", "123", "args2"], 2) # execute node result = node._run() @@ -146,6 +148,8 @@ def test_execute_code_output_validator(setup_code_executor_mock): } node = init_code_node(code_config) + node.graph_runtime_state.variable_pool.add(["1", "123", "args1"], 1) + node.graph_runtime_state.variable_pool.add(["1", "123", "args2"], 2) # execute node result = node._run() From 4d9160ca9f5b9529de9ee184f413c2fe23f7fb80 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Tue, 22 Oct 2024 11:01:32 +0800 Subject: [PATCH 21/35] refactor: use dify_config to replace legacy usage of flask app's config (#9089) --- api/app.py | 16 ++-- api/app_factory.py | 6 +- api/configs/feature/__init__.py | 15 +++ api/controllers/console/admin.py | 6 +- api/core/hosting_configuration.py | 91 +++++++++---------- .../datasource/vdb/qdrant/qdrant_vector.py | 3 +- api/extensions/ext_celery.py | 18 ++-- api/extensions/ext_compress.py | 4 +- api/extensions/ext_logging.py | 12 ++- api/extensions/ext_mail.py | 45 +++++---- api/extensions/ext_redis.py | 26 +++--- api/extensions/ext_sentry.py | 13 +-- api/extensions/ext_storage.py | 2 +- api/extensions/storage/aliyun_oss_storage.py | 20 ++-- api/extensions/storage/azure_blob_storage.py | 15 ++- api/extensions/storage/baidu_obs_storage.py | 15 ++- api/extensions/storage/base_storage.py | 8 +- .../storage/google_cloud_storage.py | 12 +-- api/extensions/storage/huawei_obs_storage.py | 16 ++-- api/extensions/storage/local_fs_storage.py | 11 ++- api/extensions/storage/oracle_oci_storage.py | 18 ++-- api/extensions/storage/tencent_cos_storage.py | 18 ++-- .../storage/volcengine_tos_storage.py | 17 ++-- api/libs/helper.py | 5 +- api/libs/login.py | 11 +-- .../controllers/app_fixture.py | 3 +- .../oss/volcengine_tos/test_volcengine_tos.py | 2 +- 27 files changed, 221 insertions(+), 207 deletions(-) diff --git a/api/app.py b/api/app.py index a3efabf06c..ed214bde97 100644 --- a/api/app.py +++ b/api/app.py @@ -1,5 +1,7 @@ import os +from configs import dify_config + if os.environ.get("DEBUG", "false").lower() != "true": from gevent import monkey @@ -36,17 +38,11 @@ if hasattr(time, "tzset"): time.tzset() -# ------------- -# Configuration -# ------------- -config_type = os.getenv("EDITION", default="SELF_HOSTED") # ce edition first - - # create app app = create_app() celery = app.extensions["celery"] -if app.config.get("TESTING"): +if dify_config.TESTING: print("App is running in TESTING mode") @@ -54,15 +50,15 @@ if app.config.get("TESTING"): def after_request(response): """Add Version headers to the response.""" response.set_cookie("remember_token", "", expires=0) - response.headers.add("X-Version", app.config["CURRENT_VERSION"]) - response.headers.add("X-Env", app.config["DEPLOY_ENV"]) + response.headers.add("X-Version", dify_config.CURRENT_VERSION) + response.headers.add("X-Env", dify_config.DEPLOY_ENV) return response @app.route("/health") def health(): return Response( - json.dumps({"pid": os.getpid(), "status": "ok", "version": app.config["CURRENT_VERSION"]}), + json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}), status=200, content_type="application/json", ) diff --git a/api/app_factory.py b/api/app_factory.py index b7bfe947f5..aba78ccab8 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -68,7 +68,7 @@ def create_flask_app_with_configs() -> Flask: def create_app() -> Flask: app = create_flask_app_with_configs() - app.secret_key = app.config["SECRET_KEY"] + app.secret_key = dify_config.SECRET_KEY initialize_extensions(app) register_blueprints(app) register_commands(app) @@ -150,7 +150,7 @@ def register_blueprints(app): CORS( web_bp, - resources={r"/*": {"origins": app.config["WEB_API_CORS_ALLOW_ORIGINS"]}}, + resources={r"/*": {"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS}}, supports_credentials=True, allow_headers=["Content-Type", "Authorization", "X-App-Code"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], @@ -161,7 +161,7 @@ def register_blueprints(app): CORS( console_app_bp, - resources={r"/*": {"origins": app.config["CONSOLE_CORS_ALLOW_ORIGINS"]}}, + resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}}, supports_credentials=True, allow_headers=["Content-Type", "Authorization"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 307bc94a79..8f87356934 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -32,6 +32,21 @@ class SecurityConfig(BaseSettings): default=5, ) + LOGIN_DISABLED: bool = Field( + description="Whether to disable login checks", + default=False, + ) + + ADMIN_API_KEY_ENABLE: bool = Field( + description="Whether to enable admin api key for authentication", + default=False, + ) + + ADMIN_API_KEY: Optional[str] = Field( + description="admin api key for authentication", + default=None, + ) + class AppExecutionConfig(BaseSettings): """ diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index f78ea9b288..a70c4a31c7 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -1,10 +1,10 @@ -import os from functools import wraps from flask import request from flask_restful import Resource, reqparse from werkzeug.exceptions import NotFound, Unauthorized +from configs import dify_config from constants.languages import supported_language from controllers.console import api from controllers.console.wraps import only_edition_cloud @@ -15,7 +15,7 @@ from models.model import App, InstalledApp, RecommendedApp def admin_required(view): @wraps(view) def decorated(*args, **kwargs): - if not os.getenv("ADMIN_API_KEY"): + if not dify_config.ADMIN_API_KEY: raise Unauthorized("API key is invalid.") auth_header = request.headers.get("Authorization") @@ -31,7 +31,7 @@ def admin_required(view): if auth_scheme != "bearer": raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") - if os.getenv("ADMIN_API_KEY") != auth_token: + if dify_config.ADMIN_API_KEY != auth_token: raise Unauthorized("API key is invalid.") return view(*args, **kwargs) diff --git a/api/core/hosting_configuration.py b/api/core/hosting_configuration.py index eeeccc2349..b47ba67f2f 100644 --- a/api/core/hosting_configuration.py +++ b/api/core/hosting_configuration.py @@ -1,8 +1,9 @@ from typing import Optional -from flask import Config, Flask +from flask import Flask from pydantic import BaseModel +from configs import dify_config from core.entities.provider_entities import QuotaUnit, RestrictModel from core.model_runtime.entities.model_entities import ModelType from models.provider import ProviderQuotaType @@ -44,32 +45,30 @@ class HostingConfiguration: moderation_config: HostedModerationConfig = None def init_app(self, app: Flask) -> None: - config = app.config - - if config.get("EDITION") != "CLOUD": + if dify_config.EDITION != "CLOUD": return - self.provider_map["azure_openai"] = self.init_azure_openai(config) - self.provider_map["openai"] = self.init_openai(config) - self.provider_map["anthropic"] = self.init_anthropic(config) - self.provider_map["minimax"] = self.init_minimax(config) - self.provider_map["spark"] = self.init_spark(config) - self.provider_map["zhipuai"] = self.init_zhipuai(config) + self.provider_map["azure_openai"] = self.init_azure_openai() + self.provider_map["openai"] = self.init_openai() + self.provider_map["anthropic"] = self.init_anthropic() + self.provider_map["minimax"] = self.init_minimax() + self.provider_map["spark"] = self.init_spark() + self.provider_map["zhipuai"] = self.init_zhipuai() - self.moderation_config = self.init_moderation_config(config) + self.moderation_config = self.init_moderation_config() @staticmethod - def init_azure_openai(app_config: Config) -> HostingProvider: + def init_azure_openai() -> HostingProvider: quota_unit = QuotaUnit.TIMES - if app_config.get("HOSTED_AZURE_OPENAI_ENABLED"): + if dify_config.HOSTED_AZURE_OPENAI_ENABLED: credentials = { - "openai_api_key": app_config.get("HOSTED_AZURE_OPENAI_API_KEY"), - "openai_api_base": app_config.get("HOSTED_AZURE_OPENAI_API_BASE"), + "openai_api_key": dify_config.HOSTED_AZURE_OPENAI_API_KEY, + "openai_api_base": dify_config.HOSTED_AZURE_OPENAI_API_BASE, "base_model_name": "gpt-35-turbo", } quotas = [] - hosted_quota_limit = int(app_config.get("HOSTED_AZURE_OPENAI_QUOTA_LIMIT", "1000")) + hosted_quota_limit = dify_config.HOSTED_AZURE_OPENAI_QUOTA_LIMIT trial_quota = TrialHostingQuota( quota_limit=hosted_quota_limit, restrict_models=[ @@ -122,31 +121,31 @@ class HostingConfiguration: quota_unit=quota_unit, ) - def init_openai(self, app_config: Config) -> HostingProvider: + def init_openai(self) -> HostingProvider: quota_unit = QuotaUnit.CREDITS quotas = [] - if app_config.get("HOSTED_OPENAI_TRIAL_ENABLED"): - hosted_quota_limit = int(app_config.get("HOSTED_OPENAI_QUOTA_LIMIT", "200")) - trial_models = self.parse_restrict_models_from_env(app_config, "HOSTED_OPENAI_TRIAL_MODELS") + if dify_config.HOSTED_OPENAI_TRIAL_ENABLED: + hosted_quota_limit = dify_config.HOSTED_OPENAI_QUOTA_LIMIT + trial_models = self.parse_restrict_models_from_env("HOSTED_OPENAI_TRIAL_MODELS") trial_quota = TrialHostingQuota(quota_limit=hosted_quota_limit, restrict_models=trial_models) quotas.append(trial_quota) - if app_config.get("HOSTED_OPENAI_PAID_ENABLED"): - paid_models = self.parse_restrict_models_from_env(app_config, "HOSTED_OPENAI_PAID_MODELS") + if dify_config.HOSTED_OPENAI_PAID_ENABLED: + paid_models = self.parse_restrict_models_from_env("HOSTED_OPENAI_PAID_MODELS") paid_quota = PaidHostingQuota(restrict_models=paid_models) quotas.append(paid_quota) if len(quotas) > 0: credentials = { - "openai_api_key": app_config.get("HOSTED_OPENAI_API_KEY"), + "openai_api_key": dify_config.HOSTED_OPENAI_API_KEY, } - if app_config.get("HOSTED_OPENAI_API_BASE"): - credentials["openai_api_base"] = app_config.get("HOSTED_OPENAI_API_BASE") + if dify_config.HOSTED_OPENAI_API_BASE: + credentials["openai_api_base"] = dify_config.HOSTED_OPENAI_API_BASE - if app_config.get("HOSTED_OPENAI_API_ORGANIZATION"): - credentials["openai_organization"] = app_config.get("HOSTED_OPENAI_API_ORGANIZATION") + if dify_config.HOSTED_OPENAI_API_ORGANIZATION: + credentials["openai_organization"] = dify_config.HOSTED_OPENAI_API_ORGANIZATION return HostingProvider(enabled=True, credentials=credentials, quota_unit=quota_unit, quotas=quotas) @@ -156,26 +155,26 @@ class HostingConfiguration: ) @staticmethod - def init_anthropic(app_config: Config) -> HostingProvider: + def init_anthropic() -> HostingProvider: quota_unit = QuotaUnit.TOKENS quotas = [] - if app_config.get("HOSTED_ANTHROPIC_TRIAL_ENABLED"): - hosted_quota_limit = int(app_config.get("HOSTED_ANTHROPIC_QUOTA_LIMIT", "0")) + if dify_config.HOSTED_ANTHROPIC_TRIAL_ENABLED: + hosted_quota_limit = dify_config.HOSTED_ANTHROPIC_QUOTA_LIMIT trial_quota = TrialHostingQuota(quota_limit=hosted_quota_limit) quotas.append(trial_quota) - if app_config.get("HOSTED_ANTHROPIC_PAID_ENABLED"): + if dify_config.HOSTED_ANTHROPIC_PAID_ENABLED: paid_quota = PaidHostingQuota() quotas.append(paid_quota) if len(quotas) > 0: credentials = { - "anthropic_api_key": app_config.get("HOSTED_ANTHROPIC_API_KEY"), + "anthropic_api_key": dify_config.HOSTED_ANTHROPIC_API_KEY, } - if app_config.get("HOSTED_ANTHROPIC_API_BASE"): - credentials["anthropic_api_url"] = app_config.get("HOSTED_ANTHROPIC_API_BASE") + if dify_config.HOSTED_ANTHROPIC_API_BASE: + credentials["anthropic_api_url"] = dify_config.HOSTED_ANTHROPIC_API_BASE return HostingProvider(enabled=True, credentials=credentials, quota_unit=quota_unit, quotas=quotas) @@ -185,9 +184,9 @@ class HostingConfiguration: ) @staticmethod - def init_minimax(app_config: Config) -> HostingProvider: + def init_minimax() -> HostingProvider: quota_unit = QuotaUnit.TOKENS - if app_config.get("HOSTED_MINIMAX_ENABLED"): + if dify_config.HOSTED_MINIMAX_ENABLED: quotas = [FreeHostingQuota()] return HostingProvider( @@ -203,9 +202,9 @@ class HostingConfiguration: ) @staticmethod - def init_spark(app_config: Config) -> HostingProvider: + def init_spark() -> HostingProvider: quota_unit = QuotaUnit.TOKENS - if app_config.get("HOSTED_SPARK_ENABLED"): + if dify_config.HOSTED_SPARK_ENABLED: quotas = [FreeHostingQuota()] return HostingProvider( @@ -221,9 +220,9 @@ class HostingConfiguration: ) @staticmethod - def init_zhipuai(app_config: Config) -> HostingProvider: + def init_zhipuai() -> HostingProvider: quota_unit = QuotaUnit.TOKENS - if app_config.get("HOSTED_ZHIPUAI_ENABLED"): + if dify_config.HOSTED_ZHIPUAI_ENABLED: quotas = [FreeHostingQuota()] return HostingProvider( @@ -239,17 +238,15 @@ class HostingConfiguration: ) @staticmethod - def init_moderation_config(app_config: Config) -> HostedModerationConfig: - if app_config.get("HOSTED_MODERATION_ENABLED") and app_config.get("HOSTED_MODERATION_PROVIDERS"): - return HostedModerationConfig( - enabled=True, providers=app_config.get("HOSTED_MODERATION_PROVIDERS").split(",") - ) + def init_moderation_config() -> HostedModerationConfig: + if dify_config.HOSTED_MODERATION_ENABLED and dify_config.HOSTED_MODERATION_PROVIDERS: + return HostedModerationConfig(enabled=True, providers=dify_config.HOSTED_MODERATION_PROVIDERS.split(",")) return HostedModerationConfig(enabled=False) @staticmethod - def parse_restrict_models_from_env(app_config: Config, env_var: str) -> list[RestrictModel]: - models_str = app_config.get(env_var) + def parse_restrict_models_from_env(env_var: str) -> list[RestrictModel]: + models_str = dify_config.model_dump().get(env_var) models_list = models_str.split(",") if models_str else [] return [ RestrictModel(model=model_name.strip(), model_type=ModelType.LLM) diff --git a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py index 69d2aa4f76..3811458e02 100644 --- a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py +++ b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py @@ -428,14 +428,13 @@ class QdrantVectorFactory(AbstractVectorFactory): if not dataset.index_struct_dict: dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.QDRANT, collection_name)) - config = current_app.config return QdrantVector( collection_name=collection_name, group_id=dataset.id, config=QdrantConfig( endpoint=dify_config.QDRANT_URL, api_key=dify_config.QDRANT_API_KEY, - root_path=config.root_path, + root_path=current_app.config.root_path, timeout=dify_config.QDRANT_CLIENT_TIMEOUT, grpc_port=dify_config.QDRANT_GRPC_PORT, prefer_grpc=dify_config.QDRANT_GRPC_ENABLED, diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 0ff9f90847..b9b019373d 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -3,6 +3,8 @@ from datetime import timedelta from celery import Celery, Task from flask import Flask +from configs import dify_config + def init_app(app: Flask) -> Celery: class FlaskTask(Task): @@ -12,19 +14,19 @@ def init_app(app: Flask) -> Celery: broker_transport_options = {} - if app.config.get("CELERY_USE_SENTINEL"): + if dify_config.CELERY_USE_SENTINEL: broker_transport_options = { - "master_name": app.config.get("CELERY_SENTINEL_MASTER_NAME"), + "master_name": dify_config.CELERY_SENTINEL_MASTER_NAME, "sentinel_kwargs": { - "socket_timeout": app.config.get("CELERY_SENTINEL_SOCKET_TIMEOUT", 0.1), + "socket_timeout": dify_config.CELERY_SENTINEL_SOCKET_TIMEOUT, }, } celery_app = Celery( app.name, task_cls=FlaskTask, - broker=app.config.get("CELERY_BROKER_URL"), - backend=app.config.get("CELERY_BACKEND"), + broker=dify_config.CELERY_BROKER_URL, + backend=dify_config.CELERY_BACKEND, task_ignore_result=True, ) @@ -37,12 +39,12 @@ def init_app(app: Flask) -> Celery: } celery_app.conf.update( - result_backend=app.config.get("CELERY_RESULT_BACKEND"), + result_backend=dify_config.CELERY_RESULT_BACKEND, broker_transport_options=broker_transport_options, broker_connection_retry_on_startup=True, ) - if app.config.get("BROKER_USE_SSL"): + if dify_config.BROKER_USE_SSL: celery_app.conf.update( broker_use_ssl=ssl_options, # Add the SSL options to the broker configuration ) @@ -54,7 +56,7 @@ def init_app(app: Flask) -> Celery: "schedule.clean_embedding_cache_task", "schedule.clean_unused_datasets_task", ] - day = app.config.get("CELERY_BEAT_SCHEDULER_TIME") + day = dify_config.CELERY_BEAT_SCHEDULER_TIME beat_schedule = { "clean_embedding_cache_task": { "task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task", diff --git a/api/extensions/ext_compress.py b/api/extensions/ext_compress.py index 38e67749fc..a6de28597b 100644 --- a/api/extensions/ext_compress.py +++ b/api/extensions/ext_compress.py @@ -1,8 +1,10 @@ from flask import Flask +from configs import dify_config + def init_app(app: Flask): - if app.config.get("API_COMPRESSION_ENABLED"): + if dify_config.API_COMPRESSION_ENABLED: from flask_compress import Compress app.config["COMPRESS_MIMETYPES"] = [ diff --git a/api/extensions/ext_logging.py b/api/extensions/ext_logging.py index 22f868881f..9e1a241b67 100644 --- a/api/extensions/ext_logging.py +++ b/api/extensions/ext_logging.py @@ -5,10 +5,12 @@ from logging.handlers import RotatingFileHandler from flask import Flask +from configs import dify_config + def init_app(app: Flask): log_handlers = None - log_file = app.config.get("LOG_FILE") + log_file = dify_config.LOG_FILE if log_file: log_dir = os.path.dirname(log_file) os.makedirs(log_dir, exist_ok=True) @@ -22,13 +24,13 @@ def init_app(app: Flask): ] logging.basicConfig( - level=app.config.get("LOG_LEVEL"), - format=app.config.get("LOG_FORMAT"), - datefmt=app.config.get("LOG_DATEFORMAT"), + level=dify_config.LOG_LEVEL, + format=dify_config.LOG_FORMAT, + datefmt=dify_config.LOG_DATEFORMAT, handlers=log_handlers, force=True, ) - log_tz = app.config.get("LOG_TZ") + log_tz = dify_config.LOG_TZ if log_tz: from datetime import datetime diff --git a/api/extensions/ext_mail.py b/api/extensions/ext_mail.py index b435294abc..5c5b331d8a 100644 --- a/api/extensions/ext_mail.py +++ b/api/extensions/ext_mail.py @@ -4,6 +4,8 @@ from typing import Optional import resend from flask import Flask +from configs import dify_config + class Mail: def __init__(self): @@ -14,41 +16,44 @@ class Mail: return self._client is not None def init_app(self, app: Flask): - if app.config.get("MAIL_TYPE"): - if app.config.get("MAIL_DEFAULT_SEND_FROM"): - self._default_send_from = app.config.get("MAIL_DEFAULT_SEND_FROM") + mail_type = dify_config.MAIL_TYPE + if not mail_type: + logging.warning("MAIL_TYPE is not set") + return - if app.config.get("MAIL_TYPE") == "resend": - api_key = app.config.get("RESEND_API_KEY") + if dify_config.MAIL_DEFAULT_SEND_FROM: + self._default_send_from = dify_config.MAIL_DEFAULT_SEND_FROM + + match mail_type: + case "resend": + api_key = dify_config.RESEND_API_KEY if not api_key: raise ValueError("RESEND_API_KEY is not set") - api_url = app.config.get("RESEND_API_URL") + api_url = dify_config.RESEND_API_URL if api_url: resend.api_url = api_url resend.api_key = api_key self._client = resend.Emails - elif app.config.get("MAIL_TYPE") == "smtp": + case "smtp": from libs.smtp import SMTPClient - if not app.config.get("SMTP_SERVER") or not app.config.get("SMTP_PORT"): + if not dify_config.SMTP_SERVER or not dify_config.SMTP_PORT: raise ValueError("SMTP_SERVER and SMTP_PORT are required for smtp mail type") - if not app.config.get("SMTP_USE_TLS") and app.config.get("SMTP_OPPORTUNISTIC_TLS"): + if not dify_config.SMTP_USE_TLS and dify_config.SMTP_OPPORTUNISTIC_TLS: raise ValueError("SMTP_OPPORTUNISTIC_TLS is not supported without enabling SMTP_USE_TLS") self._client = SMTPClient( - server=app.config.get("SMTP_SERVER"), - port=app.config.get("SMTP_PORT"), - username=app.config.get("SMTP_USERNAME"), - password=app.config.get("SMTP_PASSWORD"), - _from=app.config.get("MAIL_DEFAULT_SEND_FROM"), - use_tls=app.config.get("SMTP_USE_TLS"), - opportunistic_tls=app.config.get("SMTP_OPPORTUNISTIC_TLS"), + server=dify_config.SMTP_SERVER, + port=dify_config.SMTP_PORT, + username=dify_config.SMTP_USERNAME, + password=dify_config.SMTP_PASSWORD, + _from=dify_config.MAIL_DEFAULT_SEND_FROM, + use_tls=dify_config.SMTP_USE_TLS, + opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS, ) - else: - raise ValueError("Unsupported mail type {}".format(app.config.get("MAIL_TYPE"))) - else: - logging.warning("MAIL_TYPE is not set") + case _: + raise ValueError("Unsupported mail type {}".format(mail_type)) def send(self, to: str, subject: str, html: str, from_: Optional[str] = None): if not self._client: diff --git a/api/extensions/ext_redis.py b/api/extensions/ext_redis.py index 054769e7ff..e1f8409f21 100644 --- a/api/extensions/ext_redis.py +++ b/api/extensions/ext_redis.py @@ -2,6 +2,8 @@ import redis from redis.connection import Connection, SSLConnection from redis.sentinel import Sentinel +from configs import dify_config + class RedisClientWrapper(redis.Redis): """ @@ -43,37 +45,37 @@ redis_client = RedisClientWrapper() def init_app(app): global redis_client connection_class = Connection - if app.config.get("REDIS_USE_SSL"): + if dify_config.REDIS_USE_SSL: connection_class = SSLConnection redis_params = { - "username": app.config.get("REDIS_USERNAME"), - "password": app.config.get("REDIS_PASSWORD"), - "db": app.config.get("REDIS_DB"), + "username": dify_config.REDIS_USERNAME, + "password": dify_config.REDIS_PASSWORD, + "db": dify_config.REDIS_DB, "encoding": "utf-8", "encoding_errors": "strict", "decode_responses": False, } - if app.config.get("REDIS_USE_SENTINEL"): + if dify_config.REDIS_USE_SENTINEL: sentinel_hosts = [ - (node.split(":")[0], int(node.split(":")[1])) for node in app.config.get("REDIS_SENTINELS").split(",") + (node.split(":")[0], int(node.split(":")[1])) for node in dify_config.REDIS_SENTINELS.split(",") ] sentinel = Sentinel( sentinel_hosts, sentinel_kwargs={ - "socket_timeout": app.config.get("REDIS_SENTINEL_SOCKET_TIMEOUT", 0.1), - "username": app.config.get("REDIS_SENTINEL_USERNAME"), - "password": app.config.get("REDIS_SENTINEL_PASSWORD"), + "socket_timeout": dify_config.REDIS_SENTINEL_SOCKET_TIMEOUT, + "username": dify_config.REDIS_SENTINEL_USERNAME, + "password": dify_config.REDIS_SENTINEL_PASSWORD, }, ) - master = sentinel.master_for(app.config.get("REDIS_SENTINEL_SERVICE_NAME"), **redis_params) + master = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params) redis_client.initialize(master) else: redis_params.update( { - "host": app.config.get("REDIS_HOST"), - "port": app.config.get("REDIS_PORT"), + "host": dify_config.REDIS_HOST, + "port": dify_config.REDIS_PORT, "connection_class": connection_class, } ) diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index e255e7eb35..11f1dd93c6 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -5,6 +5,7 @@ from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.flask import FlaskIntegration from werkzeug.exceptions import HTTPException +from configs import dify_config from core.model_runtime.errors.invoke import InvokeRateLimitError @@ -18,9 +19,9 @@ def before_send(event, hint): def init_app(app): - if app.config.get("SENTRY_DSN"): + if dify_config.SENTRY_DSN: sentry_sdk.init( - dsn=app.config.get("SENTRY_DSN"), + dsn=dify_config.SENTRY_DSN, integrations=[FlaskIntegration(), CeleryIntegration()], ignore_errors=[ HTTPException, @@ -29,9 +30,9 @@ def init_app(app): InvokeRateLimitError, parse_error.defaultErrorResponse, ], - traces_sample_rate=app.config.get("SENTRY_TRACES_SAMPLE_RATE", 1.0), - profiles_sample_rate=app.config.get("SENTRY_PROFILES_SAMPLE_RATE", 1.0), - environment=app.config.get("DEPLOY_ENV"), - release=f"dify-{app.config.get('CURRENT_VERSION')}-{app.config.get('COMMIT_SHA')}", + traces_sample_rate=dify_config.SENTRY_TRACES_SAMPLE_RATE, + profiles_sample_rate=dify_config.SENTRY_PROFILES_SAMPLE_RATE, + environment=dify_config.DEPLOY_ENV, + release=f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}", before_send=before_send, ) diff --git a/api/extensions/ext_storage.py b/api/extensions/ext_storage.py index 5fc4f88832..50c5d7aebc 100644 --- a/api/extensions/ext_storage.py +++ b/api/extensions/ext_storage.py @@ -15,7 +15,7 @@ class Storage: def init_app(self, app: Flask): storage_factory = self.get_storage_factory(dify_config.STORAGE_TYPE) - self.storage_runner = storage_factory(app=app) + self.storage_runner = storage_factory() @staticmethod def get_storage_factory(storage_type: str) -> type[BaseStorage]: diff --git a/api/extensions/storage/aliyun_oss_storage.py b/api/extensions/storage/aliyun_oss_storage.py index ae6911e945..01c1000e50 100644 --- a/api/extensions/storage/aliyun_oss_storage.py +++ b/api/extensions/storage/aliyun_oss_storage.py @@ -1,29 +1,27 @@ from collections.abc import Generator import oss2 as aliyun_s3 -from flask import Flask +from configs import dify_config from extensions.storage.base_storage import BaseStorage class AliyunOssStorage(BaseStorage): """Implementation for Aliyun OSS storage.""" - def __init__(self, app: Flask): - super().__init__(app) - - app_config = self.app.config - self.bucket_name = app_config.get("ALIYUN_OSS_BUCKET_NAME") - self.folder = app.config.get("ALIYUN_OSS_PATH") + def __init__(self): + super().__init__() + self.bucket_name = dify_config.ALIYUN_OSS_BUCKET_NAME + self.folder = dify_config.ALIYUN_OSS_PATH oss_auth_method = aliyun_s3.Auth region = None - if app_config.get("ALIYUN_OSS_AUTH_VERSION") == "v4": + if dify_config.ALIYUN_OSS_AUTH_VERSION == "v4": oss_auth_method = aliyun_s3.AuthV4 - region = app_config.get("ALIYUN_OSS_REGION") - oss_auth = oss_auth_method(app_config.get("ALIYUN_OSS_ACCESS_KEY"), app_config.get("ALIYUN_OSS_SECRET_KEY")) + region = dify_config.ALIYUN_OSS_REGION + oss_auth = oss_auth_method(dify_config.ALIYUN_OSS_ACCESS_KEY, dify_config.ALIYUN_OSS_SECRET_KEY) self.client = aliyun_s3.Bucket( oss_auth, - app_config.get("ALIYUN_OSS_ENDPOINT"), + dify_config.ALIYUN_OSS_ENDPOINT, self.bucket_name, connect_timeout=30, region=region, diff --git a/api/extensions/storage/azure_blob_storage.py b/api/extensions/storage/azure_blob_storage.py index daea660a49..477507feda 100644 --- a/api/extensions/storage/azure_blob_storage.py +++ b/api/extensions/storage/azure_blob_storage.py @@ -2,8 +2,8 @@ from collections.abc import Generator from datetime import datetime, timedelta, timezone from azure.storage.blob import AccountSasPermissions, BlobServiceClient, ResourceTypes, generate_account_sas -from flask import Flask +from configs import dify_config from extensions.ext_redis import redis_client from extensions.storage.base_storage import BaseStorage @@ -11,13 +11,12 @@ from extensions.storage.base_storage import BaseStorage class AzureBlobStorage(BaseStorage): """Implementation for Azure Blob storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("AZURE_BLOB_CONTAINER_NAME") - self.account_url = app_config.get("AZURE_BLOB_ACCOUNT_URL") - self.account_name = app_config.get("AZURE_BLOB_ACCOUNT_NAME") - self.account_key = app_config.get("AZURE_BLOB_ACCOUNT_KEY") + def __init__(self): + super().__init__() + self.bucket_name = dify_config.AZURE_BLOB_CONTAINER_NAME + self.account_url = dify_config.AZURE_BLOB_ACCOUNT_URL + self.account_name = dify_config.AZURE_BLOB_ACCOUNT_NAME + self.account_key = dify_config.AZURE_BLOB_ACCOUNT_KEY def save(self, filename, data): client = self._sync_client() diff --git a/api/extensions/storage/baidu_obs_storage.py b/api/extensions/storage/baidu_obs_storage.py index c5acff4a9d..cd69439749 100644 --- a/api/extensions/storage/baidu_obs_storage.py +++ b/api/extensions/storage/baidu_obs_storage.py @@ -5,24 +5,23 @@ from collections.abc import Generator from baidubce.auth.bce_credentials import BceCredentials from baidubce.bce_client_configuration import BceClientConfiguration from baidubce.services.bos.bos_client import BosClient -from flask import Flask +from configs import dify_config from extensions.storage.base_storage import BaseStorage class BaiduObsStorage(BaseStorage): """Implementation for Baidu OBS storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("BAIDU_OBS_BUCKET_NAME") + def __init__(self): + super().__init__() + self.bucket_name = dify_config.BAIDU_OBS_BUCKET_NAME client_config = BceClientConfiguration( credentials=BceCredentials( - access_key_id=app_config.get("BAIDU_OBS_ACCESS_KEY"), - secret_access_key=app_config.get("BAIDU_OBS_SECRET_KEY"), + access_key_id=dify_config.BAIDU_OBS_ACCESS_KEY, + secret_access_key=dify_config.BAIDU_OBS_SECRET_KEY, ), - endpoint=app_config.get("BAIDU_OBS_ENDPOINT"), + endpoint=dify_config.BAIDU_OBS_ENDPOINT, ) self.client = BosClient(config=client_config) diff --git a/api/extensions/storage/base_storage.py b/api/extensions/storage/base_storage.py index c3fe9ec82a..50abab8537 100644 --- a/api/extensions/storage/base_storage.py +++ b/api/extensions/storage/base_storage.py @@ -3,16 +3,12 @@ from abc import ABC, abstractmethod from collections.abc import Generator -from flask import Flask - class BaseStorage(ABC): """Interface for file storage.""" - app = None - - def __init__(self, app: Flask): - self.app = app + def __init__(self): # noqa: B027 + pass @abstractmethod def save(self, filename, data): diff --git a/api/extensions/storage/google_cloud_storage.py b/api/extensions/storage/google_cloud_storage.py index 2d1224fd74..e90392a6ba 100644 --- a/api/extensions/storage/google_cloud_storage.py +++ b/api/extensions/storage/google_cloud_storage.py @@ -3,20 +3,20 @@ import io import json from collections.abc import Generator -from flask import Flask from google.cloud import storage as google_cloud_storage +from configs import dify_config from extensions.storage.base_storage import BaseStorage class GoogleCloudStorage(BaseStorage): """Implementation for Google Cloud storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("GOOGLE_STORAGE_BUCKET_NAME") - service_account_json_str = app_config.get("GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64") + def __init__(self): + super().__init__() + + self.bucket_name = dify_config.GOOGLE_STORAGE_BUCKET_NAME + service_account_json_str = dify_config.GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64 # if service_account_json_str is empty, use Application Default Credentials if service_account_json_str: service_account_json = base64.b64decode(service_account_json_str).decode("utf-8") diff --git a/api/extensions/storage/huawei_obs_storage.py b/api/extensions/storage/huawei_obs_storage.py index dd243d4001..3c443d87ac 100644 --- a/api/extensions/storage/huawei_obs_storage.py +++ b/api/extensions/storage/huawei_obs_storage.py @@ -1,22 +1,22 @@ from collections.abc import Generator -from flask import Flask from obs import ObsClient +from configs import dify_config from extensions.storage.base_storage import BaseStorage class HuaweiObsStorage(BaseStorage): """Implementation for Huawei OBS storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("HUAWEI_OBS_BUCKET_NAME") + def __init__(self): + super().__init__() + + self.bucket_name = dify_config.HUAWEI_OBS_BUCKET_NAME self.client = ObsClient( - access_key_id=app_config.get("HUAWEI_OBS_ACCESS_KEY"), - secret_access_key=app_config.get("HUAWEI_OBS_SECRET_KEY"), - server=app_config.get("HUAWEI_OBS_SERVER"), + access_key_id=dify_config.HUAWEI_OBS_ACCESS_KEY, + secret_access_key=dify_config.HUAWEI_OBS_SECRET_KEY, + server=dify_config.HUAWEI_OBS_SERVER, ) def save(self, filename, data): diff --git a/api/extensions/storage/local_fs_storage.py b/api/extensions/storage/local_fs_storage.py index 9308c4d180..e458b3ce8a 100644 --- a/api/extensions/storage/local_fs_storage.py +++ b/api/extensions/storage/local_fs_storage.py @@ -3,19 +3,20 @@ import shutil from collections.abc import Generator from pathlib import Path -from flask import Flask +from flask import current_app +from configs import dify_config from extensions.storage.base_storage import BaseStorage class LocalFsStorage(BaseStorage): """Implementation for local filesystem storage.""" - def __init__(self, app: Flask): - super().__init__(app) - folder = self.app.config.get("STORAGE_LOCAL_PATH") + def __init__(self): + super().__init__() + folder = dify_config.STORAGE_LOCAL_PATH if not os.path.isabs(folder): - folder = os.path.join(app.root_path, folder) + folder = os.path.join(current_app.root_path, folder) self.folder = folder def save(self, filename, data): diff --git a/api/extensions/storage/oracle_oci_storage.py b/api/extensions/storage/oracle_oci_storage.py index 5295dbdca2..e4f50b34e9 100644 --- a/api/extensions/storage/oracle_oci_storage.py +++ b/api/extensions/storage/oracle_oci_storage.py @@ -2,24 +2,24 @@ from collections.abc import Generator import boto3 from botocore.exceptions import ClientError -from flask import Flask +from configs import dify_config from extensions.storage.base_storage import BaseStorage class OracleOCIStorage(BaseStorage): """Implementation for Oracle OCI storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("OCI_BUCKET_NAME") + def __init__(self): + super().__init__() + + self.bucket_name = dify_config.OCI_BUCKET_NAME self.client = boto3.client( "s3", - aws_secret_access_key=app_config.get("OCI_SECRET_KEY"), - aws_access_key_id=app_config.get("OCI_ACCESS_KEY"), - endpoint_url=app_config.get("OCI_ENDPOINT"), - region_name=app_config.get("OCI_REGION"), + aws_secret_access_key=dify_config.OCI_SECRET_KEY, + aws_access_key_id=dify_config.OCI_ACCESS_KEY, + endpoint_url=dify_config.OCI_ENDPOINT, + region_name=dify_config.OCI_REGION, ) def save(self, filename, data): diff --git a/api/extensions/storage/tencent_cos_storage.py b/api/extensions/storage/tencent_cos_storage.py index c529dce7ad..8fd8e703a1 100644 --- a/api/extensions/storage/tencent_cos_storage.py +++ b/api/extensions/storage/tencent_cos_storage.py @@ -1,23 +1,23 @@ from collections.abc import Generator -from flask import Flask from qcloud_cos import CosConfig, CosS3Client +from configs import dify_config from extensions.storage.base_storage import BaseStorage class TencentCosStorage(BaseStorage): """Implementation for Tencent Cloud COS storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("TENCENT_COS_BUCKET_NAME") + def __init__(self): + super().__init__() + + self.bucket_name = dify_config.TENCENT_COS_BUCKET_NAME config = CosConfig( - Region=app_config.get("TENCENT_COS_REGION"), - SecretId=app_config.get("TENCENT_COS_SECRET_ID"), - SecretKey=app_config.get("TENCENT_COS_SECRET_KEY"), - Scheme=app_config.get("TENCENT_COS_SCHEME"), + Region=dify_config.TENCENT_COS_REGION, + SecretId=dify_config.TENCENT_COS_SECRET_ID, + SecretKey=dify_config.TENCENT_COS_SECRET_KEY, + Scheme=dify_config.TENCENT_COS_SCHEME, ) self.client = CosS3Client(config) diff --git a/api/extensions/storage/volcengine_tos_storage.py b/api/extensions/storage/volcengine_tos_storage.py index 1bedcf24c2..389c5630e3 100644 --- a/api/extensions/storage/volcengine_tos_storage.py +++ b/api/extensions/storage/volcengine_tos_storage.py @@ -1,23 +1,22 @@ from collections.abc import Generator import tos -from flask import Flask +from configs import dify_config from extensions.storage.base_storage import BaseStorage class VolcengineTosStorage(BaseStorage): """Implementation for Volcengine TOS storage.""" - def __init__(self, app: Flask): - super().__init__(app) - app_config = self.app.config - self.bucket_name = app_config.get("VOLCENGINE_TOS_BUCKET_NAME") + def __init__(self): + super().__init__() + self.bucket_name = dify_config.VOLCENGINE_TOS_BUCKET_NAME self.client = tos.TosClientV2( - ak=app_config.get("VOLCENGINE_TOS_ACCESS_KEY"), - sk=app_config.get("VOLCENGINE_TOS_SECRET_KEY"), - endpoint=app_config.get("VOLCENGINE_TOS_ENDPOINT"), - region=app_config.get("VOLCENGINE_TOS_REGION"), + ak=dify_config.VOLCENGINE_TOS_ACCESS_KEY, + sk=dify_config.VOLCENGINE_TOS_SECRET_KEY, + endpoint=dify_config.VOLCENGINE_TOS_ENDPOINT, + region=dify_config.VOLCENGINE_TOS_REGION, ) def save(self, filename, data): diff --git a/api/libs/helper.py b/api/libs/helper.py index e674d7e84b..7638796508 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -12,9 +12,10 @@ from hashlib import sha256 from typing import Any, Optional, Union from zoneinfo import available_timezones -from flask import Response, current_app, stream_with_context +from flask import Response, stream_with_context from flask_restful import fields +from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.file import helpers as file_helpers from extensions.ext_redis import redis_client @@ -214,7 +215,7 @@ class TokenManager: if additional_data: token_data.update(additional_data) - expiry_minutes = current_app.config[f"{token_type.upper()}_TOKEN_EXPIRY_MINUTES"] + expiry_minutes = dify_config.model_dump().get(f"{token_type.upper()}_TOKEN_EXPIRY_MINUTES") token_key = cls._get_token_key(token, token_type) expiry_time = int(expiry_minutes * 60) redis_client.setex(token_key, expiry_time, json.dumps(token_data)) diff --git a/api/libs/login.py b/api/libs/login.py index 7f05eb8404..0ea191a185 100644 --- a/api/libs/login.py +++ b/api/libs/login.py @@ -1,4 +1,3 @@ -import os from functools import wraps from flask import current_app, g, has_request_context, request @@ -7,6 +6,7 @@ from flask_login.config import EXEMPT_METHODS from werkzeug.exceptions import Unauthorized from werkzeug.local import LocalProxy +from configs import dify_config from extensions.ext_database import db from models.account import Account, Tenant, TenantAccountJoin @@ -52,8 +52,7 @@ def login_required(func): @wraps(func) def decorated_view(*args, **kwargs): auth_header = request.headers.get("Authorization") - admin_api_key_enable = os.getenv("ADMIN_API_KEY_ENABLE", default="False") - if admin_api_key_enable.lower() == "true": + if dify_config.ADMIN_API_KEY_ENABLE: if auth_header: if " " not in auth_header: raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") @@ -61,10 +60,10 @@ def login_required(func): auth_scheme = auth_scheme.lower() if auth_scheme != "bearer": raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") - admin_api_key = os.getenv("ADMIN_API_KEY") + admin_api_key = dify_config.ADMIN_API_KEY if admin_api_key: - if os.getenv("ADMIN_API_KEY") == auth_token: + if admin_api_key == auth_token: workspace_id = request.headers.get("X-WORKSPACE-ID") if workspace_id: tenant_account_join = ( @@ -82,7 +81,7 @@ def login_required(func): account.current_tenant = tenant current_app.login_manager._update_request_context_with_user(account) user_logged_in.send(current_app._get_current_object(), user=_get_user()) - if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"): + if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED: pass elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() diff --git a/api/tests/integration_tests/controllers/app_fixture.py b/api/tests/integration_tests/controllers/app_fixture.py index 93065ee95c..32e8c11d19 100644 --- a/api/tests/integration_tests/controllers/app_fixture.py +++ b/api/tests/integration_tests/controllers/app_fixture.py @@ -1,6 +1,7 @@ import pytest from app_factory import create_app +from configs import dify_config mock_user = type( "MockUser", @@ -20,5 +21,5 @@ mock_user = type( @pytest.fixture def app(): app = create_app() - app.config["LOGIN_DISABLED"] = True + dify_config.LOGIN_DISABLED = True return app diff --git a/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py b/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py index 3f334a3764..545d18044d 100644 --- a/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py +++ b/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py @@ -25,7 +25,7 @@ class VolcengineTosTest: return cls._instance def __init__(self): - self.storage = VolcengineTosStorage(app=Flask(__name__)) + self.storage = VolcengineTosStorage() self.storage.bucket_name = get_example_bucket() self.storage.client = TosClientV2( ak="dify", From d170d7853067f6bc505b1d174a32e866e54c2172 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Tue, 22 Oct 2024 13:01:37 +0800 Subject: [PATCH 22/35] chore: (#9089 followup) fix storage factory constructor (#9609) --- api/extensions/storage/aws_s3_storage.py | 5 ++--- api/extensions/storage/supabase_storage.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/api/extensions/storage/aws_s3_storage.py b/api/extensions/storage/aws_s3_storage.py index 507a303223..42ff434306 100644 --- a/api/extensions/storage/aws_s3_storage.py +++ b/api/extensions/storage/aws_s3_storage.py @@ -4,7 +4,6 @@ from collections.abc import Generator import boto3 from botocore.client import Config from botocore.exceptions import ClientError -from flask import Flask from extensions.storage.base_storage import BaseStorage @@ -14,8 +13,8 @@ logger = logging.getLogger(__name__) class AwsS3Storage(BaseStorage): """Implementation for Amazon Web Services S3 storage.""" - def __init__(self, app: Flask): - super().__init__(app) + def __init__(self): + super().__init__() app_config = self.app.config self.bucket_name = app_config.get("S3_BUCKET_NAME") if app_config.get("S3_USE_AWS_MANAGED_IAM"): diff --git a/api/extensions/storage/supabase_storage.py b/api/extensions/storage/supabase_storage.py index 1e399f87c8..1ec8c6b7f0 100644 --- a/api/extensions/storage/supabase_storage.py +++ b/api/extensions/storage/supabase_storage.py @@ -2,7 +2,6 @@ import io from collections.abc import Generator from pathlib import Path -from flask import Flask from supabase import Client from extensions.storage.base_storage import BaseStorage @@ -11,8 +10,8 @@ from extensions.storage.base_storage import BaseStorage class SupabaseStorage(BaseStorage): """Implementation for supabase obs storage.""" - def __init__(self, app: Flask): - super().__init__(app) + def __init__(self): + super().__init__() app_config = self.app.config self.bucket_name = app_config.get("SUPABASE_BUCKET_NAME") self.client = Client( From 5f12c173554fb284a259f38b6a17e3399220a9f0 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 13:03:50 +0800 Subject: [PATCH 23/35] fix(core): use CreatedByRole enum for role consistency (#9607) --- api/core/rag/extractor/word_extractor.py | 6 ++++-- api/models/model.py | 4 ++-- api/services/file_service.py | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index 2b6e048652..a5375991b4 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -18,6 +18,7 @@ from core.rag.extractor.extractor_base import BaseExtractor from core.rag.models.document import Document from extensions.ext_database import db from extensions.ext_storage import storage +from models.enums import CreatedByRole from models.model import UploadFile logger = logging.getLogger(__name__) @@ -109,9 +110,10 @@ class WordExtractor(BaseExtractor): key=file_key, name=file_key, size=0, - extension=image_ext, - mime_type=mime_type, + extension=str(image_ext), + mime_type=mime_type or "", created_by=self.user_id, + created_by_role=CreatedByRole.ACCOUNT, created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), used=True, used_by=self.user_id, diff --git a/api/models/model.py b/api/models/model.py index cb2855bf72..07c6247e0d 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1391,7 +1391,7 @@ class UploadFile(db.Model): size: int, extension: str, mime_type: str, - created_by_role: str, + created_by_role: CreatedByRole, created_by: str, created_at: datetime, used: bool, @@ -1406,7 +1406,7 @@ class UploadFile(db.Model): self.size = size self.extension = extension self.mime_type = mime_type - self.created_by_role = created_by_role + self.created_by_role = created_by_role.value self.created_by = created_by self.created_at = created_at self.used = used diff --git a/api/services/file_service.py b/api/services/file_service.py index 84ccc4e882..22ea923f6b 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -20,6 +20,7 @@ from core.rag.extractor.extract_processor import ExtractProcessor from extensions.ext_database import db from extensions.ext_storage import storage from models.account import Account +from models.enums import CreatedByRole from models.model import EndUser, UploadFile from services.errors.file import FileNotExistsError, FileTooLargeError, UnsupportedFileTypeError @@ -85,7 +86,7 @@ class FileService: size=file_size, extension=extension, mime_type=file.mimetype, - created_by_role=("account" if isinstance(user, Account) else "end_user"), + created_by_role=(CreatedByRole.ACCOUNT if isinstance(user, Account) else CreatedByRole.END_USER), created_by=user.id, created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), used=False, @@ -118,6 +119,7 @@ class FileService: extension="txt", mime_type="text/plain", created_by=current_user.id, + created_by_role=CreatedByRole.ACCOUNT, created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), used=True, used_by=current_user.id, From b14d59e97715a0af32b64018354914af26afd69a Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 14:04:59 +0800 Subject: [PATCH 24/35] fix(storage): use centralized config management (#9620) --- api/extensions/storage/aws_s3_storage.py | 18 +++++++++--------- api/extensions/storage/supabase_storage.py | 19 +++++++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/api/extensions/storage/aws_s3_storage.py b/api/extensions/storage/aws_s3_storage.py index 42ff434306..cb67313bb2 100644 --- a/api/extensions/storage/aws_s3_storage.py +++ b/api/extensions/storage/aws_s3_storage.py @@ -5,6 +5,7 @@ import boto3 from botocore.client import Config from botocore.exceptions import ClientError +from configs import dify_config from extensions.storage.base_storage import BaseStorage logger = logging.getLogger(__name__) @@ -15,24 +16,23 @@ class AwsS3Storage(BaseStorage): def __init__(self): super().__init__() - app_config = self.app.config - self.bucket_name = app_config.get("S3_BUCKET_NAME") - if app_config.get("S3_USE_AWS_MANAGED_IAM"): + self.bucket_name = dify_config.S3_BUCKET_NAME + if dify_config.S3_USE_AWS_MANAGED_IAM: logger.info("Using AWS managed IAM role for S3") session = boto3.Session() - region_name = app_config.get("S3_REGION") + region_name = dify_config.S3_REGION self.client = session.client(service_name="s3", region_name=region_name) else: logger.info("Using ak and sk for S3") self.client = boto3.client( "s3", - aws_secret_access_key=app_config.get("S3_SECRET_KEY"), - aws_access_key_id=app_config.get("S3_ACCESS_KEY"), - endpoint_url=app_config.get("S3_ENDPOINT"), - region_name=app_config.get("S3_REGION"), - config=Config(s3={"addressing_style": app_config.get("S3_ADDRESS_STYLE")}), + aws_secret_access_key=dify_config.S3_SECRET_KEY, + aws_access_key_id=dify_config.S3_ACCESS_KEY, + endpoint_url=dify_config.S3_ENDPOINT, + region_name=dify_config.S3_REGION, + config=Config(s3={"addressing_style": dify_config.S3_ADDRESS_STYLE}), ) # create bucket try: diff --git a/api/extensions/storage/supabase_storage.py b/api/extensions/storage/supabase_storage.py index 1ec8c6b7f0..1119244574 100644 --- a/api/extensions/storage/supabase_storage.py +++ b/api/extensions/storage/supabase_storage.py @@ -4,6 +4,7 @@ from pathlib import Path from supabase import Client +from configs import dify_config from extensions.storage.base_storage import BaseStorage @@ -12,14 +13,16 @@ class SupabaseStorage(BaseStorage): def __init__(self): super().__init__() - app_config = self.app.config - self.bucket_name = app_config.get("SUPABASE_BUCKET_NAME") - self.client = Client( - supabase_url=app_config.get("SUPABASE_URL"), supabase_key=app_config.get("SUPABASE_API_KEY") - ) - self.create_bucket( - id=app_config.get("SUPABASE_BUCKET_NAME"), bucket_name=app_config.get("SUPABASE_BUCKET_NAME") - ) + if dify_config.SUPABASE_URL is None: + raise ValueError("SUPABASE_URL is not set") + if dify_config.SUPABASE_API_KEY is None: + raise ValueError("SUPABASE_API_KEY is not set") + if dify_config.SUPABASE_BUCKET_NAME is None: + raise ValueError("SUPABASE_BUCKET_NAME is not set") + + self.bucket_name = dify_config.SUPABASE_BUCKET_NAME + self.client = Client(supabase_url=dify_config.SUPABASE_URL, supabase_key=dify_config.SUPABASE_API_KEY) + self.create_bucket(id=dify_config.SUPABASE_BUCKET_NAME, bucket_name=dify_config.SUPABASE_BUCKET_NAME) def create_bucket(self, id, bucket_name): if not self.bucket_exists(): From e8abbe0623a842aaf58debf4a819f2057abdf3e9 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 14:50:56 +0800 Subject: [PATCH 25/35] fix(storage): ensure `storage_runner` initialization within app context (#9627) --- api/extensions/ext_storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/extensions/ext_storage.py b/api/extensions/ext_storage.py index 50c5d7aebc..86fadf23d7 100644 --- a/api/extensions/ext_storage.py +++ b/api/extensions/ext_storage.py @@ -15,7 +15,8 @@ class Storage: def init_app(self, app: Flask): storage_factory = self.get_storage_factory(dify_config.STORAGE_TYPE) - self.storage_runner = storage_factory() + with app.app_context(): + self.storage_runner = storage_factory() @staticmethod def get_storage_factory(storage_type: str) -> type[BaseStorage]: From b7bf14ab72ac440c7ff891c9393566f12e7f78ac Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Tue, 22 Oct 2024 14:53:10 +0800 Subject: [PATCH 26/35] fix: wrong url of guides doc in new feature panel (#9626) --- web/app/components/base/features/new-feature-panel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index dcdb1baefc..6e5b914b9f 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -80,7 +80,7 @@ const NewFeaturePanel = ({ {isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')} {t('workflow.common.featuresDocLink')}
From 04f8d39860968971b9c60b5575f70b458ec70e1d Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Tue, 22 Oct 2024 15:35:20 +0800 Subject: [PATCH 27/35] Fix: doc link of legacy features (#9634) --- web/app/components/base/features/new-feature-panel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index 6e5b914b9f..78204f2c05 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -80,7 +80,7 @@ const NewFeaturePanel = ({ {isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')} {t('workflow.common.featuresDocLink')}
From d992a809f5eddd2716abab974ffe2357608408e1 Mon Sep 17 00:00:00 2001 From: zhuhao <37029601+hwzhuhao@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:37:16 +0800 Subject: [PATCH 28/35] fix: update the default model to gpt-4o-mini for duckduckgo ai chat (#9614) --- api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml index 21cbae6bd3..dd049d3b5a 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_ai.yaml @@ -37,7 +37,7 @@ parameters: - value: mixtral-8x7b label: en_US: Mixtral - default: gpt-3.5 + default: gpt-4o-mini label: en_US: Choose Model zh_Hans: 选择模型 From 7263af13edd7494b35b474d8079f1517cd03b28e Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 15:37:37 +0800 Subject: [PATCH 29/35] fix(http_request): simplify JSON handling in requests (#9616) --- .../workflow/nodes/http_request/executor.py | 18 +- .../workflow/nodes/test_http_request_node.py | 169 +++++++++++++++++- 2 files changed, 174 insertions(+), 13 deletions(-) diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 71bb0ac86a..0270d7e0fd 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -1,5 +1,5 @@ import json -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from copy import deepcopy from random import randint from typing import Any, Literal @@ -60,7 +60,7 @@ class Executor: self.method = node_data.method self.auth = node_data.authorization self.timeout = timeout - self.params = None + self.params = {} self.headers = {} self.content = None self.files = None @@ -108,8 +108,10 @@ class Executor: case "raw-text": self.content = self.variable_pool.convert_template(data[0].value).text case "json": - json_object = json.loads(data[0].value) - self.json = self._parse_object_contains_variables(json_object) + json_string = self.variable_pool.convert_template(data[0].value).text + json_object = json.loads(json_string) + self.json = json_object + # self.json = self._parse_object_contains_variables(json_object) case "binary": file_selector = data[0].file file_variable = self.variable_pool.get_file(file_selector) @@ -274,14 +276,6 @@ class Executor: return raw - def _parse_object_contains_variables(self, obj: str | dict | list, /) -> Mapping[str, Any] | Sequence[Any] | str: - if isinstance(obj, dict): - return {k: self._parse_object_contains_variables(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [self._parse_object_contains_variables(v) for v in obj] - elif isinstance(obj, str): - return self.variable_pool.convert_template(obj).text - def _plain_text_to_dict(text: str, /) -> dict[str, str]: """ diff --git a/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py index 28ecdaadb0..2a5fda48b1 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_http_request_node.py @@ -1,3 +1,5 @@ +import json + import httpx from core.app.entities.app_invoke_entities import InvokeFrom @@ -14,7 +16,8 @@ from core.workflow.nodes.http_request import ( HttpRequestNodeBody, HttpRequestNodeData, ) -from core.workflow.nodes.http_request.executor import _plain_text_to_dict +from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout +from core.workflow.nodes.http_request.executor import Executor, _plain_text_to_dict from models.enums import UserFrom from models.workflow import WorkflowNodeExecutionStatus, WorkflowType @@ -200,3 +203,167 @@ def test_http_request_node_form_with_file(monkeypatch): assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.outputs is not None assert result.outputs["body"] == "" + + +def test_executor_with_json_body_and_number_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "number"], 42) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Number Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value='{"number": {{#pre_node_id.number#}}}', + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"number": 42} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '{"number": 42}' in raw_request + + +def test_executor_with_json_body_and_object_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Object Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value="{{#pre_node_id.object#}}", + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '"name": "John Doe"' in raw_request + assert '"age": 30' in raw_request + assert '"email": "john@example.com"' in raw_request + + +def test_executor_with_json_body_and_nested_object_variable(): + # Prepare the variable pool + variable_pool = VariablePool( + system_variables={}, + user_inputs={}, + ) + variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + + # Prepare the node data + node_data = HttpRequestNodeData( + title="Test JSON Body with Nested Object Variable", + method="post", + url="https://api.example.com/data", + authorization=HttpRequestNodeAuthorization(type="no-auth"), + headers="Content-Type: application/json", + params="", + body=HttpRequestNodeBody( + type="json", + data=[ + BodyData( + key="", + type="text", + value='{"object": {{#pre_node_id.object#}}}', + ) + ], + ), + ) + + # Initialize the Executor + executor = Executor( + node_data=node_data, + timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30), + variable_pool=variable_pool, + ) + + # Check the executor's data + assert executor.method == "post" + assert executor.url == "https://api.example.com/data" + assert executor.headers == {"Content-Type": "application/json"} + assert executor.params == {} + assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}} + assert executor.data is None + assert executor.files is None + assert executor.content is None + + # Check the raw request (to_log method) + raw_request = executor.to_log() + assert "POST /data HTTP/1.1" in raw_request + assert "Host: api.example.com" in raw_request + assert "Content-Type: application/json" in raw_request + assert '"object": {' in raw_request + assert '"name": "John Doe"' in raw_request + assert '"age": 30' in raw_request + assert '"email": "john@example.com"' in raw_request From 98bf7710e47c6061f42e6c8d90bd07597bedf839 Mon Sep 17 00:00:00 2001 From: crazywoola <100913391+crazywoola@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:37:53 +0800 Subject: [PATCH 30/35] fix: fields.Nested(message_file_fields) (#9632) --- api/controllers/service_api/app/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index d9a9fad13c..ecff804adc 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -48,7 +48,7 @@ class MessageListApi(Resource): "tool_input": fields.String, "created_at": TimestampField, "observation": fields.String, - "message_files": fields.List(fields.String), + "message_files": fields.List(fields.Nested(message_file_fields)), } message_fields = { From ef5f476cd6bb906b8c2df54084ff3780b08aa1ca Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 15:38:08 +0800 Subject: [PATCH 31/35] fix(api): enhance file factory URL handling (#9631) --- api/factories/file_factory.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index eac5090c2b..fa88e2b4fe 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -2,6 +2,7 @@ import mimetypes from collections.abc import Mapping, Sequence from typing import Any +import httpx from sqlalchemy import select from constants import AUDIO_EXTENSIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS @@ -154,7 +155,7 @@ def _build_from_local_file( file = File( id=mapping.get("id"), filename=row.name, - extension=row.extension, + extension="." + row.extension, mime_type=row.mime_type, tenant_id=tenant_id, type=file_type, @@ -177,25 +178,29 @@ def _build_from_remote_url( url = mapping.get("url") if not url: raise ValueError("Invalid file url") - resp = ssrf_proxy.head(url, follow_redirects=True) - resp.raise_for_status() - # Try to extract filename from response headers or URL - content_disposition = resp.headers.get("Content-Disposition") - if content_disposition: - filename = content_disposition.split("filename=")[-1].strip('"') + resp = ssrf_proxy.head(url, follow_redirects=True) + if resp.status_code == httpx.codes.OK: + # Try to extract filename from response headers or URL + content_disposition = resp.headers.get("Content-Disposition") + if content_disposition: + filename = content_disposition.split("filename=")[-1].strip('"') + else: + filename = url.split("/")[-1].split("?")[0] + # Create the File object + file_size = int(resp.headers.get("Content-Length", -1)) + mime_type = str(resp.headers.get("Content-Type", "")) else: - filename = url.split("/")[-1].split("?")[0] + filename = "" + file_size = -1 + mime_type = "" + # If filename is empty, set a default one if not filename: filename = "unknown_file" - # Determine file extension extension = "." + filename.split(".")[-1] if "." in filename else ".bin" - # Create the File object - file_size = int(resp.headers.get("Content-Length", -1)) - mime_type = str(resp.headers.get("Content-Type", "")) if not mime_type: mime_type, _ = mimetypes.guess_type(url) file = File( From bc43efba75af4e4560393bfde0fe46ac71bb843d Mon Sep 17 00:00:00 2001 From: Joe <79627742+ZhouhaoJiang@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:56:53 +0800 Subject: [PATCH 32/35] fix: remove url join (#9635) --- api/services/enterprise/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/services/enterprise/base.py b/api/services/enterprise/base.py index 380051b40d..92098f06cc 100644 --- a/api/services/enterprise/base.py +++ b/api/services/enterprise/base.py @@ -1,5 +1,4 @@ import os -from urllib.parse import urljoin import requests @@ -16,7 +15,6 @@ class EnterpriseRequest: @classmethod def send_request(cls, method, endpoint, json=None, params=None): headers = {"Content-Type": "application/json", "Enterprise-Api-Secret-Key": cls.secret_key} - url = urljoin(cls.base_url, endpoint) + url = f"{cls.base_url}{endpoint}" response = requests.request(method, url, json=json, params=params, headers=headers, proxies=cls.proxies) - return response.json() From 5f12616cb96a9a0f5ef41aeca52a89ac9269279b 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 Oct 2024 16:33:50 +0800 Subject: [PATCH 33/35] fix: file type document is not supported (#9618) --- api/core/memory/token_buffer_memory.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index 189d94e290..d92c36a2df 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -2,6 +2,7 @@ from typing import Optional from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.file import file_manager +from core.file.models import FileType from core.model_manager import ModelInstance from core.model_runtime.entities import ( AssistantPromptMessage, @@ -98,8 +99,9 @@ class TokenBufferMemory: prompt_message_contents: list[PromptMessageContent] = [] prompt_message_contents.append(TextPromptMessageContent(data=message.query)) for file_obj in file_objs: - prompt_message = file_manager.to_prompt_message_content(file_obj) - prompt_message_contents.append(prompt_message) + if file_obj.type in {FileType.IMAGE, FileType.AUDIO}: + prompt_message = file_manager.to_prompt_message_content(file_obj) + prompt_message_contents.append(prompt_message) prompt_messages.append(UserPromptMessage(content=prompt_message_contents)) else: From 36f66d40e5f4fcf62d3b9cda4c5cf96b2a26a974 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 16:34:16 +0800 Subject: [PATCH 34/35] refactor(api): simplify limit retrieval and return types (#9641) --- api/controllers/console/datasets/file.py | 11 +++++------ .../app/task_pipeline/based_generate_task_pipeline.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/datasets/file.py b/api/controllers/console/datasets/file.py index 51be7e7a7d..17d2879875 100644 --- a/api/controllers/console/datasets/file.py +++ b/api/controllers/console/datasets/file.py @@ -30,13 +30,12 @@ class FileApi(Resource): @account_initialization_required @marshal_with(upload_config_fields) def get(self): - file_size_limit = dify_config.UPLOAD_FILE_SIZE_LIMIT - batch_count_limit = dify_config.UPLOAD_FILE_BATCH_LIMIT - image_file_size_limit = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT return { - "file_size_limit": file_size_limit, - "batch_count_limit": batch_count_limit, - "image_file_size_limit": image_file_size_limit, + "file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT, + "batch_count_limit": dify_config.UPLOAD_FILE_BATCH_LIMIT, + "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, + "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, + "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, }, 200 @setup_required diff --git a/api/core/app/task_pipeline/based_generate_task_pipeline.py b/api/core/app/task_pipeline/based_generate_task_pipeline.py index a43be5fdf2..51d610e2cb 100644 --- a/api/core/app/task_pipeline/based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/based_generate_task_pipeline.py @@ -53,7 +53,7 @@ class BasedGenerateTaskPipeline: self._output_moderation_handler = self._init_output_moderation() self._stream = stream - def _handle_error(self, event: QueueErrorEvent, message: Optional[Message] = None) -> Exception: + def _handle_error(self, event: QueueErrorEvent, message: Optional[Message] = None): """ Handle error event. :param event: event @@ -100,7 +100,7 @@ class BasedGenerateTaskPipeline: return message - def _error_to_stream_response(self, e: Exception) -> ErrorStreamResponse: + def _error_to_stream_response(self, e: Exception): """ Error to stream response. :param e: exception From d6e8290a1cbd293c84da94c378ab84d7b559a3ef Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 22 Oct 2024 17:24:42 +0800 Subject: [PATCH 35/35] fix(files): update Content-Length handling for tool and remote files (#9649) --- api/controllers/files/tool_files.py | 6 +++--- api/controllers/web/file.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/controllers/files/tool_files.py b/api/controllers/files/tool_files.py index 104b7cd9bb..a298701a2f 100644 --- a/api/controllers/files/tool_files.py +++ b/api/controllers/files/tool_files.py @@ -42,10 +42,10 @@ class ToolFilePreviewApi(Resource): stream, mimetype=tool_file.mimetype, direct_passthrough=True, - headers={ - "Content-Length": str(tool_file.size), - }, + headers={}, ) + if tool_file.size > 0: + response.headers["Content-Length"] = str(tool_file.size) if args["as_attachment"]: response.headers["Content-Disposition"] = f"attachment; filename={tool_file.name}" diff --git a/api/controllers/web/file.py b/api/controllers/web/file.py index c029a07707..6eeaa0e3f0 100644 --- a/api/controllers/web/file.py +++ b/api/controllers/web/file.py @@ -46,7 +46,7 @@ class RemoteFileInfoApi(WebApiResource): response = ssrf_proxy.head(decoded_url) return { "file_type": response.headers.get("Content-Type", "application/octet-stream"), - "file_length": int(response.headers.get("Content-Length", 0)), + "file_length": int(response.headers.get("Content-Length", -1)), } except Exception as e: return {"error": str(e)}, 400