Browse Source

Merge branch 'main' into fix/chore-fix

Yeuoly 5 months ago
parent
commit
5ff9cee326
100 changed files with 388 additions and 1150 deletions
  1. 2 2
      .devcontainer/Dockerfile
  2. 1 1
      .devcontainer/devcontainer.json
  3. 1 1
      .github/actions/setup-poetry/action.yml
  4. 0 1
      .github/workflows/api-tests.yml
  5. 0 1
      .github/workflows/vdb-tests.yml
  6. 17 18
      CONTRIBUTING.md
  7. 1 1
      CONTRIBUTING_CN.md
  8. 1 1
      CONTRIBUTING_JA.md
  9. 2 2
      CONTRIBUTING_VI.md
  10. 1 1
      api/Dockerfile
  11. 1 3
      api/README.md
  12. 5 3
      api/app.py
  13. 0 1
      api/configs/app_config.py
  14. 5 0
      api/controllers/console/__init__.py
  15. 24 103
      api/controllers/console/app/app.py
  16. 90 0
      api/controllers/console/app/app_import.py
  17. 2 2
      api/controllers/console/app/conversation.py
  18. 3 3
      api/controllers/console/app/site.py
  19. 0 30
      api/controllers/console/app/workflow.py
  20. 1 1
      api/controllers/console/auth/activate.py
  21. 2 2
      api/controllers/console/auth/oauth.py
  22. 2 2
      api/controllers/console/datasets/data_source.py
  23. 9 9
      api/controllers/console/datasets/datasets_document.py
  24. 2 2
      api/controllers/console/datasets/datasets_segments.py
  25. 3 3
      api/controllers/console/explore/completion.py
  26. 2 2
      api/controllers/console/explore/installed_app.py
  27. 2 2
      api/controllers/console/workspace/account.py
  28. 2 2
      api/controllers/service_api/wraps.py
  29. 1 0
      api/core/app/app_config/easy_ui_based_app/dataset/manager.py
  30. 19 23
      api/core/app/app_config/easy_ui_based_app/model_config/converter.py
  31. 4 1
      api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py
  32. 2 2
      api/core/app/app_config/entities.py
  33. 1 1
      api/core/app/apps/advanced_chat/app_generator.py
  34. 1 1
      api/core/app/apps/agent_chat/app_generator.py
  35. 7 7
      api/core/app/apps/base_app_generator.py
  36. 1 1
      api/core/app/apps/chat/app_generator.py
  37. 3 1
      api/core/app/apps/completion/app_generator.py
  38. 2 2
      api/core/app/apps/message_based_app_generator.py
  39. 3 1
      api/core/app/apps/workflow/app_generator.py
  40. 0 3
      api/core/app/apps/workflow_app_runner.py
  41. 2 2
      api/core/app/entities/queue_entities.py
  42. 12 9
      api/core/app/task_pipeline/workflow_cycle_manage.py
  43. 6 6
      api/core/entities/provider_configuration.py
  44. 6 6
      api/core/file/enums.py
  45. 25 44
      api/core/file/file_manager.py
  46. 2 2
      api/core/helper/code_executor/code_executor.py
  47. 14 14
      api/core/indexing_runner.py
  48. 7 8
      api/core/memory/token_buffer_memory.py
  49. 2 2
      api/core/model_manager.py
  50. 5 4
      api/core/model_runtime/callbacks/base_callback.py
  51. 2 0
      api/core/model_runtime/entities/__init__.py
  52. 14 5
      api/core/model_runtime/entities/message_entities.py
  53. 5 2
      api/core/model_runtime/entities/model_entities.py
  54. 8 8
      api/core/model_runtime/model_providers/__base/large_language_model.py
  55. BIN
      api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.png
  56. 0 15
      api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.svg
  57. BIN
      api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.png
  58. 0 11
      api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.svg
  59. 0 10
      api/core/model_runtime/model_providers/gpustack/gpustack.py
  60. 0 120
      api/core/model_runtime/model_providers/gpustack/gpustack.yaml
  61. 0 0
      api/core/model_runtime/model_providers/gpustack/llm/__init__.py
  62. 0 45
      api/core/model_runtime/model_providers/gpustack/llm/llm.py
  63. 0 0
      api/core/model_runtime/model_providers/gpustack/rerank/__init__.py
  64. 0 146
      api/core/model_runtime/model_providers/gpustack/rerank/rerank.py
  65. 0 0
      api/core/model_runtime/model_providers/gpustack/text_embedding/__init__.py
  66. 0 35
      api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py
  67. 0 55
      api/core/model_runtime/model_providers/vertex_ai/llm/anthropic.claude-3.5-sonnet-v2.yaml
  68. 0 0
      api/core/model_runtime/model_providers/vessl_ai/__init__.py
  69. BIN
      api/core/model_runtime/model_providers/vessl_ai/_assets/icon_l_en.png
  70. 0 3
      api/core/model_runtime/model_providers/vessl_ai/_assets/icon_s_en.svg
  71. 0 0
      api/core/model_runtime/model_providers/vessl_ai/llm/__init__.py
  72. 0 83
      api/core/model_runtime/model_providers/vessl_ai/llm/llm.py
  73. 0 10
      api/core/model_runtime/model_providers/vessl_ai/vessl_ai.py
  74. 0 56
      api/core/model_runtime/model_providers/vessl_ai/vessl_ai.yaml
  75. 0 0
      api/core/model_runtime/model_providers/x/__init__.py
  76. 0 1
      api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg
  77. 0 0
      api/core/model_runtime/model_providers/x/llm/__init__.py
  78. 0 63
      api/core/model_runtime/model_providers/x/llm/grok-beta.yaml
  79. 0 37
      api/core/model_runtime/model_providers/x/llm/llm.py
  80. 0 25
      api/core/model_runtime/model_providers/x/x.py
  81. 0 38
      api/core/model_runtime/model_providers/x/x.yaml
  82. 2 2
      api/core/ops/entities/trace_entity.py
  83. 3 3
      api/core/ops/langfuse_trace/entities/langfuse_trace_entity.py
  84. 2 2
      api/core/ops/langsmith_trace/entities/langsmith_trace_entity.py
  85. 1 1
      api/core/prompt/simple_prompt_transform.py
  86. 2 1
      api/core/prompt/utils/prompt_message_util.py
  87. 1 1
      api/core/rag/cleaner/clean_processor.py
  88. 2 2
      api/core/rag/datasource/keyword/keyword_type.py
  89. 2 2
      api/core/rag/datasource/vdb/vector_type.py
  90. 2 2
      api/core/rag/extractor/word_extractor.py
  91. 3 3
      api/core/rag/rerank/rerank_model.py
  92. 2 2
      api/core/rag/rerank/rerank_type.py
  93. 3 4
      api/core/rag/rerank/weight_rerank.py
  94. 2 2
      api/core/tools/builtin_tool/providers/time/tools/current_time.py
  95. 9 7
      api/core/tools/tool_engine.py
  96. 10 2
      api/core/variables/segments.py
  97. 2 2
      api/core/variables/types.py
  98. 3 3
      api/core/workflow/entities/node_entities.py
  99. 2 2
      api/core/workflow/enums.py
  100. 0 0
      api/core/workflow/graph_engine/entities/runtime_route_state.py

+ 2 - 2
.devcontainer/Dockerfile

@@ -1,5 +1,5 @@
-FROM mcr.microsoft.com/devcontainers/python:3.10
+FROM mcr.microsoft.com/devcontainers/python:3.12
 
 # [Optional] Uncomment this section to install additional OS packages.
 # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
-#     && apt-get -y install --no-install-recommends <your-package-list-here>
+#     && apt-get -y install --no-install-recommends <your-package-list-here>

+ 1 - 1
.devcontainer/devcontainer.json

@@ -1,7 +1,7 @@
 // For format details, see https://aka.ms/devcontainer.json. For config options, see the
 // README at: https://github.com/devcontainers/templates/tree/main/src/anaconda
 {
-	"name": "Python 3.10",
+	"name": "Python 3.12",
 	"build": { 
 		"context": "..",
 		"dockerfile": "Dockerfile"

+ 1 - 1
.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

+ 0 - 1
.github/workflows/api-tests.yml

@@ -20,7 +20,6 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - "3.10"
           - "3.11"
           - "3.12"
 

+ 0 - 1
.github/workflows/vdb-tests.yml

@@ -20,7 +20,6 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - "3.10"
           - "3.11"
           - "3.12"
 

+ 17 - 18
CONTRIBUTING.md

@@ -1,6 +1,8 @@
+# CONTRIBUTING
+
 So you're looking to contribute to Dify - that's awesome, we can't wait to see what you do. As a startup with limited headcount and funding, we have grand ambitions to design the most intuitive workflow for building and managing LLM applications. Any help from the community counts, truly.
 
-We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part. 
+We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.
 
 This guide, like Dify itself, is a constant work in progress. We highly appreciate your understanding if at times it lags behind the actual project, and welcome any feedback for us to improve.
 
@@ -10,14 +12,12 @@ In terms of licensing, please take a minute to read our short [License and Contr
 
 [Find](https://github.com/langgenius/dify/issues?q=is:issue+is:open) an existing issue, or [open](https://github.com/langgenius/dify/issues/new/choose) a new one. We categorize issues into 2 types:
 
-### Feature requests:
+### Feature requests
 
 * If you're opening a new feature request, we'd like you to explain what the proposed feature achieves, and include as much context as possible. [@perzeusss](https://github.com/perzeuss) has made a solid [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) that helps you draft out your needs. Feel free to give it a try.
 
 * If you want to pick one up from the existing issues, simply drop a comment below it saying so.
 
-  
-
   A team member working in the related direction will be looped in. If all looks good, they will give the go-ahead for you to start coding. We ask that you hold off working on the feature until then, so none of your work goes to waste should we propose changes.
 
   Depending on whichever area the proposed feature falls under, you might talk to different team members. Here's rundown of the areas each our team members are working on at the moment:
@@ -40,7 +40,7 @@ In terms of licensing, please take a minute to read our short [License and Contr
   | Non-core features and minor enhancements                     | Low Priority    |
   | Valuable but not immediate                                   | Future-Feature  |
 
-### Anything else (e.g. bug report, performance optimization, typo correction):
+### Anything else (e.g. bug report, performance optimization, typo correction)
 
 * Start coding right away.
 
@@ -52,7 +52,6 @@ In terms of licensing, please take a minute to read our short [License and Contr
   | Non-critical bugs, performance boosts                        | Medium Priority |
   | Minor fixes (typos, confusing but working UI)                | Low Priority    |
 
-
 ## Installing
 
 Here are the steps to set up Dify for development:
@@ -63,7 +62,7 @@ Here are the steps to set up Dify for development:
 
  Clone the forked repository from your terminal:
 
-```
+```shell
 git clone git@github.com:<github_username>/dify.git
 ```
 
@@ -71,11 +70,11 @@ git clone git@github.com:<github_username>/dify.git
 
 Dify requires the following dependencies to build, make sure they're installed on your system:
 
-- [Docker](https://www.docker.com/)
-- [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
+* [Docker](https://www.docker.com/)
+* [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.11.x or 3.12.x
 
 ### 4. Installations
 
@@ -85,7 +84,7 @@ Check the [installation FAQ](https://docs.dify.ai/learn-more/faq/install-faq) fo
 
 ### 5. Visit dify in your browser
 
-To validate your set up, head over to [http://localhost:3000](http://localhost:3000) (the default, or your self-configured URL and port) in your browser. You should now see Dify up and running. 
+To validate your set up, head over to [http://localhost:3000](http://localhost:3000) (the default, or your self-configured URL and port) in your browser. You should now see Dify up and running.
 
 ## Developing
 
@@ -97,9 +96,9 @@ To help you quickly navigate where your contribution fits, a brief, annotated ou
 
 ### Backend
 
-Dify’s backend is written in Python using [Flask](https://flask.palletsprojects.com/en/3.0.x/). It uses [SQLAlchemy](https://www.sqlalchemy.org/) for ORM and [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) for task queueing. Authorization logic goes via Flask-login. 
+Dify’s backend is written in Python using [Flask](https://flask.palletsprojects.com/en/3.0.x/). It uses [SQLAlchemy](https://www.sqlalchemy.org/) for ORM and [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) for task queueing. Authorization logic goes via Flask-login.
 
-```
+```text
 [api/]
 ├── constants             // Constant settings used throughout code base.
 ├── controllers           // API route definitions and request handling logic.           
@@ -121,7 +120,7 @@ Dify’s backend is written in Python using [Flask](https://flask.palletsproject
 
 The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typescript and uses [Tailwind CSS](https://tailwindcss.com/) for styling. [React-i18next](https://react.i18next.com/) is used for internationalization.
 
-```
+```text
 [web/]
 ├── app                   // layouts, pages, and components
 │   ├── (commonLayout)    // common layout used throughout the app
@@ -149,10 +148,10 @@ The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typ
 
 ## Submitting your PR
 
-At last, time to open a pull request (PR) to our repo. For major features, we first merge them into the `deploy/dev` branch for testing, before they go into the `main` branch. If you run into issues like merge conflicts or don't know how to open a pull request, check out [GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests). 
+At last, time to open a pull request (PR) to our repo. For major features, we first merge them into the `deploy/dev` branch for testing, before they go into the `main` branch. If you run into issues like merge conflicts or don't know how to open a pull request, check out [GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests).
 
 And that's it! Once your PR is merged, you will be featured as a contributor in our [README](https://github.com/langgenius/dify/blob/main/README.md).
 
 ## Getting Help
 
-If you ever get stuck or got a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat. 
+If you ever get stuck or got a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.

+ 1 - 1
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. 安装
 

+ 1 - 1
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. インストール
 

+ 2 - 2
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.
+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.

+ 1 - 1
api/Dockerfile

@@ -1,5 +1,5 @@
 # base image
-FROM python:3.10-slim-bookworm AS base
+FROM python:3.12-slim-bookworm AS base
 
 WORKDIR /app/api
 

+ 1 - 3
api/README.md

@@ -42,7 +42,7 @@
 5. Install dependencies
 
    ```bash
-   poetry env use 3.10
+   poetry env use 3.12
    poetry install
    ```
 
@@ -81,5 +81,3 @@
    ```bash
    poetry run -C api bash dev/pytest/pytest_all_tests.sh
    ```
-
-

+ 5 - 3
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)
 

+ 0 - 1
api/configs/app_config.py

@@ -27,7 +27,6 @@ class DifyConfig(
         # read from dotenv format config file
         env_file=".env",
         env_file_encoding="utf-8",
-        frozen=True,
         # ignore extra attributes
         extra="ignore",
     )

+ 5 - 0
api/controllers/console/__init__.py

@@ -2,6 +2,7 @@ from flask import Blueprint
 
 from libs.external_api import ExternalApi
 
+from .app.app_import import AppImportApi, AppImportConfirmApi
 from .files import FileApi, FilePreviewApi, FileSupportTypeApi
 from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
 
@@ -17,6 +18,10 @@ api.add_resource(FileSupportTypeApi, "/files/support-type")
 api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
 api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
 
+# Import App
+api.add_resource(AppImportApi, "/apps/imports")
+api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm")
+
 # Import other controllers
 from . import admin, apikey, extension, feature, ping, setup, version
 

+ 24 - 103
api/controllers/console/app/app.py

@@ -1,7 +1,10 @@
 import uuid
+from typing import cast
 
 from flask_login import current_user
 from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
+from sqlalchemy import select
+from sqlalchemy.orm import Session
 from werkzeug.exceptions import BadRequest, Forbidden, abort
 
 from controllers.console import api
@@ -12,15 +15,16 @@ from controllers.console.wraps import (
     enterprise_license_required,
     setup_required,
 )
-from core.model_runtime.utils.encoders import jsonable_encoder
 from core.ops.ops_trace_manager import OpsTraceManager
+from extensions.ext_database import db
 from fields.app_fields import (
     app_detail_fields,
     app_detail_fields_with_site,
     app_pagination_fields,
 )
 from libs.login import login_required
-from services.app_dsl_service import AppDslService
+from models import Account, App
+from services.app_dsl_service import AppDslService, ImportMode
 from services.app_service import AppService
 
 ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
@@ -93,99 +97,6 @@ class AppListApi(Resource):
         return app, 201
 
 
-class AppImportDependenciesCheckApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    @cloud_edition_billing_resource_check("apps")
-    def post(self):
-        """Check dependencies"""
-        # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_editor:
-            raise Forbidden()
-
-        parser = reqparse.RequestParser()
-        parser.add_argument("data", type=str, required=True, nullable=False, location="json")
-        args = parser.parse_args()
-
-        leaked_dependencies = AppDslService.check_dependencies(
-            tenant_id=current_user.current_tenant_id, data=args["data"], account=current_user
-        )
-
-        return jsonable_encoder({"leaked": leaked_dependencies}), 200
-
-
-class AppImportApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    @marshal_with(app_detail_fields_with_site)
-    @cloud_edition_billing_resource_check("apps")
-    def post(self):
-        """Import app"""
-        # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_editor:
-            raise Forbidden()
-
-        parser = reqparse.RequestParser()
-        parser.add_argument("data", type=str, required=True, nullable=False, location="json")
-        parser.add_argument("name", type=str, location="json")
-        parser.add_argument("description", type=str, location="json")
-        parser.add_argument("icon_type", type=str, location="json")
-        parser.add_argument("icon", type=str, location="json")
-        parser.add_argument("icon_background", type=str, location="json")
-        args = parser.parse_args()
-
-        app = AppDslService.import_and_create_new_app(
-            tenant_id=current_user.current_tenant_id, data=args["data"], args=args, account=current_user
-        )
-
-        return app, 201
-
-
-class AppImportFromUrlApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    @marshal_with(app_detail_fields_with_site)
-    @cloud_edition_billing_resource_check("apps")
-    def post(self):
-        """Import app from url"""
-        # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_editor:
-            raise Forbidden()
-
-        parser = reqparse.RequestParser()
-        parser.add_argument("url", type=str, required=True, nullable=False, location="json")
-        parser.add_argument("name", type=str, location="json")
-        parser.add_argument("description", type=str, location="json")
-        parser.add_argument("icon", type=str, location="json")
-        parser.add_argument("icon_background", type=str, location="json")
-        args = parser.parse_args()
-
-        app = AppDslService.import_and_create_new_app_from_url(
-            tenant_id=current_user.current_tenant_id, url=args["url"], args=args, account=current_user
-        )
-
-        return app, 201
-
-
-class AppImportFromUrlDependenciesCheckApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    def post(self):
-        parser = reqparse.RequestParser()
-        parser.add_argument("url", type=str, required=True, nullable=False, location="json")
-        args = parser.parse_args()
-
-        leaked_dependencies = AppDslService.check_dependencies_from_url(
-            tenant_id=current_user.current_tenant_id, url=args["url"], account=current_user
-        )
-
-        return jsonable_encoder({"leaked": leaked_dependencies}), 200
-
-
 class AppApi(Resource):
     @setup_required
     @login_required
@@ -263,10 +174,24 @@ class AppCopyApi(Resource):
         parser.add_argument("icon_background", type=str, location="json")
         args = parser.parse_args()
 
-        data = AppDslService.export_dsl(app_model=app_model, include_secret=True)
-        app = AppDslService.import_and_create_new_app(
-            tenant_id=current_user.current_tenant_id, data=data, args=args, account=current_user
-        )
+        with Session(db.engine) as session:
+            import_service = AppDslService(session)
+            yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
+            account = cast(Account, current_user)
+            result = import_service.import_app(
+                account=account,
+                import_mode=ImportMode.YAML_CONTENT.value,
+                yaml_content=yaml_content,
+                name=args.get("name"),
+                description=args.get("description"),
+                icon_type=args.get("icon_type"),
+                icon=args.get("icon"),
+                icon_background=args.get("icon_background"),
+            )
+            session.commit()
+
+            stmt = select(App).where(App.id == result.app.id)
+            app = session.scalar(stmt)
 
         return app, 201
 
@@ -407,10 +332,6 @@ class AppTraceApi(Resource):
 
 
 api.add_resource(AppListApi, "/apps")
-api.add_resource(AppImportDependenciesCheckApi, "/apps/import/dependencies/check")
-api.add_resource(AppImportApi, "/apps/import")
-api.add_resource(AppImportFromUrlApi, "/apps/import/url")
-api.add_resource(AppImportFromUrlDependenciesCheckApi, "/apps/import/url/dependencies/check")
 api.add_resource(AppApi, "/apps/<uuid:app_id>")
 api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
 api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")

+ 90 - 0
api/controllers/console/app/app_import.py

@@ -0,0 +1,90 @@
+from typing import cast
+
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
+from sqlalchemy.orm import Session
+from werkzeug.exceptions import Forbidden
+
+from controllers.console.wraps import (
+    account_initialization_required,
+    setup_required,
+)
+from extensions.ext_database import db
+from fields.app_fields import app_import_fields
+from libs.login import login_required
+from models import Account
+from services.app_dsl_service import AppDslService, ImportStatus
+
+
+class AppImportApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @marshal_with(app_import_fields)
+    def post(self):
+        # Check user role first
+        if not current_user.is_editor:
+            raise Forbidden()
+
+        parser = reqparse.RequestParser()
+        parser.add_argument("mode", type=str, required=True, location="json")
+        parser.add_argument("yaml_content", type=str, location="json")
+        parser.add_argument("yaml_url", type=str, location="json")
+        parser.add_argument("name", type=str, location="json")
+        parser.add_argument("description", type=str, location="json")
+        parser.add_argument("icon_type", type=str, location="json")
+        parser.add_argument("icon", type=str, location="json")
+        parser.add_argument("icon_background", type=str, location="json")
+        parser.add_argument("app_id", type=str, location="json")
+        args = parser.parse_args()
+
+        # Create service with session
+        with Session(db.engine) as session:
+            import_service = AppDslService(session)
+            # Import app
+            account = cast(Account, current_user)
+            result = import_service.import_app(
+                account=account,
+                import_mode=args["mode"],
+                yaml_content=args.get("yaml_content"),
+                yaml_url=args.get("yaml_url"),
+                name=args.get("name"),
+                description=args.get("description"),
+                icon_type=args.get("icon_type"),
+                icon=args.get("icon"),
+                icon_background=args.get("icon_background"),
+                app_id=args.get("app_id"),
+            )
+            session.commit()
+
+        # Return appropriate status code based on result
+        status = result.status
+        if status == ImportStatus.FAILED.value:
+            return result.model_dump(mode="json"), 400
+        elif status == ImportStatus.PENDING.value:
+            return result.model_dump(mode="json"), 202
+        return result.model_dump(mode="json"), 200
+
+
+class AppImportConfirmApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @marshal_with(app_import_fields)
+    def post(self, import_id):
+        # Check user role first
+        if not current_user.is_editor:
+            raise Forbidden()
+
+        # Create service with session
+        with Session(db.engine) as session:
+            import_service = AppDslService(session)
+            # Confirm import
+            account = cast(Account, current_user)
+            result = import_service.confirm_import(import_id=import_id, account=account)
+            session.commit()
+
+        # Return appropriate status code based on result
+        if result.status == ImportStatus.FAILED.value:
+            return result.model_dump(mode="json"), 400
+        return result.model_dump(mode="json"), 200

+ 2 - 2
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()
 

+ 3 - 3
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

+ 0 - 30
api/controllers/console/app/workflow.py

@@ -21,7 +21,6 @@ from libs.login import current_user, login_required
 from models import App
 from models.account import Account
 from models.model import AppMode
-from services.app_dsl_service import AppDslService
 from services.app_generate_service import AppGenerateService
 from services.errors.app import WorkflowHashNotEqualError
 from services.workflow_service import WorkflowService
@@ -130,34 +129,6 @@ class DraftWorkflowApi(Resource):
         }
 
 
-class DraftWorkflowImportApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
-    @marshal_with(workflow_fields)
-    def post(self, app_model: App):
-        """
-        Import draft workflow
-        """
-        # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_editor:
-            raise Forbidden()
-
-        if not isinstance(current_user, Account):
-            raise Forbidden()
-
-        parser = reqparse.RequestParser()
-        parser.add_argument("data", type=str, required=True, nullable=False, location="json")
-        args = parser.parse_args()
-
-        workflow = AppDslService.import_and_overwrite_workflow(
-            app_model=app_model, data=args["data"], account=current_user
-        )
-
-        return workflow
-
-
 class AdvancedChatDraftWorkflowRunApi(Resource):
     @setup_required
     @login_required
@@ -490,7 +461,6 @@ class ConvertToWorkflowApi(Resource):
 
 
 api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
-api.add_resource(DraftWorkflowImportApi, "/apps/<uuid:app_id>/workflows/draft/import")
 api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
 api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
 api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")

+ 1 - 1
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))

+ 2 - 2
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
@@ -108,7 +108,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:

+ 2 - 2
api/controllers/console/datasets/data_source.py

@@ -88,7 +88,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:
@@ -97,7 +97,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:

+ 9 - 9
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
@@ -681,7 +681,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()
 
@@ -761,7 +761,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
@@ -803,7 +803,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
@@ -820,9 +820,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
@@ -837,9 +837,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:
@@ -856,7 +856,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

+ 2 - 2
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()
 

+ 3 - 3
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:

+ 2 - 2
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()

+ 2 - 2
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"}

+ 2 - 2
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

+ 1 - 0
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

+ 19 - 23
api/core/app/app_config/easy_ui_based_app/model_config/converter.py

@@ -12,7 +12,7 @@ from core.provider_manager import ProviderManager
 
 class ModelConfigConverter:
     @classmethod
-    def convert(cls, app_config: EasyUIBasedAppConfig, skip_check: bool = False) -> ModelConfigWithCredentialsEntity:
+    def convert(cls, app_config: EasyUIBasedAppConfig) -> ModelConfigWithCredentialsEntity:
         """
         Convert app model config dict to entity.
         :param app_config: app config
@@ -39,27 +39,23 @@ class ModelConfigConverter:
         )
 
         if model_credentials is None:
-            if not skip_check:
-                raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.")
-            else:
-                model_credentials = {}
-
-        if not skip_check:
-            # check model
-            provider_model = provider_model_bundle.configuration.get_provider_model(
-                model=model_config.model, model_type=ModelType.LLM
-            )
-
-            if provider_model is None:
-                model_name = model_config.model
-                raise ValueError(f"Model {model_name} not exist.")
-
-            if provider_model.status == ModelStatus.NO_CONFIGURE:
-                raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.")
-            elif provider_model.status == ModelStatus.NO_PERMISSION:
-                raise ModelCurrentlyNotSupportError(f"Dify Hosted OpenAI {model_name} currently not support.")
-            elif provider_model.status == ModelStatus.QUOTA_EXCEEDED:
-                raise QuotaExceededError(f"Model provider {provider_name} quota exceeded.")
+            raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.")
+
+        # check model
+        provider_model = provider_model_bundle.configuration.get_provider_model(
+            model=model_config.model, model_type=ModelType.LLM
+        )
+
+        if provider_model is None:
+            model_name = model_config.model
+            raise ValueError(f"Model {model_name} not exist.")
+
+        if provider_model.status == ModelStatus.NO_CONFIGURE:
+            raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.")
+        elif provider_model.status == ModelStatus.NO_PERMISSION:
+            raise ModelCurrentlyNotSupportError(f"Dify Hosted OpenAI {model_name} currently not support.")
+        elif provider_model.status == ModelStatus.QUOTA_EXCEEDED:
+            raise QuotaExceededError(f"Model provider {provider_name} quota exceeded.")
 
         # model config
         completion_params = model_config.parameters
@@ -77,7 +73,7 @@ class ModelConfigConverter:
             if model_schema and model_schema.model_properties.get(ModelPropertyKey.MODE):
                 model_mode = LLMMode.value_of(model_schema.model_properties[ModelPropertyKey.MODE]).value
 
-        if not skip_check and not model_schema:
+        if not model_schema:
             raise ValueError(f"Model {model_name} not exist.")
 
         return ModelConfigWithCredentialsEntity(

+ 4 - 1
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)

+ 2 - 2
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"

+ 1 - 1
api/core/app/apps/advanced_chat/app_generator.py

@@ -138,7 +138,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,

+ 1 - 1
api/core/app/apps/agent_chat/app_generator.py

@@ -139,7 +139,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,

+ 7 - 7
api/core/app/apps/base_app_generator.py

@@ -1,5 +1,5 @@
 import json
-from collections.abc import Generator, Mapping
+from collections.abc import Generator, Mapping, Sequence
 from typing import TYPE_CHECKING, Any, Optional, Union
 
 from core.app.app_config.entities import VariableEntityType
@@ -7,7 +7,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:
@@ -15,23 +15,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,
@@ -45,7 +45,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,

+ 1 - 1
api/core/app/apps/chat/app_generator.py

@@ -142,7 +142,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,

+ 3 - 1
api/core/app/apps/completion/app_generator.py

@@ -123,7 +123,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,

+ 2 - 2
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(

+ 3 - 1
api/core/app/apps/workflow/app_generator.py

@@ -108,7 +108,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,

+ 0 - 3
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

+ 2 - 2
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
     """

+ 12 - 9
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,15 +211,18 @@ 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()
             db.session.commit()
 
-        db.session.refresh(workflow_run)
         db.session.close()
 
+        with Session(db.engine, expire_on_commit=False) as session:
+            session.add(workflow_run)
+            session.refresh(workflow_run)
+
         if trace_manager:
             trace_manager.add_trace_task(
                 TraceTask(
@@ -259,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()
@@ -282,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(
@@ -326,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
@@ -654,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()),

+ 6 - 6
api/core/entities/provider_configuration.py

@@ -246,7 +246,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()
@@ -401,7 +401,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()
@@ -474,7 +474,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()
@@ -508,7 +508,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()
@@ -574,7 +574,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()
@@ -608,7 +608,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()

+ 6 - 6
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"

+ 25 - 44
api/core/file/file_manager.py

@@ -3,7 +3,12 @@ import base64
 from configs import dify_config
 from core.file import file_repository
 from core.helper import ssrf_proxy
-from core.model_runtime.entities import AudioPromptMessageContent, ImagePromptMessageContent, VideoPromptMessageContent
+from core.model_runtime.entities import (
+    AudioPromptMessageContent,
+    DocumentPromptMessageContent,
+    ImagePromptMessageContent,
+    VideoPromptMessageContent,
+)
 from extensions.ext_database import db
 from extensions.ext_storage import storage
 
@@ -29,35 +34,17 @@ def get_attr(*, file: File, attr: FileAttribute):
             return file.remote_url
         case FileAttribute.EXTENSION:
             return file.extension
-        case _:
-            raise ValueError(f"Invalid file attribute: {attr}")
 
 
 def to_prompt_message_content(
     f: File,
     /,
     *,
-    image_detail_config: ImagePromptMessageContent.DETAIL = ImagePromptMessageContent.DETAIL.LOW,
+    image_detail_config: ImagePromptMessageContent.DETAIL | None = None,
 ):
-    """
-    Convert a File object to an ImagePromptMessageContent or AudioPromptMessageContent object.
-
-    This function takes a File object and converts it to an appropriate PromptMessageContent
-    object, which can be used as a prompt for image or audio-based AI models.
-
-    Args:
-        f (File): The File object to convert.
-        detail (Optional[ImagePromptMessageContent.DETAIL]): The detail level for image prompts.
-            If not provided, defaults to ImagePromptMessageContent.DETAIL.LOW.
-
-    Returns:
-        Union[ImagePromptMessageContent, AudioPromptMessageContent]: An object containing the file data and detail level
-
-    Raises:
-        ValueError: If the file type is not supported or if required data is missing.
-    """
     match f.type:
         case FileType.IMAGE:
+            image_detail_config = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
             if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == "url":
                 data = _to_url(f)
             else:
@@ -65,7 +52,7 @@ def to_prompt_message_content(
 
             return ImagePromptMessageContent(data=data, detail=image_detail_config)
         case FileType.AUDIO:
-            encoded_string = _file_to_encoded_string(f)
+            encoded_string = _get_encoded_string(f)
             if f.extension is None:
                 raise ValueError("Missing file extension")
             return AudioPromptMessageContent(data=encoded_string, format=f.extension.lstrip("."))
@@ -74,9 +61,20 @@ def to_prompt_message_content(
                 data = _to_url(f)
             else:
                 data = _to_base64_data_string(f)
+            if f.extension is None:
+                raise ValueError("Missing file extension")
             return VideoPromptMessageContent(data=data, format=f.extension.lstrip("."))
+        case FileType.DOCUMENT:
+            data = _get_encoded_string(f)
+            if f.mime_type is None:
+                raise ValueError("Missing file mime_type")
+            return DocumentPromptMessageContent(
+                encode_format="base64",
+                mime_type=f.mime_type,
+                data=data,
+            )
         case _:
-            raise ValueError("file type f.type is not supported")
+            raise ValueError(f"file type {f.type} is not supported")
 
 
 def download(f: File, /):
@@ -118,21 +116,16 @@ def _get_encoded_string(f: File, /):
         case FileTransferMethod.REMOTE_URL:
             response = ssrf_proxy.get(f.remote_url, follow_redirects=True)
             response.raise_for_status()
-            content = response.content
-            encoded_string = base64.b64encode(content).decode("utf-8")
-            return encoded_string
+            data = response.content
         case FileTransferMethod.LOCAL_FILE:
             upload_file = file_repository.get_upload_file(session=db.session(), file=f)
             data = _download_file_content(upload_file.key)
-            encoded_string = base64.b64encode(data).decode("utf-8")
-            return encoded_string
         case FileTransferMethod.TOOL_FILE:
             tool_file = file_repository.get_tool_file(session=db.session(), file=f)
             data = _download_file_content(tool_file.file_key)
-            encoded_string = base64.b64encode(data).decode("utf-8")
-            return encoded_string
-        case _:
-            raise ValueError(f"Unsupported transfer method: {f.transfer_method}")
+
+    encoded_string = base64.b64encode(data).decode("utf-8")
+    return encoded_string
 
 
 def _to_base64_data_string(f: File, /):
@@ -140,18 +133,6 @@ def _to_base64_data_string(f: File, /):
     return f"data:{f.mime_type};base64,{encoded_string}"
 
 
-def _file_to_encoded_string(f: File, /):
-    match f.type:
-        case FileType.IMAGE:
-            return _to_base64_data_string(f)
-        case FileType.VIDEO:
-            return _to_base64_data_string(f)
-        case FileType.AUDIO:
-            return _get_encoded_string(f)
-        case _:
-            raise ValueError(f"file type {f.type} is not supported")
-
-
 def _to_url(f: File, /):
     if f.transfer_method == FileTransferMethod.REMOTE_URL:
         if f.remote_url is None:

+ 2 - 2
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"

+ 14 - 14
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

+ 7 - 8
api/core/memory/token_buffer_memory.py

@@ -1,8 +1,8 @@
+from collections.abc import Sequence
 from typing import Optional
 
 from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
 from core.file import file_manager
-from core.file.models import FileType
 from core.model_manager import ModelInstance
 from core.model_runtime.entities import (
     AssistantPromptMessage,
@@ -27,7 +27,7 @@ class TokenBufferMemory:
 
     def get_history_prompt_messages(
         self, max_token_limit: int = 2000, message_limit: Optional[int] = None
-    ) -> list[PromptMessage]:
+    ) -> Sequence[PromptMessage]:
         """
         Get history prompt messages.
         :param max_token_limit: max token limit
@@ -102,12 +102,11 @@ class TokenBufferMemory:
                     prompt_message_contents: list[PromptMessageContent] = []
                     prompt_message_contents.append(TextPromptMessageContent(data=message.query))
                     for file in file_objs:
-                        if file.type in {FileType.IMAGE, FileType.AUDIO}:
-                            prompt_message = file_manager.to_prompt_message_content(
-                                file,
-                                image_detail_config=detail,
-                            )
-                            prompt_message_contents.append(prompt_message)
+                        prompt_message = file_manager.to_prompt_message_content(
+                            file,
+                            image_detail_config=detail,
+                        )
+                        prompt_message_contents.append(prompt_message)
 
                     prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
 

+ 2 - 2
api/core/model_manager.py

@@ -136,10 +136,10 @@ class ModelInstance:
 
     def invoke_llm(
         self,
-        prompt_messages: list[PromptMessage],
+        prompt_messages: Sequence[PromptMessage],
         model_parameters: Optional[dict] = None,
         tools: Sequence[PromptMessageTool] | None = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,

+ 5 - 4
api/core/model_runtime/callbacks/base_callback.py

@@ -1,4 +1,5 @@
 from abc import ABC, abstractmethod
+from collections.abc import Sequence
 from typing import Optional
 
 from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
@@ -31,7 +32,7 @@ class Callback(ABC):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
     ) -> None:
@@ -60,7 +61,7 @@ class Callback(ABC):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
     ):
@@ -90,7 +91,7 @@ class Callback(ABC):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
     ) -> None:
@@ -120,7 +121,7 @@ class Callback(ABC):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
     ) -> None:

+ 2 - 0
api/core/model_runtime/entities/__init__.py

@@ -2,6 +2,7 @@ from .llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsa
 from .message_entities import (
     AssistantPromptMessage,
     AudioPromptMessageContent,
+    DocumentPromptMessageContent,
     ImagePromptMessageContent,
     PromptMessage,
     PromptMessageContent,
@@ -37,4 +38,5 @@ __all__ = [
     "LLMResultChunk",
     "LLMResultChunkDelta",
     "AudioPromptMessageContent",
+    "DocumentPromptMessageContent",
 ]

+ 14 - 5
api/core/model_runtime/entities/message_entities.py

@@ -1,6 +1,7 @@
 from abc import ABC
-from enum import Enum
-from typing import Optional
+from collections.abc import Sequence
+from enum import Enum, StrEnum
+from typing import Literal, Optional
 
 from pydantic import BaseModel, Field, field_validator
 
@@ -48,7 +49,7 @@ class PromptMessageFunction(BaseModel):
     function: PromptMessageTool
 
 
-class PromptMessageContentType(Enum):
+class PromptMessageContentType(StrEnum):
     """
     Enum class for prompt message content type.
     """
@@ -57,6 +58,7 @@ class PromptMessageContentType(Enum):
     IMAGE = "image"
     AUDIO = "audio"
     VIDEO = "video"
+    DOCUMENT = "document"
 
 
 class PromptMessageContent(BaseModel):
@@ -93,7 +95,7 @@ class ImagePromptMessageContent(PromptMessageContent):
     Model class for image prompt message content.
     """
 
-    class DETAIL(str, Enum):
+    class DETAIL(StrEnum):
         LOW = "low"
         HIGH = "high"
 
@@ -101,13 +103,20 @@ class ImagePromptMessageContent(PromptMessageContent):
     detail: DETAIL = DETAIL.LOW
 
 
+class DocumentPromptMessageContent(PromptMessageContent):
+    type: PromptMessageContentType = PromptMessageContentType.DOCUMENT
+    encode_format: Literal["base64"]
+    mime_type: str
+    data: str
+
+
 class PromptMessage(ABC, BaseModel):
     """
     Model class for prompt message.
     """
 
     role: PromptMessageRole
-    content: Optional[str | list[PromptMessageContent]] = None
+    content: Optional[str | Sequence[PromptMessageContent]] = None
     name: Optional[str] = None
 
     def is_empty(self) -> bool:

+ 5 - 2
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
@@ -82,9 +82,12 @@ class ModelFeature(Enum):
     AGENT_THOUGHT = "agent-thought"
     VISION = "vision"
     STREAM_TOOL_CALL = "stream-tool-call"
+    DOCUMENT = "document"
+    VIDEO = "video"
+    AUDIO = "audio"
 
 
-class DefaultParameterName(str, Enum):
+class DefaultParameterName(StrEnum):
     """
     Enum class for parameter template variable.
     """

+ 8 - 8
api/core/model_runtime/model_providers/__base/large_language_model.py

@@ -1,6 +1,6 @@
 import logging
 import time
-from collections.abc import Generator
+from collections.abc import Generator, Sequence
 from typing import Optional, Union
 
 from pydantic import ConfigDict
@@ -41,7 +41,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: Optional[dict] = None,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,
@@ -96,7 +96,7 @@ class LargeLanguageModel(AIModel):
                 model_parameters=model_parameters,
                 prompt_messages=prompt_messages,
                 tools=tools,
-                stop=stop,
+                stop=list(stop) if stop else None,
                 stream=stream,
             )
 
@@ -176,7 +176,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,
@@ -318,7 +318,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,
@@ -364,7 +364,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,
@@ -411,7 +411,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,
@@ -459,7 +459,7 @@ class LargeLanguageModel(AIModel):
         prompt_messages: list[PromptMessage],
         model_parameters: dict,
         tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
+        stop: Optional[Sequence[str]] = None,
         stream: bool = True,
         user: Optional[str] = None,
         callbacks: Optional[list[Callback]] = None,

BIN
api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.png


File diff suppressed because it is too large
+ 0 - 15
api/core/model_runtime/model_providers/gpustack/_assets/icon_l_en.svg


BIN
api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.png


+ 0 - 11
api/core/model_runtime/model_providers/gpustack/_assets/icon_s_en.svg

@@ -1,11 +0,0 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect width="24" height="24" rx="6" fill="url(#paint0_linear_7301_16076)"/>
-<path d="M20 12.0116C15.7043 12.42 12.3692 15.757 11.9995 20C11.652 15.8183 8.20301 12.361 4 12.0181C8.21855 11.6991 11.6656 8.1853 12.006 4C12.2833 8.19653 15.8057 11.7005 20 12.0116Z" fill="white" fill-opacity="0.88"/>
-<defs>
-<linearGradient id="paint0_linear_7301_16076" x1="-9" y1="29.5" x2="19.4387" y2="1.43791" gradientUnits="userSpaceOnUse">
-<stop offset="0.192878" stop-color="#1C7DFF"/>
-<stop offset="0.520213" stop-color="#1C69FF"/>
-<stop offset="1" stop-color="#F0DCD6"/>
-</linearGradient>
-</defs>
-</svg>

+ 0 - 10
api/core/model_runtime/model_providers/gpustack/gpustack.py

@@ -1,10 +0,0 @@
-import logging
-
-from core.model_runtime.model_providers.__base.model_provider import ModelProvider
-
-logger = logging.getLogger(__name__)
-
-
-class GPUStackProvider(ModelProvider):
-    def validate_provider_credentials(self, credentials: dict) -> None:
-        pass

+ 0 - 120
api/core/model_runtime/model_providers/gpustack/gpustack.yaml

@@ -1,120 +0,0 @@
-provider: gpustack
-label:
-  en_US: GPUStack
-icon_small:
-  en_US: icon_s_en.png
-icon_large:
-  en_US: icon_l_en.png
-supported_model_types:
-  - llm
-  - text-embedding
-  - rerank
-configurate_methods:
-  - customizable-model
-model_credential_schema:
-  model:
-    label:
-      en_US: Model Name
-      zh_Hans: 模型名称
-    placeholder:
-      en_US: Enter your model name
-      zh_Hans: 输入模型名称
-  credential_form_schemas:
-    - variable: endpoint_url
-      label:
-        zh_Hans: 服务器地址
-        en_US: Server URL
-      type: text-input
-      required: true
-      placeholder:
-        zh_Hans: 输入 GPUStack 的服务器地址,如 http://192.168.1.100
-        en_US: Enter the GPUStack server URL, e.g. http://192.168.1.100
-    - variable: api_key
-      label:
-        en_US: API Key
-      type: secret-input
-      required: true
-      placeholder:
-        zh_Hans: 输入您的 API Key
-        en_US: Enter your API Key
-    - variable: mode
-      show_on:
-        - variable: __model_type
-          value: llm
-      label:
-        en_US: Completion mode
-      type: select
-      required: false
-      default: chat
-      placeholder:
-        zh_Hans: 选择补全类型
-        en_US: Select completion type
-      options:
-        - value: completion
-          label:
-            en_US: Completion
-            zh_Hans: 补全
-        - value: chat
-          label:
-            en_US: Chat
-            zh_Hans: 对话
-    - variable: context_size
-      label:
-        zh_Hans: 模型上下文长度
-        en_US: Model context size
-      required: true
-      type: text-input
-      default: "8192"
-      placeholder:
-        zh_Hans: 输入您的模型上下文长度
-        en_US: Enter your Model context size
-    - variable: max_tokens_to_sample
-      label:
-        zh_Hans: 最大 token 上限
-        en_US: Upper bound for max tokens
-      show_on:
-        - variable: __model_type
-          value: llm
-      default: "8192"
-      type: text-input
-    - variable: function_calling_type
-      show_on:
-        - variable: __model_type
-          value: llm
-      label:
-        en_US: Function calling
-      type: select
-      required: false
-      default: no_call
-      options:
-        - value: function_call
-          label:
-            en_US: Function Call
-            zh_Hans: Function Call
-        - value: tool_call
-          label:
-            en_US: Tool Call
-            zh_Hans: Tool Call
-        - value: no_call
-          label:
-            en_US: Not Support
-            zh_Hans: 不支持
-    - variable: vision_support
-      show_on:
-        - variable: __model_type
-          value: llm
-      label:
-        zh_Hans: Vision 支持
-        en_US: Vision Support
-      type: select
-      required: false
-      default: no_support
-      options:
-        - value: support
-          label:
-            en_US: Support
-            zh_Hans: 支持
-        - value: no_support
-          label:
-            en_US: Not Support
-            zh_Hans: 不支持

+ 0 - 0
api/core/model_runtime/model_providers/gpustack/llm/__init__.py


+ 0 - 45
api/core/model_runtime/model_providers/gpustack/llm/llm.py

@@ -1,45 +0,0 @@
-from collections.abc import Generator
-
-from yarl import URL
-
-from core.model_runtime.entities.llm_entities import LLMResult
-from core.model_runtime.entities.message_entities import (
-    PromptMessage,
-    PromptMessageTool,
-)
-from core.model_runtime.model_providers.openai_api_compatible.llm.llm import (
-    OAIAPICompatLargeLanguageModel,
-)
-
-
-class GPUStackLanguageModel(OAIAPICompatLargeLanguageModel):
-    def _invoke(
-        self,
-        model: str,
-        credentials: dict,
-        prompt_messages: list[PromptMessage],
-        model_parameters: dict,
-        tools: list[PromptMessageTool] | None = None,
-        stop: list[str] | None = None,
-        stream: bool = True,
-        user: str | None = None,
-    ) -> LLMResult | Generator:
-        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)
-        super().validate_credentials(model, credentials)
-
-    @staticmethod
-    def _add_custom_parameters(credentials: dict) -> None:
-        credentials["endpoint_url"] = str(URL(credentials["endpoint_url"]) / "v1-openai")
-        credentials["mode"] = "chat"

+ 0 - 0
api/core/model_runtime/model_providers/gpustack/rerank/__init__.py


+ 0 - 146
api/core/model_runtime/model_providers/gpustack/rerank/rerank.py

@@ -1,146 +0,0 @@
-from json import dumps
-from typing import Optional
-
-import httpx
-from requests import post
-from yarl import URL
-
-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.entities.rerank_entities import RerankDocument, RerankResult
-from core.model_runtime.errors.invoke import (
-    InvokeAuthorizationError,
-    InvokeBadRequestError,
-    InvokeConnectionError,
-    InvokeError,
-    InvokeRateLimitError,
-    InvokeServerUnavailableError,
-)
-from core.model_runtime.errors.validate import CredentialsValidateFailedError
-from core.model_runtime.model_providers.__base.rerank_model import RerankModel
-
-
-class GPUStackRerankModel(RerankModel):
-    """
-    Model class for GPUStack rerank model.
-    """
-
-    def _invoke(
-        self,
-        model: str,
-        credentials: dict,
-        query: str,
-        docs: list[str],
-        score_threshold: Optional[float] = None,
-        top_n: Optional[int] = None,
-        user: Optional[str] = None,
-    ) -> RerankResult:
-        """
-        Invoke rerank model
-
-        :param model: model name
-        :param credentials: model credentials
-        :param query: search query
-        :param docs: docs for reranking
-        :param score_threshold: score threshold
-        :param top_n: top n documents to return
-        :param user: unique user id
-        :return: rerank result
-        """
-        if len(docs) == 0:
-            return RerankResult(model=model, docs=[])
-
-        endpoint_url = credentials["endpoint_url"]
-        headers = {
-            "Authorization": f"Bearer {credentials.get('api_key')}",
-            "Content-Type": "application/json",
-        }
-
-        data = {"model": model, "query": query, "documents": docs, "top_n": top_n}
-
-        try:
-            response = post(
-                str(URL(endpoint_url) / "v1" / "rerank"),
-                headers=headers,
-                data=dumps(data),
-                timeout=10,
-            )
-            response.raise_for_status()
-            results = response.json()
-
-            rerank_documents = []
-            for result in results["results"]:
-                index = result["index"]
-                if "document" in result:
-                    text = result["document"]["text"]
-                else:
-                    text = docs[index]
-
-                rerank_document = RerankDocument(
-                    index=index,
-                    text=text,
-                    score=result["relevance_score"],
-                )
-
-                if score_threshold is None or result["relevance_score"] >= score_threshold:
-                    rerank_documents.append(rerank_document)
-
-            return RerankResult(model=model, docs=rerank_documents)
-        except httpx.HTTPStatusError as e:
-            raise InvokeServerUnavailableError(str(e))
-
-    def validate_credentials(self, model: str, credentials: dict) -> None:
-        """
-        Validate model credentials
-
-        :param model: model name
-        :param credentials: model credentials
-        :return:
-        """
-        try:
-            self._invoke(
-                model=model,
-                credentials=credentials,
-                query="What is the capital of the United States?",
-                docs=[
-                    "Carson City is the capital city of the American state of Nevada. At the 2010 United States "
-                    "Census, Carson City had a population of 55,274.",
-                    "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that "
-                    "are a political division controlled by the United States. Its capital is Saipan.",
-                ],
-                score_threshold=0.8,
-            )
-        except Exception as ex:
-            raise CredentialsValidateFailedError(str(ex))
-
-    @property
-    def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
-        """
-        Map model invoke error to unified error
-        """
-        return {
-            InvokeConnectionError: [httpx.ConnectError],
-            InvokeServerUnavailableError: [httpx.RemoteProtocolError],
-            InvokeRateLimitError: [],
-            InvokeAuthorizationError: [httpx.HTTPStatusError],
-            InvokeBadRequestError: [httpx.RequestError],
-        }
-
-    def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
-        """
-        generate custom model entities from credentials
-        """
-        entity = AIModelEntity(
-            model=model,
-            label=I18nObject(en_US=model),
-            model_type=ModelType.RERANK,
-            fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
-            model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))},
-        )
-
-        return entity

+ 0 - 0
api/core/model_runtime/model_providers/gpustack/text_embedding/__init__.py


+ 0 - 35
api/core/model_runtime/model_providers/gpustack/text_embedding/text_embedding.py

@@ -1,35 +0,0 @@
-from typing import Optional
-
-from yarl import URL
-
-from core.entities.embedding_type import EmbeddingInputType
-from core.model_runtime.entities.text_embedding_entities import (
-    TextEmbeddingResult,
-)
-from core.model_runtime.model_providers.openai_api_compatible.text_embedding.text_embedding import (
-    OAICompatEmbeddingModel,
-)
-
-
-class GPUStackTextEmbeddingModel(OAICompatEmbeddingModel):
-    """
-    Model class for GPUStack text embedding model.
-    """
-
-    def _invoke(
-        self,
-        model: str,
-        credentials: dict,
-        texts: list[str],
-        user: Optional[str] = None,
-        input_type: EmbeddingInputType = EmbeddingInputType.DOCUMENT,
-    ) -> TextEmbeddingResult:
-        return super()._invoke(model, credentials, texts, user, input_type)
-
-    def validate_credentials(self, model: str, credentials: dict) -> None:
-        self._add_custom_parameters(credentials)
-        super().validate_credentials(model, credentials)
-
-    @staticmethod
-    def _add_custom_parameters(credentials: dict) -> None:
-        credentials["endpoint_url"] = str(URL(credentials["endpoint_url"]) / "v1-openai")

+ 0 - 55
api/core/model_runtime/model_providers/vertex_ai/llm/anthropic.claude-3.5-sonnet-v2.yaml

@@ -1,55 +0,0 @@
-model: claude-3-5-sonnet-v2@20241022
-label:
-  en_US: Claude 3.5 Sonnet v2
-model_type: llm
-features:
-  - agent-thought
-  - vision
-model_properties:
-  mode: chat
-  context_size: 200000
-parameter_rules:
-  - name: max_tokens
-    use_template: max_tokens
-    required: true
-    type: int
-    default: 8192
-    min: 1
-    max: 8192
-    help:
-      zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
-      en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
-  - name: temperature
-    use_template: temperature
-    required: false
-    type: float
-    default: 1
-    min: 0.0
-    max: 1.0
-    help:
-      zh_Hans: 生成内容的随机性。
-      en_US: The amount of randomness injected into the response.
-  - name: top_p
-    required: false
-    type: float
-    default: 0.999
-    min: 0.000
-    max: 1.000
-    help:
-      zh_Hans: 在核采样中,Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p,但不能同时更改两者。
-      en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
-  - name: top_k
-    required: false
-    type: int
-    default: 0
-    min: 0
-    # tip docs from aws has error, max value is 500
-    max: 500
-    help:
-      zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
-      en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
-pricing:
-  input: '0.003'
-  output: '0.015'
-  unit: '0.001'
-  currency: USD

+ 0 - 0
api/core/model_runtime/model_providers/vessl_ai/__init__.py


BIN
api/core/model_runtime/model_providers/vessl_ai/_assets/icon_l_en.png


+ 0 - 3
api/core/model_runtime/model_providers/vessl_ai/_assets/icon_s_en.svg

@@ -1,3 +0,0 @@
-<svg width="1200" height="925" viewBox="0 0 1200 925" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M780.152 250.999L907.882 462.174C907.882 462.174 880.925 510.854 867.43 535.21C834.845 594.039 764.171 612.49 710.442 508.333L420.376 0H0L459.926 803.307C552.303 964.663 787.366 964.663 879.743 803.307C989.874 610.952 1089.87 441.97 1200 249.646L1052.28 0H639.519L780.152 250.999Z" fill="#3366FF"/>
-</svg>

+ 0 - 0
api/core/model_runtime/model_providers/vessl_ai/llm/__init__.py


+ 0 - 83
api/core/model_runtime/model_providers/vessl_ai/llm/llm.py

@@ -1,83 +0,0 @@
-from decimal import Decimal
-
-from core.model_runtime.entities.common_entities import I18nObject
-from core.model_runtime.entities.llm_entities import LLMMode
-from core.model_runtime.entities.model_entities import (
-    AIModelEntity,
-    DefaultParameterName,
-    FetchFrom,
-    ModelPropertyKey,
-    ModelType,
-    ParameterRule,
-    ParameterType,
-    PriceConfig,
-)
-from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel
-
-
-class VesslAILargeLanguageModel(OAIAPICompatLargeLanguageModel):
-    def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
-        features = []
-
-        entity = AIModelEntity(
-            model=model,
-            label=I18nObject(en_US=model),
-            model_type=ModelType.LLM,
-            fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
-            features=features,
-            model_properties={
-                ModelPropertyKey.MODE: credentials.get("mode"),
-            },
-            parameter_rules=[
-                ParameterRule(
-                    name=DefaultParameterName.TEMPERATURE.value,
-                    label=I18nObject(en_US="Temperature"),
-                    type=ParameterType.FLOAT,
-                    default=float(credentials.get("temperature", 0.7)),
-                    min=0,
-                    max=2,
-                    precision=2,
-                ),
-                ParameterRule(
-                    name=DefaultParameterName.TOP_P.value,
-                    label=I18nObject(en_US="Top P"),
-                    type=ParameterType.FLOAT,
-                    default=float(credentials.get("top_p", 1)),
-                    min=0,
-                    max=1,
-                    precision=2,
-                ),
-                ParameterRule(
-                    name=DefaultParameterName.TOP_K.value,
-                    label=I18nObject(en_US="Top K"),
-                    type=ParameterType.INT,
-                    default=int(credentials.get("top_k", 50)),
-                    min=-2147483647,
-                    max=2147483647,
-                    precision=0,
-                ),
-                ParameterRule(
-                    name=DefaultParameterName.MAX_TOKENS.value,
-                    label=I18nObject(en_US="Max Tokens"),
-                    type=ParameterType.INT,
-                    default=512,
-                    min=1,
-                    max=int(credentials.get("max_tokens_to_sample", 4096)),
-                ),
-            ],
-            pricing=PriceConfig(
-                input=Decimal(credentials.get("input_price", 0)),
-                output=Decimal(credentials.get("output_price", 0)),
-                unit=Decimal(credentials.get("unit", 0)),
-                currency=credentials.get("currency", "USD"),
-            ),
-        )
-
-        if credentials["mode"] == "chat":
-            entity.model_properties[ModelPropertyKey.MODE] = LLMMode.CHAT.value
-        elif credentials["mode"] == "completion":
-            entity.model_properties[ModelPropertyKey.MODE] = LLMMode.COMPLETION.value
-        else:
-            raise ValueError(f"Unknown completion type {credentials['completion_type']}")
-
-        return entity

+ 0 - 10
api/core/model_runtime/model_providers/vessl_ai/vessl_ai.py

@@ -1,10 +0,0 @@
-import logging
-
-from core.model_runtime.model_providers.__base.model_provider import ModelProvider
-
-logger = logging.getLogger(__name__)
-
-
-class VesslAIProvider(ModelProvider):
-    def validate_provider_credentials(self, credentials: dict) -> None:
-        pass

+ 0 - 56
api/core/model_runtime/model_providers/vessl_ai/vessl_ai.yaml

@@ -1,56 +0,0 @@
-provider: vessl_ai
-label:
-  en_US: VESSL AI
-icon_small:
-  en_US: icon_s_en.svg
-icon_large:
-  en_US: icon_l_en.png
-background: "#F1EFED"
-help:
-  title:
-    en_US: How to deploy VESSL AI LLM Model Endpoint
-  url:
-    en_US: https://docs.vessl.ai/guides/get-started/llama3-deployment
-supported_model_types:
-  - llm
-configurate_methods:
-  - customizable-model
-model_credential_schema:
-  model:
-    label:
-      en_US: Model Name
-    placeholder:
-      en_US: Enter model name
-  credential_form_schemas:
-    - variable: endpoint_url
-      label:
-        en_US: Endpoint Url
-      type: text-input
-      required: true
-      placeholder:
-        en_US: Enter VESSL AI service endpoint url
-    - variable: api_key
-      required: true
-      label:
-        en_US: API Key
-      type: secret-input
-      placeholder:
-        en_US: Enter VESSL AI secret key
-    - variable: mode
-      show_on:
-        - variable: __model_type
-          value: llm
-      label:
-        en_US: Completion Mode
-      type: select
-      required: false
-      default: chat
-      placeholder:
-        en_US: Select completion mode
-      options:
-        - value: completion
-          label:
-            en_US: Completion
-        - value: chat
-          label:
-            en_US: Chat

+ 0 - 0
api/core/model_runtime/model_providers/x/__init__.py


+ 0 - 1
api/core/model_runtime/model_providers/x/_assets/x-ai-logo.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" class="" focusable="false" style="fill:currentColor;height:28px;width:28px"><path d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"></path></svg>

+ 0 - 0
api/core/model_runtime/model_providers/x/llm/__init__.py


+ 0 - 63
api/core/model_runtime/model_providers/x/llm/grok-beta.yaml

@@ -1,63 +0,0 @@
-model: grok-beta
-label:
-  en_US: Grok beta
-model_type: llm
-features:
-  - multi-tool-call
-model_properties:
-  mode: chat
-  context_size: 131072
-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: "用于追踪和区分不同用户的对话请求。"

+ 0 - 37
api/core/model_runtime/model_providers/x/llm/llm.py

@@ -1,37 +0,0 @@
-from collections.abc import Generator
-from typing import Optional, Union
-
-from yarl import URL
-
-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_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel
-
-
-class XAILargeLanguageModel(OAIAPICompatLargeLanguageModel):
-    def _invoke(
-        self,
-        model: str,
-        credentials: dict,
-        prompt_messages: list[PromptMessage],
-        model_parameters: dict,
-        tools: Optional[list[PromptMessageTool]] = None,
-        stop: Optional[list[str]] = None,
-        stream: bool = True,
-        user: Optional[str] = None,
-    ) -> Union[LLMResult, Generator]:
-        self._add_custom_parameters(credentials)
-        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)
-
-    @staticmethod
-    def _add_custom_parameters(credentials) -> None:
-        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"

+ 0 - 25
api/core/model_runtime/model_providers/x/x.py

@@ -1,25 +0,0 @@
-import logging
-
-from core.model_runtime.entities.model_entities import ModelType
-from core.model_runtime.errors.validate import CredentialsValidateFailedError
-from core.model_runtime.model_providers.__base.model_provider import ModelProvider
-
-logger = logging.getLogger(__name__)
-
-
-class XAIProvider(ModelProvider):
-    def validate_provider_credentials(self, credentials: dict) -> None:
-        """
-        Validate provider credentials
-        if validate failed, raise exception
-
-        :param credentials: provider credentials, credentials form defined in `provider_credential_schema`.
-        """
-        try:
-            model_instance = self.get_model_instance(ModelType.LLM)
-            model_instance.validate_credentials(model="grok-beta", credentials=credentials)
-        except CredentialsValidateFailedError as ex:
-            raise ex
-        except Exception as ex:
-            logger.exception(f"{self.get_provider_schema().provider} credentials validate failed")
-            raise ex

+ 0 - 38
api/core/model_runtime/model_providers/x/x.yaml

@@ -1,38 +0,0 @@
-provider: x
-label:
-  en_US: xAI
-description:
-  en_US: xAI is a company working on building artificial intelligence to accelerate human scientific discovery. We are guided by our mission to advance our collective understanding of the universe.
-icon_small:
-  en_US: x-ai-logo.svg
-icon_large:
-  en_US: x-ai-logo.svg
-help:
-  title:
-    en_US: Get your token from xAI
-    zh_Hans: 从 xAI 获取 token
-  url:
-    en_US: https://x.ai/api
-supported_model_types:
-  - llm
-configurate_methods:
-  - predefined-model
-provider_credential_schema:
-  credential_form_schemas:
-    - variable: api_key
-      label:
-        en_US: API Key
-      type: secret-input
-      required: true
-      placeholder:
-        zh_Hans: 在此输入您的 API Key
-        en_US: Enter your API Key
-    - variable: endpoint_url
-      label:
-        en_US: API Base
-      type: text-input
-      required: false
-      default: https://api.x.ai/v1
-      placeholder:
-        zh_Hans: 在此输入您的 API Base
-        en_US: Enter your API Base

+ 2 - 2
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"

+ 3 - 3
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"

+ 2 - 2
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"

+ 1 - 1
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"
 

+ 2 - 1
api/core/prompt/utils/prompt_message_util.py

@@ -1,3 +1,4 @@
+from collections.abc import Sequence
 from typing import cast
 
 from core.model_runtime.entities import (
@@ -14,7 +15,7 @@ from core.prompt.simple_prompt_transform import ModelMode
 
 class PromptMessageUtil:
     @staticmethod
-    def prompt_messages_to_prompt_for_saving(model_mode: str, prompt_messages: list[PromptMessage]) -> list[dict]:
+    def prompt_messages_to_prompt_for_saving(model_mode: str, prompt_messages: Sequence[PromptMessage]) -> list[dict]:
         """
         Prompt messages to prompt for saving.
         :param model_mode: model mode

+ 1 - 1
api/core/rag/cleaner/clean_processor.py

@@ -12,7 +12,7 @@ class CleanProcessor:
         # Unicode  U+FFFE
         text = re.sub("\ufffe", "", text)
 
-        rules = process_rule["rules"] if process_rule else None
+        rules = process_rule["rules"] if process_rule else {}
         if "pre_processing_rules" in rules:
             pre_processing_rules = rules["pre_processing_rules"]
             for pre_processing_rule in pre_processing_rules:

+ 2 - 2
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"

+ 2 - 2
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"

+ 2 - 2
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)

+ 3 - 3
api/core/rag/rerank/rerank_model.py

@@ -27,11 +27,11 @@ class RerankModelRunner(BaseRerankRunner):
         :return:
         """
         docs = []
-        doc_id = set()
+        doc_ids = set()
         unique_documents = []
         for document in documents:
-            if document.provider == "dify" and document.metadata["doc_id"] not in doc_id:
-                doc_id.add(document.metadata["doc_id"])
+            if document.provider == "dify" and document.metadata["doc_id"] not in doc_ids:
+                doc_ids.add(document.metadata["doc_id"])
                 docs.append(document.page_content)
                 unique_documents.append(document)
             elif document.provider == "external":

+ 2 - 2
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"

+ 3 - 4
api/core/rag/rerank/weight_rerank.py

@@ -37,11 +37,10 @@ class WeightRerankRunner(BaseRerankRunner):
         :return:
         """
         unique_documents = []
-        doc_id = set()
+        doc_ids = set()
         for document in documents:
-            doc_id = document.metadata.get("doc_id")
-            if doc_id not in doc_id:
-                doc_id.add(doc_id)
+            if document.metadata["doc_id"] not in doc_ids:
+                doc_ids.add(document.metadata["doc_id"])
                 unique_documents.append(document)
 
         documents = unique_documents

+ 2 - 2
api/core/tools/builtin_tool/providers/time/tools/current_time.py

@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
 from typing import Any, Optional, Union
 
 from pytz import timezone as pytz_timezone
@@ -23,7 +23,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)

+ 9 - 7
api/core/tools/tool_engine.py

@@ -1,7 +1,7 @@
 import json
 from collections.abc import Generator, Iterable
 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, cast
 
@@ -64,7 +64,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:
@@ -195,10 +200,7 @@ class ToolEngine:
         """
         Invoke the tool with the given arguments.
         """
-        if not tool.runtime:
-            raise ValueError("missing runtime in tool")
-
-        started_at = datetime.now(timezone.utc)
+        started_at = datetime.now(UTC)
         meta = ToolInvokeMeta(
             time_cost=0.0,
             error=None,
@@ -216,7 +218,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()
             yield meta
 

+ 10 - 2
api/core/variables/segments.py

@@ -118,11 +118,11 @@ class FileSegment(Segment):
 
     @property
     def log(self) -> str:
-        return str(self.value)
+        return ""
 
     @property
     def text(self) -> str:
-        return str(self.value)
+        return ""
 
 
 class ArrayAnySegment(ArraySegment):
@@ -155,3 +155,11 @@ class ArrayFileSegment(ArraySegment):
         for item in self.value:
             items.append(item.markdown)
         return "\n".join(items)
+
+    @property
+    def log(self) -> str:
+        return ""
+
+    @property
+    def text(self) -> str:
+        return ""

+ 2 - 2
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"

+ 3 - 3
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.
     """
@@ -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
 

+ 2 - 2
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.
     """

+ 0 - 0
api/core/workflow/graph_engine/entities/runtime_route_state.py


Some files were not shown because too many files changed in this diff