From 2927493cf3aaaf5fc1276c81625205236a70d11a Mon Sep 17 00:00:00 2001 From: yihong Date: Tue, 26 Nov 2024 19:39:55 +0800 Subject: [PATCH 01/65] fix: better way to handle github dsl url close #11113 (#11125) Signed-off-by: yihong0618 --- api/services/app_dsl_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index b3c919dbd9..a4d71d5424 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -113,6 +113,10 @@ class AppDslService: ) try: max_size = 10 * 1024 * 1024 # 10MB + # tricky way to handle url from github to github raw url + if yaml_url.startswith("https://github.com") and yaml_url.endswith((".yml", ".yaml")): + yaml_url = yaml_url.replace("https://github.com", "https://raw.githubusercontent.com") + yaml_url = yaml_url.replace("/blob/", "/") response = ssrf_proxy.get(yaml_url.strip(), follow_redirects=True, timeout=(10, 10)) response.raise_for_status() content = response.content From 223a30401c3d97984afb709d594469fd5b5998e0 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 20:56:48 +0800 Subject: [PATCH 02/65] fix: LLM invoke error should not be raised (#11141) Signed-off-by: -LAN- --- api/core/workflow/nodes/llm/node.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 39480e34b3..d1d7b983ff 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -197,7 +197,6 @@ class LLMNode(BaseNode[LLMNodeData]): ) return except Exception as e: - logger.exception(f"Node {self.node_id} failed to run") yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, From f458580dee84066b0bbd1b32eb3359b90de2125d Mon Sep 17 00:00:00 2001 From: "Charlie.Wei" Date: Tue, 26 Nov 2024 21:46:56 +0800 Subject: [PATCH 03/65] fix parameter extractor function call Expected str (#11142) --- .../nodes/parameter_extractor/parameter_extractor_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py index b64bde8ac5..5b960ea615 100644 --- a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py +++ b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py @@ -235,7 +235,7 @@ class ParameterExtractorNode(LLMNode): raise InvalidInvokeResultError(f"Invalid invoke result: {invoke_result}") text = invoke_result.message.content - if not isinstance(text, str): + if not isinstance(text, str | None): raise InvalidTextContentTypeError(f"Invalid text content type: {type(text)}. Expected str.") usage = invoke_result.usage From 9789905a1f08c551cf08c0442a881a2296035243 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 22:03:19 +0800 Subject: [PATCH 04/65] chore(*): Removes debugging print statements (#11145) Signed-off-by: -LAN- --- api/controllers/console/auth/data_source_oauth.py | 1 - api/controllers/console/auth/oauth.py | 1 - .../output_parser/suggested_questions_after_answer.py | 1 - api/core/rag/datasource/vdb/oracle/oraclevector.py | 1 - api/core/workflow/graph_engine/graph_engine.py | 1 - 5 files changed, 5 deletions(-) diff --git a/api/controllers/console/auth/data_source_oauth.py b/api/controllers/console/auth/data_source_oauth.py index 3c3f45260a..faca67bb17 100644 --- a/api/controllers/console/auth/data_source_oauth.py +++ b/api/controllers/console/auth/data_source_oauth.py @@ -34,7 +34,6 @@ class OAuthDataSource(Resource): OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers() with current_app.app_context(): oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider) - print(vars(oauth_provider)) if not oauth_provider: return {"error": "Invalid provider"}, 400 if dify_config.NOTION_INTEGRATION_TYPE == "internal": diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index f53c28e2ec..5de8c6766d 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -52,7 +52,6 @@ class OAuthLogin(Resource): OAUTH_PROVIDERS = get_oauth_providers() with current_app.app_context(): oauth_provider = OAUTH_PROVIDERS.get(provider) - print(vars(oauth_provider)) if not oauth_provider: return {"error": "Invalid provider"}, 400 diff --git a/api/core/llm_generator/output_parser/suggested_questions_after_answer.py b/api/core/llm_generator/output_parser/suggested_questions_after_answer.py index 182aeed98f..c451bf514c 100644 --- a/api/core/llm_generator/output_parser/suggested_questions_after_answer.py +++ b/api/core/llm_generator/output_parser/suggested_questions_after_answer.py @@ -15,6 +15,5 @@ class SuggestedQuestionsAfterAnswerOutputParser: json_obj = json.loads(action_match.group(0).strip()) else: json_obj = [] - print(f"Could not parse LLM output: {text}") return json_obj diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py index 4ced5d61e5..71c58c9d0c 100644 --- a/api/core/rag/datasource/vdb/oracle/oraclevector.py +++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py @@ -230,7 +230,6 @@ class OracleVector(BaseVector): except LookupError: nltk.download("punkt") nltk.download("stopwords") - print("run download") e_str = re.sub(r"[^\w ]", "", query) all_tokens = nltk.word_tokenize(e_str) stop_words = stopwords.words("english") diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 60a5901b21..035c34dcf4 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -64,7 +64,6 @@ class GraphEngineThreadPool(ThreadPoolExecutor): self.submit_count -= 1 def check_is_full(self) -> None: - print(f"submit_count: {self.submit_count}, max_submit_count: {self.max_submit_count}") if self.submit_count > self.max_submit_count: raise ValueError(f"Max submit count {self.max_submit_count} of workflow thread pool reached.") From a918cea2feee0b0addf6c99629b860d0514aa5e8 Mon Sep 17 00:00:00 2001 From: Hiroshi Fujita Date: Wed, 27 Nov 2024 12:42:42 +0900 Subject: [PATCH 05/65] feat: add VTT file support to Document Extractor (#11148) --- api/core/workflow/nodes/document_extractor/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index c3cacdab7f..d963241f07 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -113,7 +113,7 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str: def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str: """Extract text from a file based on its file extension.""" match file_extension: - case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml": + case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml" | ".vtt": return _extract_text_from_plain_text(file_content) case ".json": return _extract_text_from_json(file_content) From baef18ceddb51ce692ec30e7a453c017afa18933 Mon Sep 17 00:00:00 2001 From: Novice <857526207@qq.com> Date: Wed, 27 Nov 2024 13:42:28 +0800 Subject: [PATCH 06/65] fix: Incorrect iteration log display in workflow with multiple parallel mode iteartaion nodes (#11158) Co-authored-by: Novice Lee --- .../workflow/hooks/use-workflow-run.ts | 19 +++++++++++++------ web/app/components/workflow/run/index.tsx | 10 +++++++++- web/app/components/workflow/store.ts | 6 +++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index eab3535505..24b20b5274 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -271,13 +271,18 @@ export const useWorkflowRun = () => { } as any) } else { - if (!iterParallelLogMap.has(data.parallel_run_id)) - iterParallelLogMap.set(data.parallel_run_id, [{ ...data, status: NodeRunningStatus.Running } as any]) + const nodeId = iterations?.node_id as string + if (!iterParallelLogMap.has(nodeId as string)) + iterParallelLogMap.set(iterations?.node_id as string, new Map()) + + const currentIterLogMap = iterParallelLogMap.get(nodeId)! + if (!currentIterLogMap.has(data.parallel_run_id)) + currentIterLogMap.set(data.parallel_run_id, [{ ...data, status: NodeRunningStatus.Running } as any]) else - iterParallelLogMap.get(data.parallel_run_id)!.push({ ...data, status: NodeRunningStatus.Running } as any) + currentIterLogMap.get(data.parallel_run_id)!.push({ ...data, status: NodeRunningStatus.Running } as any) setIterParallelLogMap(iterParallelLogMap) if (iterations) - iterations.details = Array.from(iterParallelLogMap.values()) + iterations.details = Array.from(currentIterLogMap.values()) } })) } @@ -373,7 +378,7 @@ export const useWorkflowRun = () => { if (iterations && iterations.details) { const iterRunID = data.execution_metadata?.parallel_mode_run_id - const currIteration = iterParallelLogMap.get(iterRunID) + const currIteration = iterParallelLogMap.get(iterations.node_id)?.get(iterRunID) const nodeIndex = currIteration?.findIndex(node => node.node_id === data.node_id && ( node?.parallel_run_id === data.execution_metadata?.parallel_mode_run_id), @@ -392,7 +397,9 @@ export const useWorkflowRun = () => { } } setIterParallelLogMap(iterParallelLogMap) - iterations.details = Array.from(iterParallelLogMap.values()) + const iterLogMap = iterParallelLogMap.get(iterations.node_id) + if (iterLogMap) + iterations.details = Array.from(iterLogMap.values()) } })) } diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 5267cf257d..5503103bdb 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -62,7 +62,7 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe const formatNodeList = useCallback((list: NodeTracing[]) => { const allItems = [...list].reverse() const result: NodeTracing[] = [] - const groupMap = new Map() + const nodeGroupMap = new Map>() const processIterationNode = (item: NodeTracing) => { result.push({ @@ -70,11 +70,19 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe details: [], }) } + const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => { + if (!nodeGroupMap.has(iterationNode.node_id)) + nodeGroupMap.set(iterationNode.node_id, new Map()) + + const groupMap = nodeGroupMap.get(iterationNode.node_id)! + if (!groupMap.has(runId)) groupMap.set(runId, [item]) + else groupMap.get(runId)!.push(item) + if (item.status === 'failed') { iterationNode.status = 'failed' iterationNode.error = item.error diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index c4a625c777..23f4188d85 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -169,8 +169,8 @@ type Shape = { setShowTips: (showTips: string) => void iterTimes: number setIterTimes: (iterTimes: number) => void - iterParallelLogMap: Map - setIterParallelLogMap: (iterParallelLogMap: Map) => void + iterParallelLogMap: Map> + setIterParallelLogMap: (iterParallelLogMap: Map>) => void } export const createWorkflowStore = () => { @@ -288,7 +288,7 @@ export const createWorkflowStore = () => { setShowTips: showTips => set(() => ({ showTips })), iterTimes: 1, setIterTimes: iterTimes => set(() => ({ iterTimes })), - iterParallelLogMap: new Map(), + iterParallelLogMap: new Map>(), setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })), })) From 40fc6f529e77c50fef23e7cb578ace96cb248b63 Mon Sep 17 00:00:00 2001 From: yihong Date: Wed, 27 Nov 2024 17:27:11 +0800 Subject: [PATCH 07/65] fix: gitee ai wrong default model, and better para (#11168) Signed-off-by: yihong0618 --- .../model_runtime/model_providers/gitee_ai/llm/llm.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py b/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py index 1b85b7fced..0c253a4a0a 100644 --- a/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py +++ b/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py @@ -32,12 +32,12 @@ class GiteeAILargeLanguageModel(OAIAPICompatLargeLanguageModel): return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) def validate_credentials(self, model: str, credentials: dict) -> None: - self._add_custom_parameters(credentials, model, None) + self._add_custom_parameters(credentials, None) super().validate_credentials(model, credentials) - def _add_custom_parameters(self, credentials: dict, model: str, model_parameters: dict) -> None: + def _add_custom_parameters(self, credentials: dict, model: Optional[str]) -> None: if model is None: - model = "bge-large-zh-v1.5" + model = "Qwen2-72B-Instruct" model_identity = GiteeAILargeLanguageModel.MODEL_TO_IDENTITY.get(model, model) credentials["endpoint_url"] = f"https://ai.gitee.com/api/serverless/{model_identity}/" @@ -47,5 +47,7 @@ class GiteeAILargeLanguageModel(OAIAPICompatLargeLanguageModel): credentials["mode"] = LLMMode.CHAT.value schema = self.get_model_schema(model, credentials) + assert schema is not None, f"Model schema not found for model {model}" + assert schema.features is not None, f"Model features not found for model {model}" if ModelFeature.TOOL_CALL in schema.features or ModelFeature.MULTI_TOOL_CALL in schema.features: credentials["function_calling_type"] = "tool_call" From 787285d58fadd1de3024e868a25e19a9c49541a7 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 27 Nov 2024 17:28:01 +0800 Subject: [PATCH 08/65] fix(file_factory): convert tool file correctly. (#11167) Signed-off-by: -LAN- --- api/factories/file_factory.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index ad8dba8190..d866195f25 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -52,8 +52,6 @@ def build_from_mapping( tenant_id: str, config: FileUploadConfig | None = None, ) -> File: - config = config or FileUploadConfig() - transfer_method = FileTransferMethod.value_of(mapping.get("transfer_method")) build_functions: dict[FileTransferMethod, Callable] = { @@ -72,7 +70,7 @@ def build_from_mapping( transfer_method=transfer_method, ) - if not _is_file_valid_with_config( + if config and not _is_file_valid_with_config( input_file_type=mapping.get("type", FileType.CUSTOM), file_extension=file.extension, file_transfer_method=file.transfer_method, @@ -127,7 +125,7 @@ def _build_from_local_file( if row is None: raise ValueError("Invalid upload file") - file_type = FileType(mapping.get("type")) + file_type = FileType(mapping.get("type", "custom")) file_type = _standardize_file_type(file_type, extension="." + row.extension, mime_type=row.mime_type) return File( @@ -157,7 +155,7 @@ def _build_from_remote_url( mime_type, filename, file_size = _get_remote_file_info(url) extension = mimetypes.guess_extension(mime_type) or "." + filename.split(".")[-1] if "." in filename else ".bin" - file_type = FileType(mapping.get("type")) + file_type = FileType(mapping.get("type", "custom")) file_type = _standardize_file_type(file_type, extension=extension, mime_type=mime_type) return File( @@ -208,7 +206,7 @@ def _build_from_tool_file( raise ValueError(f"ToolFile {mapping.get('tool_file_id')} not found") extension = "." + tool_file.file_key.split(".")[-1] if "." in tool_file.file_key else ".bin" - file_type = FileType(mapping.get("type")) + file_type = FileType(mapping.get("type", "custom")) file_type = _standardize_file_type(file_type, extension=extension, mime_type=tool_file.mimetype) return File( From 33d6d26bbf2d49dfb56a0a1603ff673667c602ab Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Wed, 27 Nov 2024 17:40:40 +0800 Subject: [PATCH 09/65] Adding AWS CDK deploy link in README in multi-language (#11166) --- README.md | 7 +++++++ README_AR.md | 14 ++++++++++++++ README_CN.md | 7 +++++++ README_ES.md | 14 ++++++++++++++ README_FR.md | 14 ++++++++++++++ README_JA.md | 7 +++++++ README_KL.md | 7 +++++++ README_KR.md | 7 +++++++ README_PT.md | 7 +++++++ README_SI.md | 7 +++++++ README_TR.md | 7 +++++++ README_VI.md | 7 +++++++ 12 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 4c2d803854..df6c481e78 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,13 @@ Deploy Dify to Cloud Platform with a single click using [terraform](https://www. ##### Google Cloud - [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Using AWS CDK for Deployment + +Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Contributing For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_AR.md b/README_AR.md index a4cfd744c0..d42c7508b1 100644 --- a/README_AR.md +++ b/README_AR.md @@ -190,6 +190,13 @@ docker compose up -d ##### Google Cloud - [Google Cloud Terraform بواسطة @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### استخدام AWS CDK للنشر + +انشر Dify على AWS باستخدام [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK بواسطة @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## المساهمة لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا. @@ -222,3 +229,10 @@ docker compose up -d ## الرخصة هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية. +## الكشف عن الأمان + +لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى security@dify.ai وسنقدم لك إجابة أكثر تفصيلاً. + +## الرخصة + +هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية. diff --git a/README_CN.md b/README_CN.md index 2a3f12dd05..8d1cfbf274 100644 --- a/README_CN.md +++ b/README_CN.md @@ -213,6 +213,13 @@ docker compose up -d ##### Google Cloud - [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### 使用 AWS CDK 部署 + +使用 [CDK](https://aws.amazon.com/cdk/) 将 Dify 部署到 AWS + +##### AWS +- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) diff --git a/README_ES.md b/README_ES.md index ab79ec9f85..9763de69fb 100644 --- a/README_ES.md +++ b/README_ES.md @@ -215,6 +215,13 @@ Despliega Dify en una plataforma en la nube con un solo clic utilizando [terrafo ##### Google Cloud - [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Usando AWS CDK para el Despliegue + +Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Contribuir Para aquellos que deseen contribuir con código, consulten nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). @@ -248,3 +255,10 @@ Para proteger tu privacidad, evita publicar problemas de seguridad en GitHub. En ## Licencia Este repositorio está disponible bajo la [Licencia de Código Abierto de Dify](LICENSE), que es esencialmente Apache 2.0 con algunas restricciones adicionales. +## Divulgación de Seguridad + +Para proteger tu privacidad, evita publicar problemas de seguridad en GitHub. En su lugar, envía tus preguntas a security@dify.ai y te proporcionaremos una respuesta más detallada. + +## Licencia + +Este repositorio está disponible bajo la [Licencia de Código Abierto de Dify](LICENSE), que es esencialmente Apache 2.0 con algunas restricciones adicionales. diff --git a/README_FR.md b/README_FR.md index 1c963b495f..974c0b9297 100644 --- a/README_FR.md +++ b/README_FR.md @@ -213,6 +213,13 @@ Déployez Dify sur une plateforme cloud en un clic en utilisant [terraform](http ##### Google Cloud - [Google Cloud Terraform par @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Utilisation d'AWS CDK pour le déploiement + +Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK par @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Contribuer Pour ceux qui souhaitent contribuer du code, consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). @@ -246,3 +253,10 @@ Pour protéger votre vie privée, veuillez éviter de publier des problèmes de ## Licence Ce référentiel est disponible sous la [Licence open source Dify](LICENSE), qui est essentiellement l'Apache 2.0 avec quelques restrictions supplémentaires. +## Divulgation de sécurité + +Pour protéger votre vie privée, veuillez éviter de publier des problèmes de sécurité sur GitHub. Au lieu de cela, envoyez vos questions à security@dify.ai et nous vous fournirons une réponse plus détaillée. + +## Licence + +Ce référentiel est disponible sous la [Licence open source Dify](LICENSE), qui est essentiellement l'Apache 2.0 avec quelques restrictions supplémentaires. diff --git a/README_JA.md b/README_JA.md index b0f06ff259..9651219157 100644 --- a/README_JA.md +++ b/README_JA.md @@ -212,6 +212,13 @@ docker compose up -d ##### Google Cloud - [@sotazumによるGoogle Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform) +#### AWS CDK を使用したデプロイ + +[CDK](https://aws.amazon.com/cdk/) を使用して、DifyをAWSにデプロイします + +##### AWS +- [@KevinZhaoによるAWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## 貢献 コードに貢献したい方は、[Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)を参照してください。 diff --git a/README_KL.md b/README_KL.md index be727774e9..dd37b8243b 100644 --- a/README_KL.md +++ b/README_KL.md @@ -213,6 +213,13 @@ wa'logh nIqHom neH ghun deployment toy'wI' [terraform](https://www.terraform.io/ ##### Google Cloud - [Google Cloud Terraform qachlot @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### AWS CDK atorlugh pilersitsineq + +wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo'laH. + +##### AWS +- [AWS CDK qachlot @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Contributing For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_KR.md b/README_KR.md index 9f8e072ba6..8edbb99226 100644 --- a/README_KR.md +++ b/README_KR.md @@ -205,6 +205,13 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 ##### Google Cloud - [sotazum의 Google Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform) +#### AWS CDK를 사용한 배포 + +[CDK](https://aws.amazon.com/cdk/)를 사용하여 AWS에 Dify 배포 + +##### AWS +- [KevinZhao의 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## 기여 코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. diff --git a/README_PT.md b/README_PT.md index d822cbea67..f947538952 100644 --- a/README_PT.md +++ b/README_PT.md @@ -211,6 +211,13 @@ Implante o Dify na Plataforma Cloud com um único clique usando [terraform](http ##### Google Cloud - [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Usando AWS CDK para Implantação + +Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Contribuindo Para aqueles que desejam contribuir com código, veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_SI.md b/README_SI.md index 41a44600e8..6badf47f01 100644 --- a/README_SI.md +++ b/README_SI.md @@ -145,6 +145,13 @@ namestite Dify v Cloud Platform z enim klikom z uporabo [terraform](https://www. ##### Google Cloud - [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Uporaba AWS CDK za uvajanje + +Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Prispevam Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkrati vas prosimo, da podprete Dify tako, da ga delite na družbenih medijih ter na dogodkih in konferencah. diff --git a/README_TR.md b/README_TR.md index 38fada34e9..24ed0c9a08 100644 --- a/README_TR.md +++ b/README_TR.md @@ -211,6 +211,13 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter ##### Google Cloud - [Google Cloud Terraform tarafından @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### AWS CDK ile Dağıtım + +[CDK](https://aws.amazon.com/cdk/) kullanarak Dify'ı AWS'ye dağıtın + +##### AWS +- [AWS CDK tarafından @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Katkıda Bulunma Kod katkısında bulunmak isteyenler için [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakabilirsiniz. diff --git a/README_VI.md b/README_VI.md index 6f296e508c..9076fcaae7 100644 --- a/README_VI.md +++ b/README_VI.md @@ -207,6 +207,13 @@ Triển khai Dify lên nền tảng đám mây với một cú nhấp chuột b ##### Google Cloud - [Google Cloud Terraform bởi @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) +#### Sử dụng AWS CDK để Triển khai + +Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK bởi @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + ## Đóng góp Đối với những người muốn đóng góp mã, xem [Hướng dẫn Đóng góp](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) của chúng tôi. From 41c6bf5fe423ca7bbdfede7d9132e736c5cb8912 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:41:00 +0800 Subject: [PATCH 10/65] update the scheduler of update_tidb_serverless_status_task to 1/10min (#11135) --- api/extensions/ext_celery.py | 2 +- .../update_tidb_serverless_status_task.py | 25 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 1b78e36a57..7d0f13b391 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -86,7 +86,7 @@ def init_app(app: Flask) -> Celery: }, "update_tidb_serverless_status_task": { "task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task", - "schedule": crontab(minute="30", hour="*"), + "schedule": timedelta(minutes=10), }, "clean_messages": { "task": "schedule.clean_messages.clean_messages", diff --git a/api/schedule/update_tidb_serverless_status_task.py b/api/schedule/update_tidb_serverless_status_task.py index 07eca3173b..b2d8746f9c 100644 --- a/api/schedule/update_tidb_serverless_status_task.py +++ b/api/schedule/update_tidb_serverless_status_task.py @@ -12,21 +12,18 @@ from models.dataset import TidbAuthBinding def update_tidb_serverless_status_task(): click.echo(click.style("Update tidb serverless status task.", fg="green")) start_at = time.perf_counter() - while True: - try: - # check the number of idle tidb serverless - tidb_serverless_list = TidbAuthBinding.query.filter( - TidbAuthBinding.active == False, TidbAuthBinding.status == "CREATING" - ).all() - if len(tidb_serverless_list) == 0: - break - # update tidb serverless status - iterations_per_thread = 20 - update_clusters(tidb_serverless_list) + try: + # check the number of idle tidb serverless + tidb_serverless_list = TidbAuthBinding.query.filter( + TidbAuthBinding.active == False, TidbAuthBinding.status == "CREATING" + ).all() + if len(tidb_serverless_list) == 0: + return + # update tidb serverless status + update_clusters(tidb_serverless_list) - except Exception as e: - click.echo(click.style(f"Error: {e}", fg="red")) - break + except Exception as e: + click.echo(click.style(f"Error: {e}", fg="red")) end_at = time.perf_counter() click.echo( From 6f418da3882526c563492630909988ea34fbc558 Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Wed, 27 Nov 2024 19:50:56 +0800 Subject: [PATCH 11/65] Fixes #11065: tenant_id not found when login via ADMIN_KEY (#11066) --- api/app_factory.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/app_factory.py b/api/app_factory.py index 60a584798b..46a101c4ab 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -15,6 +15,7 @@ import json from flask import Flask, Response, request from flask_cors import CORS +from flask_login import user_loaded_from_request, user_logged_in from werkzeug.exceptions import Unauthorized import contexts @@ -120,11 +121,17 @@ def load_user_from_request(request_from_flask_login): user_id = decoded.get("user_id") logged_in_account = AccountService.load_logged_in_account(account_id=user_id) - if logged_in_account: - contexts.tenant_id.set(logged_in_account.current_tenant_id) return logged_in_account +@user_logged_in.connect +@user_loaded_from_request.connect +def on_user_logged_in(_sender, user): + """Called when a user logged in.""" + if user: + contexts.tenant_id.set(user.current_tenant_id) + + @login_manager.unauthorized_handler def unauthorized_handler(): """Handle unauthorized requests.""" From 9049dd772556aeef36c8ab139858c9b1181107ed Mon Sep 17 00:00:00 2001 From: jiangbo721 <365065261@qq.com> Date: Wed, 27 Nov 2024 23:44:51 +0800 Subject: [PATCH 12/65] fix: code linting (#11143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 刘江波 --- api/services/account_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index aeb373bb26..f0c6ac7ebd 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -573,7 +573,7 @@ class TenantService: return tenant @staticmethod - def switch_tenant(account: Account, tenant_id: Optional[int] = None) -> None: + def switch_tenant(account: Account, tenant_id: Optional[str] = None) -> None: """Switch the current workspace for the account""" # Ensure tenant_id is provided @@ -672,7 +672,7 @@ class TenantService: return db.session.query(func.count(Tenant.id)).scalar() @staticmethod - def check_member_permission(tenant: Tenant, operator: Account, member: Account, action: str) -> None: + def check_member_permission(tenant: Tenant, operator: Account, member: Account | None, action: str) -> None: """Check member permission""" perms = { "add": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN], From 0a30a5b077d340e62685237764121fb7b2aed383 Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 28 Nov 2024 11:02:25 +0800 Subject: [PATCH 13/65] Feat: remove github star and community links if it is enterprise version (#11180) --- web/app/(commonLayout)/apps/page.tsx | 20 ++++++++++++-------- web/app/components/header/index.tsx | 10 ++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx index 76985de34f..ab9852e462 100644 --- a/web/app/(commonLayout)/apps/page.tsx +++ b/web/app/(commonLayout)/apps/page.tsx @@ -1,23 +1,27 @@ +'use client' +import { useContextSelector } from 'use-context-selector' +import { useTranslation } from 'react-i18next' import style from '../list.module.css' import Apps from './Apps' import classNames from '@/utils/classnames' -import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server' +import AppContext from '@/context/app-context' +import { LicenseStatus } from '@/types/feature' -const AppList = async () => { - const locale = getLocaleOnServer() - const { t } = await translate(locale, 'app') +const AppList = () => { + const { t } = useTranslation() + const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) return (
-
-

{t('join')}

-

{t('communityIntro')}

+ {systemFeatures.license.status === LicenseStatus.NONE &&
+

{t('app.join')}

+

{t('app.communityIntro')}

-
+
}
) } diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 3757d552df..1d7349ccd0 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { useBoolean } from 'ahooks' import { useSelectedLayoutSegment } from 'next/navigation' import { Bars3Icon } from '@heroicons/react/20/solid' +import { useContextSelector } from 'use-context-selector' import HeaderBillingBtn from '../billing/header-billing-btn' import AccountDropdown from './account-dropdown' import AppNav from './app-nav' @@ -14,11 +15,12 @@ import ToolsNav from './tools-nav' import GithubStar from './github-star' import LicenseNav from './license-env' import { WorkspaceProvider } from '@/context/workspace-context' -import { useAppContext } from '@/context/app-context' +import AppContext, { useAppContext } from '@/context/app-context' import LogoSite from '@/app/components/base/logo/logo-site' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' +import { LicenseStatus } from '@/types/feature' const navClassName = ` flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl @@ -28,7 +30,7 @@ const navClassName = ` const Header = () => { const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() - + const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) const selectedSegment = useSelectedLayoutSegment() const media = useBreakpoints() const isMobile = media === MediaType.mobile @@ -60,7 +62,7 @@ const Header = () => { - + {systemFeatures.license.status === LicenseStatus.NONE && } } {isMobile && ( @@ -68,7 +70,7 @@ const Header = () => { - + {systemFeatures.license.status === LicenseStatus.NONE && } )} {!isMobile && ( From 18d3ffc194f64a4810e4106ddd435cdc66c46591 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Thu, 28 Nov 2024 12:26:02 +0800 Subject: [PATCH 14/65] Feat: new pagination (#11170) --- web/app/components/app/annotation/index.tsx | 42 +--- .../app/annotation/style.module.css | 3 - .../view-annotation-modal/index.tsx | 36 +--- .../components/app/log-annotation/index.tsx | 2 +- web/app/components/app/log/index.tsx | 50 ++--- web/app/components/app/log/style.module.css | 3 - web/app/components/app/workflow-log/index.tsx | 42 +--- web/app/components/app/workflow-log/list.tsx | 3 +- .../app/workflow-log/style.module.css | 3 - web/app/components/base/pagination/hook.ts | 95 +++++++++ web/app/components/base/pagination/index.tsx | 161 ++++++++++++--- .../components/base/pagination/pagination.tsx | 189 ++++++++++++++++++ .../base/pagination/style.module.css | 3 - web/app/components/base/pagination/type.ts | 58 ++++++ web/i18n/en-US/common.ts | 3 + web/i18n/zh-Hans/common.ts | 3 + web/package.json | 1 - web/yarn.lock | 7 - 18 files changed, 524 insertions(+), 180 deletions(-) delete mode 100644 web/app/components/app/annotation/style.module.css delete mode 100644 web/app/components/app/log/style.module.css delete mode 100644 web/app/components/app/workflow-log/style.module.css create mode 100644 web/app/components/base/pagination/hook.ts create mode 100644 web/app/components/base/pagination/pagination.tsx delete mode 100644 web/app/components/base/pagination/style.module.css create mode 100644 web/app/components/base/pagination/type.ts diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 0783c3fa66..418079abe8 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -2,19 +2,17 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import Toast from '../../base/toast' import Filter from './filter' import type { QueryParam } from './filter' import List from './list' import EmptyElement from './empty-element' import HeaderOpts from './header-opts' -import s from './style.module.css' import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type' import ViewAnnotationModal from './view-annotation-modal' import cn from '@/utils/classnames' +import Pagination from '@/app/components/base/pagination' import Switch from '@/app/components/base/switch' import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation' import Loading from '@/app/components/base/loading' @@ -69,9 +67,10 @@ const Annotation: FC = ({ const [queryParams, setQueryParams] = useState({}) const [currPage, setCurrPage] = React.useState(0) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, keyword: debouncedQueryParams.keyword || '', } @@ -228,35 +227,12 @@ const Annotation: FC = ({ {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null} {isShowViewModal && ( diff --git a/web/app/components/app/annotation/style.module.css b/web/app/components/app/annotation/style.module.css deleted file mode 100644 index 24179c1ca1..0000000000 --- a/web/app/components/app/annotation/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} \ No newline at end of file diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index daa8434ff7..0fb8bbc31e 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -2,13 +2,12 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pagination } from 'react-headless-pagination' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item' import type { AnnotationItem, HitHistoryItem } from '../type' import s from './style.module.css' import HitHistoryNoData from './hit-history-no-data' import cn from '@/utils/classnames' +import Pagination from '@/app/components/base/pagination' import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' import Confirm from '@/app/components/base/confirm' @@ -150,35 +149,10 @@ const ViewAnnotationModal: FC = ({ {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + /> : null} diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx index c84d941143..3fa13019f9 100644 --- a/web/app/components/app/log-annotation/index.tsx +++ b/web/app/components/app/log-annotation/index.tsx @@ -52,7 +52,7 @@ const LogAnnotation: FC = ({ options={options} /> )} -
+
{pageType === PageType.log && appDetail.mode !== 'workflow' && ()} {pageType === PageType.annotation && ()} {pageType === PageType.log && appDetail.mode === 'workflow' && ()} diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index e076f587ea..592233facd 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -2,17 +2,15 @@ import type { FC, SVGProps } from 'react' import React, { useState } from 'react' import useSWR from 'swr' +import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' import { omit } from 'lodash-es' import dayjs from 'dayjs' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { Trans, useTranslation } from 'react-i18next' -import Link from 'next/link' import List from './list' import Filter, { TIME_PERIOD_MAPPING } from './filter' -import s from './style.module.css' +import Pagination from '@/app/components/base/pagination' import Loading from '@/app/components/base/loading' import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' import { APP_PAGE_LIMIT } from '@/config' @@ -60,6 +58,7 @@ const Logs: FC = ({ appDetail }) => { sort_by: '-created_at', }) const [currPage, setCurrPage] = React.useState(0) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) // Get the app type first @@ -67,7 +66,7 @@ const Logs: FC = ({ appDetail }) => { const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, ...((debouncedQueryParams.period !== '9') ? { start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), @@ -102,9 +101,9 @@ const Logs: FC = ({ appDetail }) => { const total = isChatMode ? chatConversations?.total : completionConversations?.total return ( -
-

{t('appLog.description')}

-
+
+

{t('appLog.description')}

+
{total === undefined ? @@ -115,35 +114,12 @@ const Logs: FC = ({ appDetail }) => { {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null}
diff --git a/web/app/components/app/log/style.module.css b/web/app/components/app/log/style.module.css deleted file mode 100644 index adb32a39db..0000000000 --- a/web/app/components/app/log/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 7a891f5895..5a8fa41a14 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -3,14 +3,12 @@ import type { FC, SVGProps } from 'react' import React, { useState } from 'react' import useSWR from 'swr' import { usePathname } from 'next/navigation' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { Trans, useTranslation } from 'react-i18next' import Link from 'next/link' import List from './list' import Filter from './filter' -import s from './style.module.css' +import Pagination from '@/app/components/base/pagination' import Loading from '@/app/components/base/loading' import { fetchWorkflowLogs } from '@/service/log' import { APP_PAGE_LIMIT } from '@/config' @@ -53,10 +51,11 @@ const Logs: FC = ({ appDetail }) => { const [queryParams, setQueryParams] = useState({ status: 'all' }) const [currPage, setCurrPage] = React.useState(0) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, ...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}), ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}), } @@ -89,35 +88,12 @@ const Logs: FC = ({ appDetail }) => { {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null}
diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index e43d95d5ad..e3de4a957f 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -// import s from './style.module.css' import DetailPanel from './detail' -import cn from '@/utils/classnames' import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' import type { App } from '@/types/app' import Loading from '@/app/components/base/loading' @@ -12,6 +10,7 @@ import Drawer from '@/app/components/base/drawer' import Indicator from '@/app/components/header/indicator' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' +import cn from '@/utils/classnames' type ILogs = { logs?: WorkflowLogsResponse diff --git a/web/app/components/app/workflow-log/style.module.css b/web/app/components/app/workflow-log/style.module.css deleted file mode 100644 index adb32a39db..0000000000 --- a/web/app/components/app/workflow-log/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} diff --git a/web/app/components/base/pagination/hook.ts b/web/app/components/base/pagination/hook.ts new file mode 100644 index 0000000000..6501d6f457 --- /dev/null +++ b/web/app/components/base/pagination/hook.ts @@ -0,0 +1,95 @@ +import React, { useCallback } from 'react' +import type { IPaginationProps, IUsePagination } from './type' + +const usePagination = ({ + currentPage, + setCurrentPage, + truncableText = '...', + truncableClassName = '', + totalPages, + edgePageCount, + middlePagesSiblingCount, +}: IPaginationProps): IUsePagination => { + const pages = Array(totalPages) + .fill(0) + .map((_, i) => i + 1) + + const hasPreviousPage = currentPage > 1 + const hasNextPage = currentPage < totalPages + + const isReachedToFirst = currentPage <= middlePagesSiblingCount + const isReachedToLast = currentPage + middlePagesSiblingCount >= totalPages + + const middlePages = React.useMemo(() => { + const middlePageCount = middlePagesSiblingCount * 2 + 1 + if (isReachedToFirst) + return pages.slice(0, middlePageCount) + + if (isReachedToLast) + return pages.slice(-middlePageCount) + + return pages.slice( + currentPage - middlePagesSiblingCount, + currentPage + middlePagesSiblingCount + 1, + ) + }, [currentPage, isReachedToFirst, isReachedToLast, middlePagesSiblingCount, pages]) + + const getAllPreviousPages = useCallback(() => { + return pages.slice(0, middlePages[0] - 1) + }, [middlePages, pages]) + + const previousPages = React.useMemo(() => { + if (isReachedToFirst || getAllPreviousPages().length < 1) + return [] + + return pages + .slice(0, edgePageCount) + .filter(p => !middlePages.includes(p)) + }, [edgePageCount, getAllPreviousPages, isReachedToFirst, middlePages, pages]) + + const getAllNextPages = React.useMemo(() => { + return pages.slice( + middlePages[middlePages.length - 1], + pages[pages.length], + ) + }, [pages, middlePages]) + + const nextPages = React.useMemo(() => { + if (isReachedToLast) + return [] + + if (getAllNextPages.length < 1) + return [] + + return pages + .slice(pages.length - edgePageCount, pages.length) + .filter(p => !middlePages.includes(p)) + }, [edgePageCount, getAllNextPages.length, isReachedToLast, middlePages, pages]) + + const isPreviousTruncable = React.useMemo(() => { + // Is truncable if first value of middlePage is larger than last value of previousPages + return middlePages[0] > previousPages[previousPages.length - 1] + 1 + }, [previousPages, middlePages]) + + const isNextTruncable = React.useMemo(() => { + // Is truncable if last value of middlePage is larger than first value of previousPages + return middlePages[middlePages.length - 1] + 1 < nextPages[0] + }, [nextPages, middlePages]) + + return { + currentPage, + setCurrentPage, + truncableText, + truncableClassName, + pages, + hasPreviousPage, + hasNextPage, + previousPages, + isPreviousTruncable, + middlePages, + isNextTruncable, + nextPages, + } +} + +export default usePagination diff --git a/web/app/components/base/pagination/index.tsx b/web/app/components/base/pagination/index.tsx index f8c5684b55..b64c712425 100644 --- a/web/app/components/base/pagination/index.tsx +++ b/web/app/components/base/pagination/index.tsx @@ -1,50 +1,165 @@ import type { FC } from 'react' import React from 'react' -import { Pagination } from 'react-headless-pagination' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' -import s from './style.module.css' +import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react' +import { useDebounceFn } from 'ahooks' +import { Pagination } from './pagination' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import cn from '@/utils/classnames' type Props = { + className?: string current: number onChange: (cur: number) => void total: number limit?: number + onLimitChange?: (limit: number) => void } -const CustomizedPagination: FC = ({ current, onChange, total, limit = 10 }) => { +const CustomizedPagination: FC = ({ + className, + current, + onChange, + total, + limit = 10, + onLimitChange, +}) => { const { t } = useTranslation() const totalPages = Math.ceil(total / limit) + const inputRef = React.useRef(null) + const [showInput, setShowInput] = React.useState(false) + const [inputValue, setInputValue] = React.useState(current + 1) + const [showPerPageTip, setShowPerPageTip] = React.useState(false) + + const { run: handlePaging } = useDebounceFn((value: string) => { + if (parseInt(value) > totalPages) { + setInputValue(totalPages) + onChange(totalPages - 1) + setShowInput(false) + return + } + if (parseInt(value) < 1) { + setInputValue(1) + onChange(0) + setShowInput(false) + return + } + onChange(parseInt(value) - 1) + setInputValue(parseInt(value)) + setShowInput(false) + }, { wait: 500 }) + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (!value) + return setInputValue('') + if (isNaN(parseInt(value))) + return setInputValue('') + setInputValue(parseInt(value)) + handlePaging(value) + } + return ( - - - {t('appLog.table.pagination.previous')} - -
+
+
} + disabled={current === 0} + > + + + {!showInput && ( +
setShowInput(true)} + > +
{current + 1}
+
/
+
{totalPages}
+
+ )} + {showInput && ( + setShowInput(false)} + /> + )} +
} + disabled={current === totalPages - 1} + > + + +
+
- - {t('appLog.table.pagination.next')} - - + {onLimitChange && ( +
+
{showPerPageTip ? t('common.pagination.perPage') : ''}
+
setShowPerPageTip(true)} + onMouseLeave={() => setShowPerPageTip(false)} + > +
onLimitChange?.(10)} + >10
+
onLimitChange?.(25)} + >25
+
onLimitChange?.(50)} + >50
+
+
+ )} ) } diff --git a/web/app/components/base/pagination/pagination.tsx b/web/app/components/base/pagination/pagination.tsx new file mode 100644 index 0000000000..5898c4e924 --- /dev/null +++ b/web/app/components/base/pagination/pagination.tsx @@ -0,0 +1,189 @@ +import React from 'react' +import clsx from 'clsx' +import usePagination from './hook' +import type { + ButtonProps, + IPagination, + IPaginationProps, + PageButtonProps, +} from './type' + +const defaultState: IPagination = { + currentPage: 0, + setCurrentPage: () => {}, + truncableText: '...', + truncableClassName: '', + pages: [], + hasPreviousPage: false, + hasNextPage: false, + previousPages: [], + isPreviousTruncable: false, + middlePages: [], + isNextTruncable: false, + nextPages: [], +} + +const PaginationContext: React.Context = React.createContext(defaultState) + +export const PrevButton = ({ + className, + children, + dataTestId, + as =
diff --git a/web/i18n/de-DE/dataset-creation.ts b/web/i18n/de-DE/dataset-creation.ts index 18f08814cb..b27a732d18 100644 --- a/web/i18n/de-DE/dataset-creation.ts +++ b/web/i18n/de-DE/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { websiteSource: 'Preprocess-Website', webpageUnit: 'Seiten', separatorTip: 'Ein Trennzeichen ist das Zeichen, das zum Trennen von Text verwendet wird. \\n\\n und \\n sind häufig verwendete Trennzeichen zum Trennen von Absätzen und Zeilen. In Kombination mit Kommas (\\n\\n,\\n) werden Absätze nach Zeilen segmentiert, wenn die maximale Blocklänge überschritten wird. Sie können auch spezielle, von Ihnen selbst definierte Trennzeichen verwenden (z. B. ***).', - maxLengthCheck: 'Die maximale Stücklänge sollte weniger als 4000 betragen', + maxLengthCheck: 'Die maximale Stücklänge sollte weniger als {{limit}} betragen', }, stepThree: { creationTitle: '🎉 Wissen erstellt', diff --git a/web/i18n/en-US/dataset-creation.ts b/web/i18n/en-US/dataset-creation.ts index de885671a7..6caf0056f8 100644 --- a/web/i18n/en-US/dataset-creation.ts +++ b/web/i18n/en-US/dataset-creation.ts @@ -103,7 +103,7 @@ const translation = { separatorTip: 'A delimiter is the character used to separate text. \\n\\n and \\n are commonly used delimiters for separating paragraphs and lines. Combined with commas (\\n\\n,\\n), paragraphs will be segmented by lines when exceeding the maximum chunk length. You can also use special delimiters defined by yourself (e.g. ***).', separatorPlaceholder: '\\n\\n for separating paragraphs; \\n for separating lines', maxLength: 'Maximum chunk length', - maxLengthCheck: 'Maximum chunk length should be less than 4000', + maxLengthCheck: 'Maximum chunk length should be less than {{limit}}', overlap: 'Chunk overlap', overlapTip: 'Setting the chunk overlap can maintain the semantic relevance between them, enhancing the retrieve effect. It is recommended to set 10%-25% of the maximum chunk size.', overlapCheck: 'chunk overlap should not bigger than maximum chunk length', diff --git a/web/i18n/es-ES/dataset-creation.ts b/web/i18n/es-ES/dataset-creation.ts index 02415b4359..2b6cac1b0e 100644 --- a/web/i18n/es-ES/dataset-creation.ts +++ b/web/i18n/es-ES/dataset-creation.ts @@ -147,7 +147,7 @@ const translation = { retrievalSettingTip: 'Para cambiar el método de índice, por favor ve a la ', datasetSettingLink: 'configuración del conocimiento.', separatorTip: 'Un delimitador es el carácter que se utiliza para separar el texto. \\n\\n y \\n son delimitadores comúnmente utilizados para separar párrafos y líneas. Combinado con comas (\\n\\n,\\n), los párrafos se segmentarán por líneas cuando excedan la longitud máxima del fragmento. También puede utilizar delimitadores especiales definidos por usted mismo (por ejemplo, ***).', - maxLengthCheck: 'La longitud máxima del fragmento debe ser inferior a 4000', + maxLengthCheck: 'La longitud máxima del fragmento debe ser inferior a {{limit}}', }, stepThree: { creationTitle: '🎉 Conocimiento creado', diff --git a/web/i18n/fa-IR/dataset-creation.ts b/web/i18n/fa-IR/dataset-creation.ts index 195d968228..0b5f42827e 100644 --- a/web/i18n/fa-IR/dataset-creation.ts +++ b/web/i18n/fa-IR/dataset-creation.ts @@ -147,7 +147,7 @@ const translation = { retrievalSettingTip: 'برای تغییر روش شاخص، لطفاً به', datasetSettingLink: 'تنظیمات دانش بروید.', separatorTip: 'جداکننده نویسه ای است که برای جداسازی متن استفاده می شود. \\n\\n و \\n معمولا برای جداسازی پاراگراف ها و خطوط استفاده می شوند. همراه با کاما (\\n\\n,\\n)، پاراگراف ها زمانی که از حداکثر طول تکه فراتر می روند، با خطوط تقسیم بندی می شوند. همچنین می توانید از جداکننده های خاصی که توسط خودتان تعریف شده اند استفاده کنید (مثلا ***).', - maxLengthCheck: 'حداکثر طول تکه باید کمتر از 4000 باشد', + maxLengthCheck: 'حداکثر طول تکه باید کمتر از {{limit}} باشد', }, stepThree: { creationTitle: ' دانش ایجاد شد', diff --git a/web/i18n/fr-FR/dataset-creation.ts b/web/i18n/fr-FR/dataset-creation.ts index 0c866d51e6..c777af4fbe 100644 --- a/web/i18n/fr-FR/dataset-creation.ts +++ b/web/i18n/fr-FR/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { webpageUnit: 'Pages', websiteSource: 'Site web de prétraitement', separatorTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n et \\n sont des délimiteurs couramment utilisés pour séparer les paragraphes et les lignes. Combiné à des virgules (\\n\\n,\\n), les paragraphes seront segmentés par des lignes lorsqu’ils dépasseront la longueur maximale des morceaux. Vous pouvez également utiliser des délimiteurs spéciaux définis par vous-même (par exemple ***).', - maxLengthCheck: 'La longueur maximale des morceaux doit être inférieure à 4000', + maxLengthCheck: 'La longueur maximale des morceaux doit être inférieure à {{limit}}', }, stepThree: { creationTitle: '🎉 Connaissance créée', diff --git a/web/i18n/hi-IN/dataset-creation.ts b/web/i18n/hi-IN/dataset-creation.ts index a242eebb24..51063dcb14 100644 --- a/web/i18n/hi-IN/dataset-creation.ts +++ b/web/i18n/hi-IN/dataset-creation.ts @@ -164,7 +164,7 @@ const translation = { retrievalSettingTip: 'इंडेक्स विधि बदलने के लिए, कृपया जाएं ', datasetSettingLink: 'ज्ञान सेटिंग्स।', separatorTip: 'एक सीमांकक पाठ को अलग करने के लिए उपयोग किया जाने वाला वर्ण है। \\n\\n और \\n आमतौर पर पैराग्राफ और लाइनों को अलग करने के लिए उपयोग किए जाने वाले सीमांकक हैं। अल्पविराम (\\n\\n,\\n) के साथ संयुक्त, अधिकतम खंड लंबाई से अधिक होने पर अनुच्छेदों को पंक्तियों द्वारा खंडित किया जाएगा। आप स्वयं द्वारा परिभाषित विशेष सीमांकक का भी उपयोग कर सकते हैं (उदा. ***).', - maxLengthCheck: 'अधिकतम चंक लंबाई 4000 से कम होनी चाहिए', + maxLengthCheck: 'अधिकतम चंक लंबाई {{limit}} से कम होनी चाहिए', }, stepThree: { creationTitle: '🎉 ज्ञान बनाया गया', diff --git a/web/i18n/it-IT/dataset-creation.ts b/web/i18n/it-IT/dataset-creation.ts index ebbe34f143..af174ce41f 100644 --- a/web/i18n/it-IT/dataset-creation.ts +++ b/web/i18n/it-IT/dataset-creation.ts @@ -167,7 +167,7 @@ const translation = { retrievalSettingTip: 'Per cambiare il metodo di indicizzazione, vai alle ', datasetSettingLink: 'impostazioni della Conoscenza.', separatorTip: 'Un delimitatore è il carattere utilizzato per separare il testo. \\n\\n e \\n sono delimitatori comunemente usati per separare paragrafi e righe. In combinazione con le virgole (\\n\\n,\\n), i paragrafi verranno segmentati per righe quando superano la lunghezza massima del blocco. È inoltre possibile utilizzare delimitatori speciali definiti dall\'utente (ad es. ***).', - maxLengthCheck: 'La lunghezza massima del blocco deve essere inferiore a 4000', + maxLengthCheck: 'La lunghezza massima del blocco deve essere inferiore a {{limit}}', }, stepThree: { creationTitle: '🎉 Conoscenza creata', diff --git a/web/i18n/ja-JP/dataset-creation.ts b/web/i18n/ja-JP/dataset-creation.ts index 597ec5f8f1..9ae6c4e737 100644 --- a/web/i18n/ja-JP/dataset-creation.ts +++ b/web/i18n/ja-JP/dataset-creation.ts @@ -147,7 +147,7 @@ const translation = { retrievalSettingTip: '検索方法を変更するには、', datasetSettingLink: 'ナレッジ設定', separatorTip: '区切り文字は、テキストを区切るために使用される文字です。\\n\\n と \\n は、段落と行を区切るために一般的に使用される区切り記号です。カンマ (\\n\\n,\\n) と組み合わせると、最大チャンク長を超えると、段落は行で区切られます。自分で定義した特別な区切り文字を使用することもできます(例:***)。', - maxLengthCheck: 'チャンクの最大長は 4000 未満にする必要があります', + maxLengthCheck: 'チャンクの最大長は {{limit}} 未満にする必要があります', }, stepThree: { creationTitle: '🎉 ナレッジが作成されました', diff --git a/web/i18n/ko-KR/dataset-creation.ts b/web/i18n/ko-KR/dataset-creation.ts index 52f7aff75d..ee5abb5189 100644 --- a/web/i18n/ko-KR/dataset-creation.ts +++ b/web/i18n/ko-KR/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { webpageUnit: '페이지', websiteSource: '웹 사이트 전처리', separatorTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n\\n 및 \\n은 단락과 줄을 구분하는 데 일반적으로 사용되는 구분 기호입니다. 쉼표(\\n\\n,\\n)와 함께 사용하면 최대 청크 길이를 초과할 경우 단락이 줄로 분할됩니다. 직접 정의한 특수 구분 기호(예: ***)를 사용할 수도 있습니다.', - maxLengthCheck: '최대 청크 길이는 4000 미만이어야 합니다.', + maxLengthCheck: '최대 청크 길이는 {{limit}} 미만이어야 합니다.', }, stepThree: { creationTitle: '🎉 지식이 생성되었습니다', diff --git a/web/i18n/pl-PL/dataset-creation.ts b/web/i18n/pl-PL/dataset-creation.ts index 81adff6336..c12ed66278 100644 --- a/web/i18n/pl-PL/dataset-creation.ts +++ b/web/i18n/pl-PL/dataset-creation.ts @@ -155,7 +155,7 @@ const translation = { webpageUnit: 'Stron', websiteSource: 'Witryna internetowa przetwarzania wstępnego', separatorTip: 'Ogranicznik to znak używany do oddzielania tekstu. \\n\\n i \\n są powszechnie używanymi ogranicznikami do oddzielania akapitów i wierszy. W połączeniu z przecinkami (\\n\\n,\\n), akapity będą segmentowane wierszami po przekroczeniu maksymalnej długości fragmentu. Możesz również skorzystać ze zdefiniowanych przez siebie specjalnych ograniczników (np. ***).', - maxLengthCheck: 'Maksymalna długość porcji powinna być mniejsza niż 4000', + maxLengthCheck: 'Maksymalna długość porcji powinna być mniejsza niż {{limit}}', }, stepThree: { creationTitle: '🎉 Utworzono Wiedzę', diff --git a/web/i18n/pt-BR/dataset-creation.ts b/web/i18n/pt-BR/dataset-creation.ts index 5ab2456562..8534bdce1d 100644 --- a/web/i18n/pt-BR/dataset-creation.ts +++ b/web/i18n/pt-BR/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { websiteSource: 'Site de pré-processamento', webpageUnit: 'Páginas', separatorTip: 'Um delimitador é o caractere usado para separar o texto. \\n\\n e \\n são delimitadores comumente usados para separar parágrafos e linhas. Combinado com vírgulas (\\n\\n,\\n), os parágrafos serão segmentados por linhas ao exceder o comprimento máximo do bloco. Você também pode usar delimitadores especiais definidos por você (por exemplo, ***).', - maxLengthCheck: 'O comprimento máximo do chunk deve ser inferior a 4000', + maxLengthCheck: 'O comprimento máximo do chunk deve ser inferior a {{limit}}', }, stepThree: { creationTitle: '🎉 Conhecimento criado', diff --git a/web/i18n/ro-RO/dataset-creation.ts b/web/i18n/ro-RO/dataset-creation.ts index 2f474f440a..f18b2ac5c2 100644 --- a/web/i18n/ro-RO/dataset-creation.ts +++ b/web/i18n/ro-RO/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { webpageUnit: 'Pagini', websiteSource: 'Site-ul web de preprocesare', separatorTip: 'Un delimitator este caracterul folosit pentru a separa textul. \\n\\n și \\n sunt delimitatori utilizați în mod obișnuit pentru separarea paragrafelor și liniilor. Combinate cu virgule (\\n\\n,\\n), paragrafele vor fi segmentate pe linii atunci când depășesc lungimea maximă a bucății. De asemenea, puteți utiliza delimitatori speciali definiți de dumneavoastră (de exemplu, ***).', - maxLengthCheck: 'Lungimea maximă a bucății trebuie să fie mai mică de 4000', + maxLengthCheck: 'Lungimea maximă a bucății trebuie să fie mai mică de {{limit}}', }, stepThree: { creationTitle: '🎉 Cunoștință creată', diff --git a/web/i18n/ru-RU/dataset-creation.ts b/web/i18n/ru-RU/dataset-creation.ts index 66d92bd2b4..ca88e093bc 100644 --- a/web/i18n/ru-RU/dataset-creation.ts +++ b/web/i18n/ru-RU/dataset-creation.ts @@ -147,7 +147,7 @@ const translation = { retrievalSettingTip: 'Чтобы изменить метод индексации, пожалуйста, перейдите в ', datasetSettingLink: 'настройки базы знаний.', separatorTip: 'Разделитель — это символ, используемый для разделения текста. \\n\\n и \\n — это часто используемые разделители для разделения абзацев и строк. В сочетании с запятыми (\\n\\n,\\n) абзацы будут сегментированы по строкам, если максимальная длина блока превышает их. Вы также можете использовать специальные разделители, определенные вами (например, ***).', - maxLengthCheck: 'Максимальная длина блока должна быть меньше 4000', + maxLengthCheck: 'Максимальная длина блока должна быть меньше {{limit}}', }, stepThree: { creationTitle: '🎉 База знаний создана', diff --git a/web/i18n/sl-SI/dataset-creation.ts b/web/i18n/sl-SI/dataset-creation.ts index 402066ad40..8dcfa8c3f5 100644 --- a/web/i18n/sl-SI/dataset-creation.ts +++ b/web/i18n/sl-SI/dataset-creation.ts @@ -152,7 +152,7 @@ const translation = { indexSettingTip: 'Če želite spremeniti način indeksiranja in model vdelave, pojdite na ', retrievalSettingTip: 'Če želite spremeniti nastavitve iskanja, pojdite na ', datasetSettingLink: 'nastavitve Znanja.', - maxLengthCheck: 'Največja dolžina kosa mora biti manjša od 4000', + maxLengthCheck: 'Največja dolžina kosa mora biti manjša od {{limit}}', }, stepThree: { creationTitle: '🎉 Znanje ustvarjeno', diff --git a/web/i18n/th-TH/dataset-creation.ts b/web/i18n/th-TH/dataset-creation.ts index 850477c568..16ec1fab3e 100644 --- a/web/i18n/th-TH/dataset-creation.ts +++ b/web/i18n/th-TH/dataset-creation.ts @@ -103,7 +103,7 @@ const translation = { separatorTip: 'ตัวคั่นคืออักขระที่ใช้ในการแยกข้อความ \\n\\n และ \\n เป็นตัวคั่นที่ใช้กันทั่วไปสําหรับการแยกย่อหน้าและบรรทัด เมื่อรวมกับเครื่องหมายจุลภาค (\\n\\n,\\n) ย่อหน้าจะถูกแบ่งตามบรรทัดเมื่อเกินความยาวของก้อนสูงสุด คุณยังสามารถใช้ตัวคั่นพิเศษที่กําหนดโดยตัวคุณเอง (เช่น ***)', separatorPlaceholder: '\\n\\n สําหรับแยกย่อหน้า \\n สําหรับแยกเส้น', maxLength: 'ความยาวก้อนสูงสุด', - maxLengthCheck: 'ความยาวก้อนสูงสุดควรน้อยกว่า 4000', + maxLengthCheck: 'ความยาวก้อนสูงสุดควรน้อยกว่า {{limit}}', overlap: 'การทับซ้อนกันของก้อน', overlapTip: 'การตั้งค่าการทับซ้อนกันของกลุ่มสามารถรักษาความเกี่ยวข้องทางความหมายระหว่างกันได้ ขอแนะนําให้ตั้งค่า 10%-25% ของขนาดก้อนสูงสุด', overlapCheck: 'การทับซ้อนกันของก้อนไม่ควรใหญ่กว่าความยาวของก้อนสูงสุด', diff --git a/web/i18n/tr-TR/dataset-creation.ts b/web/i18n/tr-TR/dataset-creation.ts index 1d5dd19634..11e5789884 100644 --- a/web/i18n/tr-TR/dataset-creation.ts +++ b/web/i18n/tr-TR/dataset-creation.ts @@ -147,7 +147,7 @@ const translation = { retrievalSettingTip: 'Dizin yöntemini değiştirmek için, lütfen', datasetSettingLink: 'Bilgi ayarlarına gidin.', separatorTip: 'Sınırlayıcı, metni ayırmak için kullanılan karakterdir. \\n\\n ve \\n, paragrafları ve satırları ayırmak için yaygın olarak kullanılan sınırlayıcılardır. Virgüllerle (\\n\\n,\\n) birleştirildiğinde, paragraflar maksimum öbek uzunluğunu aştığında satırlarla bölünür. Kendiniz tarafından tanımlanan özel sınırlayıcıları da kullanabilirsiniz (örn.', - maxLengthCheck: 'Maksimum yığın uzunluğu 4000\'den az olmalıdır', + maxLengthCheck: 'Maksimum yığın uzunluğu {{limit}}\'den az olmalıdır', }, stepThree: { creationTitle: '🎉 Bilgi oluşturuldu', diff --git a/web/i18n/uk-UA/dataset-creation.ts b/web/i18n/uk-UA/dataset-creation.ts index e78934c9e9..d6ee22c2d3 100644 --- a/web/i18n/uk-UA/dataset-creation.ts +++ b/web/i18n/uk-UA/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { webpageUnit: 'Сторінок', websiteSource: 'Веб-сайт попередньої обробки', separatorTip: 'Роздільник – це символ, який використовується для поділу тексту. \\n\\n та \\n є часто використовуваними роздільниками для відокремлення абзаців та рядків. У поєднанні з комами (\\n\\n,\\n) абзаци будуть розділені лініями, якщо вони перевищують максимальну довжину фрагмента. Ви також можете використовувати спеціальні роздільники, визначені вами (наприклад, ***).', - maxLengthCheck: 'Максимальна довжина шматка має бути меншою за 4000', + maxLengthCheck: 'Максимальна довжина шматка має бути меншою за {{limit}}', }, stepThree: { creationTitle: '🎉 Знання створено', diff --git a/web/i18n/vi-VN/dataset-creation.ts b/web/i18n/vi-VN/dataset-creation.ts index 3dc26bee46..045f3d6ea5 100644 --- a/web/i18n/vi-VN/dataset-creation.ts +++ b/web/i18n/vi-VN/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { websiteSource: 'Trang web tiền xử lý', webpageUnit: 'Trang', separatorTip: 'Dấu phân cách là ký tự được sử dụng để phân tách văn bản. \\n\\n và \\n là dấu phân cách thường được sử dụng để tách các đoạn văn và dòng. Kết hợp với dấu phẩy (\\n\\n,\\n), các đoạn văn sẽ được phân đoạn theo các dòng khi vượt quá độ dài đoạn tối đa. Bạn cũng có thể sử dụng dấu phân cách đặc biệt do chính bạn xác định (ví dụ: ***).', - maxLengthCheck: 'Chiều dài đoạn tối đa phải nhỏ hơn 4000', + maxLengthCheck: 'Chiều dài đoạn tối đa phải nhỏ hơn {{limit}}', }, stepThree: { creationTitle: '🎉 Kiến thức đã được tạo', diff --git a/web/i18n/zh-Hans/dataset-creation.ts b/web/i18n/zh-Hans/dataset-creation.ts index fac809d7e2..e6c3aa00ba 100644 --- a/web/i18n/zh-Hans/dataset-creation.ts +++ b/web/i18n/zh-Hans/dataset-creation.ts @@ -103,7 +103,7 @@ const translation = { separatorTip: '分隔符是用于分隔文本的字符。\\n\\n 和 \\n 是常用于分隔段落和行的分隔符。用逗号连接分隔符(\\n\\n,\\n),当段落超过最大块长度时,会按行进行分割。你也可以使用自定义的特殊分隔符(例如 ***)。', separatorPlaceholder: '\\n\\n 用于分段;\\n 用于分行', maxLength: '分段最大长度', - maxLengthCheck: '分段最大长度不能大于 4000', + maxLengthCheck: '分段最大长度不能大于 {{limit}}', overlap: '分段重叠长度', overlapTip: '设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果。建议设置为最大分段长度的10%-25%', overlapCheck: '分段重叠长度不能大于分段最大长度', diff --git a/web/i18n/zh-Hant/dataset-creation.ts b/web/i18n/zh-Hant/dataset-creation.ts index 02374573cf..8c5673cb3f 100644 --- a/web/i18n/zh-Hant/dataset-creation.ts +++ b/web/i18n/zh-Hant/dataset-creation.ts @@ -142,7 +142,7 @@ const translation = { websiteSource: '預處理網站', webpageUnit: '頁面', separatorTip: '分隔符是用於分隔文字的字元。\\n\\n 和 \\n 是分隔段落和行的常用分隔符。與逗號 (\\n\\n,\\n) 組合使用時,當超過最大區塊長度時,段落將按行分段。您也可以使用自定義的特殊分隔符(例如 ***)。', - maxLengthCheck: '塊最大長度應小於 4000', + maxLengthCheck: '塊最大長度應小於 {{limit}}', }, stepThree: { creationTitle: '🎉 知識庫已建立', diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 95c7ce45de..e2fa608bae 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -175,6 +175,7 @@ export type ProcessMode = 'automatic' | 'custom' export type ProcessRuleResponse = { mode: ProcessMode rules: Rules + limits: Limits } export type Rules = { @@ -182,6 +183,10 @@ export type Rules = { segmentation: Segmentation } +export type Limits = { + indexing_max_segmentation_tokens_length: number +} + export type PreProcessingRule = { id: string enabled: boolean From 02572e8cca508e77029206b4ceb72919a0c42819 Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 2 Dec 2024 16:00:40 +0800 Subject: [PATCH 47/65] fix: claude can not handle empty string (#11238) Signed-off-by: yihong0618 --- api/core/model_runtime/model_providers/anthropic/llm/llm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/core/model_runtime/model_providers/anthropic/llm/llm.py b/api/core/model_runtime/model_providers/anthropic/llm/llm.py index b24324708b..3faf5abbe8 100644 --- a/api/core/model_runtime/model_providers/anthropic/llm/llm.py +++ b/api/core/model_runtime/model_providers/anthropic/llm/llm.py @@ -483,6 +483,10 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): if isinstance(message, UserPromptMessage): message = cast(UserPromptMessage, message) if isinstance(message.content, str): + # handle empty user prompt see #10013 #10520 + # responses, ignore user prompts containing only whitespace, the Claude API can't handle it. + if not message.content.strip(): + continue message_dict = {"role": "user", "content": message.content} prompt_message_dicts.append(message_dict) else: From c4fad66f2a04ea8cd7f67b6baf18b3316093ec77 Mon Sep 17 00:00:00 2001 From: Hash Brown Date: Mon, 2 Dec 2024 16:09:26 +0800 Subject: [PATCH 48/65] fix: `dialogue_count` incorrect in chatflow when there's... (#11175) --- .../app/apps/advanced_chat/app_generator.py | 8 +++++ api/core/app/apps/advanced_chat/app_runner.py | 10 ++---- .../advanced_chat/generate_task_pipeline.py | 4 ++- .../utils/get_thread_messages_length.py | 32 +++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 api/core/prompt/utils/get_thread_messages_length.py diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 4a5f43eedd..bd4fd9cd3b 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -23,6 +23,7 @@ from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from core.ops.ops_trace_manager import TraceQueueManager +from core.prompt.utils.get_thread_messages_length import get_thread_messages_length from extensions.ext_database import db from factories import file_factory from models.account import Account @@ -33,6 +34,8 @@ logger = logging.getLogger(__name__) class AdvancedChatAppGenerator(MessageBasedAppGenerator): + _dialogue_count: int + def generate( self, app_model: App, @@ -211,6 +214,9 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): db.session.commit() db.session.refresh(conversation) + # get conversation dialogue count + self._dialogue_count = get_thread_messages_length(conversation.id) + # init queue manager queue_manager = MessageBasedAppQueueManager( task_id=application_generate_entity.task_id, @@ -281,6 +287,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): queue_manager=queue_manager, conversation=conversation, message=message, + dialogue_count=self._dialogue_count, ) runner.run() @@ -334,6 +341,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): message=message, user=user, stream=stream, + dialogue_count=self._dialogue_count, ) try: diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index 65d744eddf..cf0c9d7593 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -39,12 +39,14 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): queue_manager: AppQueueManager, conversation: Conversation, message: Message, + dialogue_count: int, ) -> None: super().__init__(queue_manager) self.application_generate_entity = application_generate_entity self.conversation = conversation self.message = message + self._dialogue_count = dialogue_count def run(self) -> None: app_config = self.application_generate_entity.app_config @@ -122,19 +124,13 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): session.commit() - # Increment dialogue count. - self.conversation.dialogue_count += 1 - - conversation_dialogue_count = self.conversation.dialogue_count - db.session.commit() - # Create a variable pool. system_inputs = { SystemVariableKey.QUERY: query, SystemVariableKey.FILES: files, SystemVariableKey.CONVERSATION_ID: self.conversation.id, SystemVariableKey.USER_ID: user_id, - SystemVariableKey.DIALOGUE_COUNT: conversation_dialogue_count, + SystemVariableKey.DIALOGUE_COUNT: self._dialogue_count, SystemVariableKey.APP_ID: app_config.app_id, SystemVariableKey.WORKFLOW_ID: app_config.workflow_id, SystemVariableKey.WORKFLOW_RUN_ID: self.application_generate_entity.workflow_run_id, diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 0dd5813391..cd12690e28 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -88,6 +88,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc message: Message, user: Union[Account, EndUser], stream: bool, + dialogue_count: int, ) -> None: """ Initialize AdvancedChatAppGenerateTaskPipeline. @@ -98,6 +99,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc :param message: message :param user: user :param stream: stream + :param dialogue_count: dialogue count """ super().__init__(application_generate_entity, queue_manager, user, stream) @@ -114,7 +116,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc SystemVariableKey.FILES: application_generate_entity.files, SystemVariableKey.CONVERSATION_ID: conversation.id, SystemVariableKey.USER_ID: user_id, - SystemVariableKey.DIALOGUE_COUNT: conversation.dialogue_count, + SystemVariableKey.DIALOGUE_COUNT: dialogue_count, SystemVariableKey.APP_ID: application_generate_entity.app_config.app_id, SystemVariableKey.WORKFLOW_ID: workflow.id, SystemVariableKey.WORKFLOW_RUN_ID: application_generate_entity.workflow_run_id, diff --git a/api/core/prompt/utils/get_thread_messages_length.py b/api/core/prompt/utils/get_thread_messages_length.py new file mode 100644 index 0000000000..f49466db6d --- /dev/null +++ b/api/core/prompt/utils/get_thread_messages_length.py @@ -0,0 +1,32 @@ +from core.prompt.utils.extract_thread_messages import extract_thread_messages +from extensions.ext_database import db +from models.model import Message + + +def get_thread_messages_length(conversation_id: str) -> int: + """ + Get the number of thread messages based on the parent message id. + """ + # Fetch all messages related to the conversation + query = ( + db.session.query( + Message.id, + Message.parent_message_id, + Message.answer, + ) + .filter( + Message.conversation_id == conversation_id, + ) + .order_by(Message.created_at.desc()) + ) + + messages = query.all() + + # Extract thread messages + thread_messages = extract_thread_messages(messages) + + # Exclude the newly created message with an empty answer + if thread_messages and not thread_messages[0].answer: + thread_messages.pop(0) + + return len(thread_messages) From 668c1c07928ce09c8dab2bf3fef54cd74a95dfd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:30:52 +0800 Subject: [PATCH 49/65] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /web (#11262) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/yarn.lock b/web/yarn.lock index cf50a344fc..76c6e7f9d0 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -5665,9 +5665,9 @@ cross-env@^7.0.3: cross-spawn "^7.0.1" cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From a86f1eca794860ab8f5a20f62ad9b68df91b1c29 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Tue, 3 Dec 2024 10:14:13 +0900 Subject: [PATCH 50/65] docs: add api docs for /v1/info (#11269) --- .../develop/template/template.en.mdx | 152 +++++++---- .../develop/template/template.ja.mdx | 151 +++++++---- .../develop/template/template.zh.mdx | 154 +++++++---- .../template/template_advanced_chat.en.mdx | 54 +++- .../template/template_advanced_chat.ja.mdx | 56 +++- .../template/template_advanced_chat.zh.mdx | 46 +++- .../develop/template/template_chat.en.mdx | 54 +++- .../develop/template/template_chat.ja.mdx | 56 +++- .../develop/template/template_chat.zh.mdx | 46 +++- .../develop/template/template_workflow.en.mdx | 240 +++++++++++------- .../develop/template/template_workflow.ja.mdx | 240 +++++++++++------- .../develop/template/template_workflow.zh.mdx | 240 +++++++++++------- 12 files changed, 1008 insertions(+), 481 deletions(-) diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index c923ea30db..f469076bf3 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -379,10 +379,107 @@ The text generation application offers non-session support and is ideal for tran --- + + + + Text to speech. + + ### Request Body + + + + For text messages generated by Dify, simply pass the generated message-id directly. The backend will use the message-id to look up the corresponding content and synthesize the voice information directly. If both message_id and text are provided simultaneously, the message_id is given priority. + + + Speech generated content。 + + + The user identifier, defined by the developer, must ensure uniqueness within the app. + + + + + + + + ```bash {{ title: 'cURL' }} + curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", + "text": "Hello Dify", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: 'headers' }} + { + "Content-Type": "audio/wav" + } + ``` + + + +--- + + + + + Used to get basic information about this application + ### Query + + + + User identifier, defined by the developer's rules, must be unique within the application. + + + ### Response + - `name` (string) application name + - `description` (string) application description + - `tags` (array[string]) application tags + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + + +--- + @@ -497,56 +594,3 @@ The text generation application offers non-session support and is ideal for tran - ---- - - - - - Text to speech. - - ### Request Body - - - - For text messages generated by Dify, simply pass the generated message-id directly. The backend will use the message-id to look up the corresponding content and synthesize the voice information directly. If both message_id and text are provided simultaneously, the message_id is given priority. - - - Speech generated content。 - - - The user identifier, defined by the developer, must ensure uniqueness within the app. - - - - - - - - ```bash {{ title: 'cURL' }} - curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ - --header 'Authorization: Bearer {api_key}' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", - "text": "Hello Dify", - "user": "abc-123" - }' - ``` - - - - - ```json {{ title: 'headers' }} - { - "Content-Type": "audio/wav" - } - ``` - - - diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index a6ab109229..bd92bd7f36 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -375,13 +375,109 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from +--- + + + + + テキストを音声に変換します。 + + ### リクエストボディ + + + + Difyが生成したテキストメッセージの場合、生成されたmessage-idを直接渡すだけです。バックエンドはmessage-idを使用して対応するコンテンツを検索し、音声情報を直接合成します。message_idとtextの両方が同時に提供された場合、message_idが優先されます。 + + + 音声生成コンテンツ。 + + + 開発者が定義したユーザー識別子。アプリ内で一意性を確保する必要があります。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", + "text": "Hello Dify", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: 'headers' }} + { + "Content-Type": "audio/wav" + } + ``` + + + +--- + + + + + このアプリケーションの基本情報を取得するために使用されます + ### Query + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + ### Response + - `name` (string) アプリケーションの名前 + - `description` (string) アプリケーションの説明 + - `tags` (array[string]) アプリケーションのタグ + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- @@ -496,56 +592,3 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - ---- - - - - - テキストを音声に変換します。 - - ### リクエストボディ - - - - Difyが生成したテキストメッセージの場合、生成されたmessage-idを直接渡すだけです。バックエンドはmessage-idを使用して対応するコンテンツを検索し、音声情報を直接合成します。message_idとtextの両方が同時に提供された場合、message_idが優先されます。 - - - 音声生成コンテンツ。 - - - 開発者が定義したユーザー識別子。アプリ内で一意性を確保する必要があります。 - - - - - - - - ```bash {{ title: 'cURL' }} - curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ - --header 'Authorization: Bearer {api_key}' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", - "text": "Hello Dify", - "user": "abc-123" - }' - ``` - - - - - ```json {{ title: 'headers' }} - { - "Content-Type": "audio/wav" - } - ``` - - - diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index d193b91816..7b1bec3546 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -353,10 +353,108 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' --- + + + + 文字转语音。 + + ### Request Body + + + + Dify 生成的文本消息,那么直接传递生成的message-id 即可,后台会通过 message_id 查找相应的内容直接合成语音信息。如果同时传 message_id 和 text,优先使用 message_id。 + + + 语音生成内容。如果没有传 message-id的话,则会使用这个字段的内容 + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", + "text": "你好Dify", + "user": "abc-123", + "streaming": false + }' + ``` + + + + + ```json {{ title: 'headers' }} + { + "Content-Type": "audio/wav" + } + ``` + + + +--- + + + + + 用于获取应用的基本信息 + ### Query + + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + ### Response + - `name` (string) 应用名称 + - `description` (string) 应用描述 + - `tags` (array[string]) 应用标签 + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + + +--- + @@ -461,57 +559,3 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - ---- - - - - - 文字转语音。 - - ### Request Body - - - - Dify 生成的文本消息,那么直接传递生成的message-id 即可,后台会通过 message_id 查找相应的内容直接合成语音信息。如果同时传 message_id 和 text,优先使用 message_id。 - - - 语音生成内容。如果没有传 message-id的话,则会使用这个字段的内容 - - - 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 - - - - - - - - ```bash {{ title: 'cURL' }} - curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ - --header 'Authorization: Bearer {api_key}' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", - "text": "你好Dify", - "user": "abc-123", - "streaming": false - }' - ``` - - - - - ```json {{ title: 'headers' }} - { - "Content-Type": "audio/wav" - } - ``` - - - diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index 48df5b0298..1d12a045ea 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -936,13 +936,57 @@ Chat applications support session persistence, allowing previous chat history to +--- + + + + + Used to get basic information about this application + ### Query + + + + User identifier, defined by the developer's rules, must be unique within the application. + + + ### Response + - `name` (string) application name + - `description` (string) application description + - `tags` (array[string]) application tags + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- @@ -1096,14 +1140,14 @@ Chat applications support session persistence, allowing previous chat history to ```json {{ title: 'Response' }} { - "tool_icons": { + "tool_icons": { "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", "api_tool": { - "background": "#252525", - "content": "\ud83d\ude01" + "background": "#252525", + "content": "\ud83d\ude01" } + } } - } ``` diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx index c1dd25ca1b..2fc17d1ca9 100644 --- a/web/app/components/develop/template/template_advanced_chat.ja.mdx +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -935,13 +935,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from +--- + + + + + このアプリケーションの基本情報を取得するために使用されます + ### Query + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + ### Response + - `name` (string) アプリケーションの名前 + - `description` (string) アプリケーションの説明 + - `tags` (array[string]) アプリケーションのタグ + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- @@ -1061,7 +1105,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from @@ -1095,14 +1139,14 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ```json {{ title: '応答' }} { - "tool_icons": { + "tool_icons": { "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", "api_tool": { - "background": "#252525", - "content": "\ud83d\ude01" + "background": "#252525", + "content": "\ud83d\ude01" } + } } - } ``` diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx index f20d081dd3..734e52ae58 100755 --- a/web/app/components/develop/template/template_advanced_chat.zh.mdx +++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx @@ -969,13 +969,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' +--- + + + + + 用于获取应用的基本信息 + ### Query + + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + ### Response + - `name` (string) 应用名称 + - `description` (string) 应用描述 + - `tags` (array[string]) 应用标签 + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index 0632f2dab1..4e873b3294 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -965,13 +965,57 @@ Chat applications support session persistence, allowing previous chat history to +--- + + + + + Used to get basic information about this application + ### Query + + + + User identifier, defined by the developer's rules, must be unique within the application. + + + ### Response + - `name` (string) application name + - `description` (string) application description + - `tags` (array[string]) application tags + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- @@ -1125,14 +1169,14 @@ Chat applications support session persistence, allowing previous chat history to ```json {{ title: 'Response' }} { - "tool_icons": { + "tool_icons": { "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", "api_tool": { - "background": "#252525", - "content": "\ud83d\ude01" + "background": "#252525", + "content": "\ud83d\ude01" } + } } - } ``` diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index 94dd2adba5..b8914a4749 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -963,13 +963,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from +--- + + + + + このアプリケーションの基本情報を取得するために使用されます + ### Query + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + ### Response + - `name` (string) アプリケーションの名前 + - `description` (string) アプリケーションの説明 + - `tags` (array[string]) アプリケーションのタグ + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- @@ -1089,7 +1133,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from @@ -1123,14 +1167,14 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ```json {{ title: '応答' }} { - "tool_icons": { + "tool_icons": { "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", "api_tool": { - "background": "#252525", - "content": "\ud83d\ude01" + "background": "#252525", + "content": "\ud83d\ude01" } + } } - } ``` diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index bfa86b7f0d..70242623b7 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -978,13 +978,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' +--- + + + + + 用于获取应用的基本信息 + ### Query + + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + ### Response + - `name` (string) 应用名称 + - `description` (string) 应用描述 + - `tags` (array[string]) 应用标签 + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + --- diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index 60317f3d78..97519611aa 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -498,104 +498,6 @@ Workflow applications offers non-session support and is ideal for translation, a --- - - - - Used at the start of entering the page to obtain information such as features, input parameter names, types, and default values. - - ### Query - - - - User identifier, defined by the developer's rules, must be unique within the application. - - - - ### Response - - `user_input_form` (array[object]) User input form configuration - - `text-input` (object) Text input control - - `label` (string) Variable display label name - - `variable` (string) Variable ID - - `required` (bool) Whether it is required - - `default` (string) Default value - - `paragraph` (object) Paragraph text input control - - `label` (string) Variable display label name - - `variable` (string) Variable ID - - `required` (bool) Whether it is required - - `default` (string) Default value - - `select` (object) Dropdown control - - `label` (string) Variable display label name - - `variable` (string) Variable ID - - `required` (bool) Whether it is required - - `default` (string) Default value - - `options` (array[string]) Option values - - `file_upload` (object) File upload configuration - - `image` (object) Image settings - Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) Whether it is enabled - - `number_limits` (int) Image number limit, default is 3 - - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one - - `system_parameters` (object) System parameters - - `file_size_limit` (int) Document upload size limit (MB) - - `image_file_size_limit` (int) Image file upload size limit (MB) - - `audio_file_size_limit` (int) Audio file upload size limit (MB) - - `video_file_size_limit` (int) Video file upload size limit (MB) - - - - - - - ```bash {{ title: 'cURL' }} - curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ - --header 'Authorization: Bearer {api_key}' - ``` - - - - - ```json {{ title: 'Response' }} - { - "user_input_form": [ - { - "paragraph": { - "label": "Query", - "variable": "query", - "required": true, - "default": "" - } - } - ], - "file_upload": { - "image": { - "enabled": false, - "number_limits": 3, - "detail": "high", - "transfer_methods": [ - "remote_url", - "local_file" - ] - } - }, - "system_parameters": { - "file_size_limit": 15, - "image_file_size_limit": 10, - "audio_file_size_limit": 50, - "video_file_size_limit": 100 - } - } - ``` - - - - ---- - +--- + + + + + Used to get basic information about this application + ### Query + + + + User identifier, defined by the developer's rules, must be unique within the application. + + + ### Response + - `name` (string) application name + - `description` (string) application description + - `tags` (array[string]) application tags + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + + +--- + + + + + Used at the start of entering the page to obtain information such as features, input parameter names, types, and default values. + + ### Query + + + + User identifier, defined by the developer's rules, must be unique within the application. + + + + ### Response + - `user_input_form` (array[object]) User input form configuration + - `text-input` (object) Text input control + - `label` (string) Variable display label name + - `variable` (string) Variable ID + - `required` (bool) Whether it is required + - `default` (string) Default value + - `paragraph` (object) Paragraph text input control + - `label` (string) Variable display label name + - `variable` (string) Variable ID + - `required` (bool) Whether it is required + - `default` (string) Default value + - `select` (object) Dropdown control + - `label` (string) Variable display label name + - `variable` (string) Variable ID + - `required` (bool) Whether it is required + - `default` (string) Default value + - `options` (array[string]) Option values + - `file_upload` (object) File upload configuration + - `image` (object) Image settings + Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Image number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one + - `system_parameters` (object) System parameters + - `file_size_limit` (int) Document upload size limit (MB) + - `image_file_size_limit` (int) Image file upload size limit (MB) + - `audio_file_size_limit` (int) Audio file upload size limit (MB) + - `video_file_size_limit` (int) Video file upload size limit (MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: 'Response' }} + { + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index e032ca5b4d..56eaeda2d7 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -498,104 +498,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from --- - - - - ページに入る際に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 - - ### クエリ - - - - ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 - - - - ### 応答 - - `user_input_form` (array[object]) ユーザー入力フォームの設定 - - `text-input` (object) テキスト入力コントロール - - `label` (string) 変数表示ラベル名 - - `variable` (string) 変数ID - - `required` (bool) 必須かどうか - - `default` (string) デフォルト値 - - `paragraph` (object) 段落テキスト入力コントロール - - `label` (string) 変数表示ラベル名 - - `variable` (string) 変数ID - - `required` (bool) 必須かどうか - - `default` (string) デフォルト値 - - `select` (object) ドロップダウンコントロール - - `label` (string) 変数表示ラベル名 - - `variable` (string) 変数ID - - `required` (bool) 必須かどうか - - `default` (string) デフォルト値 - - `options` (array[string]) オプション値 - - `file_upload` (object) ファイルアップロード設定 - - `image` (object) 画像設定 - 現在サポートされている画像タイプのみ:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 有効かどうか - - `number_limits` (int) 画像数の制限、デフォルトは3 - - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります - - `system_parameters` (object) システムパラメータ - - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) - - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) - - `audio_file_size_limit` (int) オーディオファイルアップロードサイズ制限(MB) - - `video_file_size_limit` (int) ビデオファイルアップロードサイズ制限(MB) - - - - - - - ```bash {{ title: 'cURL' }} - curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ - --header 'Authorization: Bearer {api_key}' - ``` - - - - - ```json {{ title: '応答' }} - { - "user_input_form": [ - { - "paragraph": { - "label": "Query", - "variable": "query", - "required": true, - "default": "" - } - } - ], - "file_upload": { - "image": { - "enabled": false, - "number_limits": 3, - "detail": "high", - "transfer_methods": [ - "remote_url", - "local_file" - ] - } - }, - "system_parameters": { - "file_size_limit": 15, - "image_file_size_limit": 10, - "audio_file_size_limit": 50, - "video_file_size_limit": 100 - } - } - ``` - - - - ---- - +--- + + + + + このアプリケーションの基本情報を取得するために使用されます + ### Query + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + ### Response + - `name` (string) アプリケーションの名前 + - `description` (string) アプリケーションの説明 + - `tags` (array[string]) アプリケーションのタグ + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + + +--- + + + + + ページに入る際に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 + + ### クエリ + + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `user_input_form` (array[object]) ユーザー入力フォームの設定 + - `text-input` (object) テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `paragraph` (object) 段落テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `select` (object) ドロップダウンコントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `options` (array[string]) オプション値 + - `file_upload` (object) ファイルアップロード設定 + - `image` (object) 画像設定 + 現在サポートされている画像タイプのみ:`png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の制限、デフォルトは3 + - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `system_parameters` (object) システムパラメータ + - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) + - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) + - `audio_file_size_limit` (int) オーディオファイルアップロードサイズ制限(MB) + - `video_file_size_limit` (int) ビデオファイルアップロードサイズ制限(MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 70557b013c..cfebb0e319 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -490,104 +490,6 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 --- - - - - 用于进入页面一开始,获取功能开关、输入参数名称、类型及默认值等使用。 - - ### Query - - - - 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 - - - - ### Response - - `user_input_form` (array[object]) 用户输入表单配置 - - `text-input` (object) 文本输入控件 - - `label` (string) 控件展示标签名 - - `variable` (string) 控件 ID - - `required` (bool) 是否必填 - - `default` (string) 默认值 - - `paragraph` (object) 段落文本输入控件 - - `label` (string) 控件展示标签名 - - `variable` (string) 控件 ID - - `required` (bool) 是否必填 - - `default` (string) 默认值 - - `select` (object) 下拉控件 - - `label` (string) 控件展示标签名 - - `variable` (string) 控件 ID - - `required` (bool) 是否必填 - - `default` (string) 默认值 - - `options` (array[string]) 选项值 - - `file_upload` (object) 文件上传配置 - - `image` (object) 图片设置 - 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 是否开启 - - `number_limits` (int) 图片数量限制,默认 3 - - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 - - `system_parameters` (object) 系统参数 - - `file_size_limit` (int) 文档上传大小限制 (MB) - - `image_file_size_limit` (int) 图片文件上传大小限制(MB) - - `audio_file_size_limit` (int) 音频文件上传大小限制 (MB) - - `video_file_size_limit` (int) 视频文件上传大小限制 (MB) - - - - - - - ```bash {{ title: 'cURL' }} - curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ - --header 'Authorization: Bearer {api_key}' - ``` - - - - - ```json {{ title: 'Response' }} - { - "user_input_form": [ - { - "paragraph": { - "label": "Query", - "variable": "query", - "required": true, - "default": "" - } - } - ], - "file_upload": { - "image": { - "enabled": false, - "number_limits": 3, - "detail": "high", - "transfer_methods": [ - "remote_url", - "local_file" - ] - } - }, - "system_parameters": { - "file_size_limit": 15, - "image_file_size_limit": 10, - "audio_file_size_limit": 50, - "video_file_size_limit": 100 - } - } - ``` - - - - ---- - +--- + + + + + 用于获取应用的基本信息 + ### Query + + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + ### Response + - `name` (string) 应用名称 + - `description` (string) 应用描述 + - `tags` (array[string]) 应用标签 + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/info?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + ```json {{ title: 'Response' }} + { + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] + } + ``` + + + + +--- + + + + + 用于进入页面一开始,获取功能开关、输入参数名称、类型及默认值等使用。 + + ### Query + + + + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + + + + ### Response + - `user_input_form` (array[object]) 用户输入表单配置 + - `text-input` (object) 文本输入控件 + - `label` (string) 控件展示标签名 + - `variable` (string) 控件 ID + - `required` (bool) 是否必填 + - `default` (string) 默认值 + - `paragraph` (object) 段落文本输入控件 + - `label` (string) 控件展示标签名 + - `variable` (string) 控件 ID + - `required` (bool) 是否必填 + - `default` (string) 默认值 + - `select` (object) 下拉控件 + - `label` (string) 控件展示标签名 + - `variable` (string) 控件 ID + - `required` (bool) 是否必填 + - `default` (string) 默认值 + - `options` (array[string]) 选项值 + - `file_upload` (object) 文件上传配置 + - `image` (object) 图片设置 + 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) 是否开启 + - `number_limits` (int) 图片数量限制,默认 3 + - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 + - `system_parameters` (object) 系统参数 + - `file_size_limit` (int) 文档上传大小限制 (MB) + - `image_file_size_limit` (int) 图片文件上传大小限制(MB) + - `audio_file_size_limit` (int) 音频文件上传大小限制 (MB) + - `video_file_size_limit` (int) 视频文件上传大小限制 (MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: 'Response' }} + { + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + From e686f123177bb177af58494ba663458298e56f46 Mon Sep 17 00:00:00 2001 From: yihong Date: Tue, 3 Dec 2024 09:15:38 +0800 Subject: [PATCH 51/65] fix: better handle error (#11265) Signed-off-by: yihong0618 --- .../rag/datasource/vdb/oceanbase/oceanbase_vector.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py index 8dd26a073b..c44338d42a 100644 --- a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py +++ b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py @@ -104,8 +104,7 @@ class OceanBaseVector(BaseVector): val = int(row[6]) vals.append(val) if len(vals) == 0: - print("ob_vector_memory_limit_percentage not found in parameters.") - exit(1) + raise ValueError("ob_vector_memory_limit_percentage not found in parameters.") if any(val == 0 for val in vals): try: self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30") @@ -200,10 +199,10 @@ class OceanBaseVectorFactory(AbstractVectorFactory): return OceanBaseVector( collection_name, OceanBaseVectorConfig( - host=dify_config.OCEANBASE_VECTOR_HOST, - port=dify_config.OCEANBASE_VECTOR_PORT, - user=dify_config.OCEANBASE_VECTOR_USER, + host=dify_config.OCEANBASE_VECTOR_HOST or "", + port=dify_config.OCEANBASE_VECTOR_PORT or 0, + user=dify_config.OCEANBASE_VECTOR_USER or "", password=(dify_config.OCEANBASE_VECTOR_PASSWORD or ""), - database=dify_config.OCEANBASE_VECTOR_DATABASE, + database=dify_config.OCEANBASE_VECTOR_DATABASE or "", ), ) From 7b86f8f024d853fd340c0cc9f133083002d83d68 Mon Sep 17 00:00:00 2001 From: yihong Date: Tue, 3 Dec 2024 09:15:51 +0800 Subject: [PATCH 52/65] fix: double split error on redis port and some type hint (#11270) Signed-off-by: yihong0618 --- api/extensions/ext_redis.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/api/extensions/ext_redis.py b/api/extensions/ext_redis.py index f97adc058c..da41805707 100644 --- a/api/extensions/ext_redis.py +++ b/api/extensions/ext_redis.py @@ -1,3 +1,5 @@ +from typing import Any, Union + import redis from redis.cluster import ClusterNode, RedisCluster from redis.connection import Connection, SSLConnection @@ -46,11 +48,11 @@ redis_client = RedisClientWrapper() def init_app(app: DifyApp): global redis_client - connection_class = Connection + connection_class: type[Union[Connection, SSLConnection]] = Connection if dify_config.REDIS_USE_SSL: connection_class = SSLConnection - redis_params = { + redis_params: dict[str, Any] = { "username": dify_config.REDIS_USERNAME, "password": dify_config.REDIS_PASSWORD, "db": dify_config.REDIS_DB, @@ -60,6 +62,7 @@ def init_app(app: DifyApp): } if dify_config.REDIS_USE_SENTINEL: + assert dify_config.REDIS_SENTINELS is not None, "REDIS_SENTINELS must be set when REDIS_USE_SENTINEL is True" sentinel_hosts = [ (node.split(":")[0], int(node.split(":")[1])) for node in dify_config.REDIS_SENTINELS.split(",") ] @@ -74,11 +77,13 @@ def init_app(app: DifyApp): master = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params) redis_client.initialize(master) elif dify_config.REDIS_USE_CLUSTERS: + assert dify_config.REDIS_CLUSTERS is not None, "REDIS_CLUSTERS must be set when REDIS_USE_CLUSTERS is True" nodes = [ - ClusterNode(host=node.split(":")[0], port=int(node.split.split(":")[1])) + ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1])) for node in dify_config.REDIS_CLUSTERS.split(",") ] - redis_client.initialize(RedisCluster(startup_nodes=nodes, password=dify_config.REDIS_CLUSTERS_PASSWORD)) + # FIXME: mypy error here, try to figure out how to fix it + redis_client.initialize(RedisCluster(startup_nodes=nodes, password=dify_config.REDIS_CLUSTERS_PASSWORD)) # type: ignore else: redis_params.update( { From 2a448a899d1994cc2100bee2886114c4d6a6818b Mon Sep 17 00:00:00 2001 From: Novice <857526207@qq.com> Date: Tue, 3 Dec 2024 09:16:03 +0800 Subject: [PATCH 53/65] Fix: iteration not in main thread pool (#11271) Co-authored-by: Novice Lee --- api/core/workflow/nodes/iteration/iteration_node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 22f242a42f..e32e58b780 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -116,7 +116,7 @@ class IterationNode(BaseNode[IterationNodeData]): variable_pool.add([self.node_id, "item"], iterator_list_value[0]) # init graph engine - from core.workflow.graph_engine.graph_engine import GraphEngine, GraphEngineThreadPool + from core.workflow.graph_engine.graph_engine import GraphEngine graph_engine = GraphEngine( tenant_id=self.tenant_id, @@ -162,7 +162,8 @@ class IterationNode(BaseNode[IterationNodeData]): if self.node_data.is_parallel: futures: list[Future] = [] q = Queue() - thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100) + thread_pool = graph_engine.workflow_thread_pool_mapping[graph_engine.thread_pool_id] + thread_pool._max_workers = self.node_data.parallel_nums for index, item in enumerate(iterator_list_value): future: Future = thread_pool.submit( self._run_single_iter_parallel, From 643a90c48da8c27b140f2098bbe3f1c2733e1698 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 3 Dec 2024 09:16:25 +0800 Subject: [PATCH 54/65] fix: use `removeprefix()` instead of `lstrip()` to remove the `data:` prefix (#11272) Signed-off-by: -LAN- --- api/core/model_runtime/model_providers/moonshot/llm/llm.py | 2 +- .../model_providers/openai_api_compatible/llm/llm.py | 2 +- api/core/model_runtime/model_providers/stepfun/llm/llm.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 5c955c86d3..90d015942e 100644 --- a/api/core/model_runtime/model_providers/moonshot/llm/llm.py +++ b/api/core/model_runtime/model_providers/moonshot/llm/llm.py @@ -252,7 +252,7 @@ class MoonshotLargeLanguageModel(OAIAPICompatLargeLanguageModel): # ignore sse comments if chunk.startswith(":"): continue - decoded_chunk = chunk.strip().lstrip("data: ").lstrip() + decoded_chunk = chunk.strip().removeprefix("data: ") chunk_json = None try: chunk_json = json.loads(decoded_chunk) diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py index e1342fe985..26c090d30e 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py @@ -462,7 +462,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOaiApiCompat, LargeLanguageModel): # ignore sse comments if chunk.startswith(":"): continue - decoded_chunk = chunk.strip().lstrip("data: ").lstrip() + decoded_chunk = chunk.strip().removeprefix("data: ") if decoded_chunk == "[DONE]": # Some provider returns "data: [DONE]" continue 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 43b91a1aec..686809ff2b 100644 --- a/api/core/model_runtime/model_providers/stepfun/llm/llm.py +++ b/api/core/model_runtime/model_providers/stepfun/llm/llm.py @@ -250,7 +250,7 @@ class StepfunLargeLanguageModel(OAIAPICompatLargeLanguageModel): # ignore sse comments if chunk.startswith(":"): continue - decoded_chunk = chunk.strip().lstrip("data: ").lstrip() + decoded_chunk = chunk.strip().removeprefix("data: ") chunk_json = None try: chunk_json = json.loads(decoded_chunk) From e79eac688a3cf6a8910f094a1a45e3f75d74d352 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Tue, 3 Dec 2024 13:26:33 +0800 Subject: [PATCH 55/65] chore(lint): sort __all__ definitions (#11243) --- api/.ruff.toml | 3 ++ api/core/file/__init__.py | 12 +++--- api/core/model_runtime/entities/__init__.py | 34 +++++++-------- .../legacy/volc_sdk/__init__.py | 2 +- api/core/variables/__init__.py | 42 +++++++++---------- api/core/workflow/callbacks/__init__.py | 2 +- api/core/workflow/nodes/answer/__init__.py | 2 +- api/core/workflow/nodes/base/__init__.py | 2 +- api/core/workflow/nodes/end/__init__.py | 2 +- api/core/workflow/nodes/event/__init__.py | 4 +- .../workflow/nodes/http_request/__init__.py | 2 +- .../nodes/question_classifier/__init__.py | 2 +- .../nodes/variable_assigner/__init__.py | 2 +- api/models/__init__.py | 34 +++++++-------- api/services/errors/__init__.py | 16 +++---- 15 files changed, 82 insertions(+), 79 deletions(-) diff --git a/api/.ruff.toml b/api/.ruff.toml index 9a6dd46f69..0f3185223c 100644 --- a/api/.ruff.toml +++ b/api/.ruff.toml @@ -20,6 +20,8 @@ select = [ "PLC0208", # iteration-over-set "PLC2801", # unnecessary-dunder-call "PLC0414", # useless-import-alias + "PLE0604", # invalid-all-object + "PLE0605", # invalid-all-format "PLR0402", # manual-from-import "PLR1711", # useless-return "PLR1714", # repeated-equality-comparison @@ -28,6 +30,7 @@ select = [ "RUF100", # unused-noqa "RUF101", # redirected-noqa "RUF200", # invalid-pyproject-toml + "RUF022", # unsorted-dunder-all "S506", # unsafe-yaml-load "SIM", # flake8-simplify rules "TRY400", # error-instead-of-exception diff --git a/api/core/file/__init__.py b/api/core/file/__init__.py index fe9e52258a..44749ebec3 100644 --- a/api/core/file/__init__.py +++ b/api/core/file/__init__.py @@ -7,13 +7,13 @@ from .models import ( ) __all__ = [ + "FILE_MODEL_IDENTITY", + "ArrayFileAttribute", + "File", + "FileAttribute", + "FileBelongsTo", + "FileTransferMethod", "FileType", "FileUploadConfig", - "FileTransferMethod", - "FileBelongsTo", - "File", "ImageConfig", - "FileAttribute", - "ArrayFileAttribute", - "FILE_MODEL_IDENTITY", ] diff --git a/api/core/model_runtime/entities/__init__.py b/api/core/model_runtime/entities/__init__.py index 5e52f10b4c..1c73755cff 100644 --- a/api/core/model_runtime/entities/__init__.py +++ b/api/core/model_runtime/entities/__init__.py @@ -18,25 +18,25 @@ from .message_entities import ( from .model_entities import ModelPropertyKey __all__ = [ - "ImagePromptMessageContent", - "VideoPromptMessageContent", - "PromptMessage", - "PromptMessageRole", - "LLMUsage", - "ModelPropertyKey", "AssistantPromptMessage", - "PromptMessage", - "PromptMessageContent", - "PromptMessageRole", - "SystemPromptMessage", - "TextPromptMessageContent", - "UserPromptMessage", - "PromptMessageTool", - "ToolPromptMessage", - "PromptMessageContentType", + "AudioPromptMessageContent", + "DocumentPromptMessageContent", + "ImagePromptMessageContent", "LLMResult", "LLMResultChunk", "LLMResultChunkDelta", - "AudioPromptMessageContent", - "DocumentPromptMessageContent", + "LLMUsage", + "ModelPropertyKey", + "PromptMessage", + "PromptMessage", + "PromptMessageContent", + "PromptMessageContentType", + "PromptMessageRole", + "PromptMessageRole", + "PromptMessageTool", + "SystemPromptMessage", + "TextPromptMessageContent", + "ToolPromptMessage", + "UserPromptMessage", + "VideoPromptMessageContent", ] diff --git a/api/core/model_runtime/model_providers/volcengine_maas/legacy/volc_sdk/__init__.py b/api/core/model_runtime/model_providers/volcengine_maas/legacy/volc_sdk/__init__.py index 8b3eb157be..2a269557f2 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/legacy/volc_sdk/__init__.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/legacy/volc_sdk/__init__.py @@ -1,4 +1,4 @@ from .common import ChatRole from .maas import MaasError, MaasService -__all__ = ["MaasService", "ChatRole", "MaasError"] +__all__ = ["ChatRole", "MaasError", "MaasService"] diff --git a/api/core/variables/__init__.py b/api/core/variables/__init__.py index 144c1b899f..2b1a58f93a 100644 --- a/api/core/variables/__init__.py +++ b/api/core/variables/__init__.py @@ -32,32 +32,32 @@ from .variables import ( ) __all__ = [ - "IntegerVariable", - "FloatVariable", - "ObjectVariable", - "SecretVariable", - "StringVariable", - "ArrayAnyVariable", - "Variable", - "SegmentType", - "SegmentGroup", - "Segment", - "NoneSegment", - "NoneVariable", - "IntegerSegment", - "FloatSegment", - "ObjectSegment", "ArrayAnySegment", - "StringSegment", - "ArrayStringVariable", + "ArrayAnyVariable", + "ArrayFileSegment", + "ArrayFileVariable", + "ArrayNumberSegment", "ArrayNumberVariable", + "ArrayObjectSegment", "ArrayObjectVariable", "ArraySegment", - "ArrayFileSegment", - "ArrayNumberSegment", - "ArrayObjectSegment", "ArrayStringSegment", + "ArrayStringVariable", "FileSegment", "FileVariable", - "ArrayFileVariable", + "FloatSegment", + "FloatVariable", + "IntegerSegment", + "IntegerVariable", + "NoneSegment", + "NoneVariable", + "ObjectSegment", + "ObjectVariable", + "SecretVariable", + "Segment", + "SegmentGroup", + "SegmentType", + "StringSegment", + "StringVariable", + "Variable", ] diff --git a/api/core/workflow/callbacks/__init__.py b/api/core/workflow/callbacks/__init__.py index 403fbbaa2f..fba86c1e2e 100644 --- a/api/core/workflow/callbacks/__init__.py +++ b/api/core/workflow/callbacks/__init__.py @@ -2,6 +2,6 @@ from .base_workflow_callback import WorkflowCallback from .workflow_logging_callback import WorkflowLoggingCallback __all__ = [ - "WorkflowLoggingCallback", "WorkflowCallback", + "WorkflowLoggingCallback", ] diff --git a/api/core/workflow/nodes/answer/__init__.py b/api/core/workflow/nodes/answer/__init__.py index 7a10f47eed..ee7676c7e4 100644 --- a/api/core/workflow/nodes/answer/__init__.py +++ b/api/core/workflow/nodes/answer/__init__.py @@ -1,4 +1,4 @@ from .answer_node import AnswerNode from .entities import AnswerStreamGenerateRoute -__all__ = ["AnswerStreamGenerateRoute", "AnswerNode"] +__all__ = ["AnswerNode", "AnswerStreamGenerateRoute"] diff --git a/api/core/workflow/nodes/base/__init__.py b/api/core/workflow/nodes/base/__init__.py index 61f727740c..72d6392d4e 100644 --- a/api/core/workflow/nodes/base/__init__.py +++ b/api/core/workflow/nodes/base/__init__.py @@ -1,4 +1,4 @@ from .entities import BaseIterationNodeData, BaseIterationState, BaseNodeData from .node import BaseNode -__all__ = ["BaseNode", "BaseNodeData", "BaseIterationNodeData", "BaseIterationState"] +__all__ = ["BaseIterationNodeData", "BaseIterationState", "BaseNode", "BaseNodeData"] diff --git a/api/core/workflow/nodes/end/__init__.py b/api/core/workflow/nodes/end/__init__.py index adb381701c..c4c00e3ddc 100644 --- a/api/core/workflow/nodes/end/__init__.py +++ b/api/core/workflow/nodes/end/__init__.py @@ -1,4 +1,4 @@ from .end_node import EndNode from .entities import EndStreamParam -__all__ = ["EndStreamParam", "EndNode"] +__all__ = ["EndNode", "EndStreamParam"] diff --git a/api/core/workflow/nodes/event/__init__.py b/api/core/workflow/nodes/event/__init__.py index 581def9553..5e3b31e48b 100644 --- a/api/core/workflow/nodes/event/__init__.py +++ b/api/core/workflow/nodes/event/__init__.py @@ -2,9 +2,9 @@ from .event import ModelInvokeCompletedEvent, RunCompletedEvent, RunRetrieverRes from .types import NodeEvent __all__ = [ + "ModelInvokeCompletedEvent", + "NodeEvent", "RunCompletedEvent", "RunRetrieverResourceEvent", "RunStreamChunkEvent", - "NodeEvent", - "ModelInvokeCompletedEvent", ] diff --git a/api/core/workflow/nodes/http_request/__init__.py b/api/core/workflow/nodes/http_request/__init__.py index 9408c2dde0..c51c678999 100644 --- a/api/core/workflow/nodes/http_request/__init__.py +++ b/api/core/workflow/nodes/http_request/__init__.py @@ -1,4 +1,4 @@ from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData from .node import HttpRequestNode -__all__ = ["HttpRequestNodeData", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "BodyData", "HttpRequestNode"] +__all__ = ["BodyData", "HttpRequestNode", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "HttpRequestNodeData"] diff --git a/api/core/workflow/nodes/question_classifier/__init__.py b/api/core/workflow/nodes/question_classifier/__init__.py index 70414c4199..4d06b6bea3 100644 --- a/api/core/workflow/nodes/question_classifier/__init__.py +++ b/api/core/workflow/nodes/question_classifier/__init__.py @@ -1,4 +1,4 @@ from .entities import QuestionClassifierNodeData from .question_classifier_node import QuestionClassifierNode -__all__ = ["QuestionClassifierNodeData", "QuestionClassifierNode"] +__all__ = ["QuestionClassifierNode", "QuestionClassifierNodeData"] diff --git a/api/core/workflow/nodes/variable_assigner/__init__.py b/api/core/workflow/nodes/variable_assigner/__init__.py index 83da4bdc79..eedbd6d255 100644 --- a/api/core/workflow/nodes/variable_assigner/__init__.py +++ b/api/core/workflow/nodes/variable_assigner/__init__.py @@ -2,7 +2,7 @@ from .node import VariableAssignerNode from .node_data import VariableAssignerData, WriteMode __all__ = [ - "VariableAssignerNode", "VariableAssignerData", + "VariableAssignerNode", "WriteMode", ] diff --git a/api/models/__init__.py b/api/models/__init__.py index cd6c7674da..61a38870cf 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -24,30 +24,30 @@ from .workflow import ( ) __all__ = [ + "Account", + "AccountIntegrate", + "ApiToken", + "App", + "AppMode", + "Conversation", "ConversationVariable", - "Document", + "DataSourceOauthBinding", "Dataset", "DatasetProcessRule", + "Document", "DocumentSegment", - "DataSourceOauthBinding", - "AppMode", - "Workflow", - "App", - "Message", "EndUser", + "InstalledApp", + "InvitationCode", + "Message", + "MessageAnnotation", "MessageFile", + "RecommendedApp", + "Site", + "Tenant", + "ToolFile", "UploadFile", - "Account", + "Workflow", "WorkflowAppLog", "WorkflowRun", - "Site", - "InstalledApp", - "RecommendedApp", - "ApiToken", - "AccountIntegrate", - "InvitationCode", - "Tenant", - "Conversation", - "MessageAnnotation", - "ToolFile", ] diff --git a/api/services/errors/__init__.py b/api/services/errors/__init__.py index bb5711145c..eb1f055708 100644 --- a/api/services/errors/__init__.py +++ b/api/services/errors/__init__.py @@ -14,16 +14,16 @@ from . import ( ) __all__ = [ - "base", - "conversation", - "message", - "index", - "app_model_config", "account", - "document", - "dataset", "app", - "completion", + "app_model_config", "audio", + "base", + "completion", + "conversation", + "dataset", + "document", "file", + "index", + "message", ] From e135ffc2c1c613db374ee9749b34ee2902ee1e1b Mon Sep 17 00:00:00 2001 From: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:56:40 +0800 Subject: [PATCH 56/65] Feat: upgrade variable assigner (#11285) Signed-off-by: -LAN- Co-authored-by: -LAN- --- api/controllers/console/app/workflow.py | 6 +- api/core/app/apps/workflow_app_runner.py | 5 +- api/core/variables/types.py | 9 +- .../workflow/graph_engine/graph_engine.py | 5 +- .../answer/answer_stream_generate_router.py | 2 +- api/core/workflow/nodes/base/entities.py | 1 + api/core/workflow/nodes/base/node.py | 4 +- api/core/workflow/nodes/enums.py | 4 +- .../nodes/iteration/iteration_node.py | 7 +- api/core/workflow/nodes/node_mapping.py | 105 ++++++-- .../nodes/variable_assigner/__init__.py | 8 - .../variable_assigner/common/__init__.py | 0 .../nodes/variable_assigner/common/exc.py | 4 + .../nodes/variable_assigner/common/helpers.py | 19 ++ .../workflow/nodes/variable_assigner/exc.py | 2 - .../nodes/variable_assigner/v1/__init__.py | 3 + .../nodes/variable_assigner/{ => v1}/node.py | 36 +-- .../variable_assigner/{ => v1}/node_data.py | 3 - .../nodes/variable_assigner/v2/__init__.py | 3 + .../nodes/variable_assigner/v2/constants.py | 11 + .../nodes/variable_assigner/v2/entities.py | 20 ++ .../nodes/variable_assigner/v2/enums.py | 18 ++ .../nodes/variable_assigner/v2/exc.py | 31 +++ .../nodes/variable_assigner/v2/helpers.py | 91 +++++++ .../nodes/variable_assigner/v2/node.py | 159 ++++++++++++ api/core/workflow/workflow_entry.py | 11 +- api/factories/variable_factory.py | 23 +- api/models/workflow.py | 8 +- api/services/app_dsl_service.py | 4 +- api/services/workflow_service.py | 11 +- .../vdb/analyticdb/test_analyticdb.py | 2 +- .../core/app/segments/test_factory.py | 22 +- .../v1/test_variable_assigner_v1.py} | 12 +- .../variable_assigner/v2/test_helpers.py | 24 ++ .../models/test_conversation_variable.py | 2 +- api/tests/unit_tests/models/test_workflow.py | 32 ++- web/app/components/base/badge.tsx | 2 +- web/app/components/base/input/index.tsx | 4 +- .../base/list-empty/horizontal-line.tsx | 21 ++ web/app/components/base/list-empty/index.tsx | 35 +++ .../base/list-empty/vertical-line.tsx | 21 ++ web/app/components/workflow/block-icon.tsx | 1 + .../components/editor/code-editor/index.tsx | 4 +- .../workflow/nodes/_base/components/field.tsx | 1 - .../components/list-no-data-placeholder.tsx | 2 +- .../variable/assigned-var-reference-popup.tsx | 39 +++ .../variable/var-reference-picker.tsx | 21 +- .../variable/var-reference-popup.tsx | 45 +++- .../nodes/_base/hooks/use-one-step-run.ts | 4 +- .../components/operation-selector.tsx | 128 ++++++++++ .../assigner/components/var-list/index.tsx | 227 ++++++++++++++++++ .../components/var-list/use-var-list.ts | 39 +++ .../workflow/nodes/assigner/default.ts | 28 ++- .../workflow/nodes/assigner/hooks.ts | 70 ++++++ .../workflow/nodes/assigner/node.tsx | 56 ++++- .../workflow/nodes/assigner/panel.tsx | 97 +++----- .../workflow/nodes/assigner/types.ts | 29 ++- .../workflow/nodes/assigner/use-config.ts | 127 +++++----- .../workflow/nodes/assigner/utils.ts | 78 ++++++ .../components/node-variable-item.tsx | 24 +- web/i18n/en-US/workflow.ts | 27 +++ web/i18n/zh-Hans/workflow.ts | 29 ++- 62 files changed, 1565 insertions(+), 301 deletions(-) create mode 100644 api/core/workflow/nodes/variable_assigner/common/__init__.py create mode 100644 api/core/workflow/nodes/variable_assigner/common/exc.py create mode 100644 api/core/workflow/nodes/variable_assigner/common/helpers.py delete mode 100644 api/core/workflow/nodes/variable_assigner/exc.py create mode 100644 api/core/workflow/nodes/variable_assigner/v1/__init__.py rename api/core/workflow/nodes/variable_assigner/{ => v1}/node.py (69%) rename api/core/workflow/nodes/variable_assigner/{ => v1}/node_data.py (75%) create mode 100644 api/core/workflow/nodes/variable_assigner/v2/__init__.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/constants.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/entities.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/enums.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/exc.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/helpers.py create mode 100644 api/core/workflow/nodes/variable_assigner/v2/node.py rename api/tests/unit_tests/core/workflow/nodes/{test_variable_assigner.py => variable_assigner/v1/test_variable_assigner_v1.py} (92%) create mode 100644 api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_helpers.py create mode 100644 web/app/components/base/list-empty/horizontal-line.tsx create mode 100644 web/app/components/base/list-empty/index.tsx create mode 100644 web/app/components/base/list-empty/vertical-line.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx create mode 100644 web/app/components/workflow/nodes/assigner/components/operation-selector.tsx create mode 100644 web/app/components/workflow/nodes/assigner/components/var-list/index.tsx create mode 100644 web/app/components/workflow/nodes/assigner/components/var-list/use-var-list.ts create mode 100644 web/app/components/workflow/nodes/assigner/hooks.ts diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index cc05a0d509..c85d554069 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -100,11 +100,11 @@ class DraftWorkflowApi(Resource): try: environment_variables_list = args.get("environment_variables") or [] environment_variables = [ - variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list + variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list ] conversation_variables_list = args.get("conversation_variables") or [] conversation_variables = [ - variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list + variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list ] workflow = workflow_service.sync_draft_workflow( app_model=app_model, @@ -382,7 +382,7 @@ class DefaultBlockConfigApi(Resource): filters = None if args.get("q"): try: - filters = json.loads(args.get("q")) + filters = json.loads(args.get("q", "")) except json.JSONDecodeError: raise ValueError("Invalid filters") diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 1cf72ae79e..3d46b8bab0 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -43,7 +43,7 @@ from core.workflow.graph_engine.entities.event import ( ) from core.workflow.graph_engine.entities.graph import Graph from core.workflow.nodes import NodeType -from core.workflow.nodes.node_mapping import node_type_classes_mapping +from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from models.model import App @@ -138,7 +138,8 @@ class WorkflowBasedAppRunner(AppRunner): # Get node class node_type = NodeType(iteration_node_config.get("data", {}).get("type")) - node_cls = node_type_classes_mapping[node_type] + node_version = iteration_node_config.get("data", {}).get("version", "1") + node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version] # init variable pool variable_pool = VariablePool( diff --git a/api/core/variables/types.py b/api/core/variables/types.py index af6a2a2937..4387e9693e 100644 --- a/api/core/variables/types.py +++ b/api/core/variables/types.py @@ -2,16 +2,19 @@ from enum import StrEnum class SegmentType(StrEnum): - NONE = "none" NUMBER = "number" STRING = "string" + OBJECT = "object" SECRET = "secret" + + FILE = "file" + ARRAY_ANY = "array[any]" ARRAY_STRING = "array[string]" ARRAY_NUMBER = "array[number]" ARRAY_OBJECT = "array[object]" - OBJECT = "object" - FILE = "file" ARRAY_FILE = "array[file]" + NONE = "none" + GROUP = "group" diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 035c34dcf4..7cffd7bc8e 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -38,7 +38,7 @@ from core.workflow.nodes.answer.answer_stream_processor import AnswerStreamProce from core.workflow.nodes.base import BaseNode from core.workflow.nodes.end.end_stream_processor import EndStreamProcessor from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent -from core.workflow.nodes.node_mapping import node_type_classes_mapping +from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING from extensions.ext_database import db from models.enums import UserFrom from models.workflow import WorkflowNodeExecutionStatus, WorkflowType @@ -227,7 +227,8 @@ class GraphEngine: # convert to specific node node_type = NodeType(node_config.get("data", {}).get("type")) - node_cls = node_type_classes_mapping[node_type] + node_version = node_config.get("data", {}).get("version", "1") + node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version] previous_node_id = previous_route_node_state.node_id if previous_route_node_state else None diff --git a/api/core/workflow/nodes/answer/answer_stream_generate_router.py b/api/core/workflow/nodes/answer/answer_stream_generate_router.py index 96e24a7db3..8c78016f09 100644 --- a/api/core/workflow/nodes/answer/answer_stream_generate_router.py +++ b/api/core/workflow/nodes/answer/answer_stream_generate_router.py @@ -153,7 +153,7 @@ class AnswerStreamGeneratorRouter: NodeType.IF_ELSE, NodeType.QUESTION_CLASSIFIER, NodeType.ITERATION, - NodeType.CONVERSATION_VARIABLE_ASSIGNER, + NodeType.VARIABLE_ASSIGNER, }: answer_dependencies[answer_node_id].append(source_node_id) else: diff --git a/api/core/workflow/nodes/base/entities.py b/api/core/workflow/nodes/base/entities.py index 2a864dd7a8..fb50fbd6e8 100644 --- a/api/core/workflow/nodes/base/entities.py +++ b/api/core/workflow/nodes/base/entities.py @@ -7,6 +7,7 @@ from pydantic import BaseModel class BaseNodeData(ABC, BaseModel): title: str desc: Optional[str] = None + version: str = "1" class BaseIterationNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/base/node.py b/api/core/workflow/nodes/base/node.py index 1871fff618..d0fbed31cd 100644 --- a/api/core/workflow/nodes/base/node.py +++ b/api/core/workflow/nodes/base/node.py @@ -55,7 +55,9 @@ class BaseNode(Generic[GenericNodeData]): raise ValueError("Node ID is required.") self.node_id = node_id - self.node_data: GenericNodeData = cast(GenericNodeData, self._node_data_cls(**config.get("data", {}))) + + node_data = self._node_data_cls.model_validate(config.get("data", {})) + self.node_data = cast(GenericNodeData, node_data) @abstractmethod def _run(self) -> NodeRunResult | Generator[Union[NodeEvent, "InNodeEvent"], None, None]: diff --git a/api/core/workflow/nodes/enums.py b/api/core/workflow/nodes/enums.py index 9e9e52910e..44be403ee6 100644 --- a/api/core/workflow/nodes/enums.py +++ b/api/core/workflow/nodes/enums.py @@ -14,11 +14,11 @@ class NodeType(StrEnum): HTTP_REQUEST = "http-request" TOOL = "tool" VARIABLE_AGGREGATOR = "variable-aggregator" - VARIABLE_ASSIGNER = "variable-assigner" # TODO: Merge this into VARIABLE_AGGREGATOR in the database. + LEGACY_VARIABLE_AGGREGATOR = "variable-assigner" # TODO: Merge this into VARIABLE_AGGREGATOR in the database. LOOP = "loop" ITERATION = "iteration" ITERATION_START = "iteration-start" # Fake start node for iteration. PARAMETER_EXTRACTOR = "parameter-extractor" - CONVERSATION_VARIABLE_ASSIGNER = "assigner" + VARIABLE_ASSIGNER = "assigner" DOCUMENT_EXTRACTOR = "document-extractor" LIST_OPERATOR = "list-operator" diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index e32e58b780..6079edebdb 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -298,12 +298,13 @@ class IterationNode(BaseNode[IterationNodeData]): # variable selector to variable mapping try: # Get node class - from core.workflow.nodes.node_mapping import node_type_classes_mapping + from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING node_type = NodeType(sub_node_config.get("data", {}).get("type")) - node_cls = node_type_classes_mapping.get(node_type) - if not node_cls: + if node_type not in NODE_TYPE_CLASSES_MAPPING: continue + node_version = sub_node_config.get("data", {}).get("version", "1") + node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version] sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping( graph_config=graph_config, config=sub_node_config diff --git a/api/core/workflow/nodes/node_mapping.py b/api/core/workflow/nodes/node_mapping.py index c13b5ff76f..51fc5129cd 100644 --- a/api/core/workflow/nodes/node_mapping.py +++ b/api/core/workflow/nodes/node_mapping.py @@ -1,3 +1,5 @@ +from collections.abc import Mapping + from core.workflow.nodes.answer import AnswerNode from core.workflow.nodes.base import BaseNode from core.workflow.nodes.code import CodeNode @@ -16,26 +18,87 @@ from core.workflow.nodes.start import StartNode from core.workflow.nodes.template_transform import TemplateTransformNode from core.workflow.nodes.tool import ToolNode from core.workflow.nodes.variable_aggregator import VariableAggregatorNode -from core.workflow.nodes.variable_assigner import VariableAssignerNode +from core.workflow.nodes.variable_assigner.v1 import VariableAssignerNode as VariableAssignerNodeV1 +from core.workflow.nodes.variable_assigner.v2 import VariableAssignerNode as VariableAssignerNodeV2 -node_type_classes_mapping: dict[NodeType, type[BaseNode]] = { - NodeType.START: StartNode, - NodeType.END: EndNode, - NodeType.ANSWER: AnswerNode, - NodeType.LLM: LLMNode, - NodeType.KNOWLEDGE_RETRIEVAL: KnowledgeRetrievalNode, - NodeType.IF_ELSE: IfElseNode, - NodeType.CODE: CodeNode, - NodeType.TEMPLATE_TRANSFORM: TemplateTransformNode, - NodeType.QUESTION_CLASSIFIER: QuestionClassifierNode, - NodeType.HTTP_REQUEST: HttpRequestNode, - NodeType.TOOL: ToolNode, - NodeType.VARIABLE_AGGREGATOR: VariableAggregatorNode, - NodeType.VARIABLE_ASSIGNER: VariableAggregatorNode, # original name of VARIABLE_AGGREGATOR - NodeType.ITERATION: IterationNode, - NodeType.ITERATION_START: IterationStartNode, - NodeType.PARAMETER_EXTRACTOR: ParameterExtractorNode, - NodeType.CONVERSATION_VARIABLE_ASSIGNER: VariableAssignerNode, - NodeType.DOCUMENT_EXTRACTOR: DocumentExtractorNode, - NodeType.LIST_OPERATOR: ListOperatorNode, +LATEST_VERSION = "latest" + +NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { + NodeType.START: { + LATEST_VERSION: StartNode, + "1": StartNode, + }, + NodeType.END: { + LATEST_VERSION: EndNode, + "1": EndNode, + }, + NodeType.ANSWER: { + LATEST_VERSION: AnswerNode, + "1": AnswerNode, + }, + NodeType.LLM: { + LATEST_VERSION: LLMNode, + "1": LLMNode, + }, + NodeType.KNOWLEDGE_RETRIEVAL: { + LATEST_VERSION: KnowledgeRetrievalNode, + "1": KnowledgeRetrievalNode, + }, + NodeType.IF_ELSE: { + LATEST_VERSION: IfElseNode, + "1": IfElseNode, + }, + NodeType.CODE: { + LATEST_VERSION: CodeNode, + "1": CodeNode, + }, + NodeType.TEMPLATE_TRANSFORM: { + LATEST_VERSION: TemplateTransformNode, + "1": TemplateTransformNode, + }, + NodeType.QUESTION_CLASSIFIER: { + LATEST_VERSION: QuestionClassifierNode, + "1": QuestionClassifierNode, + }, + NodeType.HTTP_REQUEST: { + LATEST_VERSION: HttpRequestNode, + "1": HttpRequestNode, + }, + NodeType.TOOL: { + LATEST_VERSION: ToolNode, + "1": ToolNode, + }, + NodeType.VARIABLE_AGGREGATOR: { + LATEST_VERSION: VariableAggregatorNode, + "1": VariableAggregatorNode, + }, + NodeType.LEGACY_VARIABLE_AGGREGATOR: { + LATEST_VERSION: VariableAggregatorNode, + "1": VariableAggregatorNode, + }, # original name of VARIABLE_AGGREGATOR + NodeType.ITERATION: { + LATEST_VERSION: IterationNode, + "1": IterationNode, + }, + NodeType.ITERATION_START: { + LATEST_VERSION: IterationStartNode, + "1": IterationStartNode, + }, + NodeType.PARAMETER_EXTRACTOR: { + LATEST_VERSION: ParameterExtractorNode, + "1": ParameterExtractorNode, + }, + NodeType.VARIABLE_ASSIGNER: { + LATEST_VERSION: VariableAssignerNodeV2, + "1": VariableAssignerNodeV1, + "2": VariableAssignerNodeV2, + }, + NodeType.DOCUMENT_EXTRACTOR: { + LATEST_VERSION: DocumentExtractorNode, + "1": DocumentExtractorNode, + }, + NodeType.LIST_OPERATOR: { + LATEST_VERSION: ListOperatorNode, + "1": ListOperatorNode, + }, } diff --git a/api/core/workflow/nodes/variable_assigner/__init__.py b/api/core/workflow/nodes/variable_assigner/__init__.py index eedbd6d255..e69de29bb2 100644 --- a/api/core/workflow/nodes/variable_assigner/__init__.py +++ b/api/core/workflow/nodes/variable_assigner/__init__.py @@ -1,8 +0,0 @@ -from .node import VariableAssignerNode -from .node_data import VariableAssignerData, WriteMode - -__all__ = [ - "VariableAssignerData", - "VariableAssignerNode", - "WriteMode", -] diff --git a/api/core/workflow/nodes/variable_assigner/common/__init__.py b/api/core/workflow/nodes/variable_assigner/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/workflow/nodes/variable_assigner/common/exc.py b/api/core/workflow/nodes/variable_assigner/common/exc.py new file mode 100644 index 0000000000..a1178fb020 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/common/exc.py @@ -0,0 +1,4 @@ +class VariableOperatorNodeError(Exception): + """Base error type, don't use directly.""" + + pass diff --git a/api/core/workflow/nodes/variable_assigner/common/helpers.py b/api/core/workflow/nodes/variable_assigner/common/helpers.py new file mode 100644 index 0000000000..8031b57fa8 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/common/helpers.py @@ -0,0 +1,19 @@ +from sqlalchemy import select +from sqlalchemy.orm import Session + +from core.variables import Variable +from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError +from extensions.ext_database import db +from models import ConversationVariable + + +def update_conversation_variable(conversation_id: str, variable: Variable): + stmt = select(ConversationVariable).where( + ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id + ) + with Session(db.engine) as session: + row = session.scalar(stmt) + if not row: + raise VariableOperatorNodeError("conversation variable not found in the database") + row.data = variable.model_dump_json() + session.commit() diff --git a/api/core/workflow/nodes/variable_assigner/exc.py b/api/core/workflow/nodes/variable_assigner/exc.py deleted file mode 100644 index 914be22256..0000000000 --- a/api/core/workflow/nodes/variable_assigner/exc.py +++ /dev/null @@ -1,2 +0,0 @@ -class VariableAssignerNodeError(Exception): - pass diff --git a/api/core/workflow/nodes/variable_assigner/v1/__init__.py b/api/core/workflow/nodes/variable_assigner/v1/__init__.py new file mode 100644 index 0000000000..7eb1428e50 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v1/__init__.py @@ -0,0 +1,3 @@ +from .node import VariableAssignerNode + +__all__ = ["VariableAssignerNode"] diff --git a/api/core/workflow/nodes/variable_assigner/node.py b/api/core/workflow/nodes/variable_assigner/v1/node.py similarity index 69% rename from api/core/workflow/nodes/variable_assigner/node.py rename to api/core/workflow/nodes/variable_assigner/v1/node.py index 4e66f640df..8eb4bd5c2d 100644 --- a/api/core/workflow/nodes/variable_assigner/node.py +++ b/api/core/workflow/nodes/variable_assigner/v1/node.py @@ -1,40 +1,36 @@ -from sqlalchemy import select -from sqlalchemy.orm import Session - from core.variables import SegmentType, Variable from core.workflow.entities.node_entities import NodeRunResult from core.workflow.nodes.base import BaseNode, BaseNodeData from core.workflow.nodes.enums import NodeType -from extensions.ext_database import db +from core.workflow.nodes.variable_assigner.common import helpers as common_helpers +from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError from factories import variable_factory -from models import ConversationVariable from models.workflow import WorkflowNodeExecutionStatus -from .exc import VariableAssignerNodeError from .node_data import VariableAssignerData, WriteMode class VariableAssignerNode(BaseNode[VariableAssignerData]): _node_data_cls: type[BaseNodeData] = VariableAssignerData - _node_type: NodeType = NodeType.CONVERSATION_VARIABLE_ASSIGNER + _node_type = NodeType.VARIABLE_ASSIGNER def _run(self) -> NodeRunResult: # Should be String, Number, Object, ArrayString, ArrayNumber, ArrayObject original_variable = self.graph_runtime_state.variable_pool.get(self.node_data.assigned_variable_selector) if not isinstance(original_variable, Variable): - raise VariableAssignerNodeError("assigned variable not found") + raise VariableOperatorNodeError("assigned variable not found") match self.node_data.write_mode: case WriteMode.OVER_WRITE: income_value = self.graph_runtime_state.variable_pool.get(self.node_data.input_variable_selector) if not income_value: - raise VariableAssignerNodeError("input value not found") + raise VariableOperatorNodeError("input value not found") updated_variable = original_variable.model_copy(update={"value": income_value.value}) case WriteMode.APPEND: income_value = self.graph_runtime_state.variable_pool.get(self.node_data.input_variable_selector) if not income_value: - raise VariableAssignerNodeError("input value not found") + raise VariableOperatorNodeError("input value not found") updated_value = original_variable.value + [income_value.value] updated_variable = original_variable.model_copy(update={"value": updated_value}) @@ -43,7 +39,7 @@ class VariableAssignerNode(BaseNode[VariableAssignerData]): updated_variable = original_variable.model_copy(update={"value": income_value.to_object()}) case _: - raise VariableAssignerNodeError(f"unsupported write mode: {self.node_data.write_mode}") + raise VariableOperatorNodeError(f"unsupported write mode: {self.node_data.write_mode}") # Over write the variable. self.graph_runtime_state.variable_pool.add(self.node_data.assigned_variable_selector, updated_variable) @@ -52,8 +48,8 @@ class VariableAssignerNode(BaseNode[VariableAssignerData]): # Update conversation variable. conversation_id = self.graph_runtime_state.variable_pool.get(["sys", "conversation_id"]) if not conversation_id: - raise VariableAssignerNodeError("conversation_id not found") - update_conversation_variable(conversation_id=conversation_id.text, variable=updated_variable) + raise VariableOperatorNodeError("conversation_id not found") + common_helpers.update_conversation_variable(conversation_id=conversation_id.text, variable=updated_variable) return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, @@ -63,18 +59,6 @@ class VariableAssignerNode(BaseNode[VariableAssignerData]): ) -def update_conversation_variable(conversation_id: str, variable: Variable): - stmt = select(ConversationVariable).where( - ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id - ) - with Session(db.engine) as session: - row = session.scalar(stmt) - if not row: - raise VariableAssignerNodeError("conversation variable not found in the database") - row.data = variable.model_dump_json() - session.commit() - - def get_zero_value(t: SegmentType): match t: case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER: @@ -86,4 +70,4 @@ def get_zero_value(t: SegmentType): case SegmentType.NUMBER: return variable_factory.build_segment(0) case _: - raise VariableAssignerNodeError(f"unsupported variable type: {t}") + raise VariableOperatorNodeError(f"unsupported variable type: {t}") diff --git a/api/core/workflow/nodes/variable_assigner/node_data.py b/api/core/workflow/nodes/variable_assigner/v1/node_data.py similarity index 75% rename from api/core/workflow/nodes/variable_assigner/node_data.py rename to api/core/workflow/nodes/variable_assigner/v1/node_data.py index 474ecefe76..9734d64712 100644 --- a/api/core/workflow/nodes/variable_assigner/node_data.py +++ b/api/core/workflow/nodes/variable_assigner/v1/node_data.py @@ -1,6 +1,5 @@ from collections.abc import Sequence from enum import StrEnum -from typing import Optional from core.workflow.nodes.base import BaseNodeData @@ -12,8 +11,6 @@ class WriteMode(StrEnum): class VariableAssignerData(BaseNodeData): - title: str = "Variable Assigner" - desc: Optional[str] = "Assign a value to a variable" assigned_variable_selector: Sequence[str] write_mode: WriteMode input_variable_selector: Sequence[str] diff --git a/api/core/workflow/nodes/variable_assigner/v2/__init__.py b/api/core/workflow/nodes/variable_assigner/v2/__init__.py new file mode 100644 index 0000000000..7eb1428e50 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/__init__.py @@ -0,0 +1,3 @@ +from .node import VariableAssignerNode + +__all__ = ["VariableAssignerNode"] diff --git a/api/core/workflow/nodes/variable_assigner/v2/constants.py b/api/core/workflow/nodes/variable_assigner/v2/constants.py new file mode 100644 index 0000000000..3797bfa77a --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/constants.py @@ -0,0 +1,11 @@ +from core.variables import SegmentType + +EMPTY_VALUE_MAPPING = { + SegmentType.STRING: "", + SegmentType.NUMBER: 0, + SegmentType.OBJECT: {}, + SegmentType.ARRAY_ANY: [], + SegmentType.ARRAY_STRING: [], + SegmentType.ARRAY_NUMBER: [], + SegmentType.ARRAY_OBJECT: [], +} diff --git a/api/core/workflow/nodes/variable_assigner/v2/entities.py b/api/core/workflow/nodes/variable_assigner/v2/entities.py new file mode 100644 index 0000000000..01df33b6d4 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/entities.py @@ -0,0 +1,20 @@ +from collections.abc import Sequence +from typing import Any + +from pydantic import BaseModel + +from core.workflow.nodes.base import BaseNodeData + +from .enums import InputType, Operation + + +class VariableOperationItem(BaseModel): + variable_selector: Sequence[str] + input_type: InputType + operation: Operation + value: Any | None = None + + +class VariableAssignerNodeData(BaseNodeData): + version: str = "2" + items: Sequence[VariableOperationItem] diff --git a/api/core/workflow/nodes/variable_assigner/v2/enums.py b/api/core/workflow/nodes/variable_assigner/v2/enums.py new file mode 100644 index 0000000000..36cf68aa19 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/enums.py @@ -0,0 +1,18 @@ +from enum import StrEnum + + +class Operation(StrEnum): + OVER_WRITE = "over-write" + CLEAR = "clear" + APPEND = "append" + EXTEND = "extend" + SET = "set" + ADD = "+=" + SUBTRACT = "-=" + MULTIPLY = "*=" + DIVIDE = "/=" + + +class InputType(StrEnum): + VARIABLE = "variable" + CONSTANT = "constant" diff --git a/api/core/workflow/nodes/variable_assigner/v2/exc.py b/api/core/workflow/nodes/variable_assigner/v2/exc.py new file mode 100644 index 0000000000..5b1ef4b04f --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/exc.py @@ -0,0 +1,31 @@ +from collections.abc import Sequence +from typing import Any + +from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError + +from .enums import InputType, Operation + + +class OperationNotSupportedError(VariableOperatorNodeError): + def __init__(self, *, operation: Operation, varialbe_type: str): + super().__init__(f"Operation {operation} is not supported for type {varialbe_type}") + + +class InputTypeNotSupportedError(VariableOperatorNodeError): + def __init__(self, *, input_type: InputType, operation: Operation): + super().__init__(f"Input type {input_type} is not supported for operation {operation}") + + +class VariableNotFoundError(VariableOperatorNodeError): + def __init__(self, *, variable_selector: Sequence[str]): + super().__init__(f"Variable {variable_selector} not found") + + +class InvalidInputValueError(VariableOperatorNodeError): + def __init__(self, *, value: Any): + super().__init__(f"Invalid input value {value}") + + +class ConversationIDNotFoundError(VariableOperatorNodeError): + def __init__(self): + super().__init__("conversation_id not found") diff --git a/api/core/workflow/nodes/variable_assigner/v2/helpers.py b/api/core/workflow/nodes/variable_assigner/v2/helpers.py new file mode 100644 index 0000000000..a86c7eb94a --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/helpers.py @@ -0,0 +1,91 @@ +from typing import Any + +from core.variables import SegmentType + +from .enums import Operation + + +def is_operation_supported(*, variable_type: SegmentType, operation: Operation): + match operation: + case Operation.OVER_WRITE | Operation.CLEAR: + return True + case Operation.SET: + return variable_type in {SegmentType.OBJECT, SegmentType.STRING, SegmentType.NUMBER} + case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE: + # Only number variable can be added, subtracted, multiplied or divided + return variable_type == SegmentType.NUMBER + case Operation.APPEND | Operation.EXTEND: + # Only array variable can be appended or extended + return variable_type in { + SegmentType.ARRAY_ANY, + SegmentType.ARRAY_OBJECT, + SegmentType.ARRAY_STRING, + SegmentType.ARRAY_NUMBER, + SegmentType.ARRAY_FILE, + } + case _: + return False + + +def is_variable_input_supported(*, operation: Operation): + if operation in {Operation.SET, Operation.ADD, Operation.SUBTRACT, Operation.MULTIPLY, Operation.DIVIDE}: + return False + return True + + +def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation): + match variable_type: + case SegmentType.STRING | SegmentType.OBJECT: + return operation in {Operation.OVER_WRITE, Operation.SET} + case SegmentType.NUMBER: + return operation in { + Operation.OVER_WRITE, + Operation.SET, + Operation.ADD, + Operation.SUBTRACT, + Operation.MULTIPLY, + Operation.DIVIDE, + } + case _: + return False + + +def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, value: Any): + if operation == Operation.CLEAR: + return True + match variable_type: + case SegmentType.STRING: + return isinstance(value, str) + + case SegmentType.NUMBER: + if not isinstance(value, int | float): + return False + if operation == Operation.DIVIDE and value == 0: + return False + return True + + case SegmentType.OBJECT: + return isinstance(value, dict) + + # Array & Append + case SegmentType.ARRAY_ANY if operation == Operation.APPEND: + return isinstance(value, str | float | int | dict) + case SegmentType.ARRAY_STRING if operation == Operation.APPEND: + return isinstance(value, str) + case SegmentType.ARRAY_NUMBER if operation == Operation.APPEND: + return isinstance(value, int | float) + case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND: + return isinstance(value, dict) + + # Array & Extend / Overwrite + case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}: + return isinstance(value, list) and all(isinstance(item, str | float | int | dict) for item in value) + case SegmentType.ARRAY_STRING if operation in {Operation.EXTEND, Operation.OVER_WRITE}: + return isinstance(value, list) and all(isinstance(item, str) for item in value) + case SegmentType.ARRAY_NUMBER if operation in {Operation.EXTEND, Operation.OVER_WRITE}: + return isinstance(value, list) and all(isinstance(item, int | float) for item in value) + case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}: + return isinstance(value, list) and all(isinstance(item, dict) for item in value) + + case _: + return False diff --git a/api/core/workflow/nodes/variable_assigner/v2/node.py b/api/core/workflow/nodes/variable_assigner/v2/node.py new file mode 100644 index 0000000000..ea59a2f170 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/v2/node.py @@ -0,0 +1,159 @@ +import json +from typing import Any + +from core.variables import SegmentType, Variable +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID +from core.workflow.entities.node_entities import NodeRunResult +from core.workflow.nodes.base import BaseNode +from core.workflow.nodes.enums import NodeType +from core.workflow.nodes.variable_assigner.common import helpers as common_helpers +from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError +from models.workflow import WorkflowNodeExecutionStatus + +from . import helpers +from .constants import EMPTY_VALUE_MAPPING +from .entities import VariableAssignerNodeData +from .enums import InputType, Operation +from .exc import ( + ConversationIDNotFoundError, + InputTypeNotSupportedError, + InvalidInputValueError, + OperationNotSupportedError, + VariableNotFoundError, +) + + +class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): + _node_data_cls = VariableAssignerNodeData + _node_type = NodeType.VARIABLE_ASSIGNER + + def _run(self) -> NodeRunResult: + inputs = self.node_data.model_dump() + process_data = {} + # NOTE: This node has no outputs + updated_variables: list[Variable] = [] + + try: + for item in self.node_data.items: + variable = self.graph_runtime_state.variable_pool.get(item.variable_selector) + + # ==================== Validation Part + + # Check if variable exists + if not isinstance(variable, Variable): + raise VariableNotFoundError(variable_selector=item.variable_selector) + + # Check if operation is supported + if not helpers.is_operation_supported(variable_type=variable.value_type, operation=item.operation): + raise OperationNotSupportedError(operation=item.operation, varialbe_type=variable.value_type) + + # Check if variable input is supported + if item.input_type == InputType.VARIABLE and not helpers.is_variable_input_supported( + operation=item.operation + ): + raise InputTypeNotSupportedError(input_type=InputType.VARIABLE, operation=item.operation) + + # Check if constant input is supported + if item.input_type == InputType.CONSTANT and not helpers.is_constant_input_supported( + variable_type=variable.value_type, operation=item.operation + ): + raise InputTypeNotSupportedError(input_type=InputType.CONSTANT, operation=item.operation) + + # Get value from variable pool + if ( + item.input_type == InputType.VARIABLE + and item.operation != Operation.CLEAR + and item.value is not None + ): + value = self.graph_runtime_state.variable_pool.get(item.value) + if value is None: + raise VariableNotFoundError(variable_selector=item.value) + # Skip if value is NoneSegment + if value.value_type == SegmentType.NONE: + continue + item.value = value.value + + # If set string / bytes / bytearray to object, try convert string to object. + if ( + item.operation == Operation.SET + and variable.value_type == SegmentType.OBJECT + and isinstance(item.value, str | bytes | bytearray) + ): + try: + item.value = json.loads(item.value) + except json.JSONDecodeError: + raise InvalidInputValueError(value=item.value) + + # Check if input value is valid + if not helpers.is_input_value_valid( + variable_type=variable.value_type, operation=item.operation, value=item.value + ): + raise InvalidInputValueError(value=item.value) + + # ==================== Execution Part + + updated_value = self._handle_item( + variable=variable, + operation=item.operation, + value=item.value, + ) + variable = variable.model_copy(update={"value": updated_value}) + updated_variables.append(variable) + except VariableOperatorNodeError as e: + return NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + inputs=inputs, + process_data=process_data, + error=str(e), + ) + + # Update variables + for variable in updated_variables: + self.graph_runtime_state.variable_pool.add(variable.selector, variable) + process_data[variable.name] = variable.value + + if variable.selector[0] == CONVERSATION_VARIABLE_NODE_ID: + conversation_id = self.graph_runtime_state.variable_pool.get(["sys", "conversation_id"]) + if not conversation_id: + raise ConversationIDNotFoundError + else: + conversation_id = conversation_id.value + common_helpers.update_conversation_variable( + conversation_id=conversation_id, + variable=variable, + ) + + return NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs=inputs, + process_data=process_data, + ) + + def _handle_item( + self, + *, + variable: Variable, + operation: Operation, + value: Any, + ): + match operation: + case Operation.OVER_WRITE: + return value + case Operation.CLEAR: + return EMPTY_VALUE_MAPPING[variable.value_type] + case Operation.APPEND: + return variable.value + [value] + case Operation.EXTEND: + return variable.value + value + case Operation.SET: + return value + case Operation.ADD: + return variable.value + value + case Operation.SUBTRACT: + return variable.value - value + case Operation.MULTIPLY: + return variable.value * value + case Operation.DIVIDE: + return variable.value / value + case _: + raise OperationNotSupportedError(operation=operation, varialbe_type=variable.value_type) diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 6f7b143ad6..811e40c11e 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -2,7 +2,7 @@ import logging import time import uuid from collections.abc import Generator, Mapping, Sequence -from typing import Any, Optional, cast +from typing import Any, Optional from configs import dify_config from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError @@ -19,7 +19,7 @@ from core.workflow.graph_engine.graph_engine import GraphEngine from core.workflow.nodes import NodeType from core.workflow.nodes.base import BaseNode from core.workflow.nodes.event import NodeEvent -from core.workflow.nodes.node_mapping import node_type_classes_mapping +from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING from factories import file_factory from models.enums import UserFrom from models.workflow import ( @@ -145,11 +145,8 @@ class WorkflowEntry: # Get node class node_type = NodeType(node_config.get("data", {}).get("type")) - node_cls = node_type_classes_mapping.get(node_type) - node_cls = cast(type[BaseNode], node_cls) - - if not node_cls: - raise ValueError(f"Node class not found for node type {node_type}") + node_version = node_config.get("data", {}).get("version", "1") + node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version] # init variable pool variable_pool = VariablePool(environment_variables=workflow.environment_variables) diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index 5b004405b4..16a578728a 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -36,6 +36,7 @@ from core.variables.variables import ( StringVariable, Variable, ) +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID class InvalidSelectorError(ValueError): @@ -62,11 +63,25 @@ SEGMENT_TO_VARIABLE_MAP = { } -def build_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable: - if (value_type := mapping.get("value_type")) is None: - raise VariableError("missing value type") +def build_conversation_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable: if not mapping.get("name"): raise VariableError("missing name") + return _build_variable_from_mapping(mapping=mapping, selector=[CONVERSATION_VARIABLE_NODE_ID, mapping["name"]]) + + +def build_environment_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable: + if not mapping.get("name"): + raise VariableError("missing name") + return _build_variable_from_mapping(mapping=mapping, selector=[ENVIRONMENT_VARIABLE_NODE_ID, mapping["name"]]) + + +def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequence[str]) -> Variable: + """ + This factory function is used to create the environment variable or the conversation variable, + not support the File type. + """ + if (value_type := mapping.get("value_type")) is None: + raise VariableError("missing value type") if (value := mapping.get("value")) is None: raise VariableError("missing value") match value_type: @@ -92,6 +107,8 @@ def build_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable: raise VariableError(f"not supported value type {value_type}") if result.size > dify_config.MAX_VARIABLE_SIZE: raise VariableError(f"variable size {result.size} exceeds limit {dify_config.MAX_VARIABLE_SIZE}") + if not result.selector: + result = result.model_copy(update={"selector": selector}) return result diff --git a/api/models/workflow.py b/api/models/workflow.py index fd53f137f9..c0e70889a8 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -238,7 +238,9 @@ class Workflow(db.Model): tenant_id = contexts.tenant_id.get() environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables) - results = [variable_factory.build_variable_from_mapping(v) for v in environment_variables_dict.values()] + results = [ + variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values() + ] # decrypt secret variables value decrypt_func = ( @@ -303,7 +305,7 @@ class Workflow(db.Model): self._conversation_variables = "{}" variables_dict: dict[str, Any] = json.loads(self._conversation_variables) - results = [variable_factory.build_variable_from_mapping(v) for v in variables_dict.values()] + results = [variable_factory.build_conversation_variable_from_mapping(v) for v in variables_dict.values()] return results @conversation_variables.setter @@ -793,4 +795,4 @@ class ConversationVariable(db.Model): def to_variable(self) -> Variable: mapping = json.loads(self.data) - return variable_factory.build_variable_from_mapping(mapping) + return variable_factory.build_conversation_variable_from_mapping(mapping) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index a4d71d5424..2f202374fd 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -387,11 +387,11 @@ class AppDslService: environment_variables_list = workflow_data.get("environment_variables", []) environment_variables = [ - variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list + variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list ] conversation_variables_list = workflow_data.get("conversation_variables", []) conversation_variables = [ - variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list + variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list ] workflow_service = WorkflowService() diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index aa2babd7f7..37d7d0937c 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -12,7 +12,7 @@ from core.workflow.entities.node_entities import NodeRunResult from core.workflow.errors import WorkflowNodeRunFailedError from core.workflow.nodes import NodeType from core.workflow.nodes.event import RunCompletedEvent -from core.workflow.nodes.node_mapping import node_type_classes_mapping +from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING from core.workflow.workflow_entry import WorkflowEntry from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated from extensions.ext_database import db @@ -176,7 +176,8 @@ class WorkflowService: """ # return default block config default_block_configs = [] - for node_type, node_class in node_type_classes_mapping.items(): + for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values(): + node_class = node_class_mapping[LATEST_VERSION] default_config = node_class.get_default_config() if default_config: default_block_configs.append(default_config) @@ -190,13 +191,13 @@ class WorkflowService: :param filters: filter by node config parameters. :return: """ - node_type_enum: NodeType = NodeType(node_type) + node_type_enum = NodeType(node_type) # return default block config - node_class = node_type_classes_mapping.get(node_type_enum) - if not node_class: + if node_type_enum not in NODE_TYPE_CLASSES_MAPPING: return None + node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION] default_config = node_class.get_default_config(filters=filters) if not default_config: return None diff --git a/api/tests/integration_tests/vdb/analyticdb/test_analyticdb.py b/api/tests/integration_tests/vdb/analyticdb/test_analyticdb.py index 4f44d2ffd6..5dd4754e8e 100644 --- a/api/tests/integration_tests/vdb/analyticdb/test_analyticdb.py +++ b/api/tests/integration_tests/vdb/analyticdb/test_analyticdb.py @@ -1,4 +1,4 @@ -from core.rag.datasource.vdb.analyticdb.analyticdb_vector import AnalyticdbConfig, AnalyticdbVector +from core.rag.datasource.vdb.analyticdb.analyticdb_vector import AnalyticdbVector from core.rag.datasource.vdb.analyticdb.analyticdb_vector_openapi import AnalyticdbVectorOpenAPIConfig from core.rag.datasource.vdb.analyticdb.analyticdb_vector_sql import AnalyticdbVectorBySqlConfig from tests.integration_tests.vdb.test_vector_store import AbstractVectorTest, setup_mock_redis diff --git a/api/tests/unit_tests/core/app/segments/test_factory.py b/api/tests/unit_tests/core/app/segments/test_factory.py index 882a87239b..e6e289c12a 100644 --- a/api/tests/unit_tests/core/app/segments/test_factory.py +++ b/api/tests/unit_tests/core/app/segments/test_factory.py @@ -19,36 +19,36 @@ from factories import variable_factory def test_string_variable(): test_data = {"value_type": "string", "name": "test_text", "value": "Hello, World!"} - result = variable_factory.build_variable_from_mapping(test_data) + result = variable_factory.build_conversation_variable_from_mapping(test_data) assert isinstance(result, StringVariable) def test_integer_variable(): test_data = {"value_type": "number", "name": "test_int", "value": 42} - result = variable_factory.build_variable_from_mapping(test_data) + result = variable_factory.build_conversation_variable_from_mapping(test_data) assert isinstance(result, IntegerVariable) def test_float_variable(): test_data = {"value_type": "number", "name": "test_float", "value": 3.14} - result = variable_factory.build_variable_from_mapping(test_data) + result = variable_factory.build_conversation_variable_from_mapping(test_data) assert isinstance(result, FloatVariable) def test_secret_variable(): test_data = {"value_type": "secret", "name": "test_secret", "value": "secret_value"} - result = variable_factory.build_variable_from_mapping(test_data) + result = variable_factory.build_conversation_variable_from_mapping(test_data) assert isinstance(result, SecretVariable) def test_invalid_value_type(): test_data = {"value_type": "unknown", "name": "test_invalid", "value": "value"} with pytest.raises(VariableError): - variable_factory.build_variable_from_mapping(test_data) + variable_factory.build_conversation_variable_from_mapping(test_data) def test_build_a_blank_string(): - result = variable_factory.build_variable_from_mapping( + result = variable_factory.build_conversation_variable_from_mapping( { "value_type": "string", "name": "blank", @@ -80,7 +80,7 @@ def test_object_variable(): "key2": 2, }, } - variable = variable_factory.build_variable_from_mapping(mapping) + variable = variable_factory.build_conversation_variable_from_mapping(mapping) assert isinstance(variable, ObjectSegment) assert isinstance(variable.value["key1"], str) assert isinstance(variable.value["key2"], int) @@ -97,7 +97,7 @@ def test_array_string_variable(): "text", ], } - variable = variable_factory.build_variable_from_mapping(mapping) + variable = variable_factory.build_conversation_variable_from_mapping(mapping) assert isinstance(variable, ArrayStringVariable) assert isinstance(variable.value[0], str) assert isinstance(variable.value[1], str) @@ -114,7 +114,7 @@ def test_array_number_variable(): 2.0, ], } - variable = variable_factory.build_variable_from_mapping(mapping) + variable = variable_factory.build_conversation_variable_from_mapping(mapping) assert isinstance(variable, ArrayNumberVariable) assert isinstance(variable.value[0], int) assert isinstance(variable.value[1], float) @@ -137,7 +137,7 @@ def test_array_object_variable(): }, ], } - variable = variable_factory.build_variable_from_mapping(mapping) + variable = variable_factory.build_conversation_variable_from_mapping(mapping) assert isinstance(variable, ArrayObjectVariable) assert isinstance(variable.value[0], dict) assert isinstance(variable.value[1], dict) @@ -149,7 +149,7 @@ def test_array_object_variable(): def test_variable_cannot_large_than_200_kb(): with pytest.raises(VariableError): - variable_factory.build_variable_from_mapping( + variable_factory.build_conversation_variable_from_mapping( { "id": str(uuid4()), "value_type": "string", diff --git a/api/tests/unit_tests/core/workflow/nodes/test_variable_assigner.py b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py similarity index 92% rename from api/tests/unit_tests/core/workflow/nodes/test_variable_assigner.py rename to api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py index 096ae0ea52..9793da129d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_variable_assigner.py +++ b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py @@ -10,7 +10,8 @@ from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState -from core.workflow.nodes.variable_assigner import VariableAssignerNode, WriteMode +from core.workflow.nodes.variable_assigner.v1 import VariableAssignerNode +from core.workflow.nodes.variable_assigner.v1.node_data import WriteMode from models.enums import UserFrom from models.workflow import WorkflowType @@ -84,6 +85,7 @@ def test_overwrite_string_variable(): config={ "id": "node_id", "data": { + "title": "test", "assigned_variable_selector": ["conversation", conversation_variable.name], "write_mode": WriteMode.OVER_WRITE.value, "input_variable_selector": [DEFAULT_NODE_ID, input_variable.name], @@ -91,7 +93,7 @@ def test_overwrite_string_variable(): }, ) - with mock.patch("core.workflow.nodes.variable_assigner.node.update_conversation_variable") as mock_run: + with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: list(node.run()) mock_run.assert_called_once() @@ -166,6 +168,7 @@ def test_append_variable_to_array(): config={ "id": "node_id", "data": { + "title": "test", "assigned_variable_selector": ["conversation", conversation_variable.name], "write_mode": WriteMode.APPEND.value, "input_variable_selector": [DEFAULT_NODE_ID, input_variable.name], @@ -173,7 +176,7 @@ def test_append_variable_to_array(): }, ) - with mock.patch("core.workflow.nodes.variable_assigner.node.update_conversation_variable") as mock_run: + with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: list(node.run()) mock_run.assert_called_once() @@ -237,6 +240,7 @@ def test_clear_array(): config={ "id": "node_id", "data": { + "title": "test", "assigned_variable_selector": ["conversation", conversation_variable.name], "write_mode": WriteMode.CLEAR.value, "input_variable_selector": [], @@ -244,7 +248,7 @@ def test_clear_array(): }, ) - with mock.patch("core.workflow.nodes.variable_assigner.node.update_conversation_variable") as mock_run: + with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: list(node.run()) mock_run.assert_called_once() diff --git a/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_helpers.py b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_helpers.py new file mode 100644 index 0000000000..16c1370018 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_helpers.py @@ -0,0 +1,24 @@ +import pytest + +from core.variables import SegmentType +from core.workflow.nodes.variable_assigner.v2.enums import Operation +from core.workflow.nodes.variable_assigner.v2.helpers import is_input_value_valid + + +def test_is_input_value_valid_overwrite_array_string(): + # Valid cases + assert is_input_value_valid( + variable_type=SegmentType.ARRAY_STRING, operation=Operation.OVER_WRITE, value=["hello", "world"] + ) + assert is_input_value_valid(variable_type=SegmentType.ARRAY_STRING, operation=Operation.OVER_WRITE, value=[]) + + # Invalid cases + assert not is_input_value_valid( + variable_type=SegmentType.ARRAY_STRING, operation=Operation.OVER_WRITE, value="not an array" + ) + assert not is_input_value_valid( + variable_type=SegmentType.ARRAY_STRING, operation=Operation.OVER_WRITE, value=[1, 2, 3] + ) + assert not is_input_value_valid( + variable_type=SegmentType.ARRAY_STRING, operation=Operation.OVER_WRITE, value=["valid", 123, "invalid"] + ) diff --git a/api/tests/unit_tests/models/test_conversation_variable.py b/api/tests/unit_tests/models/test_conversation_variable.py index b879afa3e7..5d84a2ec85 100644 --- a/api/tests/unit_tests/models/test_conversation_variable.py +++ b/api/tests/unit_tests/models/test_conversation_variable.py @@ -6,7 +6,7 @@ from models import ConversationVariable def test_from_variable_and_to_variable(): - variable = variable_factory.build_variable_from_mapping( + variable = variable_factory.build_conversation_variable_from_mapping( { "id": str(uuid4()), "name": "name", diff --git a/api/tests/unit_tests/models/test_workflow.py b/api/tests/unit_tests/models/test_workflow.py index 478fa8012b..fe56f18f1b 100644 --- a/api/tests/unit_tests/models/test_workflow.py +++ b/api/tests/unit_tests/models/test_workflow.py @@ -24,10 +24,18 @@ def test_environment_variables(): ) # Create some EnvironmentVariable instances - variable1 = StringVariable.model_validate({"name": "var1", "value": "value1", "id": str(uuid4())}) - variable2 = IntegerVariable.model_validate({"name": "var2", "value": 123, "id": str(uuid4())}) - variable3 = SecretVariable.model_validate({"name": "var3", "value": "secret", "id": str(uuid4())}) - variable4 = FloatVariable.model_validate({"name": "var4", "value": 3.14, "id": str(uuid4())}) + variable1 = StringVariable.model_validate( + {"name": "var1", "value": "value1", "id": str(uuid4()), "selector": ["env", "var1"]} + ) + variable2 = IntegerVariable.model_validate( + {"name": "var2", "value": 123, "id": str(uuid4()), "selector": ["env", "var2"]} + ) + variable3 = SecretVariable.model_validate( + {"name": "var3", "value": "secret", "id": str(uuid4()), "selector": ["env", "var3"]} + ) + variable4 = FloatVariable.model_validate( + {"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]} + ) with ( mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), @@ -58,10 +66,18 @@ def test_update_environment_variables(): ) # Create some EnvironmentVariable instances - variable1 = StringVariable.model_validate({"name": "var1", "value": "value1", "id": str(uuid4())}) - variable2 = IntegerVariable.model_validate({"name": "var2", "value": 123, "id": str(uuid4())}) - variable3 = SecretVariable.model_validate({"name": "var3", "value": "secret", "id": str(uuid4())}) - variable4 = FloatVariable.model_validate({"name": "var4", "value": 3.14, "id": str(uuid4())}) + variable1 = StringVariable.model_validate( + {"name": "var1", "value": "value1", "id": str(uuid4()), "selector": ["env", "var1"]} + ) + variable2 = IntegerVariable.model_validate( + {"name": "var2", "value": 123, "id": str(uuid4()), "selector": ["env", "var2"]} + ) + variable3 = SecretVariable.model_validate( + {"name": "var3", "value": "secret", "id": str(uuid4()), "selector": ["env", "var3"]} + ) + variable4 = FloatVariable.model_validate( + {"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]} + ) with ( mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx index c3300a1e67..722fde3237 100644 --- a/web/app/components/base/badge.tsx +++ b/web/app/components/base/badge.tsx @@ -15,7 +15,7 @@ const Badge = ({ return (
{ + return ( + + + + + + + + + + + ) +} + +export default HorizontalLine diff --git a/web/app/components/base/list-empty/index.tsx b/web/app/components/base/list-empty/index.tsx new file mode 100644 index 0000000000..e925878bc1 --- /dev/null +++ b/web/app/components/base/list-empty/index.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { Variable02 } from '../icons/src/vender/solid/development' +import VerticalLine from './vertical-line' +import HorizontalLine from './horizontal-line' + +type ListEmptyProps = { + title?: string + description?: React.ReactNode +} + +const ListEmpty = ({ + title, + description, +}: ListEmptyProps) => { + return ( +
+
+
+ + + + + +
+
+
+
{title}
+ {description} +
+
+ ) +} + +export default ListEmpty diff --git a/web/app/components/base/list-empty/vertical-line.tsx b/web/app/components/base/list-empty/vertical-line.tsx new file mode 100644 index 0000000000..63e57447bf --- /dev/null +++ b/web/app/components/base/list-empty/vertical-line.tsx @@ -0,0 +1,21 @@ +type VerticalLineProps = { + className?: string +} +const VerticalLine = ({ + className, +}: VerticalLineProps) => { + return ( + + + + + + + + + + + ) +} + +export default VerticalLine diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index b115a7b3c3..1001e981c5 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -48,6 +48,7 @@ const getIcon = (type: BlockEnum, className: string) => { [BlockEnum.VariableAggregator]: , [BlockEnum.Assigner]: , [BlockEnum.Tool]: , + [BlockEnum.IterationStart]: , [BlockEnum.Iteration]: , [BlockEnum.ParameterExtractor]: , [BlockEnum.DocExtractor]: , diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 28d07936d3..2d75679b08 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -33,6 +33,7 @@ export type Props = { showFileList?: boolean onGenerated?: (value: string) => void showCodeGenerator?: boolean + className?: string } export const languageMap = { @@ -67,6 +68,7 @@ const CodeEditor: FC = ({ showFileList, onGenerated, showCodeGenerator = false, + className, }) => { const [isFocus, setIsFocus] = React.useState(false) const [isMounted, setIsMounted] = React.useState(false) @@ -187,7 +189,7 @@ const CodeEditor: FC = ({ ) return ( -
+
{noWrapper ?
= ({ triggerClassName='w-4 h-4 ml-1' /> )} -
{operations &&
{operations}
} diff --git a/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx b/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx index 4ec9d27f50..bf592deaec 100644 --- a/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx +++ b/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx @@ -10,7 +10,7 @@ const ListNoDataPlaceholder: FC = ({ children, }) => { return ( -
+
{children}
) diff --git a/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx new file mode 100644 index 0000000000..9ad5ad4a5a --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx @@ -0,0 +1,39 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import VarReferenceVars from './var-reference-vars' +import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import ListEmpty from '@/app/components/base/list-empty' + +type Props = { + vars: NodeOutPutVar[] + onChange: (value: ValueSelector, varDetail: Var) => void + itemWidth?: number +} +const AssignedVarReferencePopup: FC = ({ + vars, + onChange, + itemWidth, +}) => { + const { t } = useTranslation() + // max-h-[300px] overflow-y-auto todo: use portal to handle long list + return ( +
+ {(!vars || vars.length === 0) + ? + : + } +
+ ) +} +export default React.memo(AssignedVarReferencePopup) diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 0c553a2738..e4d354a615 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -60,6 +60,9 @@ type Props = { onRemove?: () => void typePlaceHolder?: string isSupportFileVar?: boolean + placeholder?: string + minWidth?: number + popupFor?: 'assigned' | 'toAssigned' } const VarReferencePicker: FC = ({ @@ -83,6 +86,9 @@ const VarReferencePicker: FC = ({ onRemove, typePlaceHolder, isSupportFileVar = true, + placeholder, + minWidth, + popupFor, }) => { const { t } = useTranslation() const store = useStoreApi() @@ -261,7 +267,7 @@ const VarReferencePicker: FC = ({ { }}>
) - : (
+ : (
{isSupportConstantValue ?
{ e.stopPropagation() @@ -285,7 +291,7 @@ const VarReferencePicker: FC = ({ />
: (!hasValue &&
- +
)} {isConstant ? ( @@ -329,17 +335,17 @@ const VarReferencePicker: FC = ({ {!hasValue && } {isEnv && } {isChatVar && } -
{varName}
-
{type}
{!isValidVar && } ) - :
{t('workflow.common.setVarValuePlaceholder')}
} + :
{placeholder ?? t('workflow.common.setVarValuePlaceholder')}
}
@@ -378,12 +384,13 @@ const VarReferencePicker: FC = ({ + }} className='mt-1'> {!isConstant && ( )} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index cd03da1556..d9a4d2c946 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -1,33 +1,64 @@ 'use client' import type { FC } from 'react' import React from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' import VarReferenceVars from './var-reference-vars' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import ListEmpty from '@/app/components/base/list-empty' +import { LanguagesSupported } from '@/i18n/language' +import I18n from '@/context/i18n' type Props = { vars: NodeOutPutVar[] + popupFor?: 'assigned' | 'toAssigned' onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number isSupportFileVar?: boolean } const VarReferencePopup: FC = ({ vars, + popupFor, onChange, itemWidth, isSupportFileVar = true, }) => { + const { t } = useTranslation() + const { locale } = useContext(I18n) // max-h-[300px] overflow-y-auto todo: use portal to handle long list return (
- + {((!vars || vars.length === 0) && popupFor) + ? (popupFor === 'toAssigned' + ? ( + + {t('workflow.variableReference.noVarsForOperation')} +
} + /> + ) + : ( + + {t('workflow.variableReference.assignedVarsDescription')} +
{t('workflow.variableReference.conversationVars')} +
} + /> + )) + : + }
) } diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index c500f0c8cf..6791a2f746 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -24,6 +24,7 @@ import QuestionClassifyDefault from '@/app/components/workflow/nodes/question-cl import HTTPDefault from '@/app/components/workflow/nodes/http/default' import ToolDefault from '@/app/components/workflow/nodes/tool/default' import VariableAssigner from '@/app/components/workflow/nodes/variable-assigner/default' +import Assigner from '@/app/components/workflow/nodes/assigner/default' import ParameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default' import IterationDefault from '@/app/components/workflow/nodes/iteration/default' import { ssePost } from '@/service/base' @@ -39,6 +40,7 @@ const { checkValid: checkQuestionClassifyValid } = QuestionClassifyDefault const { checkValid: checkHttpValid } = HTTPDefault const { checkValid: checkToolValid } = ToolDefault const { checkValid: checkVariableAssignerValid } = VariableAssigner +const { checkValid: checkAssignerValid } = Assigner const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault const { checkValid: checkIterationValid } = IterationDefault @@ -51,7 +53,7 @@ const checkValidFns: Record = { [BlockEnum.QuestionClassifier]: checkQuestionClassifyValid, [BlockEnum.HttpRequest]: checkHttpValid, [BlockEnum.Tool]: checkToolValid, - [BlockEnum.VariableAssigner]: checkVariableAssignerValid, + [BlockEnum.VariableAssigner]: checkAssignerValid, [BlockEnum.VariableAggregator]: checkVariableAssignerValid, [BlockEnum.ParameterExtractor]: checkParameterExtractorValid, [BlockEnum.Iteration]: checkIterationValid, diff --git a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx new file mode 100644 index 0000000000..8542bb4829 --- /dev/null +++ b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx @@ -0,0 +1,128 @@ +import type { FC } from 'react' +import { useState } from 'react' +import { + RiArrowDownSLine, + RiCheckLine, +} from '@remixicon/react' +import classNames from 'classnames' +import { useTranslation } from 'react-i18next' +import type { WriteMode } from '../types' +import { getOperationItems } from '../utils' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import type { VarType } from '@/app/components/workflow/types' +import Divider from '@/app/components/base/divider' + +type Item = { + value: string | number + name: string +} + +type OperationSelectorProps = { + value: string | number + onSelect: (value: Item) => void + placeholder?: string + disabled?: boolean + className?: string + popupClassName?: string + assignedVarType?: VarType + writeModeTypes?: WriteMode[] + writeModeTypesArr?: WriteMode[] + writeModeTypesNum?: WriteMode[] +} + +const i18nPrefix = 'workflow.nodes.assigner' + +const OperationSelector: FC = ({ + value, + onSelect, + disabled = false, + className, + popupClassName, + assignedVarType, + writeModeTypes, + writeModeTypesArr, + writeModeTypesNum, +}) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const items = getOperationItems(assignedVarType, writeModeTypes, writeModeTypesArr, writeModeTypesNum) + + const selectedItem = items.find(item => item.value === value) + + return ( + + !disabled && setOpen(v => !v)} + > +
+
+ + {selectedItem?.name ? t(`${i18nPrefix}.operations.${selectedItem?.name}`) : t(`${i18nPrefix}.operations.title`)} + +
+ +
+
+ + +
+
+
+
{t(`${i18nPrefix}.operations.title`)}
+
+ {items.map(item => ( + item.value === 'divider' + ? ( + + ) + : ( +
{ + onSelect(item) + setOpen(false) + }} + > +
+ {t(`${i18nPrefix}.operations.${item.name}`)} +
+ {item.value === value && ( +
+ +
+ )} +
+ ) + ))} +
+
+
+
+ ) +} + +export default OperationSelector diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx new file mode 100644 index 0000000000..42ee9845dd --- /dev/null +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -0,0 +1,227 @@ +'use client' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import React, { useCallback } from 'react' +import produce from 'immer' +import { RiDeleteBinLine } from '@remixicon/react' +import OperationSelector from '../operation-selector' +import { AssignerNodeInputType, WriteMode } from '../../types' +import type { AssignerNodeOperation } from '../../types' +import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder' +import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' +import type { ValueSelector, Var, VarType } from '@/app/components/workflow/types' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import ActionButton from '@/app/components/base/action-button' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' + +type Props = { + readonly: boolean + nodeId: string + list: AssignerNodeOperation[] + onChange: (list: AssignerNodeOperation[], value?: ValueSelector) => void + onOpen?: (index: number) => void + filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean + filterToAssignedVar?: (payload: Var, assignedVarType: VarType, write_mode: WriteMode) => boolean + getAssignedVarType?: (valueSelector: ValueSelector) => VarType + getToAssignedVarType?: (assignedVarType: VarType, write_mode: WriteMode) => VarType + writeModeTypes?: WriteMode[] + writeModeTypesArr?: WriteMode[] + writeModeTypesNum?: WriteMode[] +} + +const VarList: FC = ({ + readonly, + nodeId, + list, + onChange, + onOpen = () => { }, + filterVar, + filterToAssignedVar, + getAssignedVarType, + getToAssignedVarType, + writeModeTypes, + writeModeTypesArr, + writeModeTypesNum, +}) => { + const { t } = useTranslation() + const handleAssignedVarChange = useCallback((index: number) => { + return (value: ValueSelector | string) => { + const newList = produce(list, (draft) => { + draft[index].variable_selector = value as ValueSelector + draft[index].operation = WriteMode.overwrite + draft[index].value = undefined + }) + onChange(newList, value as ValueSelector) + } + }, [list, onChange]) + + const handleOperationChange = useCallback((index: number) => { + return (item: { value: string | number }) => { + const newList = produce(list, (draft) => { + draft[index].operation = item.value as WriteMode + draft[index].value = '' // Clear value when operation changes + if (item.value === WriteMode.set || item.value === WriteMode.increment || item.value === WriteMode.decrement + || item.value === WriteMode.multiply || item.value === WriteMode.divide) + draft[index].input_type = AssignerNodeInputType.constant + else + draft[index].input_type = AssignerNodeInputType.variable + }) + onChange(newList) + } + }, [list, onChange]) + + const handleToAssignedVarChange = useCallback((index: number) => { + return (value: ValueSelector | string | number) => { + const newList = produce(list, (draft) => { + draft[index].value = value as ValueSelector + }) + onChange(newList, value as ValueSelector) + } + }, [list, onChange]) + + const handleVarRemove = useCallback((index: number) => { + return () => { + const newList = produce(list, (draft) => { + draft.splice(index, 1) + }) + onChange(newList) + } + }, [list, onChange]) + + const handleOpen = useCallback((index: number) => { + return () => onOpen(index) + }, [onOpen]) + + const handleFilterToAssignedVar = useCallback((index: number) => { + return (payload: Var, valueSelector: ValueSelector) => { + const item = list[index] + const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined + + if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation) + return true + + return filterToAssignedVar( + payload, + assignedVarType, + item.operation, + ) + } + }, [list, filterToAssignedVar, getAssignedVarType]) + + if (list.length === 0) { + return ( + + {t('workflow.nodes.assigner.noVarTip')} + + ) + } + + return ( +
+ {list.map((item, index) => { + const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined + const toAssignedVarType = (assignedVarType && item.operation && getToAssignedVarType) + ? getToAssignedVarType(assignedVarType, item.operation) + : undefined + + return ( +
+
+
+ + +
+ {item.operation !== WriteMode.clear && item.operation !== WriteMode.set + && !writeModeTypesNum?.includes(item.operation) + && ( + + ) + } + {item.operation === WriteMode.set && assignedVarType && ( + <> + {assignedVarType === 'number' && ( + handleToAssignedVarChange(index)(Number(e.target.value))} + className='w-full' + /> + )} + {assignedVarType === 'string' && ( +