From 098ed3446954c9c78ef20dcf3e61f311453a07ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 04:29:31 +0000 Subject: [PATCH 01/12] chore(deps): bump weave from 0.52.17 to 0.52.36 in /api in the llm group (#35038) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 2 +- api/uv.lock | 47 +++++++++++++++++----------------------------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 3f70ec7bc7..370e1a46c2 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -88,7 +88,7 @@ dependencies = [ "croniter>=6.2.2", "weaviate-client==4.20.5", "apscheduler>=3.11.2", - "weave>=0.52.16", + "weave>=0.52.36", "fastopenapi[flask]>=0.7.0", "bleach~=6.3.0", ] diff --git a/api/uv.lock b/api/uv.lock index b5d0404693..db66f057ef 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1553,7 +1553,7 @@ requires-dist = [ { name = "tiktoken", specifier = "~=0.12.0" }, { name = "transformers", specifier = "~=5.3.0" }, { name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.21.5" }, - { name = "weave", specifier = ">=0.52.16" }, + { name = "weave", specifier = ">=0.52.36" }, { name = "weaviate-client", specifier = "==4.20.5" }, { name = "yarl", specifier = "~=1.23.0" }, ] @@ -1668,12 +1668,12 @@ vdb = [ ] [[package]] -name = "diskcache" -version = "5.6.3" +name = "diskcache-weave" +version = "5.6.3.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/52/634e1f43486489fdaded1a7c9bd3524b7e0ca9bcc43af426afa511c541e2/diskcache_weave-5.6.3.post1.tar.gz", hash = "sha256:1fe7e648d1d85d517c05b296f1692e7c425a71714dc31a4b7a584a8f8f5604f2", size = 68297, upload-time = "2026-03-19T14:57:54.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/92887441bc338fb8d0b8ea75eb0392c00e20a85ec0bbe02f273188849568/diskcache_weave-5.6.3.post1-py3-none-any.whl", hash = "sha256:b00e9842b74eeecf314456f9c833a6d4f7792ed12b20297b4d3b9df7859ee66f", size = 45905, upload-time = "2026-03-19T14:57:52.819Z" }, ] [[package]] @@ -1787,15 +1787,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "eval-type-backport" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/23/079e39571d6dd8d90d7a369ecb55ad766efb6bae4e77389629e14458c280/eval_type_backport-0.3.0.tar.gz", hash = "sha256:1638210401e184ff17f877e9a2fa076b60b5838790f4532a21761cc2be67aea1", size = 9272, upload-time = "2025-11-13T20:56:50.845Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" }, -] - [[package]] name = "events" version = "0.5" @@ -2381,12 +2372,8 @@ wheels = [ ] [package.optional-dependencies] -aiohttp = [ - { name = "aiohttp" }, -] -requests = [ - { name = "requests" }, - { name = "requests-toolbelt" }, +httpx = [ + { name = "httpx" }, ] [[package]] @@ -4255,13 +4242,14 @@ wheels = [ [[package]] name = "polyfile-weave" -version = "0.5.8" +version = "0.5.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "abnf" }, { name = "chardet" }, { name = "cint" }, { name = "fickling" }, + { name = "filelock" }, { name = "graphviz" }, { name = "intervaltree" }, { name = "jinja2" }, @@ -4271,11 +4259,10 @@ dependencies = [ { name = "pillow" }, { name = "pyreadline3", marker = "sys_platform == 'win32'" }, { name = "pyyaml" }, - { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/d4/76e56e4429646d9353b4287794f8324ff94201bdb0a2c35ce88cf3de90d0/polyfile_weave-0.5.8.tar.gz", hash = "sha256:cf2ca6a1351165fbbf2971ace4b8bebbb03b2c00e4f2159ff29bed88854e7b32", size = 5989602, upload-time = "2026-01-08T04:21:26.689Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/55/e5400762e3884f743d59291e71eaaa9c52dd7e144b75a11911e74ec1bac9/polyfile_weave-0.5.9.tar.gz", hash = "sha256:12341fab03e06ede1bfebbd3627dd24015fde5353ea74ece2da186321b818bdb", size = 6024974, upload-time = "2026-01-22T22:08:48.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/32/c09fd626366c00325d1981e310be5cac8661c09206098d267a592e0c5000/polyfile_weave-0.5.8-py3-none-any.whl", hash = "sha256:f68c570ef189a4219798a7c797730fc3b7feace7ff5bd7e662490f89b772964a", size = 1656208, upload-time = "2026-01-08T04:21:15.213Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/215005530a48c5f7d4ec4a31acdb5828f2bfb985cc6e577b0eaa5882c0e2/polyfile_weave-0.5.9-py3-none-any.whl", hash = "sha256:6ae4b1b5eeac9f5bfc862474484d6d3e33655fab31749d93af0b0a91fddabfc7", size = 1700174, upload-time = "2026-01-22T22:08:46.346Z" }, ] [[package]] @@ -6749,13 +6736,13 @@ wheels = [ [[package]] name = "weave" -version = "0.52.17" +version = "0.52.36" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cachetools" }, { name = "click" }, - { name = "diskcache" }, - { name = "eval-type-backport" }, - { name = "gql", extra = ["aiohttp", "requests"] }, + { name = "diskcache-weave" }, + { name = "gql", extra = ["httpx"] }, { name = "jsonschema" }, { name = "packaging" }, { name = "polyfile-weave" }, @@ -6765,9 +6752,9 @@ dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "wandb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/95/27e05d954972a83372a3ceb6b5db6136bc4f649fa69d8009b27c144ca111/weave-0.52.17.tar.gz", hash = "sha256:940aaf892b65c72c67cb893e97ed5339136a4b33a7ea85d52ed36671111826ef", size = 609149, upload-time = "2025-11-13T22:09:51.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ee/63064875e0d1d7bf261484da7a8ef1d447d38f739b3e93e4d8673d51c882/weave-0.52.36.tar.gz", hash = "sha256:f58b37786de5444914e408e64026c3131b5c4417e6889d5a61fdcbec12c8e8dd", size = 793026, upload-time = "2026-04-01T17:23:50.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/0b/ae7860d2b0c02e7efab26815a9a5286d3b0f9f4e0356446f2896351bf770/weave-0.52.17-py3-none-any.whl", hash = "sha256:5772ef82521a033829c921115c5779399581a7ae06d81dfd527126e2115d16d4", size = 765887, upload-time = "2025-11-13T22:09:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/78/34/57a0843b3016e3dc63660cd06bfa52322f3333225ae7e7cfa6db97a59f8c/weave-0.52.36-py3-none-any.whl", hash = "sha256:4ff5b53323d20cc321aec665a4b4da746d6d85d432eda2ccca0e85bc8891649d", size = 983539, upload-time = "2026-04-01T17:23:48.819Z" }, ] [[package]] From 8ac9cbf733a1f8c663ce952b2e48ce2d56872837 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:12:23 +0900 Subject: [PATCH 02/12] chore(deps-dev): bump mypy from 1.20.0 to 1.20.1 in /api in the dev group (#35039) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 2 +- api/uv.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 370e1a46c2..fdc6a0f9d7 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -166,7 +166,7 @@ dev = [ "import-linter>=2.3", "types-redis>=4.6.0.20241004", "celery-types>=0.23.0", - "mypy~=1.20.0", + "mypy~=1.20.1", # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", diff --git a/api/uv.lock b/api/uv.lock index db66f057ef..e73ecf867d 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1569,7 +1569,7 @@ dev = [ { name = "hypothesis", specifier = ">=6.151.12" }, { name = "import-linter", specifier = ">=2.3" }, { name = "lxml-stubs", specifier = "~=0.5.1" }, - { name = "mypy", specifier = "~=1.20.0" }, + { name = "mypy", specifier = "~=1.20.1" }, { name = "pandas-stubs", specifier = "~=3.0.0" }, { name = "pyrefly", specifier = ">=0.60.0" }, { name = "pytest", specifier = "~=9.0.3" }, @@ -3388,7 +3388,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.0" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -3396,16 +3396,16 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, - { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, - { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] From 6faf26683c5442d4953141edecabc7d62c4af02c Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Sun, 12 Apr 2026 22:13:15 -0700 Subject: [PATCH 03/12] refactor: remove marshal_with and inline api.model from app_import (#34934) Co-authored-by: ai-hpc --- api/controllers/console/app/app_import.py | 40 +++++++---------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index 12d6951a48..80bd7d1d8d 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -1,7 +1,8 @@ -from flask_restx import Resource, fields, marshal_with +from flask_restx import Resource from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker +from controllers.common.schema import register_schema_models from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( account_initialization_required, @@ -10,35 +11,15 @@ from controllers.console.wraps import ( setup_required, ) from extensions.ext_database import db -from fields.app_fields import ( - app_import_check_dependencies_fields, - app_import_fields, - leaked_dependency_fields, -) from libs.login import current_account_with_tenant, login_required from models.model import App -from services.app_dsl_service import AppDslService +from services.app_dsl_service import AppDslService, Import from services.enterprise.enterprise_service import EnterpriseService -from services.entities.dsl_entities import ImportStatus +from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus from services.feature_service import FeatureService from .. import console_ns -# Register models for flask_restx to avoid dict type issues in Swagger -# Register base model first -leaked_dependency_model = console_ns.model("LeakedDependency", leaked_dependency_fields) - -app_import_model = console_ns.model("AppImport", app_import_fields) - -# For nested models, need to replace nested dict with registered model -app_import_check_dependencies_fields_copy = app_import_check_dependencies_fields.copy() -app_import_check_dependencies_fields_copy["leaked_dependencies"] = fields.List(fields.Nested(leaked_dependency_model)) -app_import_check_dependencies_model = console_ns.model( - "AppImportCheckDependencies", app_import_check_dependencies_fields_copy -) - -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" - class AppImportPayload(BaseModel): mode: str = Field(..., description="Import mode") @@ -52,18 +33,18 @@ class AppImportPayload(BaseModel): app_id: str | None = Field(None) -console_ns.schema_model( - AppImportPayload.__name__, AppImportPayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) -) +register_schema_models(console_ns, AppImportPayload, Import, CheckDependenciesResult) @console_ns.route("/apps/imports") class AppImportApi(Resource): @console_ns.expect(console_ns.models[AppImportPayload.__name__]) + @console_ns.response(200, "Import completed", console_ns.models[Import.__name__]) + @console_ns.response(202, "Import pending confirmation", console_ns.models[Import.__name__]) + @console_ns.response(400, "Import failed", console_ns.models[Import.__name__]) @setup_required @login_required @account_initialization_required - @marshal_with(app_import_model) @cloud_edition_billing_resource_check("apps") @edit_permission_required def post(self): @@ -104,10 +85,11 @@ class AppImportApi(Resource): @console_ns.route("/apps/imports//confirm") class AppImportConfirmApi(Resource): + @console_ns.response(200, "Import confirmed", console_ns.models[Import.__name__]) + @console_ns.response(400, "Import failed", console_ns.models[Import.__name__]) @setup_required @login_required @account_initialization_required - @marshal_with(app_import_model) @edit_permission_required def post(self, import_id): # Check user role first @@ -128,11 +110,11 @@ class AppImportConfirmApi(Resource): @console_ns.route("/apps/imports//check-dependencies") class AppImportCheckDependenciesApi(Resource): + @console_ns.response(200, "Dependencies checked", console_ns.models[CheckDependenciesResult.__name__]) @setup_required @login_required @get_app_model @account_initialization_required - @marshal_with(app_import_check_dependencies_model) @edit_permission_required def get(self, app_model: App): with sessionmaker(db.engine).begin() as session: From ced3780787b018ccd19ee96a26ad4c5b8c02ae29 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Sun, 12 Apr 2026 22:13:53 -0700 Subject: [PATCH 04/12] refactor: migrate mcp_server from marshal_with/api.model to Pydantic BaseModel (#34935) Co-authored-by: ai-hpc Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/app/mcp_server.py | 67 ++++++++++++------ .../console/app/test_mcp_server_response.py | 70 +++++++++++++++++++ 2 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 412fc8795a..9454d28bcf 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -1,23 +1,27 @@ import json +from datetime import datetime +from typing import Any -from flask_restx import Resource, marshal_with -from pydantic import BaseModel, Field +from flask_restx import Resource +from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from werkzeug.exceptions import NotFound +from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required from extensions.ext_database import db -from fields.app_fields import app_server_fields +from fields.base import ResponseModel from libs.login import current_account_with_tenant, login_required from models.enums import AppMCPServerStatus from models.model import AppMCPServer -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" -# Register model for flask_restx to avoid dict type issues in Swagger -app_server_model = console_ns.model("AppServer", app_server_fields) +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value class MCPServerCreatePayload(BaseModel): @@ -32,8 +36,33 @@ class MCPServerUpdatePayload(BaseModel): status: str | None = Field(default=None, description="Server status") -for model in (MCPServerCreatePayload, MCPServerUpdatePayload): - console_ns.schema_model(model.__name__, model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)) +class AppMCPServerResponse(ResponseModel): + id: str + name: str + server_code: str + description: str + status: str + parameters: dict[str, Any] | list[Any] | str + created_at: int | None = None + updated_at: int | None = None + + @field_validator("parameters", mode="before") + @classmethod + def _parse_json_string(cls, value: Any) -> Any: + if isinstance(value, str): + try: + return json.loads(value) + except (json.JSONDecodeError, TypeError): + return value + return value + + @field_validator("created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +register_schema_models(console_ns, MCPServerCreatePayload, MCPServerUpdatePayload, AppMCPServerResponse) @console_ns.route("/apps//server") @@ -41,27 +70,27 @@ class AppMCPServerController(Resource): @console_ns.doc("get_app_mcp_server") @console_ns.doc(description="Get MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(200, "MCP server configuration retrieved successfully", app_server_model) + @console_ns.response(200, "Server configuration", console_ns.models[AppMCPServerResponse.__name__]) @login_required @account_initialization_required @setup_required @get_app_model - @marshal_with(app_server_model) def get(self, app_model): server = db.session.scalar(select(AppMCPServer).where(AppMCPServer.app_id == app_model.id).limit(1)) - return server + if server is None: + return {} + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") @console_ns.doc("create_app_mcp_server") @console_ns.doc(description="Create MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerCreatePayload.__name__]) - @console_ns.response(201, "MCP server configuration created successfully", app_server_model) + @console_ns.response(200, "Server created", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @account_initialization_required @get_app_model @login_required @setup_required - @marshal_with(app_server_model) @edit_permission_required def post(self, app_model): _, current_tenant_id = current_account_with_tenant() @@ -82,20 +111,19 @@ class AppMCPServerController(Resource): ) db.session.add(server) db.session.commit() - return server + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") @console_ns.doc("update_app_mcp_server") @console_ns.doc(description="Update MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerUpdatePayload.__name__]) - @console_ns.response(200, "MCP server configuration updated successfully", app_server_model) + @console_ns.response(200, "Server updated", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @get_app_model @login_required @setup_required @account_initialization_required - @marshal_with(app_server_model) @edit_permission_required def put(self, app_model): payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {}) @@ -118,7 +146,7 @@ class AppMCPServerController(Resource): except ValueError: raise ValueError("Invalid status") db.session.commit() - return server + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") @console_ns.route("/apps//server/refresh") @@ -126,13 +154,12 @@ class AppMCPServerRefreshController(Resource): @console_ns.doc("refresh_app_mcp_server") @console_ns.doc(description="Refresh MCP server configuration and regenerate server code") @console_ns.doc(params={"server_id": "Server ID"}) - @console_ns.response(200, "MCP server refreshed successfully", app_server_model) + @console_ns.response(200, "Server refreshed", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @setup_required @login_required @account_initialization_required - @marshal_with(app_server_model) @edit_permission_required def get(self, server_id): _, current_tenant_id = current_account_with_tenant() @@ -145,4 +172,4 @@ class AppMCPServerRefreshController(Resource): raise NotFound() server.server_code = AppMCPServer.generate_server_code(16) db.session.commit() - return server + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") diff --git a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py new file mode 100644 index 0000000000..baac4cd4e0 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py @@ -0,0 +1,70 @@ +import datetime + +from controllers.console.app.mcp_server import AppMCPServerResponse + + +class TestAppMCPServerResponse: + def test_parameters_json_string_parsed(self): + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": '{"key": "value"}', + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.parameters == {"key": "value"} + + def test_parameters_invalid_json_returns_original(self): + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": "not-valid-json", + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.parameters == "not-valid-json" + + def test_parameters_dict_passthrough(self): + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": {"already": "parsed"}, + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.parameters == {"already": "parsed"} + + def test_timestamps_normalized(self): + dt = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC) + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": {}, + "created_at": dt, + "updated_at": dt, + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.created_at == int(dt.timestamp()) + assert resp.updated_at == int(dt.timestamp()) + + def test_timestamps_none(self): + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": {}, + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.created_at is None + assert resp.updated_at is None From a3170f744c5e4abc9bfa25a29522d16e8e5af8d8 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Sun, 12 Apr 2026 22:18:16 -0700 Subject: [PATCH 05/12] refactor: migrate app site from marshal_with/api.model to Pydantic BaseModel (#34933) Co-authored-by: ai-hpc --- api/controllers/console/app/site.py | 42 ++++++++++++------- .../controllers/console/app/test_app_apis.py | 36 +++++++++++++++- .../controllers/console/app/test_message.py | 10 +++-- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 7f44a99ff1..9991d78d94 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -1,11 +1,12 @@ from typing import Literal -from flask_restx import Resource, marshal_with +from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from werkzeug.exceptions import NotFound from constants.languages import supported_language +from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -15,13 +16,11 @@ from controllers.console.wraps import ( setup_required, ) from extensions.ext_database import db -from fields.app_fields import app_site_fields +from fields.base import ResponseModel from libs.datetime_utils import naive_utc_now from libs.login import current_account_with_tenant, login_required from models import Site -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" - class AppSiteUpdatePayload(BaseModel): title: str | None = Field(default=None) @@ -49,13 +48,26 @@ class AppSiteUpdatePayload(BaseModel): return supported_language(value) -console_ns.schema_model( - AppSiteUpdatePayload.__name__, - AppSiteUpdatePayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), -) +class AppSiteResponse(ResponseModel): + app_id: str + access_token: str | None = Field(default=None, validation_alias="code") + code: str | None = None + title: str + icon: str | None = None + icon_background: str | None = None + description: str | None = None + default_language: str + customize_domain: str | None = None + copyright: str | None = None + privacy_policy: str | None = None + custom_disclaimer: str | None = None + customize_token_strategy: str + prompt_public: bool + show_workflow_steps: bool + use_icon_as_answer_icon: bool -# Register model for flask_restx to avoid dict type issues in Swagger -app_site_model = console_ns.model("AppSite", app_site_fields) + +register_schema_models(console_ns, AppSiteUpdatePayload, AppSiteResponse) @console_ns.route("/apps//site") @@ -64,7 +76,7 @@ class AppSite(Resource): @console_ns.doc(description="Update application site configuration") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[AppSiteUpdatePayload.__name__]) - @console_ns.response(200, "Site configuration updated successfully", app_site_model) + @console_ns.response(200, "Site configuration updated successfully", console_ns.models[AppSiteResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "App not found") @setup_required @@ -72,7 +84,6 @@ class AppSite(Resource): @edit_permission_required @account_initialization_required @get_app_model - @marshal_with(app_site_model) def post(self, app_model): args = AppSiteUpdatePayload.model_validate(console_ns.payload or {}) current_user, _ = current_account_with_tenant() @@ -106,7 +117,7 @@ class AppSite(Resource): site.updated_at = naive_utc_now() db.session.commit() - return site + return AppSiteResponse.model_validate(site, from_attributes=True).model_dump(mode="json") @console_ns.route("/apps//site/access-token-reset") @@ -114,7 +125,7 @@ class AppSiteAccessTokenReset(Resource): @console_ns.doc("reset_app_site_access_token") @console_ns.doc(description="Reset access token for application site") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(200, "Access token reset successfully", app_site_model) + @console_ns.response(200, "Access token reset successfully", console_ns.models[AppSiteResponse.__name__]) @console_ns.response(403, "Insufficient permissions (admin/owner required)") @console_ns.response(404, "App or site not found") @setup_required @@ -122,7 +133,6 @@ class AppSiteAccessTokenReset(Resource): @is_admin_or_owner_required @account_initialization_required @get_app_model - @marshal_with(app_site_model) def post(self, app_model): current_user, _ = current_account_with_tenant() site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1)) @@ -135,4 +145,4 @@ class AppSiteAccessTokenReset(Resource): site.updated_at = naive_utc_now() db.session.commit() - return site + return AppSiteResponse.model_validate(site, from_attributes=True).model_dump(mode="json") diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index c3a861c3e1..54e0496dbd 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -313,6 +313,21 @@ class TestSiteEndpoints: method = _unwrap(api.post) site = MagicMock() + site.app_id = "app-1" + site.code = "test-code" + site.title = "My Site" + site.icon = None + site.icon_background = None + site.description = "Test site" + site.default_language = "en-US" + site.customize_domain = None + site.copyright = None + site.privacy_policy = None + site.custom_disclaimer = "" + site.customize_token_strategy = "not_allow" + site.prompt_public = False + site.show_workflow_steps = True + site.use_icon_as_answer_icon = False monkeypatch.setattr( site_module.db, "session", @@ -328,13 +343,29 @@ class TestSiteEndpoints: with app.test_request_context("/", json={"title": "My Site"}): result = method(app_model=SimpleNamespace(id="app-1")) - assert result is site + assert isinstance(result, dict) + assert result["title"] == "My Site" def test_app_site_access_token_reset(self, app, monkeypatch): api = site_module.AppSiteAccessTokenReset() method = _unwrap(api.post) site = MagicMock() + site.app_id = "app-1" + site.code = "old-code" + site.title = "My Site" + site.icon = None + site.icon_background = None + site.description = None + site.default_language = "en-US" + site.customize_domain = None + site.copyright = None + site.privacy_policy = None + site.custom_disclaimer = "" + site.customize_token_strategy = "not_allow" + site.prompt_public = False + site.show_workflow_steps = True + site.use_icon_as_answer_icon = False monkeypatch.setattr( site_module.db, "session", @@ -351,7 +382,8 @@ class TestSiteEndpoints: with app.test_request_context("/"): result = method(app_model=SimpleNamespace(id="app-1")) - assert result is site + assert isinstance(result, dict) + assert result["access_token"] == "code" class TestWorkflowEndpoints: diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py index 6b51ec98bc..eff6dd789d 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py @@ -148,14 +148,18 @@ def test_chat_message_list_success( account.id, created_at_offset_seconds=1, ) + # Capture IDs before the HTTP request detaches ORM instances from the session + app_id = app.id + conversation_id = conversation.id + second_id = second.id with patch( "controllers.console.app.message.attach_message_extra_contents", side_effect=_attach_message_extra_contents, ): response = test_client_with_containers.get( - f"/console/api/apps/{app.id}/chat-messages", - query_string={"conversation_id": conversation.id, "limit": 1}, + f"/console/api/apps/{app_id}/chat-messages", + query_string={"conversation_id": conversation_id, "limit": 1}, headers=authenticate_console_client(test_client_with_containers, account), ) @@ -165,7 +169,7 @@ def test_chat_message_list_success( assert payload["limit"] == 1 assert payload["has_more"] is True assert len(payload["data"]) == 1 - assert payload["data"][0]["id"] == second.id + assert payload["data"][0]["id"] == second_id def test_message_feedback_not_found( From e37aaa482df4b381f8bb13d913afbb8c0d74dd67 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Sun, 12 Apr 2026 22:18:42 -0700 Subject: [PATCH 06/12] refactor: migrate apikey from marshal_with/api.model to Pydantic BaseModel (#34932) Co-authored-by: ai-hpc Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/apikey.py | 56 +++++++++++-------- api/controllers/console/auth/activate.py | 32 +++++------ api/controllers/console/datasets/datasets.py | 15 ++--- .../console/datasets/test_datasets.py | 39 ++++++++++--- 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py index 772bb9d0f1..b03d9b4a4c 100644 --- a/api/controllers/console/apikey.py +++ b/api/controllers/console/apikey.py @@ -1,12 +1,16 @@ +from datetime import datetime + import flask_restx -from flask_restx import Resource, fields, marshal_with +from flask_restx import Resource from flask_restx._http import HTTPStatus +from pydantic import field_validator from sqlalchemy import delete, func, select from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden +from controllers.common.schema import register_schema_models from extensions.ext_database import db -from libs.helper import TimestampField +from fields.base import ResponseModel from libs.login import current_account_with_tenant, login_required from models.dataset import Dataset from models.enums import ApiTokenType @@ -16,21 +20,31 @@ from services.api_token_service import ApiTokenCache from . import console_ns from .wraps import account_initialization_required, edit_permission_required, setup_required -api_key_fields = { - "id": fields.String, - "type": fields.String, - "token": fields.String, - "last_used_at": TimestampField, - "created_at": TimestampField, -} -api_key_item_model = console_ns.model("ApiKeyItem", api_key_fields) +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value -api_key_list = {"data": fields.List(fields.Nested(api_key_item_model), attribute="items")} -api_key_list_model = console_ns.model( - "ApiKeyList", {"data": fields.List(fields.Nested(api_key_item_model), attribute="items")} -) +class ApiKeyItem(ResponseModel): + id: str + type: str + token: str + last_used_at: int | None = None + created_at: int | None = None + + @field_validator("last_used_at", "created_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class ApiKeyList(ResponseModel): + data: list[ApiKeyItem] + + +register_schema_models(console_ns, ApiKeyItem, ApiKeyList) def _get_resource(resource_id, tenant_id, resource_model): @@ -54,7 +68,6 @@ class BaseApiKeyListResource(Resource): token_prefix: str | None = None max_keys = 10 - @marshal_with(api_key_list_model) def get(self, resource_id): assert self.resource_id_field is not None, "resource_id_field must be set" resource_id = str(resource_id) @@ -66,9 +79,8 @@ class BaseApiKeyListResource(Resource): ApiToken.type == self.resource_type, getattr(ApiToken, self.resource_id_field) == resource_id ) ).all() - return {"items": keys} + return ApiKeyList.model_validate({"data": keys}, from_attributes=True).model_dump(mode="json") - @marshal_with(api_key_item_model) @edit_permission_required def post(self, resource_id): assert self.resource_id_field is not None, "resource_id_field must be set" @@ -100,7 +112,7 @@ class BaseApiKeyListResource(Resource): api_token.type = self.resource_type db.session.add(api_token) db.session.commit() - return api_token, 201 + return ApiKeyItem.model_validate(api_token, from_attributes=True).model_dump(mode="json"), 201 class BaseApiKeyResource(Resource): @@ -147,7 +159,7 @@ class AppApiKeyListResource(BaseApiKeyListResource): @console_ns.doc("get_app_api_keys") @console_ns.doc(description="Get all API keys for an app") @console_ns.doc(params={"resource_id": "App ID"}) - @console_ns.response(200, "Success", api_key_list_model) + @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) def get(self, resource_id): # type: ignore """Get all API keys for an app""" return super().get(resource_id) @@ -155,7 +167,7 @@ class AppApiKeyListResource(BaseApiKeyListResource): @console_ns.doc("create_app_api_key") @console_ns.doc(description="Create a new API key for an app") @console_ns.doc(params={"resource_id": "App ID"}) - @console_ns.response(201, "API key created successfully", api_key_item_model) + @console_ns.response(201, "API key created successfully", console_ns.models[ApiKeyItem.__name__]) @console_ns.response(400, "Maximum keys exceeded") def post(self, resource_id): # type: ignore """Create a new API key for an app""" @@ -187,7 +199,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource): @console_ns.doc("get_dataset_api_keys") @console_ns.doc(description="Get all API keys for a dataset") @console_ns.doc(params={"resource_id": "Dataset ID"}) - @console_ns.response(200, "Success", api_key_list_model) + @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) def get(self, resource_id): # type: ignore """Get all API keys for a dataset""" return super().get(resource_id) @@ -195,7 +207,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource): @console_ns.doc("create_dataset_api_key") @console_ns.doc(description="Create a new API key for a dataset") @console_ns.doc(params={"resource_id": "Dataset ID"}) - @console_ns.response(201, "API key created successfully", api_key_item_model) + @console_ns.response(201, "API key created successfully", console_ns.models[ApiKeyItem.__name__]) @console_ns.response(400, "Maximum keys exceeded") def post(self, resource_id): # type: ignore """Create a new API key for a dataset""" diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index f741107b87..e6316bfd62 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,8 +1,9 @@ from flask import request -from flask_restx import Resource, fields +from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from constants.languages import supported_language +from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.error import AlreadyActivateError from extensions.ext_database import db @@ -11,8 +12,6 @@ from libs.helper import EmailStr, timezone from models import AccountStatus from services.account_service import RegisterService -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" - class ActivateCheckQuery(BaseModel): workspace_id: str | None = Field(default=None) @@ -39,8 +38,16 @@ class ActivatePayload(BaseModel): return timezone(value) -for model in (ActivateCheckQuery, ActivatePayload): - console_ns.schema_model(model.__name__, model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)) +class ActivationCheckResponse(BaseModel): + is_valid: bool = Field(description="Whether token is valid") + data: dict | None = Field(default=None, description="Activation data if valid") + + +class ActivationResponse(BaseModel): + result: str = Field(description="Operation result") + + +register_schema_models(console_ns, ActivateCheckQuery, ActivatePayload, ActivationCheckResponse, ActivationResponse) @console_ns.route("/activate/check") @@ -51,13 +58,7 @@ class ActivateCheckApi(Resource): @console_ns.response( 200, "Success", - console_ns.model( - "ActivationCheckResponse", - { - "is_valid": fields.Boolean(description="Whether token is valid"), - "data": fields.Raw(description="Activation data if valid"), - }, - ), + console_ns.models[ActivationCheckResponse.__name__], ) def get(self): args = ActivateCheckQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore @@ -95,12 +96,7 @@ class ActivateApi(Resource): @console_ns.response( 200, "Account activated successfully", - console_ns.model( - "ActivationResponse", - { - "result": fields.String(description="Operation result"), - }, - ), + console_ns.models[ActivationResponse.__name__], ) @console_ns.response(400, "Already activated or invalid token") def post(self): diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index f23c7eb431..b2a905366a 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -11,10 +11,7 @@ import services from configs import dify_config from controllers.common.schema import get_or_create_model, register_schema_models from controllers.console import console_ns -from controllers.console.apikey import ( - api_key_item_model, - api_key_list_model, -) +from controllers.console.apikey import ApiKeyItem, ApiKeyList from controllers.console.app.error import ProviderNotInitializeError from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError from controllers.console.wraps import ( @@ -785,23 +782,23 @@ class DatasetApiKeyApi(Resource): @console_ns.doc("get_dataset_api_keys") @console_ns.doc(description="Get dataset API keys") - @console_ns.response(200, "API keys retrieved successfully", api_key_list_model) + @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) @setup_required @login_required @account_initialization_required - @marshal_with(api_key_list_model) def get(self): _, current_tenant_id = current_account_with_tenant() keys = db.session.scalars( select(ApiToken).where(ApiToken.type == self.resource_type, ApiToken.tenant_id == current_tenant_id) ).all() - return {"items": keys} + return ApiKeyList.model_validate({"data": keys}, from_attributes=True).model_dump(mode="json") + @console_ns.response(200, "API key created successfully", console_ns.models[ApiKeyItem.__name__]) + @console_ns.response(400, "Maximum keys exceeded") @setup_required @login_required @is_admin_or_owner_required @account_initialization_required - @marshal_with(api_key_item_model) def post(self): _, current_tenant_id = current_account_with_tenant() @@ -828,7 +825,7 @@ class DatasetApiKeyApi(Resource): api_token.type = self.resource_type db.session.add(api_token) db.session.commit() - return api_token, 200 + return ApiKeyItem.model_validate(api_token, from_attributes=True).model_dump(mode="json"), 200 @console_ns.route("/datasets/api-keys/") diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py index 8555900f4e..94d6c17915 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py @@ -1555,7 +1555,17 @@ class TestDatasetApiKeyApi: method = unwrap(api.get) mock_key_1 = MagicMock(spec=ApiToken) + mock_key_1.id = "key-1" + mock_key_1.type = "dataset" + mock_key_1.token = "ds-abc" + mock_key_1.last_used_at = None + mock_key_1.created_at = None mock_key_2 = MagicMock(spec=ApiToken) + mock_key_2.id = "key-2" + mock_key_2.type = "dataset" + mock_key_2.token = "ds-def" + mock_key_2.last_used_at = None + mock_key_2.created_at = None with ( app.test_request_context("/"), @@ -1570,13 +1580,26 @@ class TestDatasetApiKeyApi: ): response = method(api) - assert "items" in response - assert response["items"] == [mock_key_1, mock_key_2] + assert "data" in response + assert len(response["data"]) == 2 + assert response["data"][0]["id"] == "key-1" + assert response["data"][0]["token"] == "ds-abc" + assert response["data"][1]["id"] == "key-2" + assert response["data"][1]["token"] == "ds-def" def test_post_create_api_key_success(self, app): api = DatasetApiKeyApi() method = unwrap(api.post) + mock_token = MagicMock() + mock_token.id = "new-key-id" + mock_token.last_used_at = None + mock_token.created_at = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC) + + mock_api_token_cls = MagicMock() + mock_api_token_cls.return_value = mock_token + mock_api_token_cls.generate_api_key.return_value = "dataset-abc123" + with ( app.test_request_context("/"), patch( @@ -1588,8 +1611,8 @@ class TestDatasetApiKeyApi: return_value=3, ), patch( - "controllers.console.datasets.datasets.ApiToken.generate_api_key", - return_value="dataset-abc123", + "controllers.console.datasets.datasets.ApiToken", + mock_api_token_cls, ), patch( "controllers.console.datasets.datasets.db.session.add", @@ -1603,9 +1626,11 @@ class TestDatasetApiKeyApi: response, status = method(api) assert status == 200 - assert isinstance(response, ApiToken) - assert response.token == "dataset-abc123" - assert response.type == "dataset" + assert isinstance(response, dict) + assert response["id"] == "new-key-id" + assert response["token"] == "dataset-abc123" + assert response["type"] == "dataset" + assert response["created_at"] is not None def test_post_exceed_max_keys(self, app): api = DatasetApiKeyApi() From ea9f74b5811769c0debd4c2963b55dfc73c8213d Mon Sep 17 00:00:00 2001 From: sxxtony <166789813+sxxtony@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:19:49 -0700 Subject: [PATCH 07/12] refactor: migrate RecommendedApp to TypeBase (#34808) --- api/models/model.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/api/models/model.py b/api/models/model.py index 0ea2259a19..365564d784 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -838,7 +838,7 @@ class AppModelConfig(TypeBase): return self -class RecommendedApp(Base): # bug +class RecommendedApp(TypeBase): __tablename__ = "recommended_apps" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="recommended_app_pkey"), @@ -846,20 +846,37 @@ class RecommendedApp(Base): # bug sa.Index("recommended_app_is_listed_idx", "is_listed", "language"), ) - id = mapped_column(StringUUID, primary_key=True, default=lambda: str(uuid4())) - app_id = mapped_column(StringUUID, nullable=False) - description = mapped_column(sa.JSON, nullable=False) + id: Mapped[str] = mapped_column( + StringUUID, + primary_key=True, + insert_default=lambda: str(uuid4()), + default_factory=lambda: str(uuid4()), + init=False, + ) + app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + description: Mapped[Any] = mapped_column(sa.JSON, nullable=False) copyright: Mapped[str] = mapped_column(String(255), nullable=False) privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False) - custom_disclaimer: Mapped[str] = mapped_column(LongText, default="") category: Mapped[str] = mapped_column(String(255), nullable=False) + custom_disclaimer: Mapped[str] = mapped_column(LongText, default="") position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) - language = mapped_column(String(255), nullable=False, server_default=sa.text("'en-US'")) - created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column( - sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + language: Mapped[str] = mapped_column( + String(255), + nullable=False, + server_default=sa.text("'en-US'"), + default="en-US", + ) + created_at: Mapped[datetime] = mapped_column( + sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False + ) + updated_at: Mapped[datetime] = mapped_column( + sa.DateTime, + nullable=False, + server_default=func.current_timestamp(), + onupdate=func.current_timestamp(), + init=False, ) @property From 3b9aad2ba784ea7a1c4bc3ccc6ddf182ca911690 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Sun, 12 Apr 2026 22:21:46 -0700 Subject: [PATCH 08/12] refactor: replace inline api.model response schemas with register_schema_models in activate (#34929) Co-authored-by: ai-hpc Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> From 7dd507af04943d7eab07b8241c1628a3cbfbbcc1 Mon Sep 17 00:00:00 2001 From: sxxtony <166789813+sxxtony@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:22:43 -0700 Subject: [PATCH 09/12] refactor: migrate SegmentAttachmentBinding to TypeBase (#34810) --- api/core/rag/docstore/dataset_docstore.py | 2 +- api/models/dataset.py | 10 +++++--- .../rag/docstore/test_dataset_docstore.py | 24 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/api/core/rag/docstore/dataset_docstore.py b/api/core/rag/docstore/dataset_docstore.py index 40f45953af..8e9ebdd17a 100644 --- a/api/core/rag/docstore/dataset_docstore.py +++ b/api/core/rag/docstore/dataset_docstore.py @@ -244,7 +244,7 @@ class DatasetDocumentStore: return document_segment def add_multimodel_documents_binding(self, segment_id: str, multimodel_documents: list[AttachmentDocument] | None): - if multimodel_documents: + if multimodel_documents and self._document_id is not None: for multimodel_document in multimodel_documents: binding = SegmentAttachmentBinding( tenant_id=self._dataset.tenant_id, diff --git a/api/models/dataset.py b/api/models/dataset.py index a8ed821c3a..a48afa7ea7 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -1688,7 +1688,7 @@ class PipelineRecommendedPlugin(TypeBase): ) -class SegmentAttachmentBinding(Base): +class SegmentAttachmentBinding(TypeBase): __tablename__ = "segment_attachment_bindings" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="segment_attachment_binding_pkey"), @@ -1701,13 +1701,17 @@ class SegmentAttachmentBinding(Base): ), sa.Index("segment_attachment_binding_attachment_idx", "attachment_id"), ) - id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuidv7())) + id: Mapped[str] = mapped_column( + StringUUID, insert_default=lambda: str(uuidv7()), default_factory=lambda: str(uuidv7()), init=False + ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) document_id: Mapped[str] = mapped_column(StringUUID, nullable=False) segment_id: Mapped[str] = mapped_column(StringUUID, nullable=False) attachment_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column( + sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False + ) class DocumentSegmentSummary(Base): diff --git a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py index a7b7c1595b..007a76aa66 100644 --- a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py +++ b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py @@ -721,6 +721,30 @@ class TestDatasetDocumentStoreMultimodelBinding: mock_db.session.add.assert_not_called() + def test_add_multimodel_documents_binding_with_none_document_id(self): + """Test that no bindings are added when document_id is None.""" + + mock_dataset = MagicMock(spec=Dataset) + mock_dataset.id = "test-dataset-id" + mock_dataset.tenant_id = "tenant-1" + + mock_attachment = MagicMock(spec=AttachmentDocument) + mock_attachment.metadata = {"doc_id": "attachment-1"} + + with patch("core.rag.docstore.dataset_docstore.db") as mock_db: + mock_session = MagicMock() + mock_db.session = mock_session + + store = DatasetDocumentStore( + dataset=mock_dataset, + user_id="test-user-id", + document_id=None, + ) + + store.add_multimodel_documents_binding("seg-1", [mock_attachment]) + + mock_db.session.add.assert_not_called() + class TestDatasetDocumentStoreAddDocumentsUpdateChild: """Tests for add_documents when updating existing documents with children.""" From 9121f24181fea6e2d0c2105fb6894fc2bd93931b Mon Sep 17 00:00:00 2001 From: Jake Armstrong <65635253+jakearmstrong59@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:27:35 +0200 Subject: [PATCH 10/12] refactor(api): deduplicate TextToAudioPayload and MessageListQuery into controller_schemas.py (#34757) --- api/controllers/common/controller_schemas.py | 14 +++++++------- api/controllers/service_api/app/audio.py | 9 +-------- api/controllers/web/message.py | 18 ++---------------- .../controllers/web/test_pydantic_models.py | 4 ++-- 4 files changed, 12 insertions(+), 33 deletions(-) diff --git a/api/controllers/common/controller_schemas.py b/api/controllers/common/controller_schemas.py index 39e3b5857d..ec5c72374d 100644 --- a/api/controllers/common/controller_schemas.py +++ b/api/controllers/common/controller_schemas.py @@ -23,9 +23,9 @@ class ConversationRenamePayload(BaseModel): class MessageListQuery(BaseModel): - conversation_id: UUIDStrOrEmpty - first_id: UUIDStrOrEmpty | None = None - limit: int = Field(default=20, ge=1, le=100) + conversation_id: UUIDStrOrEmpty = Field(description="Conversation UUID") + first_id: UUIDStrOrEmpty | None = Field(default=None, description="First message ID for pagination") + limit: int = Field(default=20, ge=1, le=100, description="Number of messages to return (1-100)") class MessageFeedbackPayload(BaseModel): @@ -73,7 +73,7 @@ class WorkflowUpdatePayload(BaseModel): class TextToAudioPayload(BaseModel): - message_id: str | None = None - voice: str | None = None - text: str | None = None - streaming: bool | None = None + message_id: str | None = Field(default=None, description="Message ID") + voice: str | None = Field(default=None, description="Voice to use for TTS") + text: str | None = Field(default=None, description="Text to convert to audio") + streaming: bool | None = Field(default=None, description="Enable streaming response") diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index 6228cfc25b..907dd1b06d 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -3,10 +3,10 @@ import logging from flask import request from flask_restx import Resource from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError import services +from controllers.common.controller_schemas import TextToAudioPayload from controllers.common.schema import register_schema_model from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( @@ -86,13 +86,6 @@ class AudioApi(Resource): raise InternalServerError() -class TextToAudioPayload(BaseModel): - message_id: str | None = Field(default=None, description="Message ID") - voice: str | None = Field(default=None, description="Voice to use for TTS") - text: str | None = Field(default=None, description="Text to convert to audio") - streaming: bool | None = Field(default=None, description="Enable streaming response") - - register_schema_model(service_api_ns, TextToAudioPayload) diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index 25cb6b2b9e..39afdd843f 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -3,10 +3,10 @@ from typing import Literal from flask import request from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, Field, TypeAdapter, field_validator +from pydantic import BaseModel, Field, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound -from controllers.common.controller_schemas import MessageFeedbackPayload +from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery from controllers.common.schema import register_schema_models from controllers.web import web_ns from controllers.web.error import ( @@ -25,7 +25,6 @@ from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotIni from fields.conversation_fields import ResultResponse from fields.message_fields import SuggestedQuestionsResponse, WebMessageInfiniteScrollPagination, WebMessageListItem from libs import helper -from libs.helper import uuid_value from models.enums import FeedbackRating from models.model import AppMode from services.app_generate_service import AppGenerateService @@ -41,19 +40,6 @@ from services.message_service import MessageService logger = logging.getLogger(__name__) -class MessageListQuery(BaseModel): - conversation_id: str = Field(description="Conversation UUID") - first_id: str | None = Field(default=None, description="First message ID for pagination") - limit: int = Field(default=20, ge=1, le=100, description="Number of messages to return (1-100)") - - @field_validator("conversation_id", "first_id") - @classmethod - def validate_uuid(cls, value: str | None) -> str | None: - if value is None: - return value - return uuid_value(value) - - class MessageMoreLikeThisQuery(BaseModel): response_mode: Literal["blocking", "streaming"] = Field( description="Response mode", diff --git a/api/tests/unit_tests/controllers/web/test_pydantic_models.py b/api/tests/unit_tests/controllers/web/test_pydantic_models.py index dcf8133712..bceb65b89f 100644 --- a/api/tests/unit_tests/controllers/web/test_pydantic_models.py +++ b/api/tests/unit_tests/controllers/web/test_pydantic_models.py @@ -198,7 +198,7 @@ class TestMessageListQuery: assert q.limit == 20 def test_invalid_conversation_id(self) -> None: - with pytest.raises(ValidationError, match="not a valid uuid"): + with pytest.raises(ValidationError, match="must be a valid UUID"): MessageListQuery(conversation_id="bad") def test_limit_bounds(self) -> None: @@ -216,7 +216,7 @@ class TestMessageListQuery: def test_invalid_first_id(self) -> None: cid = str(uuid4()) - with pytest.raises(ValidationError, match="not a valid uuid"): + with pytest.raises(ValidationError, match="must be a valid UUID"): MessageListQuery(conversation_id=cid, first_id="invalid") From f4d5e2f43d34d02511d7773695e9d51785fa772f Mon Sep 17 00:00:00 2001 From: dev-miro26 <121471669+dev-miro26@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:29:10 -0400 Subject: [PATCH 11/12] refactor(api): improve type safety in MCPToolManageService.execute_auth_actions (#34824) --- .../tools/mcp_tools_manage_service.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/api/services/tools/mcp_tools_manage_service.py b/api/services/tools/mcp_tools_manage_service.py index 690b06ea7d..89762d6772 100644 --- a/api/services/tools/mcp_tools_manage_service.py +++ b/api/services/tools/mcp_tools_manage_service.py @@ -17,6 +17,7 @@ from core.helper import encrypter from core.helper.provider_cache import NoOpProviderCredentialCache from core.mcp.auth.auth_flow import auth from core.mcp.auth_client import MCPClientWithAuthRetry +from core.mcp.entities import AuthActionType, AuthResult from core.mcp.error import MCPAuthError, MCPError from core.mcp.types import Tool as MCPTool from core.tools.entities.api_entities import ToolProviderApiEntity @@ -496,7 +497,13 @@ class MCPToolManageService: ) as mcp_client: return mcp_client.list_tools() - def execute_auth_actions(self, auth_result: Any) -> dict[str, str]: + _ACTION_TO_OAUTH: dict[AuthActionType, OAuthDataType] = { + AuthActionType.SAVE_CLIENT_INFO: OAuthDataType.CLIENT_INFO, + AuthActionType.SAVE_TOKENS: OAuthDataType.TOKENS, + AuthActionType.SAVE_CODE_VERIFIER: OAuthDataType.CODE_VERIFIER, + } + + def execute_auth_actions(self, auth_result: AuthResult) -> dict[str, str]: """ Execute the actions returned by the auth function. @@ -508,19 +515,13 @@ class MCPToolManageService: Returns: The response from the auth result """ - from core.mcp.entities import AuthAction, AuthActionType - - action: AuthAction for action in auth_result.actions: if action.provider_id is None or action.tenant_id is None: continue - if action.action_type == AuthActionType.SAVE_CLIENT_INFO: - self.save_oauth_data(action.provider_id, action.tenant_id, action.data, OAuthDataType.CLIENT_INFO) - elif action.action_type == AuthActionType.SAVE_TOKENS: - self.save_oauth_data(action.provider_id, action.tenant_id, action.data, OAuthDataType.TOKENS) - elif action.action_type == AuthActionType.SAVE_CODE_VERIFIER: - self.save_oauth_data(action.provider_id, action.tenant_id, action.data, OAuthDataType.CODE_VERIFIER) + oauth_type = self._ACTION_TO_OAUTH.get(action.action_type) + if oauth_type is not None: + self.save_oauth_data(action.provider_id, action.tenant_id, action.data, oauth_type) return auth_result.response From 5a446f8200ef19b055ea928bf164c505a04edbb3 Mon Sep 17 00:00:00 2001 From: Jake Armstrong <65635253+jakearmstrong59@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:33:20 +0200 Subject: [PATCH 12/12] refactor(api): deduplicate dataset controller schemas into controller_schemas.py (#34756) --- api/controllers/common/controller_schemas.py | 25 +++++++++++++++++++ .../console/datasets/datasets_document.py | 11 +------- .../console/datasets/datasets_segments.py | 9 +------ api/controllers/console/datasets/metadata.py | 7 +----- .../service_api/dataset/document.py | 10 +------- .../service_api/dataset/metadata.py | 7 +----- .../service_api/dataset/segment.py | 9 +------ 7 files changed, 31 insertions(+), 47 deletions(-) diff --git a/api/controllers/common/controller_schemas.py b/api/controllers/common/controller_schemas.py index ec5c72374d..c12d576473 100644 --- a/api/controllers/common/controller_schemas.py +++ b/api/controllers/common/controller_schemas.py @@ -1,4 +1,5 @@ from typing import Any, Literal +from uuid import UUID from pydantic import BaseModel, Field, model_validator @@ -69,6 +70,30 @@ class WorkflowUpdatePayload(BaseModel): marked_comment: str | None = Field(default=None, max_length=100) +# --- Dataset schemas --- + + +DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS = 100 + + +class ChildChunkCreatePayload(BaseModel): + content: str + + +class ChildChunkUpdatePayload(BaseModel): + content: str + + +class DocumentBatchDownloadZipPayload(BaseModel): + """Request payload for bulk downloading documents as a zip archive.""" + + document_ids: list[UUID] = Field(..., min_length=1, max_length=DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS) + + +class MetadataUpdatePayload(BaseModel): + name: str + + # --- Audio schemas --- diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index b7584f1f00..de8fe1c0e2 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -4,7 +4,6 @@ from argparse import ArgumentTypeError from collections.abc import Sequence from contextlib import ExitStack from typing import Any, Literal, cast -from uuid import UUID import sqlalchemy as sa from flask import request, send_file @@ -16,6 +15,7 @@ from sqlalchemy import asc, desc, func, select from werkzeug.exceptions import Forbidden, NotFound import services +from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload from controllers.common.schema import get_or_create_model, register_schema_models from controllers.console import console_ns from core.errors.error import ( @@ -71,9 +71,6 @@ from ..wraps import ( logger = logging.getLogger(__name__) -# NOTE: Keep constants near the top of the module for discoverability. -DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS = 100 - # Register models for flask_restx to avoid dict type issues in Swagger dataset_model = get_or_create_model("Dataset", dataset_fields) @@ -110,12 +107,6 @@ class GenerateSummaryPayload(BaseModel): document_list: list[str] -class DocumentBatchDownloadZipPayload(BaseModel): - """Request payload for bulk downloading documents as a zip archive.""" - - document_ids: list[UUID] = Field(..., min_length=1, max_length=DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS) - - class DocumentDatasetListParam(BaseModel): page: int = Field(1, title="Page", description="Page number.") limit: int = Field(20, title="Limit", description="Page size.") diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index c5f4e3a6e2..354c299bef 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -10,6 +10,7 @@ from werkzeug.exceptions import Forbidden, NotFound import services from configs import dify_config +from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.error import ProviderNotInitializeError @@ -82,14 +83,6 @@ class BatchImportPayload(BaseModel): upload_file_id: str -class ChildChunkCreatePayload(BaseModel): - content: str - - -class ChildChunkUpdatePayload(BaseModel): - content: str - - class ChildChunkBatchUpdatePayload(BaseModel): chunks: list[ChildChunkUpdateArgs] diff --git a/api/controllers/console/datasets/metadata.py b/api/controllers/console/datasets/metadata.py index 2e69ddc5ab..d966e1629e 100644 --- a/api/controllers/console/datasets/metadata.py +++ b/api/controllers/console/datasets/metadata.py @@ -1,9 +1,9 @@ from typing import Literal from flask_restx import Resource, marshal_with -from pydantic import BaseModel from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import MetadataUpdatePayload from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required @@ -18,11 +18,6 @@ from services.entities.knowledge_entities.knowledge_entities import ( ) from services.metadata_service import MetadataService - -class MetadataUpdatePayload(BaseModel): - name: str - - register_schema_models( console_ns, MetadataArgs, MetadataOperationData, MetadataUpdatePayload, DocumentMetadataOperation, MetadataDetail ) diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index db34aa408e..6db047567f 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -10,6 +10,7 @@ from sqlalchemy import desc, func, select from werkzeug.exceptions import Forbidden, NotFound import services +from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload from controllers.common.errors import ( FilenameNotExistsError, FileTooLargeError, @@ -100,15 +101,6 @@ class DocumentListQuery(BaseModel): status: str | None = Field(default=None, description="Document status filter") -DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS = 100 - - -class DocumentBatchDownloadZipPayload(BaseModel): - """Request payload for bulk downloading uploaded documents as a ZIP archive.""" - - document_ids: list[UUID] = Field(..., min_length=1, max_length=DOCUMENT_BATCH_DOWNLOAD_ZIP_MAX_DOCS) - - register_enum_models(service_api_ns, RetrievalMethod) register_schema_models( diff --git a/api/controllers/service_api/dataset/metadata.py b/api/controllers/service_api/dataset/metadata.py index 52166f7fcc..21db7d0cb8 100644 --- a/api/controllers/service_api/dataset/metadata.py +++ b/api/controllers/service_api/dataset/metadata.py @@ -2,9 +2,9 @@ from typing import Literal from flask_login import current_user from flask_restx import marshal -from pydantic import BaseModel from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import MetadataUpdatePayload from controllers.common.schema import register_schema_model, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check @@ -18,11 +18,6 @@ from services.entities.knowledge_entities.knowledge_entities import ( ) from services.metadata_service import MetadataService - -class MetadataUpdatePayload(BaseModel): - name: str - - register_schema_model(service_api_ns, MetadataUpdatePayload) register_schema_models( service_api_ns, diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index 5b16da81e0..9ad999b93e 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -8,6 +8,7 @@ from sqlalchemy import select from werkzeug.exceptions import NotFound from configs import dify_config +from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload from controllers.common.schema import register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import ProviderNotInitializeError @@ -69,20 +70,12 @@ class SegmentUpdatePayload(BaseModel): segment: SegmentUpdateArgs -class ChildChunkCreatePayload(BaseModel): - content: str - - class ChildChunkListQuery(BaseModel): limit: int = Field(default=20, ge=1) keyword: str | None = None page: int = Field(default=1, ge=1) -class ChildChunkUpdatePayload(BaseModel): - content: str - - register_schema_models( service_api_ns, SegmentCreatePayload,