From 31348af2e3a42e79eee384838e677df0a2152350 Mon Sep 17 00:00:00 2001 From: Hiroshi Fujita Date: Sun, 24 Nov 2024 12:15:24 +0900 Subject: [PATCH 001/130] doc: Updated Python version requirements to match English version (#11015) --- CONTRIBUTING_CN.md | 2 +- CONTRIBUTING_JA.md | 2 +- CONTRIBUTING_VI.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index 310c55090a..4fa43b24ee 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -71,7 +71,7 @@ Dify 依赖以下工具和库: - [Docker Compose](https://docs.docker.com/compose/install/) - [Node.js v18.x (LTS)](http://nodejs.org) - [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/) -- [Python](https://www.python.org/) version 3.10.x +- [Python](https://www.python.org/) version 3.11.x or 3.12.x ### 4. 安装 diff --git a/CONTRIBUTING_JA.md b/CONTRIBUTING_JA.md index a68bdeddbc..22e30e9c03 100644 --- a/CONTRIBUTING_JA.md +++ b/CONTRIBUTING_JA.md @@ -74,7 +74,7 @@ Dify を構築するには次の依存関係が必要です。それらがシス - [Docker Compose](https://docs.docker.com/compose/install/) - [Node.js v18.x (LTS)](http://nodejs.org) - [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/) -- [Python](https://www.python.org/) version 3.10.x +- [Python](https://www.python.org/) version 3.11.x or 3.12.x ### 4. インストール diff --git a/CONTRIBUTING_VI.md b/CONTRIBUTING_VI.md index a77239ff38..ad41f51aeb 100644 --- a/CONTRIBUTING_VI.md +++ b/CONTRIBUTING_VI.md @@ -73,7 +73,7 @@ Dify yêu cầu các phụ thuộc sau để build, hãy đảm bảo chúng đ - [Docker Compose](https://docs.docker.com/compose/install/) - [Node.js v18.x (LTS)](http://nodejs.org) - [npm](https://www.npmjs.com/) phiên bản 8.x.x hoặc [Yarn](https://yarnpkg.com/) -- [Python](https://www.python.org/) phiên bản 3.10.x +- [Python](https://www.python.org/) phiên bản 3.11.x hoặc 3.12.x ### 4. Cài đặt @@ -153,4 +153,4 @@ Và thế là xong! Khi PR của bạn được merge, bạn sẽ được giớ ## Nhận trợ giúp -Nếu bạn gặp khó khăn hoặc có câu hỏi cấp bách trong quá trình đóng góp, hãy đặt câu hỏi của bạn trong vấn đề GitHub liên quan, hoặc tham gia [Discord](https://discord.gg/8Tpq4AcN9c) của chúng tôi để trò chuyện nhanh chóng. \ No newline at end of file +Nếu bạn gặp khó khăn hoặc có câu hỏi cấp bách trong quá trình đóng góp, hãy đặt câu hỏi của bạn trong vấn đề GitHub liên quan, hoặc tham gia [Discord](https://discord.gg/8Tpq4AcN9c) của chúng tôi để trò chuyện nhanh chóng. From d0648e27e253d0de27f277a88a8cded386b0229e Mon Sep 17 00:00:00 2001 From: johnpccd Date: Sun, 24 Nov 2024 04:15:46 +0100 Subject: [PATCH 002/130] Fix typo (#11024) --- web/i18n/en-US/common.ts | 2 +- web/i18n/en-US/share-app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index f9d815be2c..8043a74224 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -363,7 +363,7 @@ const translation = { deprecated: 'Deprecated', confirmDelete: 'Confirm deletion?', quotaTip: 'Remaining available free tokens', - loadPresets: 'Load Presents', + loadPresets: 'Load Presets', parameters: 'PARAMETERS', loadBalancing: 'Load balancing', loadBalancingDescription: 'Reduce pressure with multiple sets of credentials.', diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share-app.ts index b5a219c998..5d47fd31cf 100644 --- a/web/i18n/en-US/share-app.ts +++ b/web/i18n/en-US/share-app.ts @@ -14,7 +14,7 @@ const translation = { prompt: 'Prompt', privatePromptConfigTitle: 'Conversation settings', publicPromptConfigTitle: 'Initial Prompt', - configStatusDes: 'Before start, you can modify conversation settings', + configStatusDes: 'Before starting, you can modify the conversation settings', configDisabled: 'Previous session settings have been used for this session.', startChat: 'Start Chat', From 7f00c5a02ede75bbb1317972143455423c528a05 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 24 Nov 2024 11:17:55 +0800 Subject: [PATCH 003/130] fix: uuid not import bug (#11014) Signed-off-by: yihong0618 --- api/core/app/app_config/easy_ui_based_app/dataset/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/core/app/app_config/easy_ui_based_app/dataset/manager.py b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py index a22395b8e3..b9aae7904f 100644 --- a/api/core/app/app_config/easy_ui_based_app/dataset/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py @@ -1,3 +1,4 @@ +import uuid from typing import Optional from core.app.app_config.entities import DatasetEntity, DatasetRetrieveConfigEntity From 0181f1c08c2ad6750f739f24c8343a323a336f18 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 24 Nov 2024 12:18:19 +0800 Subject: [PATCH 004/130] fix: wrong convert in PromptTemplateConfigManager (#11016) Signed-off-by: yihong0618 --- .../app_config/easy_ui_based_app/prompt_template/manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py index 82a0e56ce8..fa30511f63 100644 --- a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py @@ -1,4 +1,5 @@ from core.app.app_config.entities import ( + AdvancedChatMessageEntity, AdvancedChatPromptTemplateEntity, AdvancedCompletionPromptTemplateEntity, PromptTemplateEntity, @@ -25,7 +26,9 @@ class PromptTemplateConfigManager: chat_prompt_messages = [] for message in chat_prompt_config.get("prompt", []): chat_prompt_messages.append( - {"text": message["text"], "role": PromptMessageRole.value_of(message["role"])} + AdvancedChatMessageEntity( + **{"text": message["text"], "role": PromptMessageRole.value_of(message["role"])} + ) ) advanced_chat_prompt_template = AdvancedChatPromptTemplateEntity(messages=chat_prompt_messages) From 6c8e208ef3d1b3ab042ae70e56fc64b21ff77fe9 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Sun, 24 Nov 2024 13:28:46 +0800 Subject: [PATCH 005/130] chore: bump minimum supported Python version to 3.11 (#10386) --- .github/actions/setup-poetry/action.yml | 2 +- .github/workflows/api-tests.yml | 1 - .github/workflows/vdb-tests.yml | 1 - api/app.py | 8 +- api/controllers/console/app/conversation.py | 4 +- api/controllers/console/app/site.py | 6 +- api/controllers/console/auth/activate.py | 2 +- api/controllers/console/auth/oauth.py | 4 +- .../console/datasets/data_source.py | 4 +- .../console/datasets/datasets_document.py | 18 +-- .../console/datasets/datasets_segments.py | 4 +- api/controllers/console/explore/completion.py | 6 +- .../console/explore/installed_app.py | 4 +- api/controllers/console/workspace/account.py | 4 +- api/controllers/service_api/wraps.py | 4 +- api/core/agent/base_agent_runner.py | 4 +- api/core/app/app_config/entities.py | 4 +- .../app/apps/message_based_app_generator.py | 4 +- api/core/app/entities/queue_entities.py | 4 +- .../task_pipeline/workflow_cycle_manage.py | 16 +-- api/core/entities/provider_configuration.py | 12 +- api/core/file/enums.py | 12 +- .../helper/code_executor/code_executor.py | 4 +- api/core/indexing_runner.py | 28 ++-- .../entities/message_entities.py | 6 +- .../model_runtime/entities/model_entities.py | 4 +- api/core/ops/entities/trace_entity.py | 4 +- .../entities/langfuse_trace_entity.py | 6 +- .../entities/langsmith_trace_entity.py | 4 +- api/core/prompt/simple_prompt_transform.py | 2 +- .../rag/datasource/keyword/keyword_type.py | 4 +- api/core/rag/datasource/vdb/vector_type.py | 4 +- api/core/rag/extractor/word_extractor.py | 4 +- api/core/rag/rerank/rerank_type.py | 4 +- api/core/tools/entities/tool_entities.py | 4 +- .../builtin/time/tools/current_time.py | 4 +- api/core/tools/tool/tool.py | 4 +- api/core/tools/tool_engine.py | 6 +- api/core/variables/types.py | 4 +- api/core/workflow/entities/node_entities.py | 4 +- api/core/workflow/enums.py | 4 +- .../entities/runtime_route_state.py | 6 +- api/core/workflow/nodes/enums.py | 4 +- api/core/workflow/nodes/iteration/entities.py | 4 +- .../nodes/iteration/iteration_node.py | 12 +- .../nodes/variable_assigner/node_data.py | 4 +- .../event_handlers/create_document_index.py | 2 +- ...vider_last_used_at_when_message_created.py | 4 +- api/extensions/storage/azure_blob_storage.py | 4 +- api/extensions/storage/storage_type.py | 4 +- api/libs/oauth_data_source.py | 6 +- api/models/account.py | 6 +- api/models/dataset.py | 2 +- api/models/enums.py | 8 +- api/models/model.py | 4 +- api/models/task.py | 8 +- api/models/workflow.py | 4 +- api/poetry.lock | 121 +++++++----------- api/pyproject.toml | 4 +- api/services/account_service.py | 18 +-- api/services/annotation_service.py | 2 +- api/services/app_dsl_service.py | 6 +- api/services/app_service.py | 12 +- api/services/auth/auth_type.py | 4 +- api/services/conversation_service.py | 4 +- api/services/dataset_service.py | 26 ++-- api/services/external_knowledge_service.py | 4 +- api/services/feature_service.py | 4 +- api/services/file_service.py | 6 +- api/services/model_load_balancing_service.py | 2 +- .../recommend_app/recommend_app_type.py | 4 +- api/services/workflow_service.py | 10 +- api/tasks/add_document_to_index_task.py | 2 +- .../enable_annotation_reply_task.py | 2 +- .../batch_create_segment_to_index_task.py | 4 +- api/tasks/create_segment_to_index_task.py | 6 +- api/tasks/document_indexing_sync_task.py | 2 +- api/tasks/document_indexing_task.py | 4 +- api/tasks/document_indexing_update_task.py | 2 +- api/tasks/enable_segment_to_index_task.py | 2 +- .../answer/test_answer_stream_processor.py | 6 +- 81 files changed, 271 insertions(+), 300 deletions(-) diff --git a/.github/actions/setup-poetry/action.yml b/.github/actions/setup-poetry/action.yml index 5feab33d1d..2e76676f37 100644 --- a/.github/actions/setup-poetry/action.yml +++ b/.github/actions/setup-poetry/action.yml @@ -4,7 +4,7 @@ inputs: python-version: description: Python version to use and the Poetry installed with required: true - default: '3.10' + default: '3.11' poetry-version: description: Poetry version to set up required: true diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 76e844aaad..e1c0bf33a4 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -20,7 +20,6 @@ jobs: strategy: matrix: python-version: - - "3.10" - "3.11" - "3.12" diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index caddd23bab..a5ba51ce0e 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -20,7 +20,6 @@ jobs: strategy: matrix: python-version: - - "3.10" - "3.11" - "3.12" diff --git a/api/app.py b/api/app.py index a667a84fd6..c1acb8bd0d 100644 --- a/api/app.py +++ b/api/app.py @@ -1,6 +1,11 @@ import os import sys +python_version = sys.version_info +if not ((3, 11) <= python_version < (3, 13)): + print(f"Python 3.11 or 3.12 is required, current version is {python_version.major}.{python_version.minor}") + raise SystemExit(1) + from configs import dify_config if not dify_config.DEBUG: @@ -30,9 +35,6 @@ from models import account, dataset, model, source, task, tool, tools, web # no # DO NOT REMOVE ABOVE -if sys.version_info[:2] == (3, 10): - print("Warning: Python 3.10 will not be supported in the next version.") - warnings.simplefilter("ignore", ResourceWarning) diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 7b78f622b9..a25004be4d 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import pytz from flask_login import current_user @@ -314,7 +314,7 @@ def _get_conversation(app_model, conversation_id): raise NotFound("Conversation Not Exists.") if not conversation.read_at: - conversation.read_at = datetime.now(timezone.utc).replace(tzinfo=None) + conversation.read_at = datetime.now(UTC).replace(tzinfo=None) conversation.read_account_id = current_user.id db.session.commit() diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 2f5645852f..407f689819 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from flask_login import current_user from flask_restful import Resource, marshal_with, reqparse @@ -75,7 +75,7 @@ class AppSite(Resource): setattr(site, attr_name, value) site.updated_by = current_user.id - site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + site.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return site @@ -99,7 +99,7 @@ class AppSiteAccessTokenReset(Resource): site.code = Site.generate_code(16) site.updated_by = current_user.id - site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + site.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return site diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index be353cefac..d2aa7c903b 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -65,7 +65,7 @@ class ActivateApi(Resource): account.timezone = args["timezone"] account.interface_theme = "light" account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index d27e3353c9..f53c28e2ec 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional import requests @@ -106,7 +106,7 @@ class OAuthCallback(Resource): if account.status == AccountStatus.PENDING.value: account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() try: diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index ef1e87905a..278295ca39 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -83,7 +83,7 @@ class DataSourceApi(Resource): if action == "enable": if data_source_binding.disabled: data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(data_source_binding) db.session.commit() else: @@ -92,7 +92,7 @@ class DataSourceApi(Resource): if action == "disable": if not data_source_binding.disabled: data_source_binding.disabled = True - data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(data_source_binding) db.session.commit() else: diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index f38408525a..f20261abc2 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -1,6 +1,6 @@ import logging from argparse import ArgumentTypeError -from datetime import datetime, timezone +from datetime import UTC, datetime from flask import request from flask_login import current_user @@ -665,7 +665,7 @@ class DocumentProcessingApi(DocumentResource): raise InvalidActionError("Document not in indexing state.") document.paused_by = current_user.id - document.paused_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.paused_at = datetime.now(UTC).replace(tzinfo=None) document.is_paused = True db.session.commit() @@ -745,7 +745,7 @@ class DocumentMetadataApi(DocumentResource): document.doc_metadata[key] = value document.doc_type = doc_type - document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return {"result": "success", "message": "Document metadata updated."}, 200 @@ -787,7 +787,7 @@ class DocumentStatusApi(DocumentResource): document.enabled = True document.disabled_at = None document.disabled_by = None - document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() # Set cache to prevent indexing the same document multiple times @@ -804,9 +804,9 @@ class DocumentStatusApi(DocumentResource): raise InvalidActionError("Document already disabled.") document.enabled = False - document.disabled_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.disabled_at = datetime.now(UTC).replace(tzinfo=None) document.disabled_by = current_user.id - document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() # Set cache to prevent indexing the same document multiple times @@ -821,9 +821,9 @@ class DocumentStatusApi(DocumentResource): raise InvalidActionError("Document already archived.") document.archived = True - document.archived_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.archived_at = datetime.now(UTC).replace(tzinfo=None) document.archived_by = current_user.id - document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() if document.enabled: @@ -840,7 +840,7 @@ class DocumentStatusApi(DocumentResource): document.archived = False document.archived_at = None document.archived_by = None - document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() # Set cache to prevent indexing the same document multiple times diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index 5d8d664e41..6f7ef86d2c 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime import pandas as pd from flask import request @@ -188,7 +188,7 @@ class DatasetDocumentSegmentApi(Resource): raise InvalidActionError("Segment is already disabled.") segment.enabled = False - segment.disabled_at = datetime.now(timezone.utc).replace(tzinfo=None) + segment.disabled_at = datetime.now(UTC).replace(tzinfo=None) segment.disabled_by = current_user.id db.session.commit() diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index 125bc1af8c..85c43f8101 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from flask_login import current_user from flask_restful import reqparse @@ -46,7 +46,7 @@ class CompletionApi(InstalledAppResource): streaming = args["response_mode"] == "streaming" args["auto_generate_name"] = False - installed_app.last_used_at = datetime.now(timezone.utc).replace(tzinfo=None) + installed_app.last_used_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() try: @@ -106,7 +106,7 @@ class ChatApi(InstalledAppResource): args["auto_generate_name"] = False - installed_app.last_used_at = datetime.now(timezone.utc).replace(tzinfo=None) + installed_app.last_used_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() try: diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index d72715a38c..b60c4e176b 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from flask_login import current_user from flask_restful import Resource, inputs, marshal_with, reqparse @@ -81,7 +81,7 @@ class InstalledAppsListApi(Resource): tenant_id=current_tenant_id, app_owner_tenant_id=app.tenant_id, is_pinned=False, - last_used_at=datetime.now(timezone.utc).replace(tzinfo=None), + last_used_at=datetime.now(UTC).replace(tzinfo=None), ) db.session.add(new_installed_app) db.session.commit() diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 750f65168f..f704783cff 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -60,7 +60,7 @@ class AccountInitApi(Resource): raise InvalidInvitationCodeError() invitation_code.status = "used" - invitation_code.used_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + invitation_code.used_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) invitation_code.used_by_tenant_id = account.current_tenant_id invitation_code.used_by_account_id = account.id @@ -68,7 +68,7 @@ class AccountInitApi(Resource): account.timezone = args["timezone"] account.interface_theme = "light" account.status = "active" - account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() return {"result": "success"} diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index b935b23ed6..2128c4c53f 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from functools import wraps from typing import Optional @@ -198,7 +198,7 @@ def validate_and_get_api_token(scope=None): if not api_token: raise Unauthorized("Access token is invalid") - api_token.last_used_at = datetime.now(timezone.utc).replace(tzinfo=None) + api_token.last_used_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return api_token diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 2f5e7c7793..ead293200e 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -2,7 +2,7 @@ import json import logging import uuid from collections.abc import Mapping, Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional, Union, cast from core.agent.entities import AgentEntity, AgentToolEntity @@ -412,7 +412,7 @@ class BaseAgentRunner(AppRunner): .first() ) - db_variables.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + db_variables.updated_at = datetime.now(UTC).replace(tzinfo=None) db_variables.variables_str = json.dumps(jsonable_encoder(tool_variables.pool)) db.session.commit() db.session.close() diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index 9b72452d7a..15bd353484 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Optional from pydantic import BaseModel, Field, field_validator @@ -88,7 +88,7 @@ class PromptTemplateEntity(BaseModel): advanced_completion_prompt_template: Optional[AdvancedCompletionPromptTemplateEntity] = None -class VariableEntityType(str, Enum): +class VariableEntityType(StrEnum): TEXT_INPUT = "text-input" SELECT = "select" PARAGRAPH = "paragraph" diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py index da206f01e7..95ae798ec1 100644 --- a/api/core/app/apps/message_based_app_generator.py +++ b/api/core/app/apps/message_based_app_generator.py @@ -1,7 +1,7 @@ import json import logging from collections.abc import Generator -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional, Union from sqlalchemy import and_ @@ -200,7 +200,7 @@ class MessageBasedAppGenerator(BaseAppGenerator): db.session.commit() db.session.refresh(conversation) else: - conversation.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() message = Message( diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 69bc0d7f9e..15543638fc 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Optional from pydantic import BaseModel, field_validator @@ -11,7 +11,7 @@ from core.workflow.nodes import NodeType from core.workflow.nodes.base import BaseNodeData -class QueueEvent(str, Enum): +class QueueEvent(StrEnum): """ QueueEvent enum """ diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 9d776f6337..9229cbcc0a 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -1,7 +1,7 @@ import json import time from collections.abc import Mapping, Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Optional, Union, cast from sqlalchemy.orm import Session @@ -144,7 +144,7 @@ class WorkflowCycleManage: workflow_run.elapsed_time = time.perf_counter() - start_at workflow_run.total_tokens = total_tokens workflow_run.total_steps = total_steps - workflow_run.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow_run.finished_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() db.session.refresh(workflow_run) @@ -191,7 +191,7 @@ class WorkflowCycleManage: workflow_run.elapsed_time = time.perf_counter() - start_at workflow_run.total_tokens = total_tokens workflow_run.total_steps = total_steps - workflow_run.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow_run.finished_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() @@ -211,7 +211,7 @@ class WorkflowCycleManage: for workflow_node_execution in running_workflow_node_executions: workflow_node_execution.status = WorkflowNodeExecutionStatus.FAILED.value workflow_node_execution.error = error - workflow_node_execution.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow_node_execution.finished_at = datetime.now(UTC).replace(tzinfo=None) workflow_node_execution.elapsed_time = ( workflow_node_execution.finished_at - workflow_node_execution.created_at ).total_seconds() @@ -262,7 +262,7 @@ class WorkflowCycleManage: NodeRunMetadataKey.ITERATION_ID: event.in_iteration_id, } ) - workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow_node_execution.created_at = datetime.now(UTC).replace(tzinfo=None) session.add(workflow_node_execution) session.commit() @@ -285,7 +285,7 @@ class WorkflowCycleManage: execution_metadata = ( json.dumps(jsonable_encoder(event.execution_metadata)) if event.execution_metadata else None ) - finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + finished_at = datetime.now(UTC).replace(tzinfo=None) elapsed_time = (finished_at - event.start_at).total_seconds() db.session.query(WorkflowNodeExecution).filter(WorkflowNodeExecution.id == workflow_node_execution.id).update( @@ -329,7 +329,7 @@ class WorkflowCycleManage: inputs = WorkflowEntry.handle_special_values(event.inputs) process_data = WorkflowEntry.handle_special_values(event.process_data) outputs = WorkflowEntry.handle_special_values(event.outputs) - finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + finished_at = datetime.now(UTC).replace(tzinfo=None) elapsed_time = (finished_at - event.start_at).total_seconds() execution_metadata = ( json.dumps(jsonable_encoder(event.execution_metadata)) if event.execution_metadata else None @@ -657,7 +657,7 @@ class WorkflowCycleManage: if event.error is None else WorkflowNodeExecutionStatus.FAILED, error=None, - elapsed_time=(datetime.now(timezone.utc).replace(tzinfo=None) - event.start_at).total_seconds(), + elapsed_time=(datetime.now(UTC).replace(tzinfo=None) - event.start_at).total_seconds(), total_tokens=event.metadata.get("total_tokens", 0) if event.metadata else 0, execution_metadata=event.metadata, finished_at=int(time.time()), diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 807f09598c..d1b34db2fe 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -240,7 +240,7 @@ class ProviderConfiguration(BaseModel): if provider_record: provider_record.encrypted_config = json.dumps(credentials) provider_record.is_valid = True - provider_record.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + provider_record.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: provider_record = Provider( @@ -394,7 +394,7 @@ class ProviderConfiguration(BaseModel): if provider_model_record: provider_model_record.encrypted_config = json.dumps(credentials) provider_model_record.is_valid = True - provider_model_record.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + provider_model_record.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: provider_model_record = ProviderModel( @@ -468,7 +468,7 @@ class ProviderConfiguration(BaseModel): if model_setting: model_setting.enabled = True - model_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + model_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: model_setting = ProviderModelSetting( @@ -503,7 +503,7 @@ class ProviderConfiguration(BaseModel): if model_setting: model_setting.enabled = False - model_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + model_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: model_setting = ProviderModelSetting( @@ -570,7 +570,7 @@ class ProviderConfiguration(BaseModel): if model_setting: model_setting.load_balancing_enabled = True - model_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + model_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: model_setting = ProviderModelSetting( @@ -605,7 +605,7 @@ class ProviderConfiguration(BaseModel): if model_setting: model_setting.load_balancing_enabled = False - model_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + model_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: model_setting = ProviderModelSetting( diff --git a/api/core/file/enums.py b/api/core/file/enums.py index f4153f1676..06b99d3eb0 100644 --- a/api/core/file/enums.py +++ b/api/core/file/enums.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class FileType(str, Enum): +class FileType(StrEnum): IMAGE = "image" DOCUMENT = "document" AUDIO = "audio" @@ -16,7 +16,7 @@ class FileType(str, Enum): raise ValueError(f"No matching enum found for value '{value}'") -class FileTransferMethod(str, Enum): +class FileTransferMethod(StrEnum): REMOTE_URL = "remote_url" LOCAL_FILE = "local_file" TOOL_FILE = "tool_file" @@ -29,7 +29,7 @@ class FileTransferMethod(str, Enum): raise ValueError(f"No matching enum found for value '{value}'") -class FileBelongsTo(str, Enum): +class FileBelongsTo(StrEnum): USER = "user" ASSISTANT = "assistant" @@ -41,7 +41,7 @@ class FileBelongsTo(str, Enum): raise ValueError(f"No matching enum found for value '{value}'") -class FileAttribute(str, Enum): +class FileAttribute(StrEnum): TYPE = "type" SIZE = "size" NAME = "name" @@ -51,5 +51,5 @@ class FileAttribute(str, Enum): EXTENSION = "extension" -class ArrayFileAttribute(str, Enum): +class ArrayFileAttribute(StrEnum): LENGTH = "length" diff --git a/api/core/helper/code_executor/code_executor.py b/api/core/helper/code_executor/code_executor.py index 03c4b8d49d..011ff382ea 100644 --- a/api/core/helper/code_executor/code_executor.py +++ b/api/core/helper/code_executor/code_executor.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from enum import Enum +from enum import StrEnum from threading import Lock from typing import Any, Optional @@ -31,7 +31,7 @@ class CodeExecutionResponse(BaseModel): data: Data -class CodeLanguage(str, Enum): +class CodeLanguage(StrEnum): PYTHON3 = "python3" JINJA2 = "jinja2" JAVASCRIPT = "javascript" diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index cae539b6a7..29e161cb74 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -86,7 +86,7 @@ class IndexingRunner: except ProviderTokenNotInitError as e: dataset_document.indexing_status = "error" dataset_document.error = str(e.description) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() except ObjectDeletedError: logging.warning("Document deleted, document id: {}".format(dataset_document.id)) @@ -94,7 +94,7 @@ class IndexingRunner: logging.exception("consume document failed") dataset_document.indexing_status = "error" dataset_document.error = str(e) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() def run_in_splitting_status(self, dataset_document: DatasetDocument): @@ -142,13 +142,13 @@ class IndexingRunner: except ProviderTokenNotInitError as e: dataset_document.indexing_status = "error" dataset_document.error = str(e.description) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() except Exception as e: logging.exception("consume document failed") dataset_document.indexing_status = "error" dataset_document.error = str(e) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() def run_in_indexing_status(self, dataset_document: DatasetDocument): @@ -200,13 +200,13 @@ class IndexingRunner: except ProviderTokenNotInitError as e: dataset_document.indexing_status = "error" dataset_document.error = str(e.description) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() except Exception as e: logging.exception("consume document failed") dataset_document.indexing_status = "error" dataset_document.error = str(e) - dataset_document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() def indexing_estimate( @@ -372,7 +372,7 @@ class IndexingRunner: after_indexing_status="splitting", extra_update_params={ DatasetDocument.word_count: sum(len(text_doc.page_content) for text_doc in text_docs), - DatasetDocument.parsing_completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DatasetDocument.parsing_completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), }, ) @@ -464,7 +464,7 @@ class IndexingRunner: doc_store.add_documents(documents) # update document status to indexing - cur_time = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + cur_time = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) self._update_document_index_status( document_id=dataset_document.id, after_indexing_status="indexing", @@ -479,7 +479,7 @@ class IndexingRunner: dataset_document_id=dataset_document.id, update_params={ DocumentSegment.status: "indexing", - DocumentSegment.indexing_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.indexing_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), }, ) @@ -680,7 +680,7 @@ class IndexingRunner: after_indexing_status="completed", extra_update_params={ DatasetDocument.tokens: tokens, - DatasetDocument.completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DatasetDocument.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), DatasetDocument.indexing_latency: indexing_end_at - indexing_start_at, DatasetDocument.error: None, }, @@ -705,7 +705,7 @@ class IndexingRunner: { DocumentSegment.status: "completed", DocumentSegment.enabled: True, - DocumentSegment.completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), } ) @@ -738,7 +738,7 @@ class IndexingRunner: { DocumentSegment.status: "completed", DocumentSegment.enabled: True, - DocumentSegment.completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), } ) @@ -849,7 +849,7 @@ class IndexingRunner: doc_store.add_documents(documents) # update document status to indexing - cur_time = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + cur_time = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) self._update_document_index_status( document_id=dataset_document.id, after_indexing_status="indexing", @@ -864,7 +864,7 @@ class IndexingRunner: dataset_document_id=dataset_document.id, update_params={ DocumentSegment.status: "indexing", - DocumentSegment.indexing_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.indexing_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), }, ) pass diff --git a/api/core/model_runtime/entities/message_entities.py b/api/core/model_runtime/entities/message_entities.py index e86fb37522..f2870209bb 100644 --- a/api/core/model_runtime/entities/message_entities.py +++ b/api/core/model_runtime/entities/message_entities.py @@ -1,6 +1,6 @@ from abc import ABC from collections.abc import Sequence -from enum import Enum +from enum import Enum, StrEnum from typing import Literal, Optional from pydantic import BaseModel, Field, field_validator @@ -49,7 +49,7 @@ class PromptMessageFunction(BaseModel): function: PromptMessageTool -class PromptMessageContentType(str, Enum): +class PromptMessageContentType(StrEnum): """ Enum class for prompt message content type. """ @@ -95,7 +95,7 @@ class ImagePromptMessageContent(PromptMessageContent): Model class for image prompt message content. """ - class DETAIL(str, Enum): + class DETAIL(StrEnum): LOW = "low" HIGH = "high" diff --git a/api/core/model_runtime/entities/model_entities.py b/api/core/model_runtime/entities/model_entities.py index 4e1ce17533..edc6eac517 100644 --- a/api/core/model_runtime/entities/model_entities.py +++ b/api/core/model_runtime/entities/model_entities.py @@ -1,5 +1,5 @@ from decimal import Decimal -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Optional from pydantic import BaseModel, ConfigDict @@ -92,7 +92,7 @@ class ModelFeature(Enum): AUDIO = "audio" -class DefaultParameterName(str, Enum): +class DefaultParameterName(StrEnum): """ Enum class for parameter template variable. """ diff --git a/api/core/ops/entities/trace_entity.py b/api/core/ops/entities/trace_entity.py index 256595286f..71ff03b6ef 100644 --- a/api/core/ops/entities/trace_entity.py +++ b/api/core/ops/entities/trace_entity.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import StrEnum from typing import Any, Optional, Union from pydantic import BaseModel, ConfigDict, field_validator @@ -122,7 +122,7 @@ trace_info_info_map = { } -class TraceTaskName(str, Enum): +class TraceTaskName(StrEnum): CONVERSATION_TRACE = "conversation" WORKFLOW_TRACE = "workflow" MESSAGE_TRACE = "message" diff --git a/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py b/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py index 447b799f1f..f486da3a6d 100644 --- a/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py +++ b/api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import StrEnum from typing import Any, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -39,7 +39,7 @@ def validate_input_output(v, field_name): return v -class LevelEnum(str, Enum): +class LevelEnum(StrEnum): DEBUG = "DEBUG" WARNING = "WARNING" ERROR = "ERROR" @@ -178,7 +178,7 @@ class LangfuseSpan(BaseModel): return validate_input_output(v, field_name) -class UnitEnum(str, Enum): +class UnitEnum(StrEnum): CHARACTERS = "CHARACTERS" TOKENS = "TOKENS" SECONDS = "SECONDS" diff --git a/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py b/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py index 16c76f363c..99221d669b 100644 --- a/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py +++ b/api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import StrEnum from typing import Any, Optional, Union from pydantic import BaseModel, Field, field_validator @@ -8,7 +8,7 @@ from pydantic_core.core_schema import ValidationInfo from core.ops.utils import replace_text_with_content -class LangSmithRunType(str, Enum): +class LangSmithRunType(StrEnum): tool = "tool" chain = "chain" llm = "llm" diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py index 5a3481b963..93dd92f188 100644 --- a/api/core/prompt/simple_prompt_transform.py +++ b/api/core/prompt/simple_prompt_transform.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from core.file.models import File -class ModelMode(str, enum.Enum): +class ModelMode(enum.StrEnum): COMPLETION = "completion" CHAT = "chat" diff --git a/api/core/rag/datasource/keyword/keyword_type.py b/api/core/rag/datasource/keyword/keyword_type.py index d6deba3fb0..d845c7111d 100644 --- a/api/core/rag/datasource/keyword/keyword_type.py +++ b/api/core/rag/datasource/keyword/keyword_type.py @@ -1,5 +1,5 @@ -from enum import Enum +from enum import StrEnum -class KeyWordType(str, Enum): +class KeyWordType(StrEnum): JIEBA = "jieba" diff --git a/api/core/rag/datasource/vdb/vector_type.py b/api/core/rag/datasource/vdb/vector_type.py index 8e53e3ae84..05183c0371 100644 --- a/api/core/rag/datasource/vdb/vector_type.py +++ b/api/core/rag/datasource/vdb/vector_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class VectorType(str, Enum): +class VectorType(StrEnum): ANALYTICDB = "analyticdb" CHROMA = "chroma" MILVUS = "milvus" diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index 313bdce48b..b23da1113e 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -114,10 +114,10 @@ class WordExtractor(BaseExtractor): mime_type=mime_type or "", created_by=self.user_id, created_by_role=CreatedByRole.ACCOUNT, - created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + created_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), used=True, used_by=self.user_id, - used_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + used_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), ) db.session.add(upload_file) diff --git a/api/core/rag/rerank/rerank_type.py b/api/core/rag/rerank/rerank_type.py index d71eb2daa8..b2d1314654 100644 --- a/api/core/rag/rerank/rerank_type.py +++ b/api/core/rag/rerank/rerank_type.py @@ -1,6 +1,6 @@ -from enum import Enum +from enum import StrEnum -class RerankMode(str, Enum): +class RerankMode(StrEnum): RERANKING_MODEL = "reranking_model" WEIGHTED_SCORE = "weighted_score" diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index d8637fd2cb..4fc383f91b 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Optional, Union, cast from pydantic import BaseModel, Field, field_validator @@ -137,7 +137,7 @@ class ToolParameterOption(BaseModel): class ToolParameter(BaseModel): - class ToolParameterType(str, Enum): + class ToolParameterType(StrEnum): STRING = "string" NUMBER = "number" BOOLEAN = "boolean" diff --git a/api/core/tools/provider/builtin/time/tools/current_time.py b/api/core/tools/provider/builtin/time/tools/current_time.py index cc38739c16..6464bb6602 100644 --- a/api/core/tools/provider/builtin/time/tools/current_time.py +++ b/api/core/tools/provider/builtin/time/tools/current_time.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Union from pytz import timezone as pytz_timezone @@ -20,7 +20,7 @@ class CurrentTimeTool(BuiltinTool): tz = tool_parameters.get("timezone", "UTC") fm = tool_parameters.get("format") or "%Y-%m-%d %H:%M:%S %Z" if tz == "UTC": - return self.create_text_message(f"{datetime.now(timezone.utc).strftime(fm)}") + return self.create_text_message(f"{datetime.now(UTC).strftime(fm)}") try: tz = pytz_timezone(tz) diff --git a/api/core/tools/tool/tool.py b/api/core/tools/tool/tool.py index f17a26dfbd..38a05ccf91 100644 --- a/api/core/tools/tool/tool.py +++ b/api/core/tools/tool/tool.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from copy import deepcopy -from enum import Enum +from enum import Enum, StrEnum from typing import TYPE_CHECKING, Any, Optional, Union from pydantic import BaseModel, ConfigDict, field_validator @@ -62,7 +62,7 @@ class Tool(BaseModel, ABC): def __init__(self, **data: Any): super().__init__(**data) - class VariableKey(str, Enum): + class VariableKey(StrEnum): IMAGE = "image" DOCUMENT = "document" VIDEO = "video" diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 01a1fe330f..dbd97c8151 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from copy import deepcopy -from datetime import datetime, timezone +from datetime import UTC, datetime from mimetypes import guess_type from typing import Any, Optional, Union @@ -158,7 +158,7 @@ class ToolEngine: """ Invoke the tool with the given arguments. """ - started_at = datetime.now(timezone.utc) + started_at = datetime.now(UTC) meta = ToolInvokeMeta( time_cost=0.0, error=None, @@ -176,7 +176,7 @@ class ToolEngine: meta.error = str(e) raise ToolEngineInvokeError(meta) finally: - ended_at = datetime.now(timezone.utc) + ended_at = datetime.now(UTC) meta.time_cost = (ended_at - started_at).total_seconds() return meta, response diff --git a/api/core/variables/types.py b/api/core/variables/types.py index 53c2e8a3aa..af6a2a2937 100644 --- a/api/core/variables/types.py +++ b/api/core/variables/types.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class SegmentType(str, Enum): +class SegmentType(StrEnum): NONE = "none" NUMBER = "number" STRING = "string" diff --git a/api/core/workflow/entities/node_entities.py b/api/core/workflow/entities/node_entities.py index a747266661..1ac64e94ef 100644 --- a/api/core/workflow/entities/node_entities.py +++ b/api/core/workflow/entities/node_entities.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from enum import Enum +from enum import StrEnum from typing import Any, Optional from pydantic import BaseModel @@ -8,7 +8,7 @@ from core.model_runtime.entities.llm_entities import LLMUsage from models.workflow import WorkflowNodeExecutionStatus -class NodeRunMetadataKey(str, Enum): +class NodeRunMetadataKey(StrEnum): """ Node Run Metadata Key. """ diff --git a/api/core/workflow/enums.py b/api/core/workflow/enums.py index 213ed57f57..9642efa1a5 100644 --- a/api/core/workflow/enums.py +++ b/api/core/workflow/enums.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class SystemVariableKey(str, Enum): +class SystemVariableKey(StrEnum): """ System Variables. """ diff --git a/api/core/workflow/graph_engine/entities/runtime_route_state.py b/api/core/workflow/graph_engine/entities/runtime_route_state.py index bb24b51112..baeec9bf01 100644 --- a/api/core/workflow/graph_engine/entities/runtime_route_state.py +++ b/api/core/workflow/graph_engine/entities/runtime_route_state.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from typing import Optional @@ -63,7 +63,7 @@ class RouteNodeState(BaseModel): raise Exception(f"Invalid route status {run_result.status}") self.node_run_result = run_result - self.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + self.finished_at = datetime.now(UTC).replace(tzinfo=None) class RuntimeRouteState(BaseModel): @@ -81,7 +81,7 @@ class RuntimeRouteState(BaseModel): :param node_id: node id """ - state = RouteNodeState(node_id=node_id, start_at=datetime.now(timezone.utc).replace(tzinfo=None)) + state = RouteNodeState(node_id=node_id, start_at=datetime.now(UTC).replace(tzinfo=None)) self.node_state_mapping[state.id] = state return state diff --git a/api/core/workflow/nodes/enums.py b/api/core/workflow/nodes/enums.py index 208144655b..9e9e52910e 100644 --- a/api/core/workflow/nodes/enums.py +++ b/api/core/workflow/nodes/enums.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class NodeType(str, Enum): +class NodeType(StrEnum): START = "start" END = "end" ANSWER = "answer" diff --git a/api/core/workflow/nodes/iteration/entities.py b/api/core/workflow/nodes/iteration/entities.py index ebcb6f82fb..7a489dd725 100644 --- a/api/core/workflow/nodes/iteration/entities.py +++ b/api/core/workflow/nodes/iteration/entities.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import StrEnum from typing import Any, Optional from pydantic import Field @@ -6,7 +6,7 @@ from pydantic import Field from core.workflow.nodes.base import BaseIterationNodeData, BaseIterationState, BaseNodeData -class ErrorHandleMode(str, Enum): +class ErrorHandleMode(StrEnum): TERMINATED = "terminated" CONTINUE_ON_ERROR = "continue-on-error" REMOVE_ABNORMAL_OUTPUT = "remove-abnormal-output" diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index d5428f0286..22f242a42f 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -2,7 +2,7 @@ import logging import uuid from collections.abc import Generator, Mapping, Sequence from concurrent.futures import Future, wait -from datetime import datetime, timezone +from datetime import UTC, datetime from queue import Empty, Queue from typing import TYPE_CHECKING, Any, Optional, cast @@ -135,7 +135,7 @@ class IterationNode(BaseNode[IterationNodeData]): thread_pool_id=self.thread_pool_id, ) - start_at = datetime.now(timezone.utc).replace(tzinfo=None) + start_at = datetime.now(UTC).replace(tzinfo=None) yield IterationRunStartedEvent( iteration_id=self.id, @@ -367,7 +367,7 @@ class IterationNode(BaseNode[IterationNodeData]): """ run single iteration """ - iter_start_at = datetime.now(timezone.utc).replace(tzinfo=None) + iter_start_at = datetime.now(UTC).replace(tzinfo=None) try: rst = graph_engine.run() @@ -440,7 +440,7 @@ class IterationNode(BaseNode[IterationNodeData]): variable_pool.add([self.node_id, "index"], next_index) if next_index < len(iterator_list_value): variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) - duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() + duration = (datetime.now(UTC).replace(tzinfo=None) - iter_start_at).total_seconds() iter_run_map[iteration_run_id] = duration yield IterationRunNextEvent( iteration_id=self.id, @@ -461,7 +461,7 @@ class IterationNode(BaseNode[IterationNodeData]): if next_index < len(iterator_list_value): variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) - duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() + duration = (datetime.now(UTC).replace(tzinfo=None) - iter_start_at).total_seconds() iter_run_map[iteration_run_id] = duration yield IterationRunNextEvent( iteration_id=self.id, @@ -503,7 +503,7 @@ class IterationNode(BaseNode[IterationNodeData]): if next_index < len(iterator_list_value): variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) - duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() + duration = (datetime.now(UTC).replace(tzinfo=None) - iter_start_at).total_seconds() iter_run_map[iteration_run_id] = duration yield IterationRunNextEvent( iteration_id=self.id, diff --git a/api/core/workflow/nodes/variable_assigner/node_data.py b/api/core/workflow/nodes/variable_assigner/node_data.py index 70ae29d45f..474ecefe76 100644 --- a/api/core/workflow/nodes/variable_assigner/node_data.py +++ b/api/core/workflow/nodes/variable_assigner/node_data.py @@ -1,11 +1,11 @@ from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import Optional from core.workflow.nodes.base import BaseNodeData -class WriteMode(str, Enum): +class WriteMode(StrEnum): OVER_WRITE = "over-write" APPEND = "append" CLEAR = "clear" diff --git a/api/events/event_handlers/create_document_index.py b/api/events/event_handlers/create_document_index.py index 5af45e1e50..24fa013697 100644 --- a/api/events/event_handlers/create_document_index.py +++ b/api/events/event_handlers/create_document_index.py @@ -33,7 +33,7 @@ def handle(sender, **kwargs): raise NotFound("Document not found") document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) documents.append(document) db.session.add(document) db.session.commit() diff --git a/api/events/event_handlers/update_provider_last_used_at_when_message_created.py b/api/events/event_handlers/update_provider_last_used_at_when_message_created.py index a80572c0de..f225ef8e88 100644 --- a/api/events/event_handlers/update_provider_last_used_at_when_message_created.py +++ b/api/events/event_handlers/update_provider_last_used_at_when_message_created.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ChatAppGenerateEntity from events.message_event import message_was_created @@ -17,5 +17,5 @@ def handle(sender, **kwargs): db.session.query(Provider).filter( Provider.tenant_id == application_generate_entity.app_config.tenant_id, Provider.provider_name == application_generate_entity.model_conf.provider, - ).update({"last_used": datetime.now(timezone.utc).replace(tzinfo=None)}) + ).update({"last_used": datetime.now(UTC).replace(tzinfo=None)}) db.session.commit() diff --git a/api/extensions/storage/azure_blob_storage.py b/api/extensions/storage/azure_blob_storage.py index 11a7544274..b26caa8671 100644 --- a/api/extensions/storage/azure_blob_storage.py +++ b/api/extensions/storage/azure_blob_storage.py @@ -1,5 +1,5 @@ from collections.abc import Generator -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from azure.storage.blob import AccountSasPermissions, BlobServiceClient, ResourceTypes, generate_account_sas @@ -67,7 +67,7 @@ class AzureBlobStorage(BaseStorage): account_key=self.account_key, resource_types=ResourceTypes(service=True, container=True, object=True), permission=AccountSasPermissions(read=True, write=True, delete=True, list=True, add=True, create=True), - expiry=datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=1), + expiry=datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1), ) redis_client.set(cache_key, sas_token, ex=3000) return BlobServiceClient(account_url=self.account_url, credential=sas_token) diff --git a/api/extensions/storage/storage_type.py b/api/extensions/storage/storage_type.py index 415bf251f6..e7fa405afa 100644 --- a/api/extensions/storage/storage_type.py +++ b/api/extensions/storage/storage_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class StorageType(str, Enum): +class StorageType(StrEnum): ALIYUN_OSS = "aliyun-oss" AZURE_BLOB = "azure-blob" BAIDU_OBS = "baidu-obs" diff --git a/api/libs/oauth_data_source.py b/api/libs/oauth_data_source.py index e747ea97ad..53aa0f2d45 100644 --- a/api/libs/oauth_data_source.py +++ b/api/libs/oauth_data_source.py @@ -70,7 +70,7 @@ class NotionOAuth(OAuthDataSource): if data_source_binding: data_source_binding.source_info = source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: new_data_source_binding = DataSourceOauthBinding( @@ -106,7 +106,7 @@ class NotionOAuth(OAuthDataSource): if data_source_binding: data_source_binding.source_info = source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: new_data_source_binding = DataSourceOauthBinding( @@ -141,7 +141,7 @@ class NotionOAuth(OAuthDataSource): } data_source_binding.source_info = new_source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() else: raise ValueError("Data source binding not found") diff --git a/api/models/account.py b/api/models/account.py index 60b4f11aad..6684e8dd6e 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -8,7 +8,7 @@ from extensions.ext_database import db from .types import StringUUID -class AccountStatus(str, enum.Enum): +class AccountStatus(enum.StrEnum): PENDING = "pending" UNINITIALIZED = "uninitialized" ACTIVE = "active" @@ -121,12 +121,12 @@ class Account(UserMixin, db.Model): return self._current_tenant.current_role == TenantAccountRole.DATASET_OPERATOR -class TenantStatus(str, enum.Enum): +class TenantStatus(enum.StrEnum): NORMAL = "normal" ARCHIVE = "archive" -class TenantAccountRole(str, enum.Enum): +class TenantAccountRole(enum.StrEnum): OWNER = "owner" ADMIN = "admin" EDITOR = "editor" diff --git a/api/models/dataset.py b/api/models/dataset.py index a8b2c419d1..8ab957e875 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -23,7 +23,7 @@ from .model import App, Tag, TagBinding, UploadFile from .types import StringUUID -class DatasetPermissionEnum(str, enum.Enum): +class DatasetPermissionEnum(enum.StrEnum): ONLY_ME = "only_me" ALL_TEAM = "all_team_members" PARTIAL_TEAM = "partial_members" diff --git a/api/models/enums.py b/api/models/enums.py index a83d35e042..7b9500ebe4 100644 --- a/api/models/enums.py +++ b/api/models/enums.py @@ -1,16 +1,16 @@ -from enum import Enum +from enum import StrEnum -class CreatedByRole(str, Enum): +class CreatedByRole(StrEnum): ACCOUNT = "account" END_USER = "end_user" -class UserFrom(str, Enum): +class UserFrom(StrEnum): ACCOUNT = "account" END_USER = "end-user" -class WorkflowRunTriggeredFrom(str, Enum): +class WorkflowRunTriggeredFrom(StrEnum): DEBUGGING = "debugging" APP_RUN = "app-run" diff --git a/api/models/model.py b/api/models/model.py index 4562d8cf29..03b8e0bea5 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -3,7 +3,7 @@ import re import uuid from collections.abc import Mapping from datetime import datetime -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Literal, Optional import sqlalchemy as sa @@ -32,7 +32,7 @@ class DifySetup(db.Model): setup_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) -class AppMode(str, Enum): +class AppMode(StrEnum): COMPLETION = "completion" WORKFLOW = "workflow" CHAT = "chat" diff --git a/api/models/task.py b/api/models/task.py index 57b147c78d..5d89ff85ac 100644 --- a/api/models/task.py +++ b/api/models/task.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from celery import states @@ -16,8 +16,8 @@ class CeleryTask(db.Model): result = db.Column(db.PickleType, nullable=True) date_done = db.Column( db.DateTime, - default=lambda: datetime.now(timezone.utc).replace(tzinfo=None), - onupdate=lambda: datetime.now(timezone.utc).replace(tzinfo=None), + default=lambda: datetime.now(UTC).replace(tzinfo=None), + onupdate=lambda: datetime.now(UTC).replace(tzinfo=None), nullable=True, ) traceback = db.Column(db.Text, nullable=True) @@ -37,4 +37,4 @@ class CeleryTaskSet(db.Model): id = db.Column(db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True) taskset_id = db.Column(db.String(155), unique=True) result = db.Column(db.PickleType, nullable=True) - date_done = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc).replace(tzinfo=None), nullable=True) + date_done = db.Column(db.DateTime, default=lambda: datetime.now(UTC).replace(tzinfo=None), nullable=True) diff --git a/api/models/workflow.py b/api/models/workflow.py index c6b3000083..5b0617828d 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping, Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from typing import Any, Optional, Union @@ -108,7 +108,7 @@ class Workflow(db.Model): ) updated_by: Mapped[Optional[str]] = mapped_column(StringUUID) updated_at: Mapped[datetime] = mapped_column( - sa.DateTime, nullable=False, default=datetime.now(tz=timezone.utc), server_onupdate=func.current_timestamp() + sa.DateTime, nullable=False, default=datetime.now(tz=UTC), server_onupdate=func.current_timestamp() ) _environment_variables: Mapped[str] = mapped_column( "environment_variables", db.Text, nullable=False, server_default="{}" diff --git a/api/poetry.lock b/api/poetry.lock index 944b6c8316..cdfc293405 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -114,7 +114,6 @@ files = [ [package.dependencies] aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" @@ -483,10 +482,8 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] @@ -519,9 +516,6 @@ files = [ {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] @@ -951,6 +945,10 @@ files = [ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, @@ -963,8 +961,14 @@ files = [ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, @@ -975,8 +979,24 @@ files = [ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, @@ -986,6 +1006,10 @@ files = [ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, @@ -997,6 +1021,10 @@ files = [ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, @@ -1009,6 +1037,10 @@ files = [ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, @@ -1021,6 +1053,10 @@ files = [ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, @@ -1092,10 +1128,8 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} packaging = ">=19.1" pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] @@ -2036,9 +2070,6 @@ files = [ {file = "dataclass_wizard-0.28.0-py2.py3-none-any.whl", hash = "sha256:996fa46475b9192a48a057c34f04597bc97be5bc2f163b99cb1de6f778ca1f7f"}, ] -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version == \"3.9\" or python_version == \"3.10\""} - [package.extras] dev = ["Sphinx (==7.4.7)", "Sphinx (==8.1.3)", "bump2version (==1.0.1)", "coverage (>=6.2)", "dataclass-factory (==2.16)", "dataclass-wizard[toml]", "dataclasses-json (==0.6.7)", "flake8 (>=3)", "jsons (==1.6.3)", "pip (>=21.3.1)", "pytest (==8.3.3)", "pytest-cov (==6.0.0)", "pytest-mock (>=3.6.1)", "pytimeparse (==1.1.8)", "sphinx-issues (==5.0.0)", "tomli (>=2,<3)", "tomli (>=2,<3)", "tomli-w (>=1,<2)", "tox (==4.23.2)", "twine (==5.1.1)", "watchdog[watchmedo] (==6.0.0)", "wheel (==0.45.0)"] timedelta = ["pytimeparse (>=1.1.7)"] @@ -2409,20 +2440,6 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "faker" version = "32.1.0" @@ -3210,14 +3227,8 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" -grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] +grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = ">=1.22.3,<2.0.0dev" protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -5550,9 +5561,6 @@ files = [ {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - [[package]] name = "multiprocess" version = "0.70.17" @@ -6410,7 +6418,6 @@ bottleneck = {version = ">=1.3.6", optional = true, markers = "extra == \"perfor numba = {version = ">=0.56.4", optional = true, markers = "extra == \"performance\""} numexpr = {version = ">=2.8.4", optional = true, markers = "extra == \"performance\""} numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] @@ -6694,7 +6701,6 @@ files = [ deprecation = ">=2.1.0,<3.0.0" httpx = {version = ">=0.26,<0.28", extras = ["http2"]} pydantic = ">=1.9,<3.0" -strenum = {version = ">=0.4.9,<0.5.0", markers = "python_version < \"3.11\""} [[package]] name = "posthog" @@ -7426,9 +7432,6 @@ files = [ {file = "pypdf-5.1.0.tar.gz", hash = "sha256:425a129abb1614183fd1aca6982f650b47f8026867c0ce7c4b9f281c443d2740"}, ] -[package.dependencies] -typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - [package.extras] crypto = ["cryptography"] cryptodome = ["PyCryptodome"] @@ -7517,11 +7520,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -7559,7 +7560,6 @@ files = [ [package.dependencies] pytest = ">=8.3.3" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] @@ -8377,7 +8377,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -9214,22 +9213,6 @@ httpx = {version = ">=0.26,<0.28", extras = ["http2"]} python-dateutil = ">=2.8.2,<3.0.0" typing-extensions = ">=4.2.0,<5.0.0" -[[package]] -name = "strenum" -version = "0.4.15" -description = "An Enum that inherits from str." -optional = false -python-versions = "*" -files = [ - {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, - {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, -] - -[package.extras] -docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] -release = ["twine"] -test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] - [[package]] name = "strictyaml" version = "1.7.3" @@ -9636,17 +9619,6 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -[[package]] -name = "tomli" -version = "2.1.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, -] - [[package]] name = "tos" version = "2.7.2" @@ -10067,7 +10039,6 @@ h11 = ">=0.8" httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} @@ -11050,5 +11021,5 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" -python-versions = ">=3.10,<3.13" -content-hash = "152b8e11ceffaa482fee6920b7991f52427aa1ffed75614e78ec1065dd5f6898" +python-versions = ">=3.11,<3.13" +content-hash = "75175c3427d13c41d84374ff2bb6f5c6cb157e3783107f9d22fad15c9eb8c177" diff --git a/api/pyproject.toml b/api/pyproject.toml index f02c063182..6dbb16d820 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,5 +1,5 @@ [project] -requires-python = ">=3.10,<3.13" +requires-python = ">=3.11,<3.13" [build-system] requires = ["poetry-core"] @@ -163,7 +163,7 @@ pydantic-settings = "~2.6.0" pydantic_extra_types = "~2.9.0" pyjwt = "~2.8.0" pypdfium2 = "~4.17.0" -python = ">=3.10,<3.13" +python = ">=3.11,<3.13" python-docx = "~1.1.0" python-dotenv = "1.0.0" pyyaml = "~6.0.1" diff --git a/api/services/account_service.py b/api/services/account_service.py index 3d7f9e7dfb..aeb373bb26 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -4,7 +4,7 @@ import logging import random import secrets import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from hashlib import sha256 from typing import Any, Optional @@ -115,15 +115,15 @@ class AccountService: available_ta.current = True db.session.commit() - if datetime.now(timezone.utc).replace(tzinfo=None) - account.last_active_at > timedelta(minutes=10): - account.last_active_at = datetime.now(timezone.utc).replace(tzinfo=None) + if datetime.now(UTC).replace(tzinfo=None) - account.last_active_at > timedelta(minutes=10): + account.last_active_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return account @staticmethod def get_account_jwt_token(account: Account) -> str: - exp_dt = datetime.now(timezone.utc) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES) + exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES) exp = int(exp_dt.timestamp()) payload = { "user_id": account.id, @@ -160,7 +160,7 @@ class AccountService: if account.status == AccountStatus.PENDING.value: account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() @@ -253,7 +253,7 @@ class AccountService: # If it exists, update the record account_integrate.open_id = open_id account_integrate.encrypted_token = "" # todo - account_integrate.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + account_integrate.updated_at = datetime.now(UTC).replace(tzinfo=None) else: # If it does not exist, create a new record account_integrate = AccountIntegrate( @@ -288,7 +288,7 @@ class AccountService: @staticmethod def update_login_info(account: Account, *, ip_address: str) -> None: """Update last login time and ip""" - account.last_login_at = datetime.now(timezone.utc).replace(tzinfo=None) + account.last_login_at = datetime.now(UTC).replace(tzinfo=None) account.last_login_ip = ip_address db.session.add(account) db.session.commit() @@ -765,7 +765,7 @@ class RegisterService: ) account.last_login_ip = ip_address - account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.now(UTC).replace(tzinfo=None) TenantService.create_owner_tenant_if_not_exist(account=account, is_setup=True) @@ -805,7 +805,7 @@ class RegisterService: is_setup=is_setup, ) account.status = AccountStatus.ACTIVE.value if not status else status.value - account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) + account.initialized_at = datetime.now(UTC).replace(tzinfo=None) if open_id is not None or provider is not None: AccountService.link_account_integrate(provider, open_id, account) diff --git a/api/services/annotation_service.py b/api/services/annotation_service.py index 915d37ec03..f45c21cb18 100644 --- a/api/services/annotation_service.py +++ b/api/services/annotation_service.py @@ -429,7 +429,7 @@ class AppAnnotationService: raise NotFound("App annotation not found") annotation_setting.score_threshold = args["score_threshold"] annotation_setting.updated_user_id = current_user.id - annotation_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + annotation_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(annotation_setting) db.session.commit() diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 02a3d0a9a1..b3c919dbd9 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -1,6 +1,6 @@ import logging import uuid -from enum import Enum +from enum import StrEnum from typing import Optional from uuid import uuid4 @@ -25,12 +25,12 @@ IMPORT_INFO_REDIS_EXPIRY = 180 # 3 minutes CURRENT_DSL_VERSION = "0.1.3" -class ImportMode(str, Enum): +class ImportMode(StrEnum): YAML_CONTENT = "yaml-content" YAML_URL = "yaml-url" -class ImportStatus(str, Enum): +class ImportStatus(StrEnum): COMPLETED = "completed" COMPLETED_WITH_WARNINGS = "completed-with-warnings" PENDING = "pending" diff --git a/api/services/app_service.py b/api/services/app_service.py index 26e3d004b6..8d8ba735ec 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -1,6 +1,6 @@ import json import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import cast from flask_login import current_user @@ -223,7 +223,7 @@ class AppService: app.icon_background = args.get("icon_background") app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) app.updated_by = current_user.id - app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() if app.max_active_requests is not None: @@ -240,7 +240,7 @@ class AppService: """ app.name = name app.updated_by = current_user.id - app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return app @@ -256,7 +256,7 @@ class AppService: app.icon = icon app.icon_background = icon_background app.updated_by = current_user.id - app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return app @@ -273,7 +273,7 @@ class AppService: app.enable_site = enable_site app.updated_by = current_user.id - app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return app @@ -290,7 +290,7 @@ class AppService: app.enable_api = enable_api app.updated_by = current_user.id - app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return app diff --git a/api/services/auth/auth_type.py b/api/services/auth/auth_type.py index 2d6e901447..2e1946841f 100644 --- a/api/services/auth/auth_type.py +++ b/api/services/auth/auth_type.py @@ -1,6 +1,6 @@ -from enum import Enum +from enum import StrEnum -class AuthType(str, Enum): +class AuthType(StrEnum): FIRECRAWL = "firecrawl" JINA = "jinareader" diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index f9e41988c0..f3e76d3300 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional, Union from sqlalchemy import asc, desc, or_ @@ -104,7 +104,7 @@ class ConversationService: return cls.auto_generate_name(app_model, conversation) else: conversation.name = name - conversation.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return conversation diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 806dbdf8c5..d38729f31e 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -600,7 +600,7 @@ class DocumentService: # update document to be paused document.is_paused = True document.paused_by = current_user.id - document.paused_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.paused_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(document) db.session.commit() @@ -1072,7 +1072,7 @@ class DocumentService: document.parsing_completed_at = None document.cleaning_completed_at = None document.splitting_completed_at = None - document.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) document.created_from = created_from document.doc_form = document_data["doc_form"] db.session.add(document) @@ -1409,8 +1409,8 @@ class SegmentService: word_count=len(content), tokens=tokens, status="completed", - indexing_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), - completed_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + indexing_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), + completed_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), created_by=current_user.id, ) if document.doc_form == "qa_model": @@ -1429,7 +1429,7 @@ class SegmentService: except Exception as e: logging.exception("create segment index failed") segment_document.enabled = False - segment_document.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment_document.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment_document.status = "error" segment_document.error = str(e) db.session.commit() @@ -1481,8 +1481,8 @@ class SegmentService: word_count=len(content), tokens=tokens, status="completed", - indexing_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), - completed_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + indexing_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), + completed_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), created_by=current_user.id, ) if document.doc_form == "qa_model": @@ -1508,7 +1508,7 @@ class SegmentService: logging.exception("create segment index failed") for segment_document in segment_data_list: segment_document.enabled = False - segment_document.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment_document.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment_document.status = "error" segment_document.error = str(e) db.session.commit() @@ -1526,7 +1526,7 @@ class SegmentService: if segment.enabled != action: if not action: segment.enabled = action - segment.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.disabled_by = current_user.id db.session.add(segment) db.session.commit() @@ -1585,10 +1585,10 @@ class SegmentService: segment.word_count = len(content) segment.tokens = tokens segment.status = "completed" - segment.indexing_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) - segment.completed_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.indexing_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + segment.completed_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.updated_by = current_user.id - segment.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.enabled = True segment.disabled_at = None segment.disabled_by = None @@ -1608,7 +1608,7 @@ class SegmentService: except Exception as e: logging.exception("update segment index failed") segment.enabled = False - segment.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.status = "error" segment.error = str(e) db.session.commit() diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index 98e5d9face..7e3cd87f1e 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -1,6 +1,6 @@ import json from copy import deepcopy -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Optional, Union import httpx @@ -99,7 +99,7 @@ class ExternalDatasetService: external_knowledge_api.description = args.get("description", "") external_knowledge_api.settings = json.dumps(args.get("settings"), ensure_ascii=False) external_knowledge_api.updated_by = user_id - external_knowledge_api.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + external_knowledge_api.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() return external_knowledge_api diff --git a/api/services/feature_service.py b/api/services/feature_service.py index d0b04628cf..c2203b167d 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import StrEnum from pydantic import BaseModel, ConfigDict @@ -22,7 +22,7 @@ class LimitationModel(BaseModel): limit: int = 0 -class LicenseStatus(str, Enum): +class LicenseStatus(StrEnum): NONE = "none" INACTIVE = "inactive" ACTIVE = "active" diff --git a/api/services/file_service.py b/api/services/file_service.py index 976111502c..b12b95ca13 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -77,7 +77,7 @@ class FileService: mime_type=mimetype, created_by_role=(CreatedByRole.ACCOUNT if isinstance(user, Account) else CreatedByRole.END_USER), created_by=user.id, - created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + created_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), used=False, hash=hashlib.sha3_256(content).hexdigest(), source_url=source_url, @@ -123,10 +123,10 @@ class FileService: mime_type="text/plain", created_by=current_user.id, created_by_role=CreatedByRole.ACCOUNT, - created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + created_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), used=True, used_by=current_user.id, - used_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + used_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), ) db.session.add(upload_file) diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index e7b9422cfe..b20bda8755 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -371,7 +371,7 @@ class ModelLoadBalancingService: load_balancing_config.name = name load_balancing_config.enabled = enabled - load_balancing_config.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + load_balancing_config.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() self._clear_credentials_cache(tenant_id, config_id) diff --git a/api/services/recommend_app/recommend_app_type.py b/api/services/recommend_app/recommend_app_type.py index 7ea93b3f64..e60e435b3a 100644 --- a/api/services/recommend_app/recommend_app_type.py +++ b/api/services/recommend_app/recommend_app_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class RecommendAppType(str, Enum): +class RecommendAppType(StrEnum): REMOTE = "remote" BUILDIN = "builtin" DATABASE = "db" diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 7187d40517..fde8673ff5 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -1,7 +1,7 @@ import json import time from collections.abc import Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager @@ -115,7 +115,7 @@ class WorkflowService: workflow.graph = json.dumps(graph) workflow.features = json.dumps(features) workflow.updated_by = account.id - workflow.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) workflow.environment_variables = environment_variables workflow.conversation_variables = conversation_variables @@ -148,7 +148,7 @@ class WorkflowService: tenant_id=app_model.tenant_id, app_id=app_model.id, type=draft_workflow.type, - version=str(datetime.now(timezone.utc).replace(tzinfo=None)), + version=str(datetime.now(UTC).replace(tzinfo=None)), graph=draft_workflow.graph, features=draft_workflow.features, created_by=account.id, @@ -257,8 +257,8 @@ class WorkflowService: workflow_node_execution.elapsed_time = time.perf_counter() - start_at workflow_node_execution.created_by_role = CreatedByRole.ACCOUNT.value workflow_node_execution.created_by = account.id - workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None) - workflow_node_execution.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + workflow_node_execution.created_at = datetime.now(UTC).replace(tzinfo=None) + workflow_node_execution.finished_at = datetime.now(UTC).replace(tzinfo=None) if run_succeeded and node_run_result: # create workflow node execution diff --git a/api/tasks/add_document_to_index_task.py b/api/tasks/add_document_to_index_task.py index b50876cc79..81531c9590 100644 --- a/api/tasks/add_document_to_index_task.py +++ b/api/tasks/add_document_to_index_task.py @@ -74,7 +74,7 @@ def add_document_to_index_task(dataset_document_id: str): except Exception as e: logging.exception("add document to index failed") dataset_document.enabled = False - dataset_document.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + dataset_document.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) dataset_document.status = "error" dataset_document.error = str(e) db.session.commit() diff --git a/api/tasks/annotation/enable_annotation_reply_task.py b/api/tasks/annotation/enable_annotation_reply_task.py index e819bf3635..0bdcd0eccd 100644 --- a/api/tasks/annotation/enable_annotation_reply_task.py +++ b/api/tasks/annotation/enable_annotation_reply_task.py @@ -52,7 +52,7 @@ def enable_annotation_reply_task( annotation_setting.score_threshold = score_threshold annotation_setting.collection_binding_id = dataset_collection_binding.id annotation_setting.updated_user_id = user_id - annotation_setting.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + annotation_setting.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(annotation_setting) else: new_app_annotation_setting = AppAnnotationSetting( diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index 5ee72c27fc..dcb7009e44 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -80,9 +80,9 @@ def batch_create_segment_to_index_task( word_count=len(content), tokens=tokens, created_by=user_id, - indexing_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + indexing_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), status="completed", - completed_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + completed_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), ) if dataset_document.doc_form == "qa_model": segment_document.answer = segment["answer"] diff --git a/api/tasks/create_segment_to_index_task.py b/api/tasks/create_segment_to_index_task.py index 26375743b6..315b01f157 100644 --- a/api/tasks/create_segment_to_index_task.py +++ b/api/tasks/create_segment_to_index_task.py @@ -38,7 +38,7 @@ def create_segment_to_index_task(segment_id: str, keywords: Optional[list[str]] # update segment status to indexing update_params = { DocumentSegment.status: "indexing", - DocumentSegment.indexing_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.indexing_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), } DocumentSegment.query.filter_by(id=segment.id).update(update_params) db.session.commit() @@ -75,7 +75,7 @@ def create_segment_to_index_task(segment_id: str, keywords: Optional[list[str]] # update segment to completed update_params = { DocumentSegment.status: "completed", - DocumentSegment.completed_at: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), + DocumentSegment.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), } DocumentSegment.query.filter_by(id=segment.id).update(update_params) db.session.commit() @@ -87,7 +87,7 @@ def create_segment_to_index_task(segment_id: str, keywords: Optional[list[str]] except Exception as e: logging.exception("create segment to index failed") segment.enabled = False - segment.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.status = "error" segment.error = str(e) db.session.commit() diff --git a/api/tasks/document_indexing_sync_task.py b/api/tasks/document_indexing_sync_task.py index 6dd755ab03..1831691393 100644 --- a/api/tasks/document_indexing_sync_task.py +++ b/api/tasks/document_indexing_sync_task.py @@ -67,7 +67,7 @@ def document_indexing_sync_task(dataset_id: str, document_id: str): # check the page is updated if last_edited_time != page_edited_time: document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() # delete all document segment and index diff --git a/api/tasks/document_indexing_task.py b/api/tasks/document_indexing_task.py index df1177d578..734dd2478a 100644 --- a/api/tasks/document_indexing_task.py +++ b/api/tasks/document_indexing_task.py @@ -50,7 +50,7 @@ def document_indexing_task(dataset_id: str, document_ids: list): if document: document.indexing_status = "error" document.error = str(e) - document.stopped_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.add(document) db.session.commit() return @@ -64,7 +64,7 @@ def document_indexing_task(dataset_id: str, document_ids: list): if document: document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) documents.append(document) db.session.add(document) db.session.commit() diff --git a/api/tasks/document_indexing_update_task.py b/api/tasks/document_indexing_update_task.py index cb38bc668d..1a52a6636b 100644 --- a/api/tasks/document_indexing_update_task.py +++ b/api/tasks/document_indexing_update_task.py @@ -30,7 +30,7 @@ def document_indexing_update_task(dataset_id: str, document_id: str): raise NotFound("Document not found") document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) db.session.commit() # delete all document segment and index diff --git a/api/tasks/enable_segment_to_index_task.py b/api/tasks/enable_segment_to_index_task.py index 1412ad9ec7..12639db939 100644 --- a/api/tasks/enable_segment_to_index_task.py +++ b/api/tasks/enable_segment_to_index_task.py @@ -71,7 +71,7 @@ def enable_segment_to_index_task(segment_id: str): except Exception as e: logging.exception("enable segment to index failed") segment.enabled = False - segment.disabled_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + segment.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.status = "error" segment.error = str(e) db.session.commit() diff --git a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py index f6b3be8250..f6555cfdde 100644 --- a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py +++ b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py @@ -1,6 +1,6 @@ import uuid from collections.abc import Generator -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from core.workflow.entities.variable_pool import VariablePool from core.workflow.enums import SystemVariableKey @@ -29,7 +29,7 @@ def _recursive_process(graph: Graph, next_node_id: str) -> Generator[GraphEngine def _publish_events(graph: Graph, next_node_id: str) -> Generator[GraphEngineEvent, None, None]: - route_node_state = RouteNodeState(node_id=next_node_id, start_at=datetime.now(timezone.utc).replace(tzinfo=None)) + route_node_state = RouteNodeState(node_id=next_node_id, start_at=datetime.now(UTC).replace(tzinfo=None)) parallel_id = graph.node_parallel_mapping.get(next_node_id) parallel_start_node_id = None @@ -68,7 +68,7 @@ def _publish_events(graph: Graph, next_node_id: str) -> Generator[GraphEngineEve ) route_node_state.status = RouteNodeState.Status.SUCCESS - route_node_state.finished_at = datetime.now(timezone.utc).replace(tzinfo=None) + route_node_state.finished_at = datetime.now(UTC).replace(tzinfo=None) yield NodeRunSucceededEvent( id=node_execution_id, node_id=next_node_id, From ae3a2cb272dc0729718ef0d6dd854692b98aaf9d Mon Sep 17 00:00:00 2001 From: litterGuy Date: Sun, 24 Nov 2024 14:19:48 +0800 Subject: [PATCH 006/130] fix: json parse err when http node send request (#11001) --- api/core/workflow/nodes/http_request/executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 80b322b068..22ad2a39f6 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -108,7 +108,7 @@ class Executor: self.content = self.variable_pool.convert_template(data[0].value).text case "json": json_string = self.variable_pool.convert_template(data[0].value).text - json_object = json.loads(json_string) + json_object = json.loads(json_string, strict=False) self.json = json_object # self.json = self._parse_object_contains_variables(json_object) case "binary": From 03ba4bc7608de396679a8e1c3f02e470f1197c4f Mon Sep 17 00:00:00 2001 From: cyflhn Date: Sun, 24 Nov 2024 15:29:30 +0800 Subject: [PATCH 007/130] fix error with xinference tool calling with qwen2-instruct and add timeout retry setttings for xinference (#11012) Co-authored-by: crazywoola <427733928@qq.com> --- .../model_providers/xinference/llm/llm.py | 14 ++++++++++--- .../xinference/xinference.yaml | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/api/core/model_runtime/model_providers/xinference/llm/llm.py b/api/core/model_runtime/model_providers/xinference/llm/llm.py index b82f0430c5..8d86d6937d 100644 --- a/api/core/model_runtime/model_providers/xinference/llm/llm.py +++ b/api/core/model_runtime/model_providers/xinference/llm/llm.py @@ -63,6 +63,9 @@ from core.model_runtime.model_providers.xinference.xinference_helper import ( ) from core.model_runtime.utils import helper +DEFAULT_MAX_RETRIES = 3 +DEFAULT_INVOKE_TIMEOUT = 60 + class XinferenceAILargeLanguageModel(LargeLanguageModel): def _invoke( @@ -315,7 +318,12 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): message_dict = {"role": "system", "content": message.content} elif isinstance(message, ToolPromptMessage): message = cast(ToolPromptMessage, message) - message_dict = {"tool_call_id": message.tool_call_id, "role": "tool", "content": message.content} + message_dict = { + "tool_call_id": message.tool_call_id, + "role": "tool", + "content": message.content, + "name": message.name, + } else: raise ValueError(f"Unknown message type {type(message)}") @@ -466,8 +474,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): client = OpenAI( base_url=f'{credentials["server_url"]}/v1', api_key=api_key, - max_retries=3, - timeout=60, + max_retries=int(credentials.get("max_retries") or DEFAULT_MAX_RETRIES), + timeout=int(credentials.get("invoke_timeout") or DEFAULT_INVOKE_TIMEOUT), ) xinference_client = Client( diff --git a/api/core/model_runtime/model_providers/xinference/xinference.yaml b/api/core/model_runtime/model_providers/xinference/xinference.yaml index be9073c1ca..3500136693 100644 --- a/api/core/model_runtime/model_providers/xinference/xinference.yaml +++ b/api/core/model_runtime/model_providers/xinference/xinference.yaml @@ -56,3 +56,23 @@ model_credential_schema: placeholder: zh_Hans: 在此输入您的API密钥 en_US: Enter the api key + - variable: invoke_timeout + label: + zh_Hans: 调用超时时间 (单位:秒) + en_US: invoke timeout (unit:second) + type: text-input + required: true + default: '60' + placeholder: + zh_Hans: 在此输入调用超时时间 + en_US: Enter invoke timeout value + - variable: max_retries + label: + zh_Hans: 调用重试次数 + en_US: max retries + type: text-input + required: true + default: '3' + placeholder: + zh_Hans: 在此输入调用重试次数 + en_US: Enter max retries From 8565c18e84f1d81989f0cb5d391190f858a28ddf Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 24 Nov 2024 15:29:43 +0800 Subject: [PATCH 008/130] feat(file_factory): Standardize custom file type into known types (#11028) Signed-off-by: -LAN- --- api/factories/file_factory.py | 100 +++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 4da0140d19..a5369a38d3 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -1,10 +1,11 @@ import mimetypes from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, cast import httpx from sqlalchemy import select +from constants import AUDIO_EXTENSIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS from core.file import File, FileBelongsTo, FileTransferMethod, FileType, FileUploadConfig from core.helper import ssrf_proxy from extensions.ext_database import db @@ -71,7 +72,12 @@ def build_from_mapping( transfer_method=transfer_method, ) - if not _is_file_valid_with_config(file=file, config=config): + if not _is_file_valid_with_config( + input_file_type=mapping.get("type", FileType.CUSTOM), + file_extension=file.extension, + file_transfer_method=file.transfer_method, + config=config, + ): raise ValueError(f"File validation failed for file: {file.filename}") return file @@ -114,17 +120,18 @@ def _build_from_local_file( tenant_id: str, transfer_method: FileTransferMethod, ) -> File: - file_type = FileType.value_of(mapping.get("type")) stmt = select(UploadFile).where( UploadFile.id == mapping.get("upload_file_id"), UploadFile.tenant_id == tenant_id, ) row = db.session.scalar(stmt) - if row is None: raise ValueError("Invalid upload file") + file_type = FileType(mapping.get("type")) + file_type = _standardize_file_type(file_type, extension="." + row.extension, mime_type=row.mime_type) + return File( id=mapping.get("id"), filename=row.name, @@ -152,11 +159,14 @@ 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 = _standardize_file_type(file_type, extension=extension, mime_type=mime_type) + return File( id=mapping.get("id"), filename=filename, tenant_id=tenant_id, - type=FileType.value_of(mapping.get("type")), + type=file_type, transfer_method=transfer_method, remote_url=url, mime_type=mime_type, @@ -171,6 +181,7 @@ def _get_remote_file_info(url: str): mime_type = mimetypes.guess_type(filename)[0] or "" resp = ssrf_proxy.head(url, follow_redirects=True) + resp = cast(httpx.Response, resp) if resp.status_code == httpx.codes.OK: if content_disposition := resp.headers.get("Content-Disposition"): filename = str(content_disposition.split("filename=")[-1].strip('"')) @@ -180,20 +191,6 @@ def _get_remote_file_info(url: str): return mime_type, filename, file_size -def _get_file_type_by_mimetype(mime_type: str) -> FileType: - if "image" in mime_type: - file_type = FileType.IMAGE - elif "video" in mime_type: - file_type = FileType.VIDEO - elif "audio" in mime_type: - file_type = FileType.AUDIO - elif "text" in mime_type or "pdf" in mime_type: - file_type = FileType.DOCUMENT - else: - file_type = FileType.CUSTOM - return file_type - - def _build_from_tool_file( *, mapping: Mapping[str, Any], @@ -213,7 +210,8 @@ 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 = mapping.get("type", _get_file_type_by_mimetype(tool_file.mimetype)) + file_type = FileType(mapping.get("type")) + file_type = _standardize_file_type(file_type, extension=extension, mime_type=tool_file.mimetype) return File( id=mapping.get("id"), @@ -229,18 +227,68 @@ def _build_from_tool_file( ) -def _is_file_valid_with_config(*, file: File, config: FileUploadConfig) -> bool: - if config.allowed_file_types and file.type not in config.allowed_file_types and file.type != FileType.CUSTOM: +def _is_file_valid_with_config( + *, + input_file_type: str, + file_extension: str, + file_transfer_method: FileTransferMethod, + config: FileUploadConfig, +) -> bool: + if ( + config.allowed_file_types + and input_file_type not in config.allowed_file_types + and input_file_type != FileType.CUSTOM + ): return False - if config.allowed_file_extensions and file.extension not in config.allowed_file_extensions: + if config.allowed_file_extensions and file_extension not in config.allowed_file_extensions: return False - if config.allowed_file_upload_methods and file.transfer_method not in config.allowed_file_upload_methods: + if config.allowed_file_upload_methods and file_transfer_method not in config.allowed_file_upload_methods: return False - if file.type == FileType.IMAGE and config.image_config: - if config.image_config.transfer_methods and file.transfer_method not in config.image_config.transfer_methods: + if input_file_type == FileType.IMAGE and config.image_config: + if config.image_config.transfer_methods and file_transfer_method not in config.image_config.transfer_methods: return False return True + + +def _standardize_file_type(file_type: FileType, /, *, extension: str = "", mime_type: str = "") -> FileType: + """ + If custom type, try to guess the file type by extension and mime_type. + """ + if file_type != FileType.CUSTOM: + return FileType(file_type) + guessed_type = None + if extension: + guessed_type = _get_file_type_by_extension(extension) + if guessed_type is None and mime_type: + guessed_type = _get_file_type_by_mimetype(mime_type) + return guessed_type or FileType.CUSTOM + + +def _get_file_type_by_extension(extension: str) -> FileType | None: + extension = extension.lstrip(".") + if extension in IMAGE_EXTENSIONS: + return FileType.IMAGE + elif extension in VIDEO_EXTENSIONS: + return FileType.VIDEO + elif extension in AUDIO_EXTENSIONS: + return FileType.AUDIO + elif extension in DOCUMENT_EXTENSIONS: + return FileType.DOCUMENT + + +def _get_file_type_by_mimetype(mime_type: str) -> FileType | None: + if "image" in mime_type: + file_type = FileType.IMAGE + elif "video" in mime_type: + file_type = FileType.VIDEO + elif "audio" in mime_type: + file_type = FileType.AUDIO + elif "text" in mime_type or "pdf" in mime_type: + file_type = FileType.DOCUMENT + else: + file_type = FileType.CUSTOM + return file_type From 60b5dac3ab50f331d91a45bd52c65a2454a1348e Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 24 Nov 2024 21:06:51 +0800 Subject: [PATCH 009/130] fix: query will be None if the query_prompt_template not exists (#11031) Signed-off-by: -LAN- --- api/core/workflow/nodes/llm/node.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 2529c76942..4a6f0ecae9 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -38,6 +38,7 @@ from core.variables import ( ObjectSegment, StringSegment, ) +from core.workflow.constants import SYSTEM_VARIABLE_NODE_ID from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult from core.workflow.entities.variable_entities import VariableSelector from core.workflow.entities.variable_pool import VariablePool @@ -133,11 +134,15 @@ class LLMNode(BaseNode[LLMNodeData]): # fetch memory memory = self._fetch_memory(node_data_memory=self.node_data.memory, model_instance=model_instance) - # fetch prompt messages + query = None if self.node_data.memory: query = self.node_data.memory.query_prompt_template - else: - query = None + if query is None and ( + query_variable := self.graph_runtime_state.variable_pool.get( + (SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY) + ) + ): + query = query_variable.text prompt_messages, stop = self._fetch_prompt_messages( user_query=query, From 365a40d11f80faaa5f49cc5f746858e332ad7a76 Mon Sep 17 00:00:00 2001 From: TakakiMoriguchi <59556279+TakakiMoriguchi@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:09:30 +0900 Subject: [PATCH 010/130] fix: Japanese typo (#11034) --- web/i18n/ja-JP/app-debug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index 05e81a2ae2..1f33197ebf 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -359,7 +359,7 @@ const translation = { 'content': 'コンテンツ', 'required': '必須', 'file': { - supportFileTypes: 'サッポトされたファイルタイプ', + supportFileTypes: 'サポートされたファイルタイプ', image: { name: '画像', }, From 04f1e18342b04376f967df8aa705aa43fe9f1418 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sun, 24 Nov 2024 21:10:01 +0800 Subject: [PATCH 011/130] fix: Validate file only when file type is set to custom (#11036) Signed-off-by: -LAN- --- api/factories/file_factory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index a5369a38d3..1c368a22ca 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -241,7 +241,11 @@ def _is_file_valid_with_config( ): return False - if config.allowed_file_extensions and file_extension not in config.allowed_file_extensions: + if ( + input_file_type == FileType.CUSTOM + and config.allowed_file_extensions is not None + and file_extension not in config.allowed_file_extensions + ): return False if config.allowed_file_upload_methods and file_transfer_method not in config.allowed_file_upload_methods: From 40a5f1c80a1daeecf935452319f9a4c2b9600dc6 Mon Sep 17 00:00:00 2001 From: Matsuda Date: Mon, 25 Nov 2024 12:02:45 +0900 Subject: [PATCH 012/130] fix: wrong param name (#11039) --- api/tasks/add_document_to_index_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tasks/add_document_to_index_task.py b/api/tasks/add_document_to_index_task.py index 81531c9590..09be661216 100644 --- a/api/tasks/add_document_to_index_task.py +++ b/api/tasks/add_document_to_index_task.py @@ -18,9 +18,9 @@ from models.dataset import DocumentSegment def add_document_to_index_task(dataset_document_id: str): """ Async Add document to index - :param document_id: + :param dataset_document_id: - Usage: add_document_to_index.delay(document_id) + Usage: add_document_to_index.delay(dataset_document_id) """ logging.info(click.style("Start add document to index: {}".format(dataset_document_id), fg="green")) start_at = time.perf_counter() From 87c831e5dd18e492b7bbf1edd1c187f8f9b7e947 Mon Sep 17 00:00:00 2001 From: cyflhn Date: Mon, 25 Nov 2024 11:02:58 +0800 Subject: [PATCH 013/130] make tool parameters parsing compatible with the response of glm4 model in xinference provider when function tool call integerated (#11049) --- api/core/tools/tool_engine.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index dbd97c8151..f92b43608e 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -61,7 +61,12 @@ class ToolEngine: if parameters and len(parameters) == 1: tool_parameters = {parameters[0].name: tool_parameters} else: - raise ValueError(f"tool_parameters should be a dict, but got a string: {tool_parameters}") + try: + tool_parameters = json.loads(tool_parameters) + except Exception as e: + pass + if not isinstance(tool_parameters, dict): + raise ValueError(f"tool_parameters should be a dict, but got a string: {tool_parameters}") # invoke the tool try: From aae29e72ae06d47a54099a25cf2f52d265d17fcd Mon Sep 17 00:00:00 2001 From: Tao Wang <74752235+taowang1993@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:03:53 -0800 Subject: [PATCH 014/130] Fix Deepseek Function/Tool Calling (#11023) --- .../deepseek/llm/deepseek-chat.yaml | 7 +- .../deepseek/llm/deepseek-coder.yaml | 1 + .../model_providers/deepseek/llm/llm.py | 98 ++----------------- 3 files changed, 15 insertions(+), 91 deletions(-) diff --git a/api/core/model_runtime/model_providers/deepseek/llm/deepseek-chat.yaml b/api/core/model_runtime/model_providers/deepseek/llm/deepseek-chat.yaml index 4973ac8ad6..0bbd27ad74 100644 --- a/api/core/model_runtime/model_providers/deepseek/llm/deepseek-chat.yaml +++ b/api/core/model_runtime/model_providers/deepseek/llm/deepseek-chat.yaml @@ -5,6 +5,7 @@ label: model_type: llm features: - agent-thought + - tool-call - multi-tool-call - stream-tool-call model_properties: @@ -72,7 +73,7 @@ parameter_rules: - text - json_object pricing: - input: '1' - output: '2' - unit: '0.000001' + input: "1" + output: "2" + unit: "0.000001" currency: RMB diff --git a/api/core/model_runtime/model_providers/deepseek/llm/deepseek-coder.yaml b/api/core/model_runtime/model_providers/deepseek/llm/deepseek-coder.yaml index caafeadadd..97310e76b9 100644 --- a/api/core/model_runtime/model_providers/deepseek/llm/deepseek-coder.yaml +++ b/api/core/model_runtime/model_providers/deepseek/llm/deepseek-coder.yaml @@ -5,6 +5,7 @@ label: model_type: llm features: - agent-thought + - tool-call - multi-tool-call - stream-tool-call model_properties: diff --git a/api/core/model_runtime/model_providers/deepseek/llm/llm.py b/api/core/model_runtime/model_providers/deepseek/llm/llm.py index 6d0a3ee262..610dc7b458 100644 --- a/api/core/model_runtime/model_providers/deepseek/llm/llm.py +++ b/api/core/model_runtime/model_providers/deepseek/llm/llm.py @@ -1,18 +1,17 @@ from collections.abc import Generator from typing import Optional, Union -from urllib.parse import urlparse -import tiktoken +from yarl import URL -from core.model_runtime.entities.llm_entities import LLMResult +from core.model_runtime.entities.llm_entities import LLMMode, LLMResult from core.model_runtime.entities.message_entities import ( PromptMessage, PromptMessageTool, ) -from core.model_runtime.model_providers.openai.llm.llm import OpenAILargeLanguageModel +from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel -class DeepSeekLargeLanguageModel(OpenAILargeLanguageModel): +class DeepseekLargeLanguageModel(OAIAPICompatLargeLanguageModel): def _invoke( self, model: str, @@ -25,92 +24,15 @@ class DeepSeekLargeLanguageModel(OpenAILargeLanguageModel): user: Optional[str] = None, ) -> Union[LLMResult, Generator]: self._add_custom_parameters(credentials) - - return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) + return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream) def validate_credentials(self, model: str, credentials: dict) -> None: self._add_custom_parameters(credentials) super().validate_credentials(model, credentials) - # refactored from openai model runtime, use cl100k_base for calculate token number - def _num_tokens_from_string(self, model: str, text: str, tools: Optional[list[PromptMessageTool]] = None) -> int: - """ - Calculate num tokens for text completion model with tiktoken package. - - :param model: model name - :param text: prompt text - :param tools: tools for tool calling - :return: number of tokens - """ - encoding = tiktoken.get_encoding("cl100k_base") - num_tokens = len(encoding.encode(text)) - - if tools: - num_tokens += self._num_tokens_for_tools(encoding, tools) - - return num_tokens - - # refactored from openai model runtime, use cl100k_base for calculate token number - def _num_tokens_from_messages( - self, model: str, messages: list[PromptMessage], tools: Optional[list[PromptMessageTool]] = None - ) -> int: - """Calculate num tokens for gpt-3.5-turbo and gpt-4 with tiktoken package. - - Official documentation: https://github.com/openai/openai-cookbook/blob/ - main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb""" - encoding = tiktoken.get_encoding("cl100k_base") - tokens_per_message = 3 - tokens_per_name = 1 - - num_tokens = 0 - messages_dict = [self._convert_prompt_message_to_dict(m) for m in messages] - for message in messages_dict: - num_tokens += tokens_per_message - for key, value in message.items(): - # Cast str(value) in case the message value is not a string - # This occurs with function messages - # TODO: The current token calculation method for the image type is not implemented, - # which need to download the image and then get the resolution for calculation, - # and will increase the request delay - if isinstance(value, list): - text = "" - for item in value: - if isinstance(item, dict) and item["type"] == "text": - text += item["text"] - - value = text - - if key == "tool_calls": - for tool_call in value: - for t_key, t_value in tool_call.items(): - num_tokens += len(encoding.encode(t_key)) - if t_key == "function": - for f_key, f_value in t_value.items(): - num_tokens += len(encoding.encode(f_key)) - num_tokens += len(encoding.encode(f_value)) - else: - num_tokens += len(encoding.encode(t_key)) - num_tokens += len(encoding.encode(t_value)) - else: - num_tokens += len(encoding.encode(str(value))) - - if key == "name": - num_tokens += tokens_per_name - - # every reply is primed with assistant - num_tokens += 3 - - if tools: - num_tokens += self._num_tokens_for_tools(encoding, tools) - - return num_tokens - @staticmethod - def _add_custom_parameters(credentials: dict) -> None: - credentials["mode"] = "chat" - credentials["openai_api_key"] = credentials["api_key"] - if "endpoint_url" not in credentials or credentials["endpoint_url"] == "": - credentials["openai_api_base"] = "https://api.deepseek.com" - else: - parsed_url = urlparse(credentials["endpoint_url"]) - credentials["openai_api_base"] = f"{parsed_url.scheme}://{parsed_url.netloc}" + def _add_custom_parameters(credentials) -> None: + credentials["endpoint_url"] = str(URL(credentials.get("endpoint_url", "https://api.deepseek.com"))) + credentials["mode"] = LLMMode.CHAT.value + credentials["function_calling_type"] = "tool_call" + credentials["stream_function_calling"] = "support" From a4fc057a1cd517186e93583bd258185084adc704 Mon Sep 17 00:00:00 2001 From: "SiliconFlow, Inc" Date: Mon, 25 Nov 2024 11:04:13 +0800 Subject: [PATCH 015/130] ISSUE=11042: add tts model in siliconflow (#11043) --- .../siliconflow/llm/_position.yaml | 1 - .../siliconflow/siliconflow.yaml | 1 + .../siliconflow/tts/__init__.py | 0 .../siliconflow/tts/fish-speech-1.4.yaml | 37 ++++++ .../model_providers/siliconflow/tts/tts.py | 105 ++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 api/core/model_runtime/model_providers/siliconflow/tts/__init__.py create mode 100644 api/core/model_runtime/model_providers/siliconflow/tts/fish-speech-1.4.yaml create mode 100644 api/core/model_runtime/model_providers/siliconflow/tts/tts.py diff --git a/api/core/model_runtime/model_providers/siliconflow/llm/_position.yaml b/api/core/model_runtime/model_providers/siliconflow/llm/_position.yaml index f010e4c826..b52df3e4e3 100644 --- a/api/core/model_runtime/model_providers/siliconflow/llm/_position.yaml +++ b/api/core/model_runtime/model_providers/siliconflow/llm/_position.yaml @@ -24,4 +24,3 @@ - meta-llama/Meta-Llama-3.1-8B-Instruct - google/gemma-2-27b-it - google/gemma-2-9b-it -- deepseek-ai/DeepSeek-V2-Chat diff --git a/api/core/model_runtime/model_providers/siliconflow/siliconflow.yaml b/api/core/model_runtime/model_providers/siliconflow/siliconflow.yaml index 71f9a92381..73a9e80769 100644 --- a/api/core/model_runtime/model_providers/siliconflow/siliconflow.yaml +++ b/api/core/model_runtime/model_providers/siliconflow/siliconflow.yaml @@ -18,6 +18,7 @@ supported_model_types: - text-embedding - rerank - speech2text + - tts configurate_methods: - predefined-model - customizable-model diff --git a/api/core/model_runtime/model_providers/siliconflow/tts/__init__.py b/api/core/model_runtime/model_providers/siliconflow/tts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/siliconflow/tts/fish-speech-1.4.yaml b/api/core/model_runtime/model_providers/siliconflow/tts/fish-speech-1.4.yaml new file mode 100644 index 0000000000..4adfd05c60 --- /dev/null +++ b/api/core/model_runtime/model_providers/siliconflow/tts/fish-speech-1.4.yaml @@ -0,0 +1,37 @@ +model: fishaudio/fish-speech-1.4 +model_type: tts +model_properties: + default_voice: 'fishaudio/fish-speech-1.4:alex' + voices: + - mode: "fishaudio/fish-speech-1.4:alex" + name: "Alex(男声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:benjamin" + name: "Benjamin(男声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:charles" + name: "Charles(男声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:david" + name: "David(男声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:anna" + name: "Anna(女声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:bella" + name: "Bella(女声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:claire" + name: "Claire(女声)" + language: [ "zh-Hans", "en-US" ] + - mode: "fishaudio/fish-speech-1.4:diana" + name: "Diana(女声)" + language: [ "zh-Hans", "en-US" ] + audio_type: 'mp3' + max_workers: 5 + # stream: false +pricing: + input: '0.015' + output: '0' + unit: '0.001' + currency: RMB diff --git a/api/core/model_runtime/model_providers/siliconflow/tts/tts.py b/api/core/model_runtime/model_providers/siliconflow/tts/tts.py new file mode 100644 index 0000000000..a5554abb73 --- /dev/null +++ b/api/core/model_runtime/model_providers/siliconflow/tts/tts.py @@ -0,0 +1,105 @@ +import concurrent.futures +from typing import Any, Optional + +from openai import OpenAI + +from core.model_runtime.errors.invoke import InvokeBadRequestError +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.__base.tts_model import TTSModel +from core.model_runtime.model_providers.openai._common import _CommonOpenAI + + +class SiliconFlowText2SpeechModel(_CommonOpenAI, TTSModel): + """ + Model class for SiliconFlow Speech to text model. + """ + + def _invoke( + self, model: str, tenant_id: str, credentials: dict, content_text: str, voice: str, user: Optional[str] = None + ) -> Any: + """ + _invoke text2speech model + + :param model: model name + :param tenant_id: user tenant id + :param credentials: model credentials + :param content_text: text content to be translated + :param voice: model timbre + :param user: unique user id + :return: text translated to audio file + """ + if not voice or voice not in [ + d["value"] for d in self.get_tts_model_voices(model=model, credentials=credentials) + ]: + voice = self._get_model_default_voice(model, credentials) + # if streaming: + return self._tts_invoke_streaming(model=model, credentials=credentials, content_text=content_text, voice=voice) + + def validate_credentials(self, model: str, credentials: dict, user: Optional[str] = None) -> None: + """ + validate credentials text2speech model + + :param model: model name + :param credentials: model credentials + :param user: unique user id + :return: text translated to audio file + """ + try: + self._tts_invoke_streaming( + model=model, + credentials=credentials, + content_text="Hello SiliconFlow!", + voice=self._get_model_default_voice(model, credentials), + ) + except Exception as ex: + raise CredentialsValidateFailedError(str(ex)) + + def _tts_invoke_streaming(self, model: str, credentials: dict, content_text: str, voice: str) -> Any: + """ + _tts_invoke_streaming text2speech model + + :param model: model name + :param credentials: model credentials + :param content_text: text content to be translated + :param voice: model timbre + :return: text translated to audio file + """ + try: + # doc: https://docs.siliconflow.cn/capabilities/text-to-speech + self._add_custom_parameters(credentials) + credentials_kwargs = self._to_credential_kwargs(credentials) + client = OpenAI(**credentials_kwargs) + model_support_voice = [ + x.get("value") for x in self.get_tts_model_voices(model=model, credentials=credentials) + ] + if not voice or voice not in model_support_voice: + voice = self._get_model_default_voice(model, credentials) + if len(content_text) > 4096: + sentences = self._split_text_into_sentences(content_text, max_length=4096) + executor = concurrent.futures.ThreadPoolExecutor(max_workers=min(3, len(sentences))) + futures = [ + executor.submit( + client.audio.speech.with_streaming_response.create, + model=model, + response_format="mp3", + input=sentences[i], + voice=voice, + ) + for i in range(len(sentences)) + ] + for future in futures: + yield from future.result().__enter__().iter_bytes(1024) # noqa:PLC2801 + + else: + response = client.audio.speech.with_streaming_response.create( + model=model, voice=voice, response_format="mp3", input=content_text.strip() + ) + + yield from response.__enter__().iter_bytes(1024) # noqa:PLC2801 + except Exception as ex: + raise InvokeBadRequestError(str(ex)) + + @classmethod + def _add_custom_parameters(cls, credentials: dict) -> None: + credentials["openai_api_base"] = "https://api.siliconflow.cn" + credentials["openai_api_key"] = credentials["api_key"] From 41772c325f71dc1a6a24f7276c5e260c431eb673 Mon Sep 17 00:00:00 2001 From: "Dr.MerdanBay" <110794035+KMerdan@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:11:00 +0900 Subject: [PATCH 016/130] Feat/add admin check (#11050) --- api/models/account.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/models/account.py b/api/models/account.py index 6684e8dd6e..18be4be036 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -108,6 +108,10 @@ class Account(UserMixin, db.Model): def is_admin_or_owner(self): return TenantAccountRole.is_privileged_role(self._current_tenant.current_role) + @property + def is_admin(self): + return TenantAccountRole.is_admin_role(self._current_tenant.current_role) + @property def is_editor(self): return TenantAccountRole.is_editing_role(self._current_tenant.current_role) @@ -147,6 +151,10 @@ class TenantAccountRole(enum.StrEnum): def is_privileged_role(role: str) -> bool: return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN} + @staticmethod + def is_admin_role(role: str) -> bool: + return role and role == TenantAccountRole.ADMIN + @staticmethod def is_non_owner_role(role: str) -> bool: return role and role in { From 13006f94e268c88f4c05d91697e85cf383e52ac9 Mon Sep 17 00:00:00 2001 From: Jiang <65766008+AlwaysBluer@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:13:06 +0800 Subject: [PATCH 017/130] fix the wrong LINDORM_PASSWORD variable name in docker-compose.yaml (#11052) Co-authored-by: jiangzhijie --- docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e316ec0c5a..285f576b0e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -177,7 +177,7 @@ x-shared-env: &shared-api-worker-env ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic} LINDORM_URL: ${LINDORM_URL:-http://lindorm:30070} LINDORM_USERNAME: ${LINDORM_USERNAME:-lindorm} - LINDORM_PASSWORD: ${LINDORM_USERNAME:-lindorm } + LINDORM_PASSWORD: ${LINDORM_PASSWORD:-lindorm } KIBANA_PORT: ${KIBANA_PORT:-5601} # AnalyticDB configuration ANALYTICDB_KEY_ID: ${ANALYTICDB_KEY_ID:-} From b791a80b75a0de4467a5160d706dc9577db7eeb6 Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 25 Nov 2024 11:14:04 +0800 Subject: [PATCH 018/130] chore: update chromadb version to 0.5.20 (#11038) Signed-off-by: yihong0618 --- api/poetry.lock | 70 ++++++++++++++++++++++++---------------------- api/pyproject.toml | 2 +- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/api/poetry.lock b/api/poetry.lock index cdfc293405..958673a00b 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1422,36 +1422,40 @@ files = [ [[package]] name = "chroma-hnswlib" -version = "0.7.3" +version = "0.7.6" description = "Chromas fork of hnswlib" optional = false python-versions = "*" files = [ - {file = "chroma-hnswlib-0.7.3.tar.gz", hash = "sha256:b6137bedde49fffda6af93b0297fe00429fc61e5a072b1ed9377f909ed95a932"}, - {file = "chroma_hnswlib-0.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59d6a7c6f863c67aeb23e79a64001d537060b6995c3eca9a06e349ff7b0998ca"}, - {file = "chroma_hnswlib-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d71a3f4f232f537b6152947006bd32bc1629a8686df22fd97777b70f416c127a"}, - {file = "chroma_hnswlib-0.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c92dc1ebe062188e53970ba13f6b07e0ae32e64c9770eb7f7ffa83f149d4210"}, - {file = "chroma_hnswlib-0.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49da700a6656fed8753f68d44b8cc8ae46efc99fc8a22a6d970dc1697f49b403"}, - {file = "chroma_hnswlib-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:108bc4c293d819b56476d8f7865803cb03afd6ca128a2a04d678fffc139af029"}, - {file = "chroma_hnswlib-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11e7ca93fb8192214ac2b9c0943641ac0daf8f9d4591bb7b73be808a83835667"}, - {file = "chroma_hnswlib-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f552e4d23edc06cdeb553cdc757d2fe190cdeb10d43093d6a3319f8d4bf1c6b"}, - {file = "chroma_hnswlib-0.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96f4d5699e486eb1fb95849fe35ab79ab0901265805be7e60f4eaa83ce263ec"}, - {file = "chroma_hnswlib-0.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:368e57fe9ebae05ee5844840fa588028a023d1182b0cfdb1d13f607c9ea05756"}, - {file = "chroma_hnswlib-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:b7dca27b8896b494456db0fd705b689ac6b73af78e186eb6a42fea2de4f71c6f"}, - {file = "chroma_hnswlib-0.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:70f897dc6218afa1d99f43a9ad5eb82f392df31f57ff514ccf4eeadecd62f544"}, - {file = "chroma_hnswlib-0.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aef10b4952708f5a1381c124a29aead0c356f8d7d6e0b520b778aaa62a356f4"}, - {file = "chroma_hnswlib-0.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee2d8d1529fca3898d512079144ec3e28a81d9c17e15e0ea4665697a7923253"}, - {file = "chroma_hnswlib-0.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:a4021a70e898783cd6f26e00008b494c6249a7babe8774e90ce4766dd288c8ba"}, - {file = "chroma_hnswlib-0.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a8f61fa1d417fda848e3ba06c07671f14806a2585272b175ba47501b066fe6b1"}, - {file = "chroma_hnswlib-0.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7563be58bc98e8f0866907368e22ae218d6060601b79c42f59af4eccbbd2e0a"}, - {file = "chroma_hnswlib-0.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51b8d411486ee70d7b66ec08cc8b9b6620116b650df9c19076d2d8b6ce2ae914"}, - {file = "chroma_hnswlib-0.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d706782b628e4f43f1b8a81e9120ac486837fbd9bcb8ced70fe0d9b95c72d77"}, - {file = "chroma_hnswlib-0.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:54f053dedc0e3ba657f05fec6e73dd541bc5db5b09aa8bc146466ffb734bdc86"}, - {file = "chroma_hnswlib-0.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e607c5a71c610a73167a517062d302c0827ccdd6e259af6e4869a5c1306ffb5d"}, - {file = "chroma_hnswlib-0.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2358a795870156af6761890f9eb5ca8cade57eb10c5f046fe94dae1faa04b9e"}, - {file = "chroma_hnswlib-0.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cea425df2e6b8a5e201fff0d922a1cc1d165b3cfe762b1408075723c8892218"}, - {file = "chroma_hnswlib-0.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:454df3dd3e97aa784fba7cf888ad191e0087eef0fd8c70daf28b753b3b591170"}, - {file = "chroma_hnswlib-0.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:df587d15007ca701c6de0ee7d5585dd5e976b7edd2b30ac72bc376b3c3f85882"}, + {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f35192fbbeadc8c0633f0a69c3d3e9f1a4eab3a46b65458bbcbcabdd9e895c36"}, + {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f007b608c96362b8f0c8b6b2ac94f67f83fcbabd857c378ae82007ec92f4d82"}, + {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:456fd88fa0d14e6b385358515aef69fc89b3c2191706fd9aee62087b62aad09c"}, + {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfaae825499c2beaa3b75a12d7ec713b64226df72a5c4097203e3ed532680da"}, + {file = "chroma_hnswlib-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:2487201982241fb1581be26524145092c95902cb09fc2646ccfbc407de3328ec"}, + {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca"}, + {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f"}, + {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170"}, + {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9"}, + {file = "chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3"}, + {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7"}, + {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912"}, + {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4"}, + {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5"}, + {file = "chroma_hnswlib-0.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2fe6ea949047beed19a94b33f41fe882a691e58b70c55fdaa90274ae78be046f"}, + {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feceff971e2a2728c9ddd862a9dd6eb9f638377ad98438876c9aeac96c9482f5"}, + {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb0633b60e00a2b92314d0bf5bbc0da3d3320be72c7e3f4a9b19f4609dc2b2ab"}, + {file = "chroma_hnswlib-0.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:a566abe32fab42291f766d667bdbfa234a7f457dcbd2ba19948b7a978c8ca624"}, + {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6be47853d9a58dedcfa90fc846af202b071f028bbafe1d8711bf64fe5a7f6111"}, + {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a7af35bdd39a88bffa49f9bb4bf4f9040b684514a024435a1ef5cdff980579d"}, + {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a53b1f1551f2b5ad94eb610207bde1bb476245fc5097a2bec2b476c653c58bde"}, + {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3085402958dbdc9ff5626ae58d696948e715aef88c86d1e3f9285a88f1afd3bc"}, + {file = "chroma_hnswlib-0.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:77326f658a15adfb806a16543f7db7c45f06fd787d699e643642d6bde8ed49c4"}, + {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93b056ab4e25adab861dfef21e1d2a2756b18be5bc9c292aa252fa12bb44e6ae"}, + {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe91f018b30452c16c811fd6c8ede01f84e5a9f3c23e0758775e57f1c3778871"}, + {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c0e627476f0f4d9e153420d36042dd9c6c3671cfd1fe511c0253e38c2a1039"}, + {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9796a4536b7de6c6d76a792ba03e08f5aaa53e97e052709568e50b4d20c04f"}, + {file = "chroma_hnswlib-0.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:d30e2db08e7ffdcc415bd072883a322de5995eb6ec28a8f8c054103bbd3ec1e0"}, + {file = "chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7"}, ] [package.dependencies] @@ -1459,26 +1463,26 @@ numpy = "*" [[package]] name = "chromadb" -version = "0.5.1" +version = "0.5.20" description = "Chroma." optional = false python-versions = ">=3.8" files = [ - {file = "chromadb-0.5.1-py3-none-any.whl", hash = "sha256:61f1f75a672b6edce7f1c8875c67e2aaaaf130dc1c1684431fbc42ad7240d01d"}, - {file = "chromadb-0.5.1.tar.gz", hash = "sha256:e2b2b6a34c2a949bedcaa42fa7775f40c7f6667848fc8094dcbf97fc0d30bee7"}, + {file = "chromadb-0.5.20-py3-none-any.whl", hash = "sha256:9550ba1b6dce911e35cac2568b301badf4b42f457b99a432bdeec2b6b9dd3680"}, + {file = "chromadb-0.5.20.tar.gz", hash = "sha256:19513a23b2d20059866216bfd80195d1d4a160ffba234b8899f5e80978160ca7"}, ] [package.dependencies] bcrypt = ">=4.0.1" build = ">=1.0.3" -chroma-hnswlib = "0.7.3" +chroma-hnswlib = "0.7.6" fastapi = ">=0.95.2" grpcio = ">=1.58.0" httpx = ">=0.27.0" importlib-resources = "*" kubernetes = ">=28.1.0" mmh3 = ">=4.0.1" -numpy = ">=1.22.5,<2.0.0" +numpy = ">=1.22.5" onnxruntime = ">=1.14.1" opentelemetry-api = ">=1.2.0" opentelemetry-exporter-otlp-proto-grpc = ">=1.2.0" @@ -1490,7 +1494,7 @@ posthog = ">=2.4.0" pydantic = ">=1.9" pypika = ">=0.48.9" PyYAML = ">=6.0.0" -requests = ">=2.28" +rich = ">=10.11.0" tenacity = ">=8.2.3" tokenizers = ">=0.13.2" tqdm = ">=4.65.0" @@ -11022,4 +11026,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "75175c3427d13c41d84374ff2bb6f5c6cb157e3783107f9d22fad15c9eb8c177" +content-hash = "983ba4f2cb89f0c867fc50cb48677cad9343f7f0828c7082cb0b5cf171d716fb" diff --git a/api/pyproject.toml b/api/pyproject.toml index 6dbb16d820..79857f8163 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -242,7 +242,7 @@ tos = "~2.7.1" [tool.poetry.group.vdb.dependencies] alibabacloud_gpdb20160503 = "~3.8.0" alibabacloud_tea_openapi = "~0.3.9" -chromadb = "0.5.1" +chromadb = "0.5.20" clickhouse-connect = "~0.7.16" couchbase = "~4.3.0" elasticsearch = "8.14.0" From 684f6b22992a8a1be1225c232dfb85575dd6ce0d Mon Sep 17 00:00:00 2001 From: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:28:52 +0900 Subject: [PATCH 019/130] fix: slidespeak text output is not the download link (#10997) --- .../tools/provider/builtin/slidespeak/tools/slides_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py index 74742bf4b7..aa4ee63e97 100644 --- a/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py +++ b/api/core/tools/provider/builtin/slidespeak/tools/slides_generator.py @@ -149,7 +149,7 @@ class SlidesGeneratorTool(BuiltinTool): presentation_bytes = await self._fetch_presentation(session, download_url) return [ - self.create_text_message("Presentation generated successfully"), + self.create_text_message(download_url), self.create_blob_message( blob=presentation_bytes, meta={"mime_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, From 2dd4c344236c3e7f14079123b1a36543e4c2fd7b Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 25 Nov 2024 12:01:57 +0800 Subject: [PATCH 020/130] fix: llm node do not pass sys.query in chatflow app init (#11053) --- web/app/components/workflow/hooks/use-workflow-template.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/components/workflow/hooks/use-workflow-template.ts b/web/app/components/workflow/hooks/use-workflow-template.ts index e36f0b61f9..c2dc956b63 100644 --- a/web/app/components/workflow/hooks/use-workflow-template.ts +++ b/web/app/components/workflow/hooks/use-workflow-template.ts @@ -22,6 +22,7 @@ export const useWorkflowTemplate = () => { ...nodesInitialData.llm, memory: { window: { enabled: false, size: 10 }, + query_prompt_template: '{{#sys.query#}}', }, selected: true, }, From 79a35c2fe618a848655d9fd7ee465f6ce8e4422b Mon Sep 17 00:00:00 2001 From: nomi3 <42667020+nomi3@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:02:56 +0900 Subject: [PATCH 021/130] feat(i18n): update Japanese translation for login page (#10993) --- web/i18n/ja-JP/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index fe4510686b..7ba8047aff 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -58,7 +58,7 @@ const translation = { registrationNotAllowed: 'アカウントが見つかりません。登録するためにシステム管理者に連絡してください。', }, license: { - tip: 'Dify Community Editionを開始する前に、GitHubの', + tip: 'GitHubのオープンソースライセンスを確認してから、Dify Community Editionを開始してください。', link: 'オープンソースライセンス', }, join: '参加する', From 3eb51d85dafc0c55dac55de6a972796e628d4eed Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 25 Nov 2024 12:46:50 +0800 Subject: [PATCH 022/130] fix(workflow_entry): Support receive File and FileList in single step run. (#10947) Signed-off-by: -LAN- Co-authored-by: JzoNg --- .../app/apps/advanced_chat/app_generator.py | 2 +- api/core/app/apps/agent_chat/app_generator.py | 2 +- api/core/app/apps/base_app_generator.py | 14 +-- api/core/app/apps/chat/app_generator.py | 2 +- api/core/app/apps/completion/app_generator.py | 4 +- api/core/app/apps/workflow/app_generator.py | 4 +- api/core/app/apps/workflow_app_runner.py | 3 - api/core/workflow/entities/node_entities.py | 2 +- api/core/workflow/workflow_entry.py | 97 +++++++------------ api/factories/file_factory.py | 10 +- api/services/workflow_service.py | 24 +++-- .../components/before-run-form/index.tsx | 6 ++ 12 files changed, 75 insertions(+), 95 deletions(-) diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 00e5a74732..c099b29c97 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -127,7 +127,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config), + else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index d1564a260e..ae0a01e8a7 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -134,7 +134,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config), + else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 2c78d95778..85b7aced55 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -1,4 +1,4 @@ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Optional from core.app.app_config.entities import VariableEntityType @@ -6,7 +6,7 @@ from core.file import File, FileUploadConfig from factories import file_factory if TYPE_CHECKING: - from core.app.app_config.entities import AppConfig, VariableEntity + from core.app.app_config.entities import VariableEntity class BaseAppGenerator: @@ -14,23 +14,23 @@ class BaseAppGenerator: self, *, user_inputs: Optional[Mapping[str, Any]], - app_config: "AppConfig", + variables: Sequence["VariableEntity"], + tenant_id: str, ) -> Mapping[str, Any]: user_inputs = user_inputs or {} # Filter input variables from form configuration, handle required fields, default values, and option values - variables = app_config.variables user_inputs = { var.variable: self._validate_inputs(value=user_inputs.get(var.variable), variable_entity=var) for var in variables } user_inputs = {k: self._sanitize_value(v) for k, v in user_inputs.items()} # Convert files in inputs to File - entity_dictionary = {item.variable: item for item in app_config.variables} + entity_dictionary = {item.variable: item for item in variables} # Convert single file to File files_inputs = { k: file_factory.build_from_mapping( mapping=v, - tenant_id=app_config.tenant_id, + tenant_id=tenant_id, config=FileUploadConfig( allowed_file_types=entity_dictionary[k].allowed_file_types, allowed_file_extensions=entity_dictionary[k].allowed_file_extensions, @@ -44,7 +44,7 @@ class BaseAppGenerator: file_list_inputs = { k: file_factory.build_from_mappings( mappings=v, - tenant_id=app_config.tenant_id, + tenant_id=tenant_id, config=FileUploadConfig( allowed_file_types=entity_dictionary[k].allowed_file_types, allowed_file_extensions=entity_dictionary[k].allowed_file_extensions, diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index e683dfef3f..cf6aae34f8 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -132,7 +132,7 @@ class ChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config), + else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 22ee8b0967..70434f24c1 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -113,7 +113,9 @@ class CompletionAppGenerator(MessageBasedAppGenerator): app_config=app_config, model_conf=ModelConfigConverter.convert(app_config), file_upload_config=file_extra_config, - inputs=self._prepare_user_inputs(user_inputs=inputs, app_config=app_config), + inputs=self._prepare_user_inputs( + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id + ), query=query, files=file_objs, user_id=user.id, diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 65da39b220..31efe43412 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -96,7 +96,9 @@ class WorkflowAppGenerator(BaseAppGenerator): task_id=str(uuid.uuid4()), app_config=app_config, file_upload_config=file_extra_config, - inputs=self._prepare_user_inputs(user_inputs=inputs, app_config=app_config), + inputs=self._prepare_user_inputs( + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.tenant_id + ), files=system_files, user_id=user.id, stream=stream, diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 2872390d46..1cf72ae79e 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -43,7 +43,6 @@ 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.iteration import IterationNodeData from core.workflow.nodes.node_mapping import node_type_classes_mapping from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db @@ -160,8 +159,6 @@ class WorkflowBasedAppRunner(AppRunner): user_inputs=user_inputs, variable_pool=variable_pool, tenant_id=workflow.tenant_id, - node_type=node_type, - node_data=IterationNodeData(**iteration_node_config.get("data", {})), ) return graph, variable_pool diff --git a/api/core/workflow/entities/node_entities.py b/api/core/workflow/entities/node_entities.py index 1ac64e94ef..e174d3baa0 100644 --- a/api/core/workflow/entities/node_entities.py +++ b/api/core/workflow/entities/node_entities.py @@ -36,7 +36,7 @@ class NodeRunResult(BaseModel): inputs: Optional[Mapping[str, Any]] = None # node inputs process_data: Optional[dict[str, Any]] = None # process data - outputs: Optional[dict[str, Any]] = None # node outputs + outputs: Optional[Mapping[str, Any]] = None # node outputs metadata: Optional[dict[NodeRunMetadataKey, Any]] = None # node metadata llm_usage: Optional[LLMUsage] = None # llm usage diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 84b251223f..6f7b143ad6 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -5,10 +5,9 @@ from collections.abc import Generator, Mapping, Sequence from typing import Any, Optional, cast from configs import dify_config -from core.app.app_config.entities import FileUploadConfig from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom -from core.file.models import File, FileTransferMethod, ImageConfig +from core.file.models import File from core.workflow.callbacks import WorkflowCallback from core.workflow.entities.variable_pool import VariablePool from core.workflow.errors import WorkflowNodeRunFailedError @@ -18,9 +17,8 @@ from core.workflow.graph_engine.entities.graph_init_params import GraphInitParam from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.graph_engine import GraphEngine from core.workflow.nodes import NodeType -from core.workflow.nodes.base import BaseNode, BaseNodeData +from core.workflow.nodes.base import BaseNode from core.workflow.nodes.event import NodeEvent -from core.workflow.nodes.llm import LLMNodeData from core.workflow.nodes.node_mapping import node_type_classes_mapping from factories import file_factory from models.enums import UserFrom @@ -115,7 +113,12 @@ class WorkflowEntry: @classmethod def single_step_run( - cls, workflow: Workflow, node_id: str, user_id: str, user_inputs: dict + cls, + *, + workflow: Workflow, + node_id: str, + user_id: str, + user_inputs: dict, ) -> tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]: """ Single step run workflow node @@ -135,13 +138,9 @@ class WorkflowEntry: raise ValueError("nodes not found in workflow graph") # fetch node config from node id - node_config = None - for node in nodes: - if node.get("id") == node_id: - node_config = node - break - - if not node_config: + try: + node_config = next(filter(lambda node: node["id"] == node_id, nodes)) + except StopIteration: raise ValueError("node id not found in workflow graph") # Get node class @@ -153,11 +152,7 @@ class WorkflowEntry: raise ValueError(f"Node class not found for node type {node_type}") # init variable pool - variable_pool = VariablePool( - system_variables={}, - user_inputs={}, - environment_variables=workflow.environment_variables, - ) + variable_pool = VariablePool(environment_variables=workflow.environment_variables) # init graph graph = Graph.init(graph_config=workflow.graph_dict) @@ -183,28 +178,24 @@ class WorkflowEntry: try: # variable selector to variable mapping - try: - variable_mapping = node_cls.extract_variable_selector_to_variable_mapping( - graph_config=workflow.graph_dict, config=node_config - ) - except NotImplementedError: - variable_mapping = {} - - cls.mapping_user_inputs_to_variable_pool( - variable_mapping=variable_mapping, - user_inputs=user_inputs, - variable_pool=variable_pool, - tenant_id=workflow.tenant_id, - node_type=node_type, - node_data=node_instance.node_data, + variable_mapping = node_cls.extract_variable_selector_to_variable_mapping( + graph_config=workflow.graph_dict, config=node_config ) + except NotImplementedError: + variable_mapping = {} + cls.mapping_user_inputs_to_variable_pool( + variable_mapping=variable_mapping, + user_inputs=user_inputs, + variable_pool=variable_pool, + tenant_id=workflow.tenant_id, + ) + try: # run node generator = node_instance.run() - - return node_instance, generator except Exception as e: raise WorkflowNodeRunFailedError(node_instance=node_instance, error=str(e)) + return node_instance, generator @staticmethod def handle_special_values(value: Optional[Mapping[str, Any]]) -> Mapping[str, Any] | None: @@ -231,12 +222,11 @@ class WorkflowEntry: @classmethod def mapping_user_inputs_to_variable_pool( cls, + *, variable_mapping: Mapping[str, Sequence[str]], user_inputs: dict, variable_pool: VariablePool, tenant_id: str, - node_type: NodeType, - node_data: BaseNodeData, ) -> None: for node_variable, variable_selector in variable_mapping.items(): # fetch node id and variable key from node_variable @@ -254,40 +244,21 @@ class WorkflowEntry: # fetch variable node id from variable selector variable_node_id = variable_selector[0] variable_key_list = variable_selector[1:] - variable_key_list = cast(list[str], variable_key_list) + variable_key_list = list(variable_key_list) # get input value input_value = user_inputs.get(node_variable) if not input_value: input_value = user_inputs.get(node_variable_key) - # FIXME: temp fix for image type - if node_type == NodeType.LLM: - new_value = [] - if isinstance(input_value, list): - node_data = cast(LLMNodeData, node_data) - - detail = node_data.vision.configs.detail if node_data.vision.configs else None - - for item in input_value: - if isinstance(item, dict) and "type" in item and item["type"] == "image": - transfer_method = FileTransferMethod.value_of(item.get("transfer_method")) - mapping = { - "id": item.get("id"), - "transfer_method": transfer_method, - "upload_file_id": item.get("upload_file_id"), - "url": item.get("url"), - } - config = FileUploadConfig(image_config=ImageConfig(detail=detail) if detail else None) - file = file_factory.build_from_mapping( - mapping=mapping, - tenant_id=tenant_id, - config=config, - ) - new_value.append(file) - - if new_value: - input_value = new_value + if isinstance(input_value, dict) and "type" in input_value and "transfer_method" in input_value: + input_value = file_factory.build_from_mapping(mapping=input_value, tenant_id=tenant_id) + if ( + isinstance(input_value, list) + and all(isinstance(item, dict) for item in input_value) + and all("type" in item and "transfer_method" in item for item in input_value) + ): + input_value = file_factory.build_from_mappings(mappings=input_value, tenant_id=tenant_id) # append variable and value to variable pool variable_pool.add([variable_node_id] + variable_key_list, input_value) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 1c368a22ca..ad8dba8190 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -86,12 +86,9 @@ def build_from_mapping( def build_from_mappings( *, mappings: Sequence[Mapping[str, Any]], - config: FileUploadConfig | None, + config: FileUploadConfig | None = None, tenant_id: str, ) -> Sequence[File]: - if not config: - return [] - files = [ build_from_mapping( mapping=mapping, @@ -102,13 +99,14 @@ def build_from_mappings( ] if ( + config # If image config is set. - config.image_config + and config.image_config # And the number of image files exceeds the maximum limit and sum(1 for _ in (filter(lambda x: x.type == FileType.IMAGE, files))) > config.image_config.number_limits ): raise ValueError(f"Number of image files exceeds the maximum limit {config.image_config.number_limits}") - if config.number_limits and len(files) > config.number_limits: + if config and config.number_limits and len(files) > config.number_limits: raise ValueError(f"Number of files exceeds the maximum limit {config.number_limits}") return files diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index fde8673ff5..aa2babd7f7 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -262,13 +262,17 @@ class WorkflowService: if run_succeeded and node_run_result: # create workflow node execution - workflow_node_execution.inputs = json.dumps(node_run_result.inputs) if node_run_result.inputs else None - workflow_node_execution.process_data = ( - json.dumps(node_run_result.process_data) if node_run_result.process_data else None - ) - workflow_node_execution.outputs = ( - json.dumps(jsonable_encoder(node_run_result.outputs)) if node_run_result.outputs else None + inputs = WorkflowEntry.handle_special_values(node_run_result.inputs) if node_run_result.inputs else None + process_data = ( + WorkflowEntry.handle_special_values(node_run_result.process_data) + if node_run_result.process_data + else None ) + outputs = WorkflowEntry.handle_special_values(node_run_result.outputs) if node_run_result.outputs else None + + workflow_node_execution.inputs = json.dumps(inputs) + workflow_node_execution.process_data = json.dumps(process_data) + workflow_node_execution.outputs = json.dumps(outputs) workflow_node_execution.execution_metadata = ( json.dumps(jsonable_encoder(node_run_result.metadata)) if node_run_result.metadata else None ) @@ -303,10 +307,10 @@ class WorkflowService: new_app = workflow_converter.convert_to_workflow( app_model=app_model, account=account, - name=args.get("name"), - icon_type=args.get("icon_type"), - icon=args.get("icon"), - icon_background=args.get("icon_background"), + name=args.get("name", "Default Name"), + icon_type=args.get("icon_type", "emoji"), + icon=args.get("icon", "🤖"), + icon_background=args.get("icon_background", "#FFEAD5"), ) return new_app diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index 6a3da3cf24..79d9c5b4dd 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -16,6 +16,7 @@ import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types import ResultPanel from '@/app/components/workflow/run/result-panel' import Toast from '@/app/components/base/toast' import { TransferMethod } from '@/types/app' +import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' const i18nPrefix = 'workflow.singleRun' @@ -39,6 +40,11 @@ function formatValue(value: string | any, type: InputVarType) { return JSON.parse(item) }) } + if (type === InputVarType.multiFiles) + return getProcessedFiles(value) + + if (type === InputVarType.singleFile) + return getProcessedFiles([value])[0] return value } From 8028e75fbbe4faf771903b5c6fa2e2d7216b3c83 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 25 Nov 2024 12:48:36 +0800 Subject: [PATCH 023/130] Improvement: update api doc of workflow (#11054) --- .../develop/template/template_workflow.en.mdx | 96 ++++++++++++++++- .../develop/template/template_workflow.ja.mdx | 96 ++++++++++++++++- .../develop/template/template_workflow.zh.mdx | 100 +++++++++++++++++- 3 files changed, 286 insertions(+), 6 deletions(-) diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index be2ef54743..e76611eb07 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -54,7 +54,7 @@ Workflow applications offers non-session support and is ideal for translation, a User identifier, used to define the identity of the end-user for retrieval and statistics. Should be uniquely defined by the developer within the application. - `files` (array[object]) Optional - File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports Vision capability. + File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports file parsing and understanding capability. - `type` (string) Supported type: - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') @@ -188,6 +188,19 @@ Workflow applications offers non-session support and is ideal for translation, a }' ``` + + + ```json {{ title: 'File variable example' }} + { + "inputs": { + "{variable_name}": { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + } + } + ``` ### Blocking Mode @@ -223,7 +236,88 @@ Workflow applications offers non-session support and is ideal for translation, a data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} ``` + + ```json {{ title: 'File upload sample code' }} + { + import requests + import json + + def upload_file(file_path, user): + upload_url = "https://api.dify.ai/v1/files/upload" + headers = { + "Authorization": "Bearer app-xxxxxxxx", + } + + try: + print("Upload file...") + with open(file_path, 'rb') as file: + files = { + 'file': (file_path, file, 'text/plain') # Make sure the file is uploaded with the appropriate MIME type + } + data = { + "user": user, + "type": "TXT" # Set the file type to TXT + } + + response = requests.post(upload_url, headers=headers, files=files, data=data) + if response.status_code == 201: # 201 means creation is successful + print("File uploaded successfully") + return response.json().get("id") # Get the uploaded file ID + else: + print(f"File upload failed, status code: {response.status_code}") + return None + except Exception as e: + print(f"Error occurred: {str(e)}") + return None + + def run_workflow(file_id, user, response_mode="blocking"): + workflow_url = "https://api.dify.ai/v1/workflows/run" + headers = { + "Authorization": "Bearer app-xxxxxxxxx", + "Content-Type": "application/json" + } + data = { + "inputs": { + "orig_mail": { + "transfer_method": "local_file", + "upload_file_id": file_id, + "type": "document" + } + }, + "response_mode": response_mode, + "user": user + } + + try: + print("Run Workflow...") + response = requests.post(workflow_url, headers=headers, json=data) + if response.status_code == 200: + print("Workflow execution successful") + return response.json() + else: + print(f"Workflow execution failed, status code: {response.status_code}") + return {"status": "error", "message": f"Failed to execute workflow, status code: {response.status_code}"} + except Exception as e: + print(f"Error occurred: {str(e)}") + return {"status": "error", "message": str(e)} + + # Usage Examples + file_path = "{your_file_path}" + user = "difyuser" + + # Upload files + file_id = upload_file(file_path, user) + if file_id: + # The file was uploaded successfully, and the workflow continues to run + result = run_workflow(file_id, user) + print(result) + else: + print("File upload failed and workflow cannot be executed") + + } + ``` + diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index ad669430f2..609f6a2891 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -54,7 +54,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 アプリケーション内で開発者によって一意に定義される必要があります。 - `files` (array[object]) オプション - ファイルリスト、テキストの理解と質問への回答を組み合わせたファイルの入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 + ファイルリストは、テキスト理解と質問への回答を組み合わせたファイルの入力に適しています。モデルがファイルの解析と理解機能をサポートしている場合にのみ使用できます。 - `type` (string) サポートされているタイプ: - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') @@ -188,6 +188,19 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from }' ``` + + + ```json {{ title: 'ファイル変数の例' }} + { + "inputs": { + "{variable_name}": { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + } + } + ``` ### ブロッキングモード @@ -223,7 +236,88 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} ``` + + ```json {{ title: 'ファイルアップロードのサンプルコード' }} + { + import requests + import json + + def upload_file(file_path, user): + upload_url = "https://api.dify.ai/v1/files/upload" + headers = { + "Authorization": "Bearer app-xxxxxxxx", + } + + try: + print("ファイルをアップロードしています...") + with open(file_path, 'rb') as file: + files = { + 'file': (file_path, file, 'text/plain') # ファイルが適切な MIME タイプでアップロードされていることを確認してください + } + data = { + "user": user, + "type": "TXT" # ファイルタイプをTXTに設定します + } + + response = requests.post(upload_url, headers=headers, files=files, data=data) + if response.status_code == 201: # 201 は作成が成功したことを意味します + print("ファイルが正常にアップロードされました") + return response.json().get("id") # アップロードされたファイルIDを取得する + else: + print(f"ファイルのアップロードに失敗しました。ステータス コード: {response.status_code}") + return None + except Exception as e: + print(f"エラーが発生しました: {str(e)}") + return None + + def run_workflow(file_id, user, response_mode="blocking"): + workflow_url = "https://api.dify.ai/v1/workflows/run" + headers = { + "Authorization": "Bearer app-xxxxxxxxx", + "Content-Type": "application/json" + } + data = { + "inputs": { + "orig_mail": { + "transfer_method": "local_file", + "upload_file_id": file_id, + "type": "document" + } + }, + "response_mode": response_mode, + "user": user + } + + try: + print("ワークフローを実行...") + response = requests.post(workflow_url, headers=headers, json=data) + if response.status_code == 200: + print("ワークフローが正常に実行されました") + return response.json() + else: + print(f"ワークフローの実行がステータス コードで失敗しました: {response.status_code}") + return {"status": "error", "message": f"Failed to execute workflow, status code: {response.status_code}"} + except Exception as e: + print(f"エラーが発生しました: {str(e)}") + return {"status": "error", "message": str(e)} + + # 使用例 + file_path = "{your_file_path}" + user = "difyuser" + + # ファイルをアップロードする + file_id = upload_file(file_path, user) + if file_id: + # ファイルは正常にアップロードされました。ワークフローの実行を続行します + result = run_workflow(file_id, user) + print(result) + else: + print("ファイルのアップロードに失敗し、ワークフローを実行できません") + + } + ``` + diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 5b6fdb1381..71c3d35675 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -52,7 +52,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 用户标识,用于定义终端用户的身份,方便检索、统计。 由开发者定义规则,需保证用户标识在应用内唯一。 - `files` (array[object]) Optional - 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision 能力时可用。 + 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持该类型文件解析能力时可用。 - `type` (string) 支持类型: - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' @@ -171,8 +171,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - - + ```bash {{ title: 'cURL' }} curl -X POST '${props.appDetail.api_base_url}/workflows/run' \ --header 'Authorization: Bearer {api_key}' \ @@ -183,7 +182,19 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 "user": "abc-123" }' ``` - + + + ```json {{ title: 'File variable example' }} + { + "inputs": { + "{variable_name}": { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + } + } + ``` ### Blocking Mode @@ -219,7 +230,88 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} ``` + + ```json {{ title: 'File upload sample code' }} + { + import requests + import json + + def upload_file(file_path, user): + upload_url = "https://api.dify.ai/v1/files/upload" + headers = { + "Authorization": "Bearer app-xxxxxxxx", + } + + try: + print("上传文件中...") + with open(file_path, 'rb') as file: + files = { + 'file': (file_path, file, 'text/plain') # 确保文件以适当的MIME类型上传 + } + data = { + "user": user, + "type": "TXT" # 设置文件类型为TXT + } + + response = requests.post(upload_url, headers=headers, files=files, data=data) + if response.status_code == 201: # 201 表示创建成功 + print("文件上传成功") + return response.json().get("id") # 获取上传的文件 ID + else: + print(f"文件上传失败,状态码: {response.status_code}") + return None + except Exception as e: + print(f"发生错误: {str(e)}") + return None + + def run_workflow(file_id, user, response_mode="blocking"): + workflow_url = "https://api.dify.ai/v1/workflows/run" + headers = { + "Authorization": "Bearer app-xxxxxxxxx", + "Content-Type": "application/json" + } + + data = { + "inputs": { + "orig_mail": { + "transfer_method": "local_file", + "upload_file_id": file_id, + "type": "document" + } + }, + "response_mode": response_mode, + "user": user + } + try: + print("运行工作流...") + response = requests.post(workflow_url, headers=headers, json=data) + if response.status_code == 200: + print("工作流执行成功") + return response.json() + else: + print(f"工作流执行失败,状态码: {response.status_code}") + return {"status": "error", "message": f"Failed to execute workflow, status code: {response.status_code}"} + except Exception as e: + print(f"发生错误: {str(e)}") + return {"status": "error", "message": str(e)} + + # 使用示例 + file_path = "{your_file_path}" + user = "difyuser" + + # 上传文件 + file_id = upload_file(file_path, user) + if file_id: + # 文件上传成功,继续运行工作流 + result = run_workflow(file_id, user) + print(result) + else: + print("文件上传失败,无法执行工作流") + + } + ``` + From 04b9a2c6055dca1a0ffc1b3ba0d6df0c808720b2 Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 25 Nov 2024 13:50:03 +0800 Subject: [PATCH 024/130] fix: better path trigger for vdb and fix the version (#11057) Signed-off-by: yihong0618 --- .github/workflows/vdb-tests.yml | 2 ++ docker-legacy/docker-compose.chroma.yaml | 2 +- docker/docker-compose.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index a5ba51ce0e..73af370063 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -8,6 +8,8 @@ on: - api/core/rag/datasource/** - docker/** - .github/workflows/vdb-tests.yml + - api/poetry.lock + - api/pyproject.toml concurrency: group: vdb-tests-${{ github.head_ref || github.run_id }} diff --git a/docker-legacy/docker-compose.chroma.yaml b/docker-legacy/docker-compose.chroma.yaml index a943d620c0..63354305de 100644 --- a/docker-legacy/docker-compose.chroma.yaml +++ b/docker-legacy/docker-compose.chroma.yaml @@ -1,7 +1,7 @@ services: # Chroma vector store. chroma: - image: ghcr.io/chroma-core/chroma:0.5.1 + image: ghcr.io/chroma-core/chroma:0.5.20 restart: always volumes: - ./volumes/chroma:/chroma/chroma diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 285f576b0e..11eab3170d 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -605,7 +605,7 @@ services: # Chroma vector database chroma: - image: ghcr.io/chroma-core/chroma:0.5.1 + image: ghcr.io/chroma-core/chroma:0.5.20 profiles: - chroma restart: always From eb542067af9a826f6e3e849b63ec73eb5553f50e Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 25 Nov 2024 16:31:49 +0800 Subject: [PATCH 025/130] feat: add cookie management (#11061) --- web/app/components/base/ga/index.tsx | 6 ++++++ web/app/signin/layout.tsx | 15 --------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/web/app/components/base/ga/index.tsx b/web/app/components/base/ga/index.tsx index 219724113f..0015edbfca 100644 --- a/web/app/components/base/ga/index.tsx +++ b/web/app/components/base/ga/index.tsx @@ -47,6 +47,12 @@ gtag('config', '${gaIdMaps[gaType]}'); nonce={nonce!} > + {/* Cookie banner */} + ) diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx index 342876bc53..b404c5c4de 100644 --- a/web/app/signin/layout.tsx +++ b/web/app/signin/layout.tsx @@ -1,25 +1,10 @@ -import Script from 'next/script' import Header from './_header' import style from './page.module.css' import cn from '@/utils/classnames' -import { IS_CE_EDITION } from '@/config' export default async function SignInLayout({ children }: any) { return <> - {!IS_CE_EDITION && ( - <> - - - - )} -
Date: Mon, 25 Nov 2024 10:00:42 +0000 Subject: [PATCH 026/130] fix: ignore empty outputs in Tool node (#10988) --- api/core/workflow/nodes/tool/tool_node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 5560f26456..951e5330a3 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -250,9 +250,8 @@ class ToolNode(BaseNode[ToolNodeData]): f"{message.message}" if message.type == ToolInvokeMessage.MessageType.TEXT else f"Link: {message.message}" - if message.type == ToolInvokeMessage.MessageType.LINK - else "" for message in tool_response + if message.type in {ToolInvokeMessage.MessageType.TEXT, ToolInvokeMessage.MessageType.LINK} ] ) From 98d85e6b747d87d5f9cdceb140caf425a1f19a6d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 25 Nov 2024 18:16:55 +0800 Subject: [PATCH 027/130] fix: WorkflowNodeExecution.created_at may be earlier than WorkflowRun.created_at (#11070) --- .../task_pipeline/workflow_cycle_manage.py | 49 ++++++++++--------- api/models/workflow.py | 10 ++-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 9229cbcc0a..d45726af46 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -3,6 +3,7 @@ import time from collections.abc import Mapping, Sequence from datetime import UTC, datetime from typing import Any, Optional, Union, cast +from uuid import uuid4 from sqlalchemy.orm import Session @@ -80,38 +81,38 @@ class WorkflowCycleManage: inputs[f"sys.{key.value}"] = value - inputs = WorkflowEntry.handle_special_values(inputs) - triggered_from = ( WorkflowRunTriggeredFrom.DEBUGGING if self._application_generate_entity.invoke_from == InvokeFrom.DEBUGGER else WorkflowRunTriggeredFrom.APP_RUN ) + # handle special values + inputs = WorkflowEntry.handle_special_values(inputs) + # init workflow run - workflow_run = WorkflowRun() - workflow_run_id = self._workflow_system_variables[SystemVariableKey.WORKFLOW_RUN_ID] - if workflow_run_id: - workflow_run.id = workflow_run_id - workflow_run.tenant_id = self._workflow.tenant_id - workflow_run.app_id = self._workflow.app_id - workflow_run.sequence_number = new_sequence_number - workflow_run.workflow_id = self._workflow.id - workflow_run.type = self._workflow.type - workflow_run.triggered_from = triggered_from.value - workflow_run.version = self._workflow.version - workflow_run.graph = self._workflow.graph - workflow_run.inputs = json.dumps(inputs) - workflow_run.status = WorkflowRunStatus.RUNNING.value - workflow_run.created_by_role = ( - CreatedByRole.ACCOUNT.value if isinstance(self._user, Account) else CreatedByRole.END_USER.value - ) - workflow_run.created_by = self._user.id + with Session(db.engine, expire_on_commit=False) as session: + workflow_run = WorkflowRun() + system_id = self._workflow_system_variables[SystemVariableKey.WORKFLOW_RUN_ID] + workflow_run.id = system_id or str(uuid4()) + workflow_run.tenant_id = self._workflow.tenant_id + workflow_run.app_id = self._workflow.app_id + workflow_run.sequence_number = new_sequence_number + workflow_run.workflow_id = self._workflow.id + workflow_run.type = self._workflow.type + workflow_run.triggered_from = triggered_from.value + workflow_run.version = self._workflow.version + workflow_run.graph = self._workflow.graph + workflow_run.inputs = json.dumps(inputs) + workflow_run.status = WorkflowRunStatus.RUNNING + workflow_run.created_by_role = ( + CreatedByRole.ACCOUNT if isinstance(self._user, Account) else CreatedByRole.END_USER + ) + workflow_run.created_by = self._user.id + workflow_run.created_at = datetime.now(UTC).replace(tzinfo=None) - db.session.add(workflow_run) - db.session.commit() - db.session.refresh(workflow_run) - db.session.close() + session.add(workflow_run) + session.commit() return workflow_run diff --git a/api/models/workflow.py b/api/models/workflow.py index 5b0617828d..fd53f137f9 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping, Sequence from datetime import UTC, datetime -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Optional, Union import sqlalchemy as sa @@ -314,7 +314,7 @@ class Workflow(db.Model): ) -class WorkflowRunStatus(Enum): +class WorkflowRunStatus(StrEnum): """ Workflow Run Status Enum """ @@ -393,13 +393,13 @@ class WorkflowRun(db.Model): version = db.Column(db.String(255), nullable=False) graph = db.Column(db.Text) inputs = db.Column(db.Text) - status = db.Column(db.String(255), nullable=False) - outputs: Mapped[str] = db.Column(db.Text) + status = db.Column(db.String(255), nullable=False) # running, succeeded, failed, stopped + outputs: Mapped[str] = mapped_column(sa.Text, default="{}") error = db.Column(db.Text) elapsed_time = db.Column(db.Float, nullable=False, server_default=db.text("0")) total_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) total_steps = db.Column(db.Integer, server_default=db.text("0")) - created_by_role = db.Column(db.String(255), nullable=False) + created_by_role = db.Column(db.String(255), nullable=False) # account, end_user created_by = db.Column(StringUUID, nullable=False) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) finished_at = db.Column(db.DateTime) From 625aaceb005a43fe4602da70e4c48affa4d250e5 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 25 Nov 2024 19:17:59 +0800 Subject: [PATCH 028/130] chore: bump version to 0.12.0 (#11056) --- api/configs/packaging/__init__.py | 2 +- docker-legacy/docker-compose.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 1f2b8224e8..7e95e79bfb 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="0.11.2", + default="0.12.0", ) COMMIT_SHA: str = Field( diff --git a/docker-legacy/docker-compose.yaml b/docker-legacy/docker-compose.yaml index 7bf2cd4708..7ddb98e272 100644 --- a/docker-legacy/docker-compose.yaml +++ b/docker-legacy/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: # API service api: - image: langgenius/dify-api:0.11.2 + image: langgenius/dify-api:0.12.0 restart: always environment: # Startup mode, 'api' starts the API server. @@ -227,7 +227,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.11.2 + image: langgenius/dify-api:0.12.0 restart: always environment: CONSOLE_WEB_URL: '' @@ -397,7 +397,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.11.2 + image: langgenius/dify-web:0.12.0 restart: always environment: # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 11eab3170d..9a135e7b54 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -291,7 +291,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.11.2 + image: langgenius/dify-api:0.12.0 restart: always environment: # Use the shared environment variables. @@ -311,7 +311,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.11.2 + image: langgenius/dify-api:0.12.0 restart: always environment: # Use the shared environment variables. @@ -330,7 +330,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.11.2 + image: langgenius/dify-web:0.12.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/package.json b/web/package.json index 596faf81ac..f65d87961b 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "0.11.2", + "version": "0.12.0", "private": true, "engines": { "node": ">=18.17.0" From 2e00829b1e7ebae50da09192dc8bf8110a2deca5 Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 25 Nov 2024 19:50:23 +0800 Subject: [PATCH 029/130] fix: drop useless and wrong code for zhipu embedding (#11069) Signed-off-by: yihong0618 --- .../zhipuai/text_embedding/text_embedding.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/api/core/model_runtime/model_providers/zhipuai/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/zhipuai/text_embedding/text_embedding.py index f629b62fd5..2428284ba9 100644 --- a/api/core/model_runtime/model_providers/zhipuai/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/zhipuai/text_embedding/text_embedding.py @@ -105,17 +105,6 @@ class ZhipuAITextEmbeddingModel(_CommonZhipuaiAI, TextEmbeddingModel): return [list(map(float, e)) for e in embeddings], embedding_used_tokens - def embed_query(self, text: str) -> list[float]: - """Call out to ZhipuAI's embedding endpoint. - - Args: - text: The text to embed. - - Returns: - Embeddings for the text. - """ - return self.embed_documents([text])[0] - def _calc_response_usage(self, model: str, credentials: dict, tokens: int) -> EmbeddingUsage: """ Calculate response usage From 56e361ac44609e0db6818e453e8b441d1dab17f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 25 Nov 2024 19:50:33 +0800 Subject: [PATCH 030/130] fix: chart tool chinese font display and raise error (#11058) --- api/core/tools/provider/builtin/chart/chart.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api/core/tools/provider/builtin/chart/chart.py b/api/core/tools/provider/builtin/chart/chart.py index dfa3fbea6a..8fa647d9ed 100644 --- a/api/core/tools/provider/builtin/chart/chart.py +++ b/api/core/tools/provider/builtin/chart/chart.py @@ -1,3 +1,4 @@ +import matplotlib import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties, fontManager @@ -5,7 +6,7 @@ from core.tools.provider.builtin_tool_provider import BuiltinToolProviderControl def set_chinese_font(): - font_list = [ + to_find_fonts = [ "PingFang SC", "SimHei", "Microsoft YaHei", @@ -15,16 +16,16 @@ def set_chinese_font(): "Noto Sans CJK SC", "Noto Sans CJK JP", ] - - for font in font_list: - if font in fontManager.ttflist: - chinese_font = FontProperties(font) - if chinese_font.get_name() == font: - return chinese_font + installed_fonts = frozenset(fontInfo.name for fontInfo in fontManager.ttflist) + for font in to_find_fonts: + if font in installed_fonts: + return FontProperties(font) return FontProperties() +# use non-interactive backend to prevent `RuntimeError: main thread is not in main loop` +matplotlib.use("Agg") # use a business theme plt.style.use("seaborn-v0_8-darkgrid") plt.rcParams["axes.unicode_minus"] = False From 1065917872d5da941dc3c4196e50a8f896d1e5bd Mon Sep 17 00:00:00 2001 From: Tao Wang <74752235+taowang1993@users.noreply.github.com> Date: Mon, 25 Nov 2024 04:53:03 -0800 Subject: [PATCH 031/130] Add grok-vision-beta to xAI + Update grok-beta Features (#11004) --- .../model_providers/x/llm/grok-beta.yaml | 5 +- .../x/llm/grok-vision-beta.yaml | 64 +++++++++++++++++++ .../model_providers/x/llm/llm.py | 2 + 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 api/core/model_runtime/model_providers/x/llm/grok-vision-beta.yaml diff --git a/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml b/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml index 7c305735b9..bb71de2bad 100644 --- a/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml +++ b/api/core/model_runtime/model_providers/x/llm/grok-beta.yaml @@ -1,9 +1,12 @@ model: grok-beta label: - en_US: Grok beta + en_US: Grok Beta model_type: llm features: + - agent-thought + - tool-call - multi-tool-call + - stream-tool-call model_properties: mode: chat context_size: 131072 diff --git a/api/core/model_runtime/model_providers/x/llm/grok-vision-beta.yaml b/api/core/model_runtime/model_providers/x/llm/grok-vision-beta.yaml new file mode 100644 index 0000000000..844f0520bc --- /dev/null +++ b/api/core/model_runtime/model_providers/x/llm/grok-vision-beta.yaml @@ -0,0 +1,64 @@ +model: grok-vision-beta +label: + en_US: Grok Vision Beta +model_type: llm +features: + - agent-thought + - vision +model_properties: + mode: chat + context_size: 8192 +parameter_rules: + - name: temperature + label: + en_US: "Temperature" + zh_Hans: "采样温度" + type: float + default: 0.7 + min: 0.0 + max: 2.0 + precision: 1 + required: true + help: + en_US: "The randomness of the sampling temperature control output. The temperature value is within the range of [0.0, 1.0]. The higher the value, the more random and creative the output; the lower the value, the more stable it is. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样温度控制输出的随机性。温度值在 [0.0, 1.0] 范围内,值越高,输出越随机和创造性;值越低,输出越稳定。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_p + label: + en_US: "Top P" + zh_Hans: "Top P" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The value range of the sampling method is [0.0, 1.0]. The top_p value determines that the model selects tokens from the top p% of candidate words with the highest probability; when top_p is 0, this parameter is invalid. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样方法的取值范围为 [0.0,1.0]。top_p 值确定模型从概率最高的前p%的候选词中选取 tokens;当 top_p 为 0 时,此参数无效。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: frequency_penalty + use_template: frequency_penalty + label: + en_US: "Frequency Penalty" + zh_Hans: "频率惩罚" + type: float + default: 0 + min: 0 + max: 2.0 + precision: 1 + required: false + help: + en_US: "Number between 0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim." + zh_Hans: "介于0和2.0之间的数字。正值会根据新标记在文本中迄今为止的现有频率来惩罚它们,从而降低模型一字不差地重复同一句话的可能性。" + + - name: user + use_template: text + label: + en_US: "User" + zh_Hans: "用户" + type: string + required: false + help: + en_US: "Used to track and differentiate conversation requests from different users." + zh_Hans: "用于追踪和区分不同用户的对话请求。" diff --git a/api/core/model_runtime/model_providers/x/llm/llm.py b/api/core/model_runtime/model_providers/x/llm/llm.py index 3f5325a857..eacd086fee 100644 --- a/api/core/model_runtime/model_providers/x/llm/llm.py +++ b/api/core/model_runtime/model_providers/x/llm/llm.py @@ -35,3 +35,5 @@ class XAILargeLanguageModel(OAIAPICompatLargeLanguageModel): credentials["endpoint_url"] = str(URL(credentials["endpoint_url"])) or "https://api.x.ai/v1" credentials["mode"] = LLMMode.CHAT.value credentials["function_calling_type"] = "tool_call" + credentials["stream_function_calling"] = "support" + credentials["vision_support"] = "support" From c032574491bad38788977fa8bfecb0430ad2a610 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 25 Nov 2024 20:53:55 +0800 Subject: [PATCH 032/130] fix: timezone not imported in conversation service. (#11076) --- api/services/conversation_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index f3e76d3300..8642972710 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from datetime import UTC, datetime from typing import Optional, Union @@ -74,14 +75,14 @@ class ConversationService: return InfiniteScrollPagination(data=conversations, limit=limit, has_more=has_more) @classmethod - def _get_sort_params(cls, sort_by: str) -> tuple[str, callable]: + def _get_sort_params(cls, sort_by: str): if sort_by.startswith("-"): return sort_by[1:], desc return sort_by, asc @classmethod def _build_filter_condition( - cls, sort_field: str, sort_direction: callable, reference_conversation: Conversation, is_next_page: bool = False + cls, sort_field: str, sort_direction: Callable, reference_conversation: Conversation, is_next_page: bool = False ): field_value = getattr(reference_conversation, sort_field) if (sort_direction == desc and not is_next_page) or (sort_direction == asc and is_next_page): @@ -160,5 +161,5 @@ class ConversationService: conversation = cls.get_conversation(app_model, conversation_id, user) conversation.is_deleted = True - conversation.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) + conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() From 8aae235a71efe3bc9e647120202b2d412acc4b9e Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 25 Nov 2024 21:04:16 +0800 Subject: [PATCH 033/130] fix: int None will cause error for context size (#11055) Signed-off-by: yihong0618 --- .../model_runtime/model_providers/gitee_ai/rerank/rerank.py | 2 +- .../model_runtime/model_providers/gpustack/rerank/rerank.py | 2 +- api/core/model_runtime/model_providers/jina/rerank/rerank.py | 2 +- .../model_providers/jina/text_embedding/text_embedding.py | 2 +- .../model_providers/ollama/text_embedding/text_embedding.py | 2 +- .../openai_api_compatible/text_embedding/text_embedding.py | 2 +- .../model_providers/perfxcloud/text_embedding/text_embedding.py | 2 +- .../model_providers/vertex_ai/text_embedding/text_embedding.py | 2 +- .../model_providers/voyage/text_embedding/text_embedding.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/core/model_runtime/model_providers/gitee_ai/rerank/rerank.py b/api/core/model_runtime/model_providers/gitee_ai/rerank/rerank.py index 231345c2f4..832ba92740 100644 --- a/api/core/model_runtime/model_providers/gitee_ai/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/gitee_ai/rerank/rerank.py @@ -122,7 +122,7 @@ class GiteeAIRerankModel(RerankModel): label=I18nObject(en_US=model), model_type=ModelType.RERANK, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512))}, ) return entity diff --git a/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py b/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py index 5ea7532564..feb5777028 100644 --- a/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/gpustack/rerank/rerank.py @@ -140,7 +140,7 @@ class GPUStackRerankModel(RerankModel): label=I18nObject(en_US=model), model_type=ModelType.RERANK, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512))}, ) return entity diff --git a/api/core/model_runtime/model_providers/jina/rerank/rerank.py b/api/core/model_runtime/model_providers/jina/rerank/rerank.py index aacc8e75d3..22f882be6b 100644 --- a/api/core/model_runtime/model_providers/jina/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/jina/rerank/rerank.py @@ -128,7 +128,7 @@ class JinaRerankModel(RerankModel): label=I18nObject(en_US=model), model_type=ModelType.RERANK, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 8000))}, ) return entity diff --git a/api/core/model_runtime/model_providers/jina/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/jina/text_embedding/text_embedding.py index 49c558f4a4..f5be7a9828 100644 --- a/api/core/model_runtime/model_providers/jina/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/jina/text_embedding/text_embedding.py @@ -193,7 +193,7 @@ class JinaTextEmbeddingModel(TextEmbeddingModel): label=I18nObject(en_US=model), model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 8000))}, ) return entity diff --git a/api/core/model_runtime/model_providers/ollama/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/ollama/text_embedding/text_embedding.py index a16c91cd7e..83c4facc8d 100644 --- a/api/core/model_runtime/model_providers/ollama/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/ollama/text_embedding/text_embedding.py @@ -139,7 +139,7 @@ class OllamaEmbeddingModel(TextEmbeddingModel): model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, model_properties={ - ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size")), + ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512)), ModelPropertyKey.MAX_CHUNKS: 1, }, parameter_rules=[], diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/openai_api_compatible/text_embedding/text_embedding.py index c2b7297aac..793c384d5a 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/text_embedding/text_embedding.py @@ -176,7 +176,7 @@ class OAICompatEmbeddingModel(_CommonOaiApiCompat, TextEmbeddingModel): model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, model_properties={ - ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size")), + ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512)), ModelPropertyKey.MAX_CHUNKS: 1, }, parameter_rules=[], diff --git a/api/core/model_runtime/model_providers/perfxcloud/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/perfxcloud/text_embedding/text_embedding.py index d78bdaa75e..7bbd31e87c 100644 --- a/api/core/model_runtime/model_providers/perfxcloud/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/perfxcloud/text_embedding/text_embedding.py @@ -182,7 +182,7 @@ class OAICompatEmbeddingModel(_CommonOaiApiCompat, TextEmbeddingModel): model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, model_properties={ - ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size")), + ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512)), ModelPropertyKey.MAX_CHUNKS: 1, }, parameter_rules=[], diff --git a/api/core/model_runtime/model_providers/vertex_ai/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/vertex_ai/text_embedding/text_embedding.py index 43233e6126..9cd0c78d99 100644 --- a/api/core/model_runtime/model_providers/vertex_ai/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/vertex_ai/text_embedding/text_embedding.py @@ -173,7 +173,7 @@ class VertexAiTextEmbeddingModel(_CommonVertexAi, TextEmbeddingModel): model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, model_properties={ - ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size")), + ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512)), ModelPropertyKey.MAX_CHUNKS: 1, }, parameter_rules=[], diff --git a/api/core/model_runtime/model_providers/voyage/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/voyage/text_embedding/text_embedding.py index e69c9fccba..16f1bd43d8 100644 --- a/api/core/model_runtime/model_providers/voyage/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/voyage/text_embedding/text_embedding.py @@ -166,7 +166,7 @@ class VoyageTextEmbeddingModel(TextEmbeddingModel): label=I18nObject(en_US=model), model_type=ModelType.TEXT_EMBEDDING, fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, + model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 512))}, ) return entity From ab6dcf7032110e5510ee6fa81e948646c319709b Mon Sep 17 00:00:00 2001 From: fengjiajie Date: Mon, 25 Nov 2024 21:13:02 +0800 Subject: [PATCH 034/130] fix: update the max tokens configuration for Azure GPT-4o (2024-08-06) to 16384 (#11074) --- .../model_runtime/model_providers/azure_openai/_constant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/azure_openai/_constant.py b/api/core/model_runtime/model_providers/azure_openai/_constant.py index e61a9e0474..4cf58275d7 100644 --- a/api/core/model_runtime/model_providers/azure_openai/_constant.py +++ b/api/core/model_runtime/model_providers/azure_openai/_constant.py @@ -779,7 +779,7 @@ LLM_BASE_MODELS = [ name="frequency_penalty", **PARAMETER_RULE_TEMPLATE[DefaultParameterName.FREQUENCY_PENALTY], ), - _get_max_tokens(default=512, min_val=1, max_val=4096), + _get_max_tokens(default=512, min_val=1, max_val=16384), ParameterRule( name="seed", label=I18nObject(zh_Hans="种子", en_US="Seed"), From 60c15497713cd27a3dd68b6b6730366ab2d70f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 26 Nov 2024 09:32:08 +0800 Subject: [PATCH 035/130] fix: import Explore Apps raise error (#11091) --- web/app/components/explore/app-list/index.tsx | 8 +++++--- web/utils/app-redirection.ts | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 6186d8164d..bd50708cb4 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -14,7 +14,7 @@ import type { App } from '@/models/explore' import Category from '@/app/components/explore/category' import AppCard from '@/app/components/explore/app-card' import { fetchAppDetail, fetchAppList } from '@/service/explore' -import { importApp } from '@/service/apps' +import { importDSL } from '@/service/apps' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import CreateAppModal from '@/app/components/explore/create-app-modal' import AppTypeSelector from '@/app/components/app/type-selector' @@ -24,6 +24,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { getRedirection } from '@/utils/app-redirection' import Input from '@/app/components/base/input' +import { DSLImportMode } from '@/models/app' type AppsProps = { pageType?: PageType @@ -127,8 +128,9 @@ const Apps = ({ currApp?.app.id as string, ) try { - const app = await importApp({ - data: export_data, + const app = await importDSL({ + mode: DSLImportMode.YAML_CONTENT, + yaml_content: export_data, name, icon_type, icon, diff --git a/web/utils/app-redirection.ts b/web/utils/app-redirection.ts index 534b019250..b14f660e63 100644 --- a/web/utils/app-redirection.ts +++ b/web/utils/app-redirection.ts @@ -4,12 +4,12 @@ export const getRedirection = ( redirectionFunc: (href: string) => void, ) => { if (!isCurrentWorkspaceEditor) { - redirectionFunc(`/app/${app.id}/overview`) + redirectionFunc(`/app/${app.app_id}/overview`) } else { if (app.mode === 'workflow' || app.mode === 'advanced-chat') - redirectionFunc(`/app/${app.id}/workflow`) + redirectionFunc(`/app/${app.app_id}/workflow`) else - redirectionFunc(`/app/${app.id}/configuration`) + redirectionFunc(`/app/${app.app_id}/configuration`) } } From af2461cccc7ff0caf09ae73c7384022e584004ef Mon Sep 17 00:00:00 2001 From: Tao Wang <74752235+taowang1993@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:32:37 -0800 Subject: [PATCH 036/130] Add query_prefix + Return TED Transcript URL for Downstream Scraping Tasks (#11090) --- .../builtin/duckduckgo/tools/ddgo_img.py | 6 +++++ .../builtin/duckduckgo/tools/ddgo_img.yaml | 11 ++++++++++ .../builtin/duckduckgo/tools/ddgo_news.py | 8 ++++++- .../builtin/duckduckgo/tools/ddgo_news.yaml | 11 ++++++++++ .../builtin/duckduckgo/tools/ddgo_search.py | 9 ++++++-- .../builtin/duckduckgo/tools/ddgo_search.yaml | 11 ++++++++++ .../builtin/duckduckgo/tools/ddgo_video.py | 22 ++++++++++++++++--- .../builtin/duckduckgo/tools/ddgo_video.yaml | 11 ++++++++++ 8 files changed, 83 insertions(+), 6 deletions(-) diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py index 54bb38755a..b3c630878f 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.py @@ -18,6 +18,12 @@ class DuckDuckGoImageSearchTool(BuiltinTool): "size": tool_parameters.get("size"), "max_results": tool_parameters.get("max_results"), } + + # Add query_prefix handling + query_prefix = tool_parameters.get("query_prefix", "").strip() + final_query = f"{query_prefix} {query_dict['keywords']}".strip() + query_dict["keywords"] = final_query + response = DDGS().images(**query_dict) markdown_result = "\n\n" json_result = [] diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.yaml index 168cface22..a543d1e218 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_img.yaml @@ -86,3 +86,14 @@ parameters: en_US: The size of the image to be searched. zh_Hans: 要搜索的图片的大小 form: form + - name: query_prefix + label: + en_US: Query Prefix + zh_Hans: 查询前缀 + type: string + required: false + default: "" + form: form + human_description: + en_US: Specific Search e.g. "site:unsplash.com" + zh_Hans: 定向搜索 e.g. "site:unsplash.com" diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.py b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.py index 3a6fd394a8..11da6f5cf7 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.py +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.py @@ -7,7 +7,7 @@ from core.tools.entities.tool_entities import ToolInvokeMessage from core.tools.tool.builtin_tool import BuiltinTool SUMMARY_PROMPT = """ -User's query: +User's query: {query} Here are the news results: @@ -30,6 +30,12 @@ class DuckDuckGoNewsSearchTool(BuiltinTool): "safesearch": "moderate", "region": "wt-wt", } + + # Add query_prefix handling + query_prefix = tool_parameters.get("query_prefix", "").strip() + final_query = f"{query_prefix} {query_dict['keywords']}".strip() + query_dict["keywords"] = final_query + try: response = list(DDGS().news(**query_dict)) if not response: diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.yaml index eb2b67b7c9..6e181e0f41 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_news.yaml @@ -69,3 +69,14 @@ parameters: en_US: Whether to pass the news results to llm for summarization. zh_Hans: 是否需要将新闻结果传给大模型总结 form: form + - name: query_prefix + label: + en_US: Query Prefix + zh_Hans: 查询前缀 + type: string + required: false + default: "" + form: form + human_description: + en_US: Specific Search e.g. "site:msn.com" + zh_Hans: 定向搜索 e.g. "site:msn.com" diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.py b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.py index cbd65d2e77..3cd35d16a6 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.py +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.py @@ -7,7 +7,7 @@ from core.tools.entities.tool_entities import ToolInvokeMessage from core.tools.tool.builtin_tool import BuiltinTool SUMMARY_PROMPT = """ -User's query: +User's query: {query} Here is the search engine result: @@ -26,7 +26,12 @@ class DuckDuckGoSearchTool(BuiltinTool): query = tool_parameters.get("query") max_results = tool_parameters.get("max_results", 5) require_summary = tool_parameters.get("require_summary", False) - response = DDGS().text(query, max_results=max_results) + + # Add query_prefix handling + query_prefix = tool_parameters.get("query_prefix", "").strip() + final_query = f"{query_prefix} {query}".strip() + + response = DDGS().text(final_query, max_results=max_results) if require_summary: results = "\n".join([res.get("body") for res in response]) results = self.summary_results(user_id=user_id, content=results, query=query) diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.yaml index 333c0cb093..54e27d9905 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_search.yaml @@ -39,3 +39,14 @@ parameters: en_US: Whether to pass the search results to llm for summarization. zh_Hans: 是否需要将搜索结果传给大模型总结 form: form + - name: query_prefix + label: + en_US: Query Prefix + zh_Hans: 查询前缀 + type: string + required: false + default: "" + form: form + human_description: + en_US: Specific Search e.g. "site:wikipedia.org" + zh_Hans: 定向搜索 e.g. "site:wikipedia.org" diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.py b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.py index 4b74b223c1..1eef0b1ba2 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.py +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.py @@ -24,7 +24,7 @@ max-width: 100%; border-radius: 8px;"> def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> list[ToolInvokeMessage]: query_dict = { - "keywords": tool_parameters.get("query"), + "keywords": tool_parameters.get("query"), # LLM's query "region": tool_parameters.get("region", "wt-wt"), "safesearch": tool_parameters.get("safesearch", "moderate"), "timelimit": tool_parameters.get("timelimit"), @@ -40,6 +40,12 @@ max-width: 100%; border-radius: 8px;"> # Get proxy URL from parameters proxy_url = tool_parameters.get("proxy_url", "").strip() + query_prefix = tool_parameters.get("query_prefix", "").strip() + final_query = f"{query_prefix} {query_dict['keywords']}".strip() + + # Update the keywords in query_dict with the final_query + query_dict["keywords"] = final_query + response = DDGS().videos(**query_dict) # Create HTML result with embedded iframes @@ -51,9 +57,13 @@ max-width: 100%; border-radius: 8px;"> embed_html = res.get("embed_html", "") description = res.get("description", "") content_url = res.get("content", "") + transcript_url = None # Handle TED.com videos - if not embed_html and "ted.com/talks" in content_url: + if "ted.com/talks" in content_url: + # Create transcript URL + transcript_url = f"{content_url}/transcript" + # Create embed URL embed_url = content_url.replace("www.ted.com", "embed.ted.com") if proxy_url: embed_url = f"{proxy_url}{embed_url}" @@ -68,8 +78,14 @@ max-width: 100%; border-radius: 8px;"> markdown_result += f"{title}\n\n" markdown_result += f"{embed_html}\n\n" + if description: + markdown_result += f"{description}\n\n" markdown_result += "---\n\n" - json_result.append(self.create_json_message(res)) + # Add transcript_url to the JSON result if available + result_dict = res.copy() + if transcript_url: + result_dict["transcript_url"] = transcript_url + json_result.append(self.create_json_message(result_dict)) return [self.create_text_message(markdown_result)] + json_result diff --git a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.yaml b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.yaml index a516d3cb98..d846244e3d 100644 --- a/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.yaml +++ b/api/core/tools/provider/builtin/duckduckgo/tools/ddgo_video.yaml @@ -95,3 +95,14 @@ parameters: en_US: Proxy URL zh_Hans: 视频代理地址 form: form + - name: query_prefix + label: + en_US: Query Prefix + zh_Hans: 查询前缀 + type: string + required: false + default: "" + form: form + human_description: + en_US: Specific Search e.g. "site:www.ted.com" + zh_Hans: 定向搜索 e.g. "site:www.ted.com" From 17ee731546a80b98d1c75563b66ba12688926675 Mon Sep 17 00:00:00 2001 From: SebastjanPrachovskij <86522260+SebastjanPrachovskij@users.noreply.github.com> Date: Tue, 26 Nov 2024 03:34:51 +0200 Subject: [PATCH 037/130] SearchApi - Return error message instead of raising a ValueError (#11083) --- api/core/tools/provider/builtin/searchapi/tools/google.py | 2 +- api/core/tools/provider/builtin/searchapi/tools/google_jobs.py | 2 +- api/core/tools/provider/builtin/searchapi/tools/google_news.py | 2 +- .../provider/builtin/searchapi/tools/youtube_transcripts.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/core/tools/provider/builtin/searchapi/tools/google.py b/api/core/tools/provider/builtin/searchapi/tools/google.py index 17e2978194..29d36f5f23 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google.py @@ -45,7 +45,7 @@ class SearchAPI: def _process_response(res: dict, type: str) -> str: """Process response from SearchAPI.""" if "error" in res: - raise ValueError(f"Got error from SearchApi: {res['error']}") + return res["error"] toret = "" if type == "text": diff --git a/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py b/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py index c478bc108b..de42360898 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google_jobs.py @@ -45,7 +45,7 @@ class SearchAPI: def _process_response(res: dict, type: str) -> str: """Process response from SearchAPI.""" if "error" in res: - raise ValueError(f"Got error from SearchApi: {res['error']}") + return res["error"] toret = "" if type == "text": diff --git a/api/core/tools/provider/builtin/searchapi/tools/google_news.py b/api/core/tools/provider/builtin/searchapi/tools/google_news.py index 562bc01964..c8b3ccda05 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/google_news.py +++ b/api/core/tools/provider/builtin/searchapi/tools/google_news.py @@ -45,7 +45,7 @@ class SearchAPI: def _process_response(res: dict, type: str) -> str: """Process response from SearchAPI.""" if "error" in res: - raise ValueError(f"Got error from SearchApi: {res['error']}") + return res["error"] toret = "" if type == "text": diff --git a/api/core/tools/provider/builtin/searchapi/tools/youtube_transcripts.py b/api/core/tools/provider/builtin/searchapi/tools/youtube_transcripts.py index 1867cf7be7..b14821f831 100644 --- a/api/core/tools/provider/builtin/searchapi/tools/youtube_transcripts.py +++ b/api/core/tools/provider/builtin/searchapi/tools/youtube_transcripts.py @@ -45,7 +45,7 @@ class SearchAPI: def _process_response(res: dict) -> str: """Process response from SearchAPI.""" if "error" in res: - raise ValueError(f"Got error from SearchApi: {res['error']}") + return res["error"] toret = "" if "transcripts" in res and "text" in res["transcripts"][0]: From 0f85e3557ba12f1bf0175ebe491452c34ec2a6f7 Mon Sep 17 00:00:00 2001 From: Hash Brown Date: Tue, 26 Nov 2024 10:23:03 +0800 Subject: [PATCH 038/130] fix: site icon not showing (#11094) --- api/libs/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index 023240a9a4..b98a4829e8 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -31,12 +31,12 @@ class AppIconUrlField(fields.Raw): if obj is None: return None - from models.model import App, IconType + from models.model import App, IconType, Site if isinstance(obj, dict) and "app" in obj: obj = obj["app"] - if isinstance(obj, App) and obj.icon_type == IconType.IMAGE.value: + if isinstance(obj, App | Site) and obj.icon_type == IconType.IMAGE.value: return file_helpers.get_signed_file_url(obj.icon) return None From f1366e8e195d4a86759a010cd5322a8571ee57ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 26 Nov 2024 10:25:42 +0800 Subject: [PATCH 039/130] fix #11091 raise redirect issue (#11092) --- web/app/components/explore/app-list/index.tsx | 2 +- web/utils/app-redirection.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index bd50708cb4..b8e7939328 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -145,7 +145,7 @@ const Apps = ({ if (onSuccess) onSuccess() localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') - getRedirection(isCurrentWorkspaceEditor, app, push) + getRedirection(isCurrentWorkspaceEditor, { id: app.app_id }, push) } catch (e) { Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) diff --git a/web/utils/app-redirection.ts b/web/utils/app-redirection.ts index b14f660e63..534b019250 100644 --- a/web/utils/app-redirection.ts +++ b/web/utils/app-redirection.ts @@ -4,12 +4,12 @@ export const getRedirection = ( redirectionFunc: (href: string) => void, ) => { if (!isCurrentWorkspaceEditor) { - redirectionFunc(`/app/${app.app_id}/overview`) + redirectionFunc(`/app/${app.id}/overview`) } else { if (app.mode === 'workflow' || app.mode === 'advanced-chat') - redirectionFunc(`/app/${app.app_id}/workflow`) + redirectionFunc(`/app/${app.id}/workflow`) else - redirectionFunc(`/app/${app.app_id}/configuration`) + redirectionFunc(`/app/${app.id}/configuration`) } } From 9f75970347f81e8556478fdbe402dcdbfdd2bc1d Mon Sep 17 00:00:00 2001 From: horochx <32632779+horochx@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:29:00 +0800 Subject: [PATCH 040/130] fix: ops_trace_manager `from_end_user_id` (#11077) --- api/core/ops/ops_trace_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index 1069889abd..b7799ce1fb 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -445,7 +445,7 @@ class TraceTask: "ls_provider": message_data.model_provider, "ls_model_name": message_data.model_id, "status": message_data.status, - "from_end_user_id": message_data.from_account_id, + "from_end_user_id": message_data.from_end_user_id, "from_account_id": message_data.from_account_id, "agent_based": message_data.agent_based, "workflow_run_id": message_data.workflow_run_id, @@ -521,7 +521,7 @@ class TraceTask: "ls_provider": message_data.model_provider, "ls_model_name": message_data.model_id, "status": message_data.status, - "from_end_user_id": message_data.from_account_id, + "from_end_user_id": message_data.from_end_user_id, "from_account_id": message_data.from_account_id, "agent_based": message_data.agent_based, "workflow_run_id": message_data.workflow_run_id, @@ -570,7 +570,7 @@ class TraceTask: "ls_provider": message_data.model_provider, "ls_model_name": message_data.model_id, "status": message_data.status, - "from_end_user_id": message_data.from_account_id, + "from_end_user_id": message_data.from_end_user_id, "from_account_id": message_data.from_account_id, "agent_based": message_data.agent_based, "workflow_run_id": message_data.workflow_run_id, From e9c098d024e02e9a40e96b3937422bc5c467ead9 Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 26 Nov 2024 11:33:04 +0800 Subject: [PATCH 041/130] Fix regenerate themes (#11101) --- web/themes/dark.css | 154 ++++++++++++++++++++++-- web/themes/light.css | 148 +++++++++++++++++++++-- web/themes/tailwind-theme-var-define.ts | 138 +++++++++++++++++++-- 3 files changed, 406 insertions(+), 34 deletions(-) diff --git a/web/themes/dark.css b/web/themes/dark.css index f89c59190d..892f48fa48 100644 --- a/web/themes/dark.css +++ b/web/themes/dark.css @@ -85,6 +85,10 @@ html[data-theme="dark"] { --color-components-button-secondary-accent-border-hover: #FFFFFF1F; --color-components-button-secondary-accent-border-disabled: #FFFFFF0D; + --color-components-button-indigo-bg: #444CE7; + --color-components-button-indigo-bg-hover: #6172F3; + --color-components-button-indigo-bg-disabled: #FFFFFF08; + --color-components-checkbox-icon: #FFFFFFF2; --color-components-checkbox-icon-disabled: #FFFFFF33; --color-components-checkbox-bg: #296DFF; @@ -95,10 +99,11 @@ html[data-theme="dark"] { --color-components-checkbox-border-disabled: #FFFFFF03; --color-components-checkbox-bg-unchecked: #FFFFFF08; --color-components-checkbox-bg-unchecked-hover: #FFFFFF0D; + --color-components-checkbox-bg-disabled-checked: #155AEF33; --color-components-radio-border-checked: #296DFF; --color-components-radio-border-checked-hover: #5289FF; - --color-components-radio-border-checked-disabled: #FFFFFF14; + --color-components-radio-border-checked-disabled: #155AEF33; --color-components-radio-bg-disabled: #FFFFFF08; --color-components-radio-border: #FFFFFF66; --color-components-radio-border-hover: #FFFFFF99; @@ -135,6 +140,9 @@ html[data-theme="dark"] { --color-components-panel-on-panel-item-bg: #27272B; --color-components-panel-on-panel-item-bg-hover: #3A3A40; --color-components-panel-on-panel-item-bg-alt: #3A3A40; + --color-components-panel-on-panel-item-bg-transparent: #2C2C30F2; + --color-components-panel-on-panel-item-bg-hover-transparent: #3A3A4000; + --color-components-panel-on-panel-item-bg-destructive-hover-transparent: #FFFBFA00; --color-components-panel-bg-transparent: #22222500; @@ -208,10 +216,12 @@ html[data-theme="dark"] { --color-components-actionbar-bg: #222225; --color-components-actionbar-border: #C8CEDA14; + --color-components-actionbar-bg-accent: #27272B; + --color-components-actionbar-border-accent: #5289FF; --color-components-dropzone-bg-alt: #18181BCC; --color-components-dropzone-bg: #18181B66; - --color-components-dropzone-bg-accent: #155AEF24; + --color-components-dropzone-bg-accent: #155AEF33; --color-components-dropzone-border: #C8CEDA24; --color-components-dropzone-border-alt: #C8CEDA33; --color-components-dropzone-border-accent: #84ABFF; @@ -228,6 +238,14 @@ html[data-theme="dark"] { --color-components-progress-gray-border: #98A2B2; --color-components-progress-gray-bg: #C8CEDA05; + --color-components-progress-warning-progress: #FDB022; + --color-components-progress-warning-border: #FDB022; + --color-components-progress-warning-bg: #F790090A; + + --color-components-progress-error-progress: #F97066; + --color-components-progress-error-border: #F97066; + --color-components-progress-error-bg: #F044380A; + --color-components-chat-input-audio-bg: #155AEF33; --color-components-chat-input-audio-wave-default: #C8CEDA24; --color-components-chat-input-bg-mask-1: #18181B0A; @@ -236,13 +254,103 @@ html[data-theme="dark"] { --color-components-chat-input-audio-wave-active: #84ABFF; --color-components-chat-input-audio-bg-alt: #18181BE5; - --color-components-Avatar-shape-fill-stop-0: #FFFFFFF2; - --color-components-Avatar-shape-fill-stop-100: #FFFFFFCC; - - --color-components-Avatar-bg-mask-stop-0: #FFFFFF33; - --color-components-Avatar-bg-mask-stop-100: #FFFFFF08; - - --color-components-Avatar-default-avatar-bg: #222225; + --color-components-avatar-shape-fill-stop-0: #FFFFFFF2; + --color-components-avatar-shape-fill-stop-100: #FFFFFFCC; + + --color-components-avatar-bg-mask-stop-0: #FFFFFF33; + --color-components-avatar-bg-mask-stop-100: #FFFFFF08; + + --color-components-avatar-default-avatar-bg: #222225; + --color-components-avatar-mask-darkmode-dimmed: #0000001F; + + --color-components-label-gray: #C8CEDA24; + + --color-components-premium-badge-blue-bg-stop-0: #5289FF; + --color-components-premium-badge-blue-bg-stop-100: #296DFF; + --color-components-premium-badge-blue-stroke-stop-0: #FFFFFF33; + --color-components-premium-badge-blue-stroke-stop-100: #296DFF; + --color-components-premium-badge-blue-text-stop-0: #EFF4FF; + --color-components-premium-badge-blue-text-stop-100: #B2CAFF; + --color-components-premium-badge-blue-glow: #004AEB; + --color-components-premium-badge-blue-bg-stop-0-hover: #84ABFF; + --color-components-premium-badge-blue-bg-stop-100-hover: #004AEB; + --color-components-premium-badge-blue-glow-hover: #D1E0FF; + --color-components-premium-badge-blue-stroke-stop-0-hover: #FFFFFF80; + --color-components-premium-badge-blue-stroke-stop-100-hover: #296DFF; + + --color-components-premium-badge-highlight-stop-0: #FFFFFF1F; + --color-components-premium-badge-highlight-stop-100: #FFFFFF33; + --color-components-premium-badge-indigo-bg-stop-0: #6172F3; + --color-components-premium-badge-indigo-bg-stop-100: #3538CD; + --color-components-premium-badge-indigo-stroke-stop-0: #FFFFFF33; + --color-components-premium-badge-indigo-stroke-stop-100: #444CE7; + --color-components-premium-badge-indigo-text-stop-0: #EEF4FF; + --color-components-premium-badge-indigo-text-stop-100: #C7D7FE; + --color-components-premium-badge-indigo-glow: #3538CD; + --color-components-premium-badge-indigo-glow-hover: #E0EAFF; + --color-components-premium-badge-indigo-bg-stop-0-hover: #A4BCFD; + --color-components-premium-badge-indigo-bg-stop-100-hover: #3538CD; + --color-components-premium-badge-indigo-stroke-stop-0-hover: #FFFFFF80; + --color-components-premium-badge-indigo-stroke-stop-100-hover: #444CE7; + + --color-components-premium-badge-grey-bg-stop-0: #676F83; + --color-components-premium-badge-grey-bg-stop-100: #495464; + --color-components-premium-badge-grey-stroke-stop-0: #FFFFFF1F; + --color-components-premium-badge-grey-stroke-stop-100: #495464; + --color-components-premium-badge-grey-text-stop-0: #F9FAFB; + --color-components-premium-badge-grey-text-stop-100: #E9EBF0; + --color-components-premium-badge-grey-glow: #354052; + --color-components-premium-badge-grey-glow-hover: #F2F4F7; + --color-components-premium-badge-grey-bg-stop-0-hover: #98A2B2; + --color-components-premium-badge-grey-bg-stop-100-hover: #354052; + --color-components-premium-badge-grey-stroke-stop-0-hover: #FFFFFF80; + --color-components-premium-badge-grey-stroke-stop-100-hover: #676F83; + + --color-components-premium-badge-orange-bg-stop-0: #FF692E; + --color-components-premium-badge-orange-bg-stop-100: #E04F16; + --color-components-premium-badge-orange-stroke-stop-0: #FFFFFF33; + --color-components-premium-badge-orange-stroke-stop-100: #FF4405; + --color-components-premium-badge-orange-text-stop-0: #FEF6EE; + --color-components-premium-badge-orange-text-stop-100: #F9DBAF; + --color-components-premium-badge-orange-glow: #B93815; + --color-components-premium-badge-orange-glow-hover: #FDEAD7; + --color-components-premium-badge-orange-bg-stop-0-hover: #FF692E; + --color-components-premium-badge-orange-bg-stop-100-hover: #B93815; + --color-components-premium-badge-orange-stroke-stop-0-hover: #FFFFFF80; + --color-components-premium-badge-orange-stroke-stop-100-hover: #FF4405; + + --color-components-progress-bar-bg: #C8CEDA14; + --color-components-progress-bar-progress: #C8CEDA24; + --color-components-progress-bar-border: #FFFFFF08; + --color-components-progress-bar-progress-solid: #FFFFFFF2; + --color-components-progress-bar-progress-highlight: #C8CEDA33; + + --color-components-icon-bg-red-solid: #D92D20; + --color-components-icon-bg-rose-solid: #E31B54; + --color-components-icon-bg-pink-solid: #DD2590; + --color-components-icon-bg-orange-dark-solid: #FF4405; + --color-components-icon-bg-yellow-solid: #EAAA08; + --color-components-icon-bg-green-solid: #4CA30D; + --color-components-icon-bg-teal-solid: #0E9384; + --color-components-icon-bg-blue-light-solid: #0BA5EC; + --color-components-icon-bg-blue-solid: #155AEF; + --color-components-icon-bg-indigo-solid: #444CE7; + --color-components-icon-bg-violet-solid: #7839EE; + --color-components-icon-bg-midnight-solid: #5D698D; + --color-components-icon-bg-rose-soft: #F63D6833; + --color-components-icon-bg-pink-soft: #EE46BC33; + --color-components-icon-bg-orange-dark-soft: #FF440533; + --color-components-icon-bg-yellow-soft: #EAAA0833; + --color-components-icon-bg-green-soft: #66C61C33; + --color-components-icon-bg-teal-soft: #15B79E33; + --color-components-icon-bg-blue-light-soft: #0BA5EC33; + --color-components-icon-bg-blue-soft: #155AEF33; + --color-components-icon-bg-indigo-soft: #6172F333; + --color-components-icon-bg-violet-soft: #875BF733; + --color-components-icon-bg-midnight-soft: #828DAD33; + --color-components-icon-bg-red-soft: #F0443833; + --color-components-icon-bg-orange-solid: #F79009; + --color-components-icon-bg-orange-soft: #F7900933; --color-text-primary: #FBFBFC; --color-text-secondary: #D9D9DE; @@ -302,6 +410,7 @@ html[data-theme="dark"] { --color-background-overlay-alt: #18181B66; --color-background-surface-white: #FFFFFFE5; --color-background-overlay-destructive: #F044384D; + --color-background-overlay-backdrop: #18181BF2; --color-shadow-shadow-1: #0000000D; --color-shadow-shadow-3: #0000001A; @@ -317,14 +426,20 @@ html[data-theme="dark"] { --color-workflow-block-border: #FFFFFF14; --color-workflow-block-parma-bg: #FFFFFF0D; --color-workflow-block-bg: #27272B; + --color-workflow-block-bg-transparent: #27272BF5; --color-workflow-block-border-highlight: #C8CEDA33; --color-workflow-canvas-workflow-dot-color: #8585AD26; --color-workflow-canvas-workflow-bg: #1D1D20; - --color-workflow-link-line-active: #296DFF; + --color-workflow-link-line-active: #5289FF; --color-workflow-link-line-normal: #676F83; - --color-workflow-link-line-handle: #296DFF; + --color-workflow-link-line-handle: #5289FF; + --color-workflow-link-line-normal-transparent: #676F8333; + --color-workflow-link-line-failure-active: #FDB022; + --color-workflow-link-line-failure-handle: #FDB022; + --color-workflow-link-line-failure-button-bg: #F79009; + --color-workflow-link-line-failure-button-hover: #DC6803; --color-workflow-link-line-success-active: #47CD89; --color-workflow-link-line-success-handle: #47CD89; @@ -341,8 +456,8 @@ html[data-theme="dark"] { --color-workflow-display-success-vignette-color: #17B26A40; --color-workflow-display-success-bg-line-pattern: #18181BCC; - --color-workflow-display-glass-1: #FFFFFF03; - --color-workflow-display-glass-2: #FFFFFF08; + --color-workflow-display-glass-1: #FFFFFF08; + --color-workflow-display-glass-2: #FFFFFF0D; --color-workflow-display-vignette-dark: #00000066; --color-workflow-display-highlight: #FFFFFF1F; --color-workflow-display-outline: #18181BF2; @@ -431,6 +546,7 @@ html[data-theme="dark"] { --color-util-colors-orange-orange-500: #EF6820; --color-util-colors-orange-orange-600: #F38744; --color-util-colors-orange-orange-700: #F7B27A; + --color-util-colors-orange-orange-100-transparent: #77291700; --color-util-colors-pink-pink-50: #4E0D30; --color-util-colors-pink-pink-100: #851651; @@ -606,4 +722,16 @@ html[data-theme="dark"] { --color-third-party-LangChain: #FFFFFF; --color-third-party-Langfuse: #FFFFFF; --color-third-party-Github: #FFFFFF; + --color-third-party-Github-tertiary: #C8CEDA99; + --color-third-party-Github-secondary: #D9D9DE; + --color-third-party-model-bg-openai: #121212; + --color-third-party-model-bg-anthropic: #1D1917; + --color-third-party-model-bg-default: #0B0B0E; + + --color-third-party-aws: #141F2E; + --color-third-party-aws-alt: #192639; + + --color-saas-background: #0B0B0E; + --color-saas-pricing-grid-bg: #C8CEDA33; + } \ No newline at end of file diff --git a/web/themes/light.css b/web/themes/light.css index 3b9c15505c..6fb2a6b00c 100644 --- a/web/themes/light.css +++ b/web/themes/light.css @@ -85,8 +85,12 @@ html[data-theme="light"] { --color-components-button-secondary-accent-border-hover: #10182824; --color-components-button-secondary-accent-border-disabled: #1018280A; + --color-components-button-indigo-bg: #444CE7; + --color-components-button-indigo-bg-hover: #3538CD; + --color-components-button-indigo-bg-disabled: #6172F324; + --color-components-checkbox-icon: #FFFFFF; - --color-components-checkbox-icon-disabled: #D0D5DC; + --color-components-checkbox-icon-disabled: #FFFFFF80; --color-components-checkbox-bg: #155AEF; --color-components-checkbox-bg-hover: #004AEB; --color-components-checkbox-bg-disabled: #F2F4F7; @@ -95,10 +99,11 @@ html[data-theme="light"] { --color-components-checkbox-border-disabled: #18181B0A; --color-components-checkbox-bg-unchecked: #FFFFFF; --color-components-checkbox-bg-unchecked-hover: #FFFFFF; + --color-components-checkbox-bg-disabled-checked: #B2CAFF; --color-components-radio-border-checked: #155AEF; --color-components-radio-border-checked-hover: #004AEB; - --color-components-radio-border-checked-disabled: #F2F4F7; + --color-components-radio-border-checked-disabled: #B2CAFF; --color-components-radio-bg-disabled: #FFFFFF00; --color-components-radio-border: #D0D5DC; --color-components-radio-border-hover: #98A2B2; @@ -135,6 +140,9 @@ html[data-theme="light"] { --color-components-panel-on-panel-item-bg: #FFFFFF; --color-components-panel-on-panel-item-bg-hover: #F9FAFB; --color-components-panel-on-panel-item-bg-alt: #F9FAFB; + --color-components-panel-on-panel-item-bg-transparent: #FFFFFFF2; + --color-components-panel-on-panel-item-bg-hover-transparent: #F9FAFB00; + --color-components-panel-on-panel-item-bg-destructive-hover-transparent: #FEF3F200; --color-components-panel-bg-transparent: #FFFFFF00; @@ -161,10 +169,10 @@ html[data-theme="light"] { --color-components-segmented-control-item-active-accent-bg: #FFFFFF; --color-components-segmented-control-item-active-accent-border: #FFFFFF; - --color-components-option-card-option-bg: #F9FAFB; + --color-components-option-card-option-bg: #FCFCFD; --color-components-option-card-option-selected-bg: #FFFFFF; --color-components-option-card-option-selected-border: #296DFF; - --color-components-option-card-option-border: #F2F4F7; + --color-components-option-card-option-border: #E9EBF0; --color-components-option-card-option-bg-hover: #FFFFFF; --color-components-option-card-option-border-hover: #D0D5DC; @@ -208,10 +216,12 @@ html[data-theme="light"] { --color-components-actionbar-bg: #FFFFFFF2; --color-components-actionbar-border: #1018280A; + --color-components-actionbar-bg-accent: #F5F7FF; + --color-components-actionbar-border-accent: #B2CAFF; --color-components-dropzone-bg-alt: #F2F4F7; --color-components-dropzone-bg: #F9FAFB; - --color-components-dropzone-bg-accent: #EFF4FF; + --color-components-dropzone-bg-accent: #155AEF24; --color-components-dropzone-border: #10182814; --color-components-dropzone-border-alt: #10182833; --color-components-dropzone-border-accent: #84ABFF; @@ -228,6 +238,14 @@ html[data-theme="light"] { --color-components-progress-gray-border: #98A2B2; --color-components-progress-gray-bg: #C8CEDA05; + --color-components-progress-warning-progress: #F79009; + --color-components-progress-warning-border: #F79009; + --color-components-progress-warning-bg: #F790090A; + + --color-components-progress-error-progress: #F04438; + --color-components-progress-error-border: #F04438; + --color-components-progress-error-bg: #F044380A; + --color-components-chat-input-audio-bg: #EFF4FF; --color-components-chat-input-audio-wave-default: #155AEF33; --color-components-chat-input-bg-mask-1: #FFFFFF03; @@ -236,13 +254,103 @@ html[data-theme="light"] { --color-components-chat-input-audio-wave-active: #296DFF; --color-components-chat-input-audio-bg-alt: #FCFCFD; - --color-components-Avatar-shape-fill-stop-0: #FFFFFF; - --color-components-Avatar-shape-fill-stop-100: #FFFFFFE5; - - --color-components-Avatar-bg-mask-stop-0: #FFFFFF1F; - --color-components-Avatar-bg-mask-stop-100: #FFFFFF14; - - --color-components-Avatar-default-avatar-bg: #D0D5DC; + --color-components-avatar-shape-fill-stop-0: #FFFFFF; + --color-components-avatar-shape-fill-stop-100: #FFFFFFE5; + + --color-components-avatar-bg-mask-stop-0: #FFFFFF1F; + --color-components-avatar-bg-mask-stop-100: #FFFFFF14; + + --color-components-avatar-default-avatar-bg: #D0D5DC; + --color-components-avatar-mask-darkmode-dimmed: #FFFFFF00; + + --color-components-label-gray: #F2F4F7; + + --color-components-premium-badge-blue-bg-stop-0: #5289FF; + --color-components-premium-badge-blue-bg-stop-100: #155AEF; + --color-components-premium-badge-blue-stroke-stop-0: #FFFFFFF2; + --color-components-premium-badge-blue-stroke-stop-100: #155AEF; + --color-components-premium-badge-blue-text-stop-0: #F5F7FF; + --color-components-premium-badge-blue-text-stop-100: #D1E0FF; + --color-components-premium-badge-blue-glow: #00329E; + --color-components-premium-badge-blue-bg-stop-0-hover: #296DFF; + --color-components-premium-badge-blue-bg-stop-100-hover: #004AEB; + --color-components-premium-badge-blue-glow-hover: #84ABFF; + --color-components-premium-badge-blue-stroke-stop-0-hover: #FFFFFFF2; + --color-components-premium-badge-blue-stroke-stop-100-hover: #00329E; + + --color-components-premium-badge-highlight-stop-0: #FFFFFF1F; + --color-components-premium-badge-highlight-stop-100: #FFFFFF4D; + --color-components-premium-badge-indigo-bg-stop-0: #8098F9; + --color-components-premium-badge-indigo-bg-stop-100: #444CE7; + --color-components-premium-badge-indigo-stroke-stop-0: #FFFFFFF2; + --color-components-premium-badge-indigo-stroke-stop-100: #6172F3; + --color-components-premium-badge-indigo-text-stop-0: #F5F8FF; + --color-components-premium-badge-indigo-text-stop-100: #E0EAFF; + --color-components-premium-badge-indigo-glow: #2D3282; + --color-components-premium-badge-indigo-glow-hover: #A4BCFD; + --color-components-premium-badge-indigo-bg-stop-0-hover: #6172F3; + --color-components-premium-badge-indigo-bg-stop-100-hover: #2D31A6; + --color-components-premium-badge-indigo-stroke-stop-0-hover: #FFFFFFF2; + --color-components-premium-badge-indigo-stroke-stop-100-hover: #2D31A6; + + --color-components-premium-badge-grey-bg-stop-0: #98A2B2; + --color-components-premium-badge-grey-bg-stop-100: #676F83; + --color-components-premium-badge-grey-stroke-stop-0: #FFFFFFF2; + --color-components-premium-badge-grey-stroke-stop-100: #676F83; + --color-components-premium-badge-grey-text-stop-0: #FCFCFD; + --color-components-premium-badge-grey-text-stop-100: #F2F4F7; + --color-components-premium-badge-grey-glow: #101828; + --color-components-premium-badge-grey-glow-hover: #D0D5DC; + --color-components-premium-badge-grey-bg-stop-0-hover: #676F83; + --color-components-premium-badge-grey-bg-stop-100-hover: #354052; + --color-components-premium-badge-grey-stroke-stop-0-hover: #FFFFFFF2; + --color-components-premium-badge-grey-stroke-stop-100-hover: #354052; + + --color-components-premium-badge-orange-bg-stop-0: #FF692E; + --color-components-premium-badge-orange-bg-stop-100: #E04F16; + --color-components-premium-badge-orange-stroke-stop-0: #FFFFFFF2; + --color-components-premium-badge-orange-stroke-stop-100: #E62E05; + --color-components-premium-badge-orange-text-stop-0: #FEFAF5; + --color-components-premium-badge-orange-text-stop-100: #FDEAD7; + --color-components-premium-badge-orange-glow: #772917; + --color-components-premium-badge-orange-glow-hover: #F7B27A; + --color-components-premium-badge-orange-bg-stop-0-hover: #FF4405; + --color-components-premium-badge-orange-bg-stop-100-hover: #B93815; + --color-components-premium-badge-orange-stroke-stop-0-hover: #FFFFFFF2; + --color-components-premium-badge-orange-stroke-stop-100-hover: #BC1B06; + + --color-components-progress-bar-bg: #155AEF0A; + --color-components-progress-bar-progress: #155AEF24; + --color-components-progress-bar-border: #1018280A; + --color-components-progress-bar-progress-solid: #296DFF; + --color-components-progress-bar-progress-highlight: #155AEF33; + + --color-components-icon-bg-red-solid: #D92D20; + --color-components-icon-bg-rose-solid: #E31B54; + --color-components-icon-bg-pink-solid: #DD2590; + --color-components-icon-bg-orange-dark-solid: #FF4405; + --color-components-icon-bg-yellow-solid: #EAAA08; + --color-components-icon-bg-green-solid: #4CA30D; + --color-components-icon-bg-teal-solid: #0E9384; + --color-components-icon-bg-blue-light-solid: #0BA5EC; + --color-components-icon-bg-blue-solid: #155AEF; + --color-components-icon-bg-indigo-solid: #444CE7; + --color-components-icon-bg-violet-solid: #7839EE; + --color-components-icon-bg-midnight-solid: #828DAD; + --color-components-icon-bg-rose-soft: #FFF1F3; + --color-components-icon-bg-pink-soft: #FDF2FA; + --color-components-icon-bg-orange-dark-soft: #FFF4ED; + --color-components-icon-bg-yellow-soft: #FEFBE8; + --color-components-icon-bg-green-soft: #F3FEE7; + --color-components-icon-bg-teal-soft: #F0FDF9; + --color-components-icon-bg-blue-light-soft: #F0F9FF; + --color-components-icon-bg-blue-soft: #EFF4FF; + --color-components-icon-bg-indigo-soft: #EEF4FF; + --color-components-icon-bg-violet-soft: #F5F3FF; + --color-components-icon-bg-midnight-soft: #F0F2F5; + --color-components-icon-bg-red-soft: #FEF3F2; + --color-components-icon-bg-orange-solid: #F79009; + --color-components-icon-bg-orange-soft: #FFFAEB; --color-text-primary: #101828; --color-text-secondary: #354052; @@ -302,6 +410,7 @@ html[data-theme="light"] { --color-background-overlay-alt: #10182866; --color-background-surface-white: #FFFFFFF2; --color-background-overlay-destructive: #F044384D; + --color-background-overlay-backdrop: #F2F4F7F2; --color-shadow-shadow-1: #09090B08; --color-shadow-shadow-3: #09090B0D; @@ -317,6 +426,7 @@ html[data-theme="light"] { --color-workflow-block-border: #FFFFFF; --color-workflow-block-parma-bg: #F2F4F7; --color-workflow-block-bg: #FCFCFD; + --color-workflow-block-bg-transparent: #FCFCFDE5; --color-workflow-block-border-highlight: #155AEF24; --color-workflow-canvas-workflow-dot-color: #8585AD26; @@ -436,6 +546,7 @@ html[data-theme="light"] { --color-util-colors-orange-orange-500: #EF6820; --color-util-colors-orange-orange-600: #E04F16; --color-util-colors-orange-orange-700: #B93815; + --color-util-colors-orange-orange-100-transparent: #FDEAD700; --color-util-colors-pink-pink-50: #FDF2FA; --color-util-colors-pink-pink-100: #FCE7F6; @@ -610,6 +721,17 @@ html[data-theme="light"] { --color-third-party-LangChain: #1C3C3C; --color-third-party-Langfuse: #000000; - --color-third-party-Github: #1B1F24; + --color-third-party-Github-tertiary: #1B1F24; + --color-third-party-Github-secondary: #1B1F24; + --color-third-party-model-bg-openai: #E3E5E8; + --color-third-party-model-bg-anthropic: #EEEDE7; + --color-third-party-model-bg-default: #F9FAFB; + + --color-third-party-aws: #141F2E; + --color-third-party-aws-alt: #0F1824; + + --color-saas-background: #FCFCFD; + --color-saas-pricing-grid-bg: #C8CEDA80; + } \ No newline at end of file diff --git a/web/themes/tailwind-theme-var-define.ts b/web/themes/tailwind-theme-var-define.ts index a81e224e9d..6329ce3d26 100644 --- a/web/themes/tailwind-theme-var-define.ts +++ b/web/themes/tailwind-theme-var-define.ts @@ -85,6 +85,10 @@ const vars = { 'components-button-secondary-accent-border-hover': 'var(--color-components-button-secondary-accent-border-hover)', 'components-button-secondary-accent-border-disabled': 'var(--color-components-button-secondary-accent-border-disabled)', + 'components-button-indigo-bg': 'var(--color-components-button-indigo-bg)', + 'components-button-indigo-bg-hover': 'var(--color-components-button-indigo-bg-hover)', + 'components-button-indigo-bg-disabled': 'var(--color-components-button-indigo-bg-disabled)', + 'components-checkbox-icon': 'var(--color-components-checkbox-icon)', 'components-checkbox-icon-disabled': 'var(--color-components-checkbox-icon-disabled)', 'components-checkbox-bg': 'var(--color-components-checkbox-bg)', @@ -95,6 +99,7 @@ const vars = { 'components-checkbox-border-disabled': 'var(--color-components-checkbox-border-disabled)', 'components-checkbox-bg-unchecked': 'var(--color-components-checkbox-bg-unchecked)', 'components-checkbox-bg-unchecked-hover': 'var(--color-components-checkbox-bg-unchecked-hover)', + 'components-checkbox-bg-disabled-checked': 'var(--color-components-checkbox-bg-disabled-checked)', 'components-radio-border-checked': 'var(--color-components-radio-border-checked)', 'components-radio-border-checked-hover': 'var(--color-components-radio-border-checked-hover)', @@ -135,6 +140,9 @@ const vars = { 'components-panel-on-panel-item-bg': 'var(--color-components-panel-on-panel-item-bg)', 'components-panel-on-panel-item-bg-hover': 'var(--color-components-panel-on-panel-item-bg-hover)', 'components-panel-on-panel-item-bg-alt': 'var(--color-components-panel-on-panel-item-bg-alt)', + 'components-panel-on-panel-item-bg-transparent': 'var(--color-components-panel-on-panel-item-bg-transparent)', + 'components-panel-on-panel-item-bg-hover-transparent': 'var(--color-components-panel-on-panel-item-bg-hover-transparent)', + 'components-panel-on-panel-item-bg-destructive-hover-transparent': 'var(--color-components-panel-on-panel-item-bg-destructive-hover-transparent)', 'components-panel-bg-transparent': 'var(--color-components-panel-bg-transparent)', @@ -208,6 +216,8 @@ const vars = { 'components-actionbar-bg': 'var(--color-components-actionbar-bg)', 'components-actionbar-border': 'var(--color-components-actionbar-border)', + 'components-actionbar-bg-accent': 'var(--color-components-actionbar-bg-accent)', + 'components-actionbar-border-accent': 'var(--color-components-actionbar-border-accent)', 'components-dropzone-bg-alt': 'var(--color-components-dropzone-bg-alt)', 'components-dropzone-bg': 'var(--color-components-dropzone-bg)', @@ -228,6 +238,14 @@ const vars = { 'components-progress-gray-border': 'var(--color-components-progress-gray-border)', 'components-progress-gray-bg': 'var(--color-components-progress-gray-bg)', + 'components-progress-warning-progress': 'var(--color-components-progress-warning-progress)', + 'components-progress-warning-border': 'var(--color-components-progress-warning-border)', + 'components-progress-warning-bg': 'var(--color-components-progress-warning-bg)', + + 'components-progress-error-progress': 'var(--color-components-progress-error-progress)', + 'components-progress-error-border': 'var(--color-components-progress-error-border)', + 'components-progress-error-bg': 'var(--color-components-progress-error-bg)', + 'components-chat-input-audio-bg': 'var(--color-components-chat-input-audio-bg)', 'components-chat-input-audio-wave-default': 'var(--color-components-chat-input-audio-wave-default)', 'components-chat-input-bg-mask-1': 'var(--color-components-chat-input-bg-mask-1)', @@ -236,13 +254,103 @@ const vars = { 'components-chat-input-audio-wave-active': 'var(--color-components-chat-input-audio-wave-active)', 'components-chat-input-audio-bg-alt': 'var(--color-components-chat-input-audio-bg-alt)', - 'components-Avatar-shape-fill-stop-0': 'var(--color-components-Avatar-shape-fill-stop-0)', - 'components-Avatar-shape-fill-stop-100': 'var(--color-components-Avatar-shape-fill-stop-100)', - - 'components-Avatar-bg-mask-stop-0': 'var(--color-components-Avatar-bg-mask-stop-0)', - 'components-Avatar-bg-mask-stop-100': 'var(--color-components-Avatar-bg-mask-stop-100)', - - 'components-Avatar-default-avatar-bg': 'var(--color-components-Avatar-default-avatar-bg)', + 'components-avatar-shape-fill-stop-0': 'var(--color-components-avatar-shape-fill-stop-0)', + 'components-avatar-shape-fill-stop-100': 'var(--color-components-avatar-shape-fill-stop-100)', + + 'components-avatar-bg-mask-stop-0': 'var(--color-components-avatar-bg-mask-stop-0)', + 'components-avatar-bg-mask-stop-100': 'var(--color-components-avatar-bg-mask-stop-100)', + + 'components-avatar-default-avatar-bg': 'var(--color-components-avatar-default-avatar-bg)', + 'components-avatar-mask-darkmode-dimmed': 'var(--color-components-avatar-mask-darkmode-dimmed)', + + 'components-label-gray': 'var(--color-components-label-gray)', + + 'components-premium-badge-blue-bg-stop-0': 'var(--color-components-premium-badge-blue-bg-stop-0)', + 'components-premium-badge-blue-bg-stop-100': 'var(--color-components-premium-badge-blue-bg-stop-100)', + 'components-premium-badge-blue-stroke-stop-0': 'var(--color-components-premium-badge-blue-stroke-stop-0)', + 'components-premium-badge-blue-stroke-stop-100': 'var(--color-components-premium-badge-blue-stroke-stop-100)', + 'components-premium-badge-blue-text-stop-0': 'var(--color-components-premium-badge-blue-text-stop-0)', + 'components-premium-badge-blue-text-stop-100': 'var(--color-components-premium-badge-blue-text-stop-100)', + 'components-premium-badge-blue-glow': 'var(--color-components-premium-badge-blue-glow)', + 'components-premium-badge-blue-bg-stop-0-hover': 'var(--color-components-premium-badge-blue-bg-stop-0-hover)', + 'components-premium-badge-blue-bg-stop-100-hover': 'var(--color-components-premium-badge-blue-bg-stop-100-hover)', + 'components-premium-badge-blue-glow-hover': 'var(--color-components-premium-badge-blue-glow-hover)', + 'components-premium-badge-blue-stroke-stop-0-hover': 'var(--color-components-premium-badge-blue-stroke-stop-0-hover)', + 'components-premium-badge-blue-stroke-stop-100-hover': 'var(--color-components-premium-badge-blue-stroke-stop-100-hover)', + + 'components-premium-badge-highlight-stop-0': 'var(--color-components-premium-badge-highlight-stop-0)', + 'components-premium-badge-highlight-stop-100': 'var(--color-components-premium-badge-highlight-stop-100)', + 'components-premium-badge-indigo-bg-stop-0': 'var(--color-components-premium-badge-indigo-bg-stop-0)', + 'components-premium-badge-indigo-bg-stop-100': 'var(--color-components-premium-badge-indigo-bg-stop-100)', + 'components-premium-badge-indigo-stroke-stop-0': 'var(--color-components-premium-badge-indigo-stroke-stop-0)', + 'components-premium-badge-indigo-stroke-stop-100': 'var(--color-components-premium-badge-indigo-stroke-stop-100)', + 'components-premium-badge-indigo-text-stop-0': 'var(--color-components-premium-badge-indigo-text-stop-0)', + 'components-premium-badge-indigo-text-stop-100': 'var(--color-components-premium-badge-indigo-text-stop-100)', + 'components-premium-badge-indigo-glow': 'var(--color-components-premium-badge-indigo-glow)', + 'components-premium-badge-indigo-glow-hover': 'var(--color-components-premium-badge-indigo-glow-hover)', + 'components-premium-badge-indigo-bg-stop-0-hover': 'var(--color-components-premium-badge-indigo-bg-stop-0-hover)', + 'components-premium-badge-indigo-bg-stop-100-hover': 'var(--color-components-premium-badge-indigo-bg-stop-100-hover)', + 'components-premium-badge-indigo-stroke-stop-0-hover': 'var(--color-components-premium-badge-indigo-stroke-stop-0-hover)', + 'components-premium-badge-indigo-stroke-stop-100-hover': 'var(--color-components-premium-badge-indigo-stroke-stop-100-hover)', + + 'components-premium-badge-grey-bg-stop-0': 'var(--color-components-premium-badge-grey-bg-stop-0)', + 'components-premium-badge-grey-bg-stop-100': 'var(--color-components-premium-badge-grey-bg-stop-100)', + 'components-premium-badge-grey-stroke-stop-0': 'var(--color-components-premium-badge-grey-stroke-stop-0)', + 'components-premium-badge-grey-stroke-stop-100': 'var(--color-components-premium-badge-grey-stroke-stop-100)', + 'components-premium-badge-grey-text-stop-0': 'var(--color-components-premium-badge-grey-text-stop-0)', + 'components-premium-badge-grey-text-stop-100': 'var(--color-components-premium-badge-grey-text-stop-100)', + 'components-premium-badge-grey-glow': 'var(--color-components-premium-badge-grey-glow)', + 'components-premium-badge-grey-glow-hover': 'var(--color-components-premium-badge-grey-glow-hover)', + 'components-premium-badge-grey-bg-stop-0-hover': 'var(--color-components-premium-badge-grey-bg-stop-0-hover)', + 'components-premium-badge-grey-bg-stop-100-hover': 'var(--color-components-premium-badge-grey-bg-stop-100-hover)', + 'components-premium-badge-grey-stroke-stop-0-hover': 'var(--color-components-premium-badge-grey-stroke-stop-0-hover)', + 'components-premium-badge-grey-stroke-stop-100-hover': 'var(--color-components-premium-badge-grey-stroke-stop-100-hover)', + + 'components-premium-badge-orange-bg-stop-0': 'var(--color-components-premium-badge-orange-bg-stop-0)', + 'components-premium-badge-orange-bg-stop-100': 'var(--color-components-premium-badge-orange-bg-stop-100)', + 'components-premium-badge-orange-stroke-stop-0': 'var(--color-components-premium-badge-orange-stroke-stop-0)', + 'components-premium-badge-orange-stroke-stop-100': 'var(--color-components-premium-badge-orange-stroke-stop-100)', + 'components-premium-badge-orange-text-stop-0': 'var(--color-components-premium-badge-orange-text-stop-0)', + 'components-premium-badge-orange-text-stop-100': 'var(--color-components-premium-badge-orange-text-stop-100)', + 'components-premium-badge-orange-glow': 'var(--color-components-premium-badge-orange-glow)', + 'components-premium-badge-orange-glow-hover': 'var(--color-components-premium-badge-orange-glow-hover)', + 'components-premium-badge-orange-bg-stop-0-hover': 'var(--color-components-premium-badge-orange-bg-stop-0-hover)', + 'components-premium-badge-orange-bg-stop-100-hover': 'var(--color-components-premium-badge-orange-bg-stop-100-hover)', + 'components-premium-badge-orange-stroke-stop-0-hover': 'var(--color-components-premium-badge-orange-stroke-stop-0-hover)', + 'components-premium-badge-orange-stroke-stop-100-hover': 'var(--color-components-premium-badge-orange-stroke-stop-100-hover)', + + 'components-progress-bar-bg': 'var(--color-components-progress-bar-bg)', + 'components-progress-bar-progress': 'var(--color-components-progress-bar-progress)', + 'components-progress-bar-border': 'var(--color-components-progress-bar-border)', + 'components-progress-bar-progress-solid': 'var(--color-components-progress-bar-progress-solid)', + 'components-progress-bar-progress-highlight': 'var(--color-components-progress-bar-progress-highlight)', + + 'components-icon-bg-red-solid': 'var(--color-components-icon-bg-red-solid)', + 'components-icon-bg-rose-solid': 'var(--color-components-icon-bg-rose-solid)', + 'components-icon-bg-pink-solid': 'var(--color-components-icon-bg-pink-solid)', + 'components-icon-bg-orange-dark-solid': 'var(--color-components-icon-bg-orange-dark-solid)', + 'components-icon-bg-yellow-solid': 'var(--color-components-icon-bg-yellow-solid)', + 'components-icon-bg-green-solid': 'var(--color-components-icon-bg-green-solid)', + 'components-icon-bg-teal-solid': 'var(--color-components-icon-bg-teal-solid)', + 'components-icon-bg-blue-light-solid': 'var(--color-components-icon-bg-blue-light-solid)', + 'components-icon-bg-blue-solid': 'var(--color-components-icon-bg-blue-solid)', + 'components-icon-bg-indigo-solid': 'var(--color-components-icon-bg-indigo-solid)', + 'components-icon-bg-violet-solid': 'var(--color-components-icon-bg-violet-solid)', + 'components-icon-bg-midnight-solid': 'var(--color-components-icon-bg-midnight-solid)', + 'components-icon-bg-rose-soft': 'var(--color-components-icon-bg-rose-soft)', + 'components-icon-bg-pink-soft': 'var(--color-components-icon-bg-pink-soft)', + 'components-icon-bg-orange-dark-soft': 'var(--color-components-icon-bg-orange-dark-soft)', + 'components-icon-bg-yellow-soft': 'var(--color-components-icon-bg-yellow-soft)', + 'components-icon-bg-green-soft': 'var(--color-components-icon-bg-green-soft)', + 'components-icon-bg-teal-soft': 'var(--color-components-icon-bg-teal-soft)', + 'components-icon-bg-blue-light-soft': 'var(--color-components-icon-bg-blue-light-soft)', + 'components-icon-bg-blue-soft': 'var(--color-components-icon-bg-blue-soft)', + 'components-icon-bg-indigo-soft': 'var(--color-components-icon-bg-indigo-soft)', + 'components-icon-bg-violet-soft': 'var(--color-components-icon-bg-violet-soft)', + 'components-icon-bg-midnight-soft': 'var(--color-components-icon-bg-midnight-soft)', + 'components-icon-bg-red-soft': 'var(--color-components-icon-bg-red-soft)', + 'components-icon-bg-orange-solid': 'var(--color-components-icon-bg-orange-solid)', + 'components-icon-bg-orange-soft': 'var(--color-components-icon-bg-orange-soft)', 'text-primary': 'var(--color-text-primary)', 'text-secondary': 'var(--color-text-secondary)', @@ -302,6 +410,7 @@ const vars = { 'background-overlay-alt': 'var(--color-background-overlay-alt)', 'background-surface-white': 'var(--color-background-surface-white)', 'background-overlay-destructive': 'var(--color-background-overlay-destructive)', + 'background-overlay-backdrop': 'var(--color-background-overlay-backdrop)', 'shadow-shadow-1': 'var(--color-shadow-shadow-1)', 'shadow-shadow-3': 'var(--color-shadow-shadow-3)', @@ -317,6 +426,7 @@ const vars = { 'workflow-block-border': 'var(--color-workflow-block-border)', 'workflow-block-parma-bg': 'var(--color-workflow-block-parma-bg)', 'workflow-block-bg': 'var(--color-workflow-block-bg)', + 'workflow-block-bg-transparent': 'var(--color-workflow-block-bg-transparent)', 'workflow-block-border-highlight': 'var(--color-workflow-block-border-highlight)', 'workflow-canvas-workflow-dot-color': 'var(--color-workflow-canvas-workflow-dot-color)', @@ -436,6 +546,7 @@ const vars = { 'util-colors-orange-orange-500': 'var(--color-util-colors-orange-orange-500)', 'util-colors-orange-orange-600': 'var(--color-util-colors-orange-orange-600)', 'util-colors-orange-orange-700': 'var(--color-util-colors-orange-orange-700)', + 'util-colors-orange-orange-100-transparent': 'var(--color-util-colors-orange-orange-100-transparent)', 'util-colors-pink-pink-50': 'var(--color-util-colors-pink-pink-50)', 'util-colors-pink-pink-100': 'var(--color-util-colors-pink-pink-100)', @@ -611,6 +722,17 @@ const vars = { 'third-party-LangChain': 'var(--color-third-party-LangChain)', 'third-party-Langfuse': 'var(--color-third-party-Langfuse)', 'third-party-Github': 'var(--color-third-party-Github)', -} + 'third-party-Github-tertiary': 'var(--color-third-party-Github-tertiary)', + 'third-party-Github-secondary': 'var(--color-third-party-Github-secondary)', + 'third-party-model-bg-openai': 'var(--color-third-party-model-bg-openai)', + 'third-party-model-bg-anthropic': 'var(--color-third-party-model-bg-anthropic)', + 'third-party-model-bg-default': 'var(--color-third-party-model-bg-default)', + + 'third-party-aws': 'var(--color-third-party-aws)', + 'third-party-aws-alt': 'var(--color-third-party-aws-alt)', + 'saas-background': 'var(--color-saas-background)', + 'saas-pricing-grid-bg': 'var(--color-saas-pricing-grid-bg)', + +} export default vars From 20c091a5e7eeebf2f3310f4766d95dc518834000 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 12:47:59 +0800 Subject: [PATCH 042/130] fix: user query be ignored if query_prompt_template is an empty string (#11103) --- api/core/workflow/nodes/llm/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 4a6f0ecae9..9f5df1edc6 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -137,7 +137,7 @@ class LLMNode(BaseNode[LLMNodeData]): query = None if self.node_data.memory: query = self.node_data.memory.query_prompt_template - if query is None and ( + if not query and ( query_variable := self.graph_runtime_state.variable_pool.get( (SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY) ) From cbb4e95928138e63cb8948f89d6e8d282327cf0d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 13:07:32 +0800 Subject: [PATCH 043/130] fix(llm_node): Ignore user query when memory is disabled. (#11106) --- api/core/workflow/nodes/llm/node.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 9f5df1edc6..8653f539a0 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -137,12 +137,12 @@ class LLMNode(BaseNode[LLMNodeData]): query = None if self.node_data.memory: query = self.node_data.memory.query_prompt_template - if not query and ( - query_variable := self.graph_runtime_state.variable_pool.get( - (SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY) - ) - ): - query = query_variable.text + if not query and ( + query_variable := self.graph_runtime_state.variable_pool.get( + (SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY) + ) + ): + query = query_variable.text prompt_messages, stop = self._fetch_prompt_messages( user_query=query, From 1db14793fa4043598b8c42fd30d5c07ba4463add Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 13:31:40 +0800 Subject: [PATCH 044/130] fix(anthropic_llm): Ignore non-text parts in the system prompt. (#11107) --- .../model_runtime/model_providers/anthropic/llm/llm.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 79701e4ea4..b5de02193b 100644 --- a/api/core/model_runtime/model_providers/anthropic/llm/llm.py +++ b/api/core/model_runtime/model_providers/anthropic/llm/llm.py @@ -461,7 +461,15 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): first_loop = True for message in prompt_messages: if isinstance(message, SystemPromptMessage): - message.content = message.content.strip() + if isinstance(message.content, str): + message.content = message.content.strip() + elif isinstance(message.content, list): + # System prompt only support text + message.content = "".join( + c.data.strip() for c in message.content if isinstance(c, TextPromptMessageContent) + ) + else: + raise ValueError(f"Unknown system prompt message content type {type(message.content)}") if first_loop: system = message.content first_loop = False From 90d5765fb64e9abc6b03d5016c4cc6940c627ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 26 Nov 2024 13:42:13 +0800 Subject: [PATCH 045/130] fix: app copy raise error (#11108) --- api/controllers/console/app/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 9687b59cd1..da72b704c7 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -190,7 +190,7 @@ class AppCopyApi(Resource): ) session.commit() - stmt = select(App).where(App.id == result.app.id) + stmt = select(App).where(App.id == result.app_id) app = session.scalar(stmt) return app, 201 From 8d5a1be2273bfcf7a919f9268f80a1e8847abc15 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 13:43:38 +0800 Subject: [PATCH 046/130] fix: Cannot use files in the user inputs. (#11112) --- api/core/app/apps/advanced_chat/app_generator.py | 4 +++- api/core/app/apps/agent_chat/app_generator.py | 4 +++- api/core/app/apps/chat/app_generator.py | 4 +++- api/core/app/apps/completion/app_generator.py | 2 +- api/models/account.py | 4 ++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index c099b29c97..ffe56ce410 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -127,7 +127,9 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), + else self._prepare_user_inputs( + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.tenant_id + ), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index ae0a01e8a7..48ee590e2f 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -134,7 +134,9 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), + else self._prepare_user_inputs( + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.tenant_id + ), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index cf6aae34f8..5b3efe12eb 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -132,7 +132,9 @@ class ChatAppGenerator(MessageBasedAppGenerator): conversation_id=conversation.id if conversation else None, inputs=conversation.inputs if conversation - else self._prepare_user_inputs(user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id), + else self._prepare_user_inputs( + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.tenant_id + ), query=query, files=file_objs, parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL, diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 70434f24c1..e9e50015bd 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -114,7 +114,7 @@ class CompletionAppGenerator(MessageBasedAppGenerator): model_conf=ModelConfigConverter.convert(app_config), file_upload_config=file_extra_config, inputs=self._prepare_user_inputs( - user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.id + user_inputs=inputs, variables=app_config.variables, tenant_id=app_model.tenant_id ), query=query, files=file_objs, diff --git a/api/models/account.py b/api/models/account.py index 18be4be036..951e836dec 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -56,8 +56,8 @@ class Account(UserMixin, db.Model): self._current_tenant = tenant @property - def current_tenant_id(self): - return self._current_tenant.id + def current_tenant_id(self) -> str | None: + return self._current_tenant.id if self._current_tenant else None @current_tenant_id.setter def current_tenant_id(self, value: str): From 5b7b328193e65eacd33d97e652acac2567bc0375 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 13:45:49 +0800 Subject: [PATCH 047/130] feat: Allow to contains files in the system prompt even model not support. (#11111) --- .../model_providers/anthropic/llm/llm.py | 2 +- .../model_providers/openai/llm/llm.py | 3 +++ api/core/workflow/nodes/llm/node.py | 23 ++++++++++++------- 3 files changed, 19 insertions(+), 9 deletions(-) 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 b5de02193b..b24324708b 100644 --- a/api/core/model_runtime/model_providers/anthropic/llm/llm.py +++ b/api/core/model_runtime/model_providers/anthropic/llm/llm.py @@ -453,7 +453,7 @@ class AnthropicLargeLanguageModel(LargeLanguageModel): return credentials_kwargs - def _convert_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]: + def _convert_prompt_messages(self, prompt_messages: Sequence[PromptMessage]) -> tuple[str, list[dict]]: """ Convert prompt messages to dict list and system """ diff --git a/api/core/model_runtime/model_providers/openai/llm/llm.py b/api/core/model_runtime/model_providers/openai/llm/llm.py index aea884e002..07cb1e2d10 100644 --- a/api/core/model_runtime/model_providers/openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai/llm/llm.py @@ -943,6 +943,9 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): } elif isinstance(message, SystemPromptMessage): message = cast(SystemPromptMessage, message) + if isinstance(message.content, list): + text_contents = filter(lambda c: isinstance(c, TextPromptMessageContent), message.content) + message.content = "".join(c.data for c in text_contents) message_dict = {"role": "system", "content": message.content} elif isinstance(message, ToolPromptMessage): message = cast(ToolPromptMessage, message) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 8653f539a0..2380829f7d 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -20,6 +20,7 @@ from core.model_runtime.entities import ( from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, + PromptMessageContent, PromptMessageRole, SystemPromptMessage, UserPromptMessage, @@ -828,14 +829,14 @@ class LLMNode(BaseNode[LLMNodeData]): } -def _combine_text_message_with_role(*, text: str, role: PromptMessageRole): +def _combine_message_content_with_role(*, contents: Sequence[PromptMessageContent], role: PromptMessageRole): match role: case PromptMessageRole.USER: - return UserPromptMessage(content=[TextPromptMessageContent(data=text)]) + return UserPromptMessage(content=contents) case PromptMessageRole.ASSISTANT: - return AssistantPromptMessage(content=[TextPromptMessageContent(data=text)]) + return AssistantPromptMessage(content=contents) case PromptMessageRole.SYSTEM: - return SystemPromptMessage(content=[TextPromptMessageContent(data=text)]) + return SystemPromptMessage(content=contents) raise NotImplementedError(f"Role {role} is not supported") @@ -877,7 +878,9 @@ def _handle_list_messages( jinjia2_variables=jinja2_variables, variable_pool=variable_pool, ) - prompt_message = _combine_text_message_with_role(text=result_text, role=message.role) + prompt_message = _combine_message_content_with_role( + contents=[TextPromptMessageContent(data=result_text)], role=message.role + ) prompt_messages.append(prompt_message) else: # Get segment group from basic message @@ -908,12 +911,14 @@ def _handle_list_messages( # Create message with text from all segments plain_text = segment_group.text if plain_text: - prompt_message = _combine_text_message_with_role(text=plain_text, role=message.role) + prompt_message = _combine_message_content_with_role( + contents=[TextPromptMessageContent(data=plain_text)], role=message.role + ) prompt_messages.append(prompt_message) if file_contents: # Create message with image contents - prompt_message = UserPromptMessage(content=file_contents) + prompt_message = _combine_message_content_with_role(contents=file_contents, role=message.role) prompt_messages.append(prompt_message) return prompt_messages @@ -1018,6 +1023,8 @@ def _handle_completion_template( else: template_text = template.text result_text = variable_pool.convert_template(template_text).text - prompt_message = _combine_text_message_with_role(text=result_text, role=PromptMessageRole.USER) + prompt_message = _combine_message_content_with_role( + contents=[TextPromptMessageContent(data=result_text)], role=PromptMessageRole.USER + ) prompt_messages.append(prompt_message) return prompt_messages From 044e7b63c254eb284cc0a17d683aec60a33f2eef Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 14:14:14 +0800 Subject: [PATCH 048/130] fix(llm_node): Ignore file if not supported. (#11114) --- api/core/workflow/nodes/llm/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 2380829f7d..39480e34b3 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -67,7 +67,6 @@ from .entities import ( ModelConfig, ) from .exc import ( - FileTypeNotSupportError, InvalidContextStructureError, InvalidVariableTypeError, LLMModeRequiredError, @@ -676,7 +675,7 @@ class LLMNode(BaseNode[LLMNodeData]): and ModelFeature.AUDIO not in model_config.model_schema.features ) ): - raise FileTypeNotSupportError(type_name=content_item.type) + continue prompt_message_content.append(content_item) if len(prompt_message_content) == 1 and prompt_message_content[0].type == PromptMessageContentType.TEXT: prompt_message.content = prompt_message_content[0].data From aa135a3780ff9136b78efd1c3d31b9afd48168df Mon Sep 17 00:00:00 2001 From: Tao Wang <74752235+taowang1993@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:14:02 -0800 Subject: [PATCH 049/130] Add TTS to OpenAI_API_Compatible (#11071) --- .../model_providers/azure_openai/tts/tts.py | 2 +- .../model_providers/gitee_ai/tts/tts.py | 2 +- .../model_providers/openai/tts/tts.py | 2 +- .../openai_api_compatible.yaml | 25 ++- .../openai_api_compatible/tts/__init__.py | 0 .../openai_api_compatible/tts/tts.py | 145 ++++++++++++++++++ 6 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 api/core/model_runtime/model_providers/openai_api_compatible/tts/__init__.py create mode 100644 api/core/model_runtime/model_providers/openai_api_compatible/tts/tts.py diff --git a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py index 133cc9f76e..173b9d250c 100644 --- a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py +++ b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py @@ -14,7 +14,7 @@ from core.model_runtime.model_providers.azure_openai._constant import TTS_BASE_M class AzureOpenAIText2SpeechModel(_CommonAzureOpenAI, TTSModel): """ - Model class for OpenAI Speech to text model. + Model class for OpenAI text2speech model. """ def _invoke( diff --git a/api/core/model_runtime/model_providers/gitee_ai/tts/tts.py b/api/core/model_runtime/model_providers/gitee_ai/tts/tts.py index ed2bd5b13d..36dcea405d 100644 --- a/api/core/model_runtime/model_providers/gitee_ai/tts/tts.py +++ b/api/core/model_runtime/model_providers/gitee_ai/tts/tts.py @@ -10,7 +10,7 @@ from core.model_runtime.model_providers.gitee_ai._common import _CommonGiteeAI class GiteeAIText2SpeechModel(_CommonGiteeAI, TTSModel): """ - Model class for OpenAI Speech to text model. + Model class for OpenAI text2speech model. """ def _invoke( diff --git a/api/core/model_runtime/model_providers/openai/tts/tts.py b/api/core/model_runtime/model_providers/openai/tts/tts.py index 2e57b95944..dac37f0c7f 100644 --- a/api/core/model_runtime/model_providers/openai/tts/tts.py +++ b/api/core/model_runtime/model_providers/openai/tts/tts.py @@ -11,7 +11,7 @@ from core.model_runtime.model_providers.openai._common import _CommonOpenAI class OpenAIText2SpeechModel(_CommonOpenAI, TTSModel): """ - Model class for OpenAI Speech to text model. + Model class for OpenAI text2speech model. """ def _invoke( diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml b/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml index 69a081f35c..2b8dcb72d8 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml +++ b/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml @@ -9,6 +9,7 @@ supported_model_types: - text-embedding - speech2text - rerank + - tts configurate_methods: - customizable-model model_credential_schema: @@ -67,7 +68,7 @@ model_credential_schema: - variable: __model_type value: llm type: text-input - default: '4096' + default: "4096" placeholder: zh_Hans: 在此输入您的模型上下文长度 en_US: Enter your Model context size @@ -80,7 +81,7 @@ model_credential_schema: - variable: __model_type value: text-embedding type: text-input - default: '4096' + default: "4096" placeholder: zh_Hans: 在此输入您的模型上下文长度 en_US: Enter your Model context size @@ -93,7 +94,7 @@ model_credential_schema: - variable: __model_type value: rerank type: text-input - default: '4096' + default: "4096" placeholder: zh_Hans: 在此输入您的模型上下文长度 en_US: Enter your Model context size @@ -104,7 +105,7 @@ model_credential_schema: show_on: - variable: __model_type value: llm - default: '4096' + default: "4096" type: text-input - variable: function_calling_type show_on: @@ -174,3 +175,19 @@ model_credential_schema: value: llm default: '\n\n' type: text-input + - variable: voices + show_on: + - variable: __model_type + value: tts + label: + en_US: Available Voices (comma-separated) + zh_Hans: 可用声音(用英文逗号分隔) + type: text-input + required: false + default: "alloy" + placeholder: + en_US: "alloy,echo,fable,onyx,nova,shimmer" + zh_Hans: "alloy,echo,fable,onyx,nova,shimmer" + help: + en_US: "List voice names separated by commas. First voice will be used as default." + zh_Hans: "用英文逗号分隔的声音列表。第一个声音将作为默认值。" diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/tts/__init__.py b/api/core/model_runtime/model_providers/openai_api_compatible/tts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/tts/tts.py b/api/core/model_runtime/model_providers/openai_api_compatible/tts/tts.py new file mode 100644 index 0000000000..8239c625f7 --- /dev/null +++ b/api/core/model_runtime/model_providers/openai_api_compatible/tts/tts.py @@ -0,0 +1,145 @@ +from collections.abc import Iterable +from typing import Optional +from urllib.parse import urljoin + +import requests + +from core.model_runtime.entities.common_entities import I18nObject +from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelPropertyKey, ModelType +from core.model_runtime.errors.invoke import InvokeBadRequestError +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.__base.tts_model import TTSModel +from core.model_runtime.model_providers.openai_api_compatible._common import _CommonOaiApiCompat + + +class OAICompatText2SpeechModel(_CommonOaiApiCompat, TTSModel): + """ + Model class for OpenAI-compatible text2speech model. + """ + + def _invoke( + self, + model: str, + tenant_id: str, + credentials: dict, + content_text: str, + voice: str, + user: Optional[str] = None, + ) -> Iterable[bytes]: + """ + Invoke TTS model + + :param model: model name + :param tenant_id: user tenant id + :param credentials: model credentials + :param content_text: text content to be translated + :param voice: model voice/speaker + :param user: unique user id + :return: audio data as bytes iterator + """ + # Set up headers with authentication if provided + headers = {} + if api_key := credentials.get("api_key"): + headers["Authorization"] = f"Bearer {api_key}" + + # Construct endpoint URL + endpoint_url = credentials.get("endpoint_url") + if not endpoint_url.endswith("/"): + endpoint_url += "/" + endpoint_url = urljoin(endpoint_url, "audio/speech") + + # Get audio format from model properties + audio_format = self._get_model_audio_type(model, credentials) + + # Split text into chunks if needed based on word limit + word_limit = self._get_model_word_limit(model, credentials) + sentences = self._split_text_into_sentences(content_text, word_limit) + + for sentence in sentences: + # Prepare request payload + payload = {"model": model, "input": sentence, "voice": voice, "response_format": audio_format} + + # Make POST request + response = requests.post(endpoint_url, headers=headers, json=payload, stream=True) + + if response.status_code != 200: + raise InvokeBadRequestError(response.text) + + # Stream the audio data + for chunk in response.iter_content(chunk_size=4096): + if chunk: + yield chunk + + def validate_credentials(self, model: str, credentials: dict) -> None: + """ + Validate model credentials + + :param model: model name + :param credentials: model credentials + :return: + """ + try: + # Get default voice for validation + voice = self._get_model_default_voice(model, credentials) + + # Test with a simple text + next( + self._invoke( + model=model, tenant_id="validate", credentials=credentials, content_text="Test.", voice=voice + ) + ) + except Exception as ex: + raise CredentialsValidateFailedError(str(ex)) + + def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]: + """ + Get customizable model schema + """ + # Parse voices from comma-separated string + voice_names = credentials.get("voices", "alloy").strip().split(",") + voices = [] + + for voice in voice_names: + voice = voice.strip() + if not voice: + continue + + # Use en-US for all voices + voices.append( + { + "name": voice, + "mode": voice, + "language": "en-US", + } + ) + + # If no voices provided or all voices were empty strings, use 'alloy' as default + if not voices: + voices = [{"name": "Alloy", "mode": "alloy", "language": "en-US"}] + + return AIModelEntity( + model=model, + label=I18nObject(en_US=model), + fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, + model_type=ModelType.TTS, + model_properties={ + ModelPropertyKey.AUDIO_TYPE: credentials.get("audio_type", "mp3"), + ModelPropertyKey.WORD_LIMIT: int(credentials.get("word_limit", 4096)), + ModelPropertyKey.DEFAULT_VOICE: voices[0]["mode"], + ModelPropertyKey.VOICES: voices, + }, + ) + + def get_tts_model_voices(self, model: str, credentials: dict, language: Optional[str] = None) -> list: + """ + Override base get_tts_model_voices to handle customizable voices + """ + model_schema = self.get_customizable_model_schema(model, credentials) + + if not model_schema or ModelPropertyKey.VOICES not in model_schema.model_properties: + raise ValueError("this model does not support voice") + + voices = model_schema.model_properties[ModelPropertyKey.VOICES] + + # Always return all voices regardless of language + return [{"name": d["name"], "value": d["mode"]} for d in voices] From 208d6d6d94975742d84483bdaa0625c78ad582ff Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 26 Nov 2024 15:46:17 +0800 Subject: [PATCH 050/130] chore: bump to 0.12.1 (#11122) --- api/configs/packaging/__init__.py | 2 +- docker-legacy/docker-compose.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 7e95e79bfb..5d2a0231b0 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="0.12.0", + default="0.12.1", ) COMMIT_SHA: str = Field( diff --git a/docker-legacy/docker-compose.yaml b/docker-legacy/docker-compose.yaml index 7ddb98e272..aaca3c9c12 100644 --- a/docker-legacy/docker-compose.yaml +++ b/docker-legacy/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: # API service api: - image: langgenius/dify-api:0.12.0 + image: langgenius/dify-api:0.12.1 restart: always environment: # Startup mode, 'api' starts the API server. @@ -227,7 +227,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.12.0 + image: langgenius/dify-api:0.12.1 restart: always environment: CONSOLE_WEB_URL: '' @@ -397,7 +397,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.12.0 + image: langgenius/dify-web:0.12.1 restart: always environment: # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 9a135e7b54..3e2b276c92 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -291,7 +291,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.12.0 + image: langgenius/dify-api:0.12.1 restart: always environment: # Use the shared environment variables. @@ -311,7 +311,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.12.0 + image: langgenius/dify-api:0.12.1 restart: always environment: # Use the shared environment variables. @@ -330,7 +330,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.12.0 + image: langgenius/dify-web:0.12.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/package.json b/web/package.json index f65d87961b..4f7ddf8e5c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "0.12.0", + "version": "0.12.1", "private": true, "engines": { "node": ">=18.17.0" From b3d65cc7df034d8678ac59f7ab30b9617e9415a6 Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 26 Nov 2024 17:44:56 +0800 Subject: [PATCH 051/130] Feat: Divider component now supports gradient background (#11130) --- web/app/components/base/divider/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/app/components/base/divider/index.tsx b/web/app/components/base/divider/index.tsx index 4b351dea99..fcee10a9e2 100644 --- a/web/app/components/base/divider/index.tsx +++ b/web/app/components/base/divider/index.tsx @@ -3,17 +3,21 @@ import React from 'react' import { type VariantProps, cva } from 'class-variance-authority' import classNames from '@/utils/classnames' -const dividerVariants = cva( - 'bg-divider-regular', +const dividerVariants = cva('', { variants: { type: { - horizontal: 'w-full h-[0.5px] my-2', + horizontal: 'w-full h-[0.5px] my-2 ', vertical: 'w-[1px] h-full mx-2', }, + bgStyle: { + gradient: 'bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent', + solid: 'bg-divider-regular', + }, }, defaultVariants: { type: 'horizontal', + bgStyle: 'solid', }, }, ) @@ -23,9 +27,9 @@ type DividerProps = { style?: CSSProperties } & VariantProps -const Divider: FC = ({ type, className = '', style }) => { +const Divider: FC = ({ type, bgStyle, className = '', style }) => { return ( -
+
) } From 79db920fa7bd9e458c3114d6a7149ead49c75d65 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 26 Nov 2024 17:55:11 +0800 Subject: [PATCH 052/130] fix: enable after disabled memory not pass user query (#11136) --- .../workflow/nodes/_base/components/memory-config.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/workflow/nodes/_base/components/memory-config.tsx b/web/app/components/workflow/nodes/_base/components/memory-config.tsx index c108608739..476f5b738c 100644 --- a/web/app/components/workflow/nodes/_base/components/memory-config.tsx +++ b/web/app/components/workflow/nodes/_base/components/memory-config.tsx @@ -53,7 +53,7 @@ type Props = { const MEMORY_DEFAULT: Memory = { window: { enabled: false, size: WINDOW_SIZE_DEFAULT }, - query_prompt_template: '', + query_prompt_template: '{{#sys.query#}}', } const MemoryConfig: FC = ({ From 2927493cf3aaaf5fc1276c81625205236a70d11a Mon Sep 17 00:00:00 2001 From: yihong Date: Tue, 26 Nov 2024 19:39:55 +0800 Subject: [PATCH 053/130] 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 054/130] 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 055/130] 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 056/130] 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 057/130] 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 058/130] 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 059/130] 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 060/130] 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 061/130] 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 062/130] 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 | 27 +++++++++---------- 2 files changed, 13 insertions(+), 16 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) - - except Exception as e: - click.echo(click.style(f"Error: {e}", fg="red")) - break + 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")) 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 063/130] 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 064/130] 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 065/130] 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 066/130] 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 099/130] 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 100/130] 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 101/130] 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 102/130] 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 103/130] 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 104/130] 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 105/130] 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 106/130] 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 107/130] chore(lint): sort __all__ definitions (#11243) --- api/.ruff.toml | 3 ++ api/core/file/__init__.py | 12 +++--- api/core/model_runtime/entities/__init__.py | 24 +++++------ .../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, 77 insertions(+), 74 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__ = [ + "AssistantPromptMessage", + "AudioPromptMessageContent", + "DocumentPromptMessageContent", "ImagePromptMessageContent", - "VideoPromptMessageContent", - "PromptMessage", - "PromptMessageRole", + "LLMResult", + "LLMResultChunk", + "LLMResultChunkDelta", "LLMUsage", "ModelPropertyKey", - "AssistantPromptMessage", + "PromptMessage", "PromptMessage", "PromptMessageContent", + "PromptMessageContentType", "PromptMessageRole", + "PromptMessageRole", + "PromptMessageTool", "SystemPromptMessage", "TextPromptMessageContent", - "UserPromptMessage", - "PromptMessageTool", "ToolPromptMessage", - "PromptMessageContentType", - "LLMResult", - "LLMResultChunk", - "LLMResultChunkDelta", - "AudioPromptMessageContent", - "DocumentPromptMessageContent", + "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", - "ArrayNumberVariable", - "ArrayObjectVariable", - "ArraySegment", + "ArrayAnyVariable", "ArrayFileSegment", + "ArrayFileVariable", "ArrayNumberSegment", + "ArrayNumberVariable", "ArrayObjectSegment", + "ArrayObjectVariable", + "ArraySegment", "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", - "MessageFile", - "UploadFile", - "Account", - "WorkflowAppLog", - "WorkflowRun", - "Site", "InstalledApp", - "RecommendedApp", - "ApiToken", - "AccountIntegrate", "InvitationCode", - "Tenant", - "Conversation", + "Message", "MessageAnnotation", + "MessageFile", + "RecommendedApp", + "Site", + "Tenant", "ToolFile", + "UploadFile", + "Workflow", + "WorkflowAppLog", + "WorkflowRun", ] 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 108/130] 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 | 125 +++++----- .../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, 1564 insertions(+), 300 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 + +LATEST_VERSION = "latest" -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, +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' && ( +