Compare commits

..
Author SHA1 Message Date
chariri 4ee3a2e6cb chore(api): restore summary index session callsites 2026-07-08 01:43:01 +09:00
chariri d5617b1a82 fix(api): shorten summary index session scopes 2026-07-07 18:35:41 +09:00
chariri 315dd3f82a refactor(api): thread db sessions through services 2026-07-07 18:18:36 +09:00
FFXNandGitHub fb92e9a347 chore: improve cherry pick missed message (#38496) 2026-07-07 07:53:14 +00:00
wangxiaoleiandGitHub 6922c45489 chore: update editor permission (#38505) 2026-07-07 07:53:01 +00:00
非法操作andGitHub faaa4708a6 fix: editor should not manage member (#38503) 2026-07-07 07:39:24 +00:00
dd0c4a2296 fix: resolve 36288 mypy errors (#37850)
Co-authored-by: WH-2099 <wh2099@pm.me>
2026-07-07 06:37:20 +00:00
Asuka MinatoandGitHub f3ba28463b chore: add sqlite3 to conftest (#38475) 2026-07-07 05:04:44 +00:00
Stephen ZhouGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
800c9f4fca test(e2e): stabilize Agent v2 external runtime checks (#38493)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-07 04:48:12 +00:00
5a342f9258 feat(mcp): support MCP protocol 2025-06-18 for workflow-as-MCP server (version negotiation + structured output) (#37892)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
2026-07-07 03:26:50 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
dbd3316615 chore: set NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW default to true (#38362)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 02:20:15 +00:00
WH-2099GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
0a3426ea38 refactor(api): clarify DSL import and plugin migration boundaries (#38483)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-07 02:06:49 +00:00
JingyiandGitHub b9c7199d34 fix(web): unify detail sidebar home control (#38487) 2026-07-07 01:44:14 +00:00
QuantumGhostGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
fc01d112a0 refactor(api): Stop masking refresh-token service errors as 401 (#38463)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-06 18:14:54 +00:00
yyhGitHubJoelzyssyz123autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>林玮盐粒 Yanli
d0ea5a5e0d chore(agent-v2): sync changes (#38442)
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: zyssyz123 <916125788@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com>
Co-authored-by: 盐粒 Yanli <mail@yanli.one>
2026-07-06 13:51:33 +00:00
yyhandGitHub bdb3469ca0 refactor(dify-ui): consolidate radio family API (#38479) 2026-07-06 12:27:49 +00:00
非法操作andGitHub c47663d77b chore: Update sidebar web app menu translations (#38473) 2026-07-06 10:09:36 +00:00
非法操作andGitHub 1247fa28f1 fix(web): redirect imported apps with creator permissions (#38460) 2026-07-06 09:11:56 +00:00
Ryuta KOBAYASHIGitHubWH-2099autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
64d72c6fb9 feat(api): pass app_id to model plugins for provider-side cost attribution (#35859)
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-06 08:54:16 +00:00
78ca1a8a76 fix(api): resolve plugin external user ids in backwards invocation (#38098)
Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local>
2026-07-06 08:25:26 +00:00
5aae5c15b3 refactor(api): migrate web auth endpoints to BaseModel (#37961)
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-07-06 07:53:21 +00:00
Asuka MinatoandGitHub b1724073fc test: example use sqlite3 as unittest backend (#38159) 2026-07-06 07:31:51 +00:00
chaririGitHubWH-2099autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>Asuka Minato
904fadde20 refactor(api): remove member field compatibility (#37966)
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-07-06 07:00:43 +00:00
chaririGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>Asuka Minato
77ae583b44 refactor(api): migrate dataset rag pipeline endpoints to BaseModel (#37958)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-07-06 07:00:04 +00:00
chaririandGitHub c3b1508712 refactor(api): migrate workspace account endpoints to BaseModel (#37954) 2026-07-06 06:57:40 +00:00
yyhandGitHub 8208b786ee docs(dify-ui): clarify radio composition stories (#38456) 2026-07-06 06:40:17 +00:00
d9c99daf29 fix(api): isolate side-effect session writes in multimodal and RAG handlers (#38210)
Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
2026-07-06 05:17:12 +00:00
KVOJJJinandGitHub 93eb6d32b5 fix(web): update snippet placeholder icon color (#38445) 2026-07-06 05:10:36 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de5af6c2da build(deps): bump the github-actions-dependencies group across 1 directory with 12 updates (#38430)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 04:39:10 +00:00
i晟andGitHub 586c8de1a0 refactor: drop redundant len(tag_ids)==0 check in get_target_ids_by_tag_ids (#38447) 2026-07-06 04:35:58 +00:00
705 changed files with 15810 additions and 8983 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ while IFS= read -r commit_sha; do
)
if [[ -z "$source_sha" ]]; then
error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT"
error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT If version differences prevent using git cherry-pick -x, manually add '(cherry picked from commit <sha>)' to the commit message."
failed=1
continue
fi
+6 -6
View File
@@ -29,13 +29,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@@ -91,13 +91,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@@ -142,13 +142,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: "3.12"
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
run: echo "autofix.ci updates pull request branches, not merge group refs."
- if: github.event_name != 'merge_group'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check Docker Compose inputs
if: github.event_name != 'merge_group'
@@ -73,12 +73,12 @@ jobs:
dify-agent/pyproject.toml
dify-agent/uv.lock
- if: github.event_name != 'merge_group'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
- if: github.event_name != 'merge_group'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
- name: Generate Docker Compose
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
+6 -6
View File
@@ -97,7 +97,7 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
@@ -107,7 +107,7 @@ jobs:
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env[matrix.image_name_env] }}
@@ -159,10 +159,10 @@ jobs:
file: "docker/local-sandbox/Dockerfile"
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Validate Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
push: false
context: ${{ matrix.build_context }}
@@ -197,14 +197,14 @@ jobs:
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env[matrix.image_name_env] }}
tags: |
+6 -6
View File
@@ -79,7 +79,7 @@ jobs:
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
@@ -123,7 +123,7 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
@@ -170,7 +170,7 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
@@ -233,7 +233,7 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
@@ -295,7 +295,7 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
@@ -351,7 +351,7 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
working-directory: ./cli
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
dify_tag: ${{ steps.resolve.outputs.dify_tag }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -98,7 +98,7 @@ jobs:
DIFY_TAG: ${{ needs.validate.outputs.dify_tag }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 1
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
shell: bash
steps:
- name: Checkout cli ref
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.cli_ref || github.ref }}
persist-credentials: false
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+4 -4
View File
@@ -13,13 +13,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: "3.12"
@@ -63,13 +63,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: "3.12"
+2 -2
View File
@@ -77,10 +77,10 @@ jobs:
file: "web/Dockerfile"
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build Docker Image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
push: false
context: ${{ matrix.context }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
name: Require cherry-pick provenance
runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
+2 -2
View File
@@ -46,8 +46,8 @@ jobs:
vdb-changed: ${{ steps.changes.outputs.vdb }}
migration-changed: ${{ steps.changes.outputs.migration }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: |
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
outputs:
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: |
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
pull-requests: write
steps:
- name: Checkout PR branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Setup Python & UV
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
@@ -21,10 +21,10 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
steps:
- name: Checkout default branch (trusted code)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Python & UV
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
pull-requests: write
steps:
- name: Checkout PR branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Setup Python & UV
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
+8 -8
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
- name: Setup UV and Python
if: steps.changed-files.outputs.any_changed == 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: false
python-version: "3.12"
@@ -88,7 +88,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -139,7 +139,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -168,7 +168,7 @@ jobs:
- name: Restore ESLint cache
if: steps.changed-files.outputs.any_changed == 'true'
id: eslint-cache-restore
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .eslintcache
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
@@ -185,7 +185,7 @@ jobs:
- name: Save ESLint cache
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .eslintcache
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
@@ -196,7 +196,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -214,7 +214,7 @@ jobs:
.editorconfig
- name: Super-linter
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
if: steps.changed-files.outputs.any_changed == 'true'
env:
BASH_SEVERITY: warning
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
working-directory: sdks/nodejs-client
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -158,7 +158,7 @@ jobs:
- name: Run Claude Code for Translation Sync
if: steps.context.outputs.CHANGED_FILES != ''
uses: anthropics/claude-code-action@806af32823ef69c8ef357086c573a902af641307 # v1.0.151
uses: anthropics/claude-code-action@558b1d6cab4085c7753fe402c10bef0fbb92ac7a # v1.0.165
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -36,7 +36,7 @@ jobs:
remove_tool_cache: true
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -33,7 +33,7 @@ jobs:
remove_tool_cache: true
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -33,7 +33,7 @@ jobs:
uses: ./.github/actions/setup-web
- name: Setup UV and Python
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: true
python-version: "3.12"
+4 -4
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -102,7 +102,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+4 -4
View File
@@ -25,7 +25,7 @@ def reset_password(email, new_password, password_confirm):
return
normalized_email = email.strip().lower()
account = AccountService.get_account_by_email_with_case_fallback(db.session, email.strip())
account = AccountService.get_account_by_email_with_case_fallback(db.session(), email.strip())
if not account:
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
@@ -67,7 +67,7 @@ def reset_email(email, new_email, email_confirm):
return
normalized_new_email = new_email.strip().lower()
account = AccountService.get_account_by_email_with_case_fallback(db.session, email.strip())
account = AccountService.get_account_by_email_with_case_fallback(db.session(), email.strip())
if not account:
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
@@ -133,9 +133,9 @@ def create_tenant(email: str, language: str | None = None, name: str | None = No
password=new_password,
language=language,
create_workspace_required=False,
session=db.session,
session=db.session(),
)
TenantService.create_owner_tenant_if_not_exist(account, name, session=db.session)
TenantService.create_owner_tenant_if_not_exist(account, name, session=db.session())
click.echo(
click.style(
+9 -6
View File
@@ -9,6 +9,7 @@ from uuid import UUID
import click
import sqlalchemy as sa
import yaml
from sqlalchemy.orm import Session
from core.db.session_factory import session_factory
from extensions.ext_database import db
@@ -108,7 +109,7 @@ def export_migration_data(input_file: str | None, output_file: str | None, overw
raw_config = _load_json_object(input_file, "Export config")
selection = ExportConfigParser().parse(raw_config)
with session_factory.create_session() as session:
result = MigrationExportService().export(session, selection)
result = MigrationExportService().export(selection, session=session)
MigrationPackageService().save_package(result.package, output_file, overwrite=overwrite)
click.echo(click.style(f"Output written to {output_file}", fg="green"))
_render_report(result.report_items, context=_with_output_path(result.report_context, output_file))
@@ -157,7 +158,6 @@ def import_migration_data(
package = MigrationPackageService().load_package(input_file)
with session_factory.create_session() as session:
result = MigrationImportService().import_package(
session,
ImportRequest(
package=package,
cli_target_tenant=target_tenant,
@@ -169,6 +169,7 @@ def import_migration_data(
create_app_api_token_on_import=create_app_api_token_on_import,
),
),
session=session,
)
_render_report(result.report_items, context=result.report_context)
except MigrationDataError as exc:
@@ -217,7 +218,9 @@ def migration_data_wizard() -> None:
default=True,
show_default=False,
)
auto_tools = _discover_auto_tools([app for app in apps if app.id in set(app_ids)], include_referenced_tools)
auto_tools = _discover_auto_tools(
[app for app in apps if app.id in set(app_ids)], include_referenced_tools, session=db.session()
)
auto_tools = _resolve_auto_tool_names(tenant.id, auto_tools)
_print_auto_tools(auto_tools)
additional_tools = _prompt_additional_tools(tenant.id, auto_tools)
@@ -253,7 +256,7 @@ def migration_data_wizard() -> None:
output_file=output_file,
)
with session_factory.create_session() as session:
result = MigrationExportService().export(session, selection)
result = MigrationExportService().export(selection, session=session)
MigrationPackageService().save_package(result.package, output_file, overwrite=overwrite)
click.echo(click.style(f"Output written to {output_file}", fg="green"))
_print_wizard_step("Report")
@@ -394,13 +397,13 @@ def _prompt_import_options() -> tuple[bool, bool, str, str]:
return include_secrets, create_tokens, id_strategy, conflict_strategy
def _discover_auto_tools(apps: list[App], include_referenced_tools: bool) -> WizardToolMap:
def _discover_auto_tools(apps: list[App], include_referenced_tools: bool, *, session: Session) -> WizardToolMap:
auto_tools: WizardToolMap = {"api_tools": {}, "workflow_tools": {}, "mcp_tools": {}}
if not include_referenced_tools:
return auto_tools
discovery_service = DependencyDiscoveryService()
for app in apps:
dsl_content = AppDslService.export_dsl(app_model=app, include_secret=False)
dsl_content = AppDslService.export_dsl(app_model=app, session=session, include_secret=False)
raw_dsl = yaml.safe_load(dsl_content) if dsl_content else {}
dsl = raw_dsl if isinstance(raw_dsl, dict) else {}
for dependency in discovery_service.discover_from_dsl(dsl):
+16 -15
View File
@@ -16,7 +16,7 @@ from core.plugin.plugin_service import PluginService
from core.tools.utils.system_encryption import encrypt_system_params
from extensions.ext_database import db
from models import Tenant
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import TenantPluginAutoUpgradeCategory, TenantPluginAutoUpgradeStrategy
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
from models.provider_ids import DatasourceProviderID, ToolProviderID
from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
@@ -188,13 +188,13 @@ def transform_datasource_credentials(environment: str):
firecrawl_plugin_id = "langgenius/firecrawl_datasource"
jina_plugin_id = "langgenius/jina_datasource"
if environment == "online":
notion_plugin_unique_identifier = plugin_migration._fetch_plugin_unique_identifier(notion_plugin_id)
firecrawl_plugin_unique_identifier = plugin_migration._fetch_plugin_unique_identifier(firecrawl_plugin_id)
jina_plugin_unique_identifier = plugin_migration._fetch_plugin_unique_identifier(jina_plugin_id)
notion_package_identifier = plugin_migration._fetch_latest_package_identifier(notion_plugin_id)
firecrawl_package_identifier = plugin_migration._fetch_latest_package_identifier(firecrawl_plugin_id)
jina_package_identifier = plugin_migration._fetch_latest_package_identifier(jina_plugin_id)
else:
notion_plugin_unique_identifier = None
firecrawl_plugin_unique_identifier = None
jina_plugin_unique_identifier = None
notion_package_identifier = None
firecrawl_package_identifier = None
jina_package_identifier = None
oauth_credential_type = CredentialType.OAUTH2
api_key_credential_type = CredentialType.API_KEY
@@ -219,9 +219,9 @@ def transform_datasource_credentials(environment: str):
installed_plugins = installer_manager.list_plugins(tenant_id)
installed_plugins_ids = [plugin.plugin_id for plugin in installed_plugins]
if notion_plugin_id not in installed_plugins_ids:
if notion_plugin_unique_identifier:
if notion_package_identifier:
# install notion plugin
PluginService.install_from_marketplace_pkg(tenant_id, [notion_plugin_unique_identifier])
PluginService.install_from_marketplace_pkg(tenant_id, [notion_package_identifier])
auth_count = 0
for notion_tenant_credential in notion_tenant_credentials:
auth_count += 1
@@ -279,9 +279,9 @@ def transform_datasource_credentials(environment: str):
installed_plugins = installer_manager.list_plugins(tenant_id)
installed_plugins_ids = [plugin.plugin_id for plugin in installed_plugins]
if firecrawl_plugin_id not in installed_plugins_ids:
if firecrawl_plugin_unique_identifier:
if firecrawl_package_identifier:
# install firecrawl plugin
PluginService.install_from_marketplace_pkg(tenant_id, [firecrawl_plugin_unique_identifier])
PluginService.install_from_marketplace_pkg(tenant_id, [firecrawl_package_identifier])
auth_count = 0
for firecrawl_tenant_credential in firecrawl_tenant_credentials:
@@ -343,10 +343,10 @@ def transform_datasource_credentials(environment: str):
installed_plugins = installer_manager.list_plugins(tenant_id)
installed_plugins_ids = [plugin.plugin_id for plugin in installed_plugins]
if jina_plugin_id not in installed_plugins_ids:
if jina_plugin_unique_identifier:
if jina_package_identifier:
# install jina plugin
logger.debug("Installing Jina plugin %s", jina_plugin_unique_identifier)
PluginService.install_from_marketplace_pkg(tenant_id, [jina_plugin_unique_identifier])
logger.debug("Installing Jina plugin %s", jina_package_identifier)
PluginService.install_from_marketplace_pkg(tenant_id, [jina_package_identifier])
auth_count = 0
for jina_tenant_credential in jina_tenant_credentials:
@@ -406,7 +406,7 @@ def migrate_data_for_plugin():
def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None):
category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory)
category_count = len(TenantPluginAutoUpgradeCategory)
stmt = (
select(TenantPluginAutoUpgradeStrategy.tenant_id)
.group_by(TenantPluginAutoUpgradeStrategy.tenant_id)
@@ -472,6 +472,7 @@ def backfill_plugin_auto_upgrade(
try:
result = PluginAutoUpgradeService.backfill_strategy_categories(
current_tenant_id,
session=db.session(),
)
except Exception as e:
failed_count += 1
+31 -4
View File
@@ -6,6 +6,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
import click
from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
from core.db.session_factory import session_factory
@@ -131,16 +132,35 @@ def _replace_member_role(
operator_account_id: str,
member_account_id: str,
role_id: str,
*,
session: Session,
) -> str:
RBACService.MemberRoles.replace(
tenant_id=tenant_id,
account_id=operator_account_id,
member_account_id=member_account_id,
role_ids=[role_id],
session=session,
)
return member_account_id
def _replace_member_role_with_new_session(
tenant_id: str,
operator_account_id: str,
member_account_id: str,
role_id: str,
) -> str:
with session_factory.create_session() as session:
return _replace_member_role(
tenant_id=tenant_id,
operator_account_id=operator_account_id,
member_account_id=member_account_id,
role_id=role_id,
session=session,
)
@click.command(
"rbac-migrate-member-roles", help="Migrate legacy workspace member roles into RBAC member-role bindings."
)
@@ -217,14 +237,21 @@ def migrate_member_roles_to_rbac(
if replace_jobs:
if workers == 1:
for member_account_id, resolved_role_id in replace_jobs:
_replace_member_role(workspace_id, owner_account_id, member_account_id, resolved_role_id)
migrated_count += 1
with session_factory.create_session() as session:
for member_account_id, resolved_role_id in replace_jobs:
_replace_member_role(
workspace_id,
owner_account_id,
member_account_id,
resolved_role_id,
session=session,
)
migrated_count += 1
else:
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = [
executor.submit(
_replace_member_role,
_replace_member_role_with_new_session,
workspace_id,
owner_account_id,
member_account_id,
@@ -0,0 +1,54 @@
from typing import Any
from sqlalchemy import select
from core.app.apps.agent_app.app_feature_projection import merge_agent_app_features
from core.app.apps.agent_app.app_variable_projection import agent_app_variables_to_user_input_form
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
from extensions.ext_database import db
from models.agent import Agent, AgentConfigSnapshot, AgentStatus
from models.agent_config_entities import AgentSoulConfig
from models.model import App
def get_published_agent_app_feature_dict_and_user_input_form(
app_model: App,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
"""Return public Agent App parameters backed by the published Agent Soul."""
app_model_config = app_model.app_model_config
agent_id = app_model.bound_agent_id
if not agent_id:
raise AgentAppGeneratorError("Agent App has no bound Agent")
agent = db.session.scalar(
select(Agent)
.where(
Agent.tenant_id == app_model.tenant_id,
Agent.id == agent_id,
Agent.status == AgentStatus.ACTIVE,
)
.limit(1)
)
if agent is None:
raise AgentAppGeneratorError("Agent App has no bound Agent")
# active_config_is_published means the draft has no unpublished edits; the public app
# can still read parameters from the active snapshot while a newer draft is pending.
if not agent.active_config_snapshot_id:
raise AgentAppNotPublishedError("Agent has not been published")
snapshot = db.session.scalar(
select(AgentConfigSnapshot)
.where(
AgentConfigSnapshot.tenant_id == app_model.tenant_id,
AgentConfigSnapshot.agent_id == agent.id,
AgentConfigSnapshot.id == agent.active_config_snapshot_id,
)
.limit(1)
)
if snapshot is None:
raise AgentAppGeneratorError("Agent published version not found")
agent_soul = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
features_dict = merge_agent_app_features(agent_soul=agent_soul, app_model_config=app_model_config)
return features_dict, agent_app_variables_to_user_input_form(agent_soul.app_variables)
+2 -1
View File
@@ -4,6 +4,7 @@ from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING
from extensions.ext_database import db
from services.enterprise import rbac_service as enterprise_rbac_service
if TYPE_CHECKING:
@@ -76,7 +77,7 @@ def resolve_app_access_filter(
inner-API round trip; otherwise it is fetched here.
"""
if permissions is None:
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(tenant_id, account_id)
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(tenant_id, account_id, session=db.session())
whitelist_scope = enterprise_rbac_service.RBACService.AppAccess.whitelist_resources(tenant_id, account_id)
can_manage_own_apps = _MANAGE_OWN_APPS_PERMISSION_KEY in permissions.workspace.permission_keys
+16 -4
View File
@@ -16,6 +16,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user_id,
)
from extensions.ext_database import db
from fields.agent_fields import (
AgentAppComposerResponse,
AgentComposerCandidatesResponse,
@@ -69,6 +70,7 @@ class WorkflowAgentComposerApi(Resource):
node_id=node_id,
account_id=account_id,
snapshot_id=query.snapshot_id,
session=db.session(),
),
)
@@ -94,6 +96,7 @@ class WorkflowAgentComposerApi(Resource):
node_id=node_id,
account_id=account_id,
payload=payload,
session=db.session(),
),
)
@@ -126,6 +129,7 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
session=db.session(),
),
)
@@ -149,8 +153,9 @@ class WorkflowAgentComposerValidateApi(Resource):
tenant_id=tenant_id,
payload=payload,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
tenant_id=tenant_id, app_id=app_model.id, node_id=node_id
tenant_id=tenant_id, app_id=app_model.id, node_id=node_id, session=db.session()
),
session=db.session(),
)
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})
@@ -174,6 +179,7 @@ class WorkflowAgentComposerCandidatesApi(Resource):
app_id=app_model.id,
node_id=node_id,
user_id=current_user_id,
session=db.session(),
),
)
@@ -196,7 +202,9 @@ class WorkflowAgentComposerImpactApi(Resource):
)
return dump_response(
AgentComposerImpactResponse,
AgentComposerService.calculate_impact(tenant_id=tenant_id, current_snapshot_id=current_snapshot_id),
AgentComposerService.calculate_impact(
tenant_id=tenant_id, current_snapshot_id=current_snapshot_id, session=db.session()
),
)
@@ -224,6 +232,7 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
node_id=node_id,
account_id=account_id,
payload=payload,
session=db.session(),
),
)
@@ -238,7 +247,7 @@ class AgentComposerApi(Resource):
def get(self, tenant_id: str, agent_id: UUID):
return dump_response(
AgentAppComposerResponse,
AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id)),
AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id), session=db.session()),
)
@console_ns.expect(console_ns.models[ComposerSavePayload.__name__])
@@ -259,6 +268,7 @@ class AgentComposerApi(Resource):
agent_id=str(agent_id),
account_id=account_id,
payload=payload,
session=db.session(),
),
)
@@ -274,7 +284,7 @@ class AgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, agent_id: UUID):
AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id))
AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id), session=db.session())
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
AgentComposerService.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
@@ -282,6 +292,7 @@ class AgentComposerValidateApi(Resource):
tenant_id=tenant_id,
payload=payload,
agent_id=str(agent_id),
session=db.session(),
)
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})
@@ -303,5 +314,6 @@ class AgentComposerCandidatesApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
user_id=current_user_id,
session=db.session(),
),
)
+11 -5
View File
@@ -534,7 +534,7 @@ class AgentAppListApi(Resource):
status="normal",
)
app_pagination = AppService().get_paginate_apps(current_user.id, current_tenant_id, params, db.session)
app_pagination = AppService().get_paginate_apps(current_user.id, current_tenant_id, params, db.session())
if app_pagination is None:
empty = AgentAppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json")
@@ -567,7 +567,7 @@ class AgentAppListApi(Resource):
icon_background=args.icon_background,
)
app = AppService().create_app(current_tenant_id, params, current_user)
app = AppService().create_app(current_tenant_id, params, current_user, session=db.session())
return _serialize_agent_app_detail(app, current_user=current_user), 201
@@ -607,7 +607,7 @@ class AgentAppApi(Resource):
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
}
updated = AppService().update_app(app_model, args_dict)
updated = AppService().update_app(app_model, args_dict, session=db.session())
return _serialize_agent_app_detail(updated, current_user=current_user)
@console_ns.response(204, "Agent app deleted successfully")
@@ -619,7 +619,7 @@ class AgentAppApi(Resource):
@with_current_tenant_id
def delete(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
AppService().delete_app(app_model)
AppService().delete_app(app_model, session=db.session())
return "", 204
@@ -668,6 +668,7 @@ class AgentPublishApi(Resource):
agent_id=str(agent_id),
account_id=current_user.id,
version_note=args.version_note,
session=db.session(),
)
@@ -688,6 +689,7 @@ class AgentBuildDraftCheckoutApi(Resource):
agent_id=str(agent_id),
account_id=current_user.id,
force=args.force,
session=db.session(),
)
@@ -705,6 +707,7 @@ class AgentBuildDraftApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
session=db.session(),
)
@console_ns.expect(console_ns.models[ComposerSavePayload.__name__])
@@ -722,6 +725,7 @@ class AgentBuildDraftApi(Resource):
agent_id=str(agent_id),
account_id=current_user.id,
payload=payload,
session=db.session(),
)
@console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__])
@@ -736,6 +740,7 @@ class AgentBuildDraftApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
session=db.session(),
)
@@ -753,6 +758,7 @@ class AgentBuildDraftApplyApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
session=db.session(),
)
@@ -810,7 +816,7 @@ class AgentApiStatusApi(Resource):
def post(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
args = AgentApiStatusPayload.model_validate(console_ns.payload)
app_model = AppService().update_app_api_status(app_model, args.enable_api)
app_model = AppService().update_app_api_status(app_model, args.enable_api, session=db.session())
return _serialize_agent_api_access(app_model)
+9 -3
View File
@@ -172,7 +172,7 @@ register_response_schema_models(
def _resolve_agent_id(app_model: App, node_id: str | None) -> str | None:
if node_id and app_model.mode != AppMode.AGENT:
return AgentComposerService.resolve_workflow_node_agent_id(
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id, session=db.session()
)
return app_model.bound_agent_id
@@ -202,6 +202,7 @@ def _upload_skill_for_app(*, current_user: Account, app_model: App):
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
session=db.session(),
)
except (SkillPackageError, AgentDriveError) as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@@ -240,6 +241,7 @@ def _commit_drive_file_for_app(*, current_user: Account, app_model: App, allow_n
value_owned_by_drive=True,
)
],
session=db.session(),
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@@ -273,6 +275,7 @@ def _delete_drive_file_for_app(*, current_user: Account, app_model: App, allow_n
user_id=current_user.id,
agent_id=agent_id,
items=[DriveCommitItem(key=key, file_ref=None)],
session=db.session(),
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@@ -298,6 +301,7 @@ def _delete_skill_for_app(*, current_user: Account, app_model: App, slug: str, a
DriveCommitItem(key=f"{slug}/SKILL.md", file_ref=None),
DriveCommitItem(key=f"{slug}/.DIFY-SKILL-FULL.zip", file_ref=None),
],
session=db.session(),
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@@ -313,7 +317,9 @@ def _infer_skill_tools_for_app(*, app_model: App, slug: str):
if "/" in slug or not slug.strip():
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
try:
return SkillToolInferenceService().infer(tenant_id=app_model.tenant_id, agent_id=agent_id, slug=slug)
return SkillToolInferenceService().infer(
tenant_id=app_model.tenant_id, agent_id=agent_id, slug=slug, session=db.session()
)
except SkillToolInferenceError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@@ -335,7 +341,7 @@ class AgentLogApi(Resource):
"""Get agent logs"""
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id)
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, db.session())
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
@@ -93,7 +93,7 @@ class AgentAppFeatureConfigResource(Resource):
app_model=app_model,
account=current_user,
config=args.model_dump(exclude_none=True),
session=db.session,
session=db.session(),
)
app_model_config_was_updated.send(app_model, app_model_config=new_app_model_config)
@@ -25,6 +25,7 @@ from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.login import login_required
from models.model import App, AppMode
@@ -108,14 +109,8 @@ class SandboxReadResponse(ResponseModel):
text: str | None = None
class SandboxToolFileResponse(ResponseModel):
transfer_method: Literal["tool_file"] = "tool_file"
reference: str
class SandboxUploadResponse(ResponseModel):
path: str
file: SandboxToolFileResponse
url: str
register_schema_models(
@@ -225,7 +220,7 @@ class AgentAppSandboxReadResource(Resource):
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/upload")
class AgentAppSandboxUploadResource(Resource):
@console_ns.doc("upload_agent_app_sandbox_file")
@console_ns.doc(description="Upload one Agent App sandbox file as a Dify ToolFile mapping")
@console_ns.doc(description="Upload one Agent App sandbox file and return a signed download URL")
@console_ns.expect(console_ns.models[AgentSandboxUploadPayload.__name__])
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
@setup_required
@@ -275,6 +270,7 @@ class WorkflowAgentSandboxListResource(Resource):
node_id=node_id,
node_execution_id=query.node_execution_id,
path=query.path,
session=db.session(),
)
except Exception as exc:
return _handle(exc)
@@ -311,6 +307,7 @@ class WorkflowAgentSandboxReadResource(Resource):
node_id=node_id,
node_execution_id=query.node_execution_id,
path=query.path,
session=db.session(),
)
except Exception as exc:
return _handle(exc)
@@ -322,7 +319,7 @@ class WorkflowAgentSandboxReadResource(Resource):
)
class WorkflowAgentSandboxUploadResource(Resource):
@console_ns.doc("upload_workflow_agent_sandbox_file")
@console_ns.doc(description="Upload one workflow Agent sandbox file as a Dify ToolFile mapping")
@console_ns.doc(description="Upload one workflow Agent sandbox file and return a signed download URL")
@console_ns.expect(console_ns.models[WorkflowAgentSandboxUploadPayload.__name__])
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
@setup_required
@@ -340,6 +337,7 @@ class WorkflowAgentSandboxUploadResource(Resource):
node_id=node_id,
node_execution_id=payload.node_execution_id,
path=payload.path,
session=db.session(),
)
except Exception as exc:
return _handle(exc)
@@ -253,6 +253,7 @@ def _resolve_agent_id(app_model: App, node_id: str | None) -> str | None:
tenant_id=app_model.tenant_id,
app_id=app_model.id,
node_id=node_id,
session=db.session(),
)
return app_model.bound_agent_id
@@ -288,13 +289,16 @@ def _resolve_console_version(
tenant_id=tenant_id,
agent_id=agent_id,
account_id=account_id,
session=db.session(),
)
draft = state.get("draft") or {}
draft_id = draft.get("id")
if isinstance(draft_id, str) and draft_id:
return draft_id, AgentConfigVersionKind.BUILD_DRAFT
else:
state = AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=agent_id)
state = AgentComposerService.load_agent_composer(
tenant_id=tenant_id, agent_id=agent_id, session=db.session()
)
draft = state.get("draft") or {}
draft_id = draft.get("id")
if isinstance(draft_id, str) and draft_id:
@@ -28,6 +28,7 @@ from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.login import login_required
from models.model import App, AppMode
@@ -147,7 +148,7 @@ def _resolve_agent_id(app_model: App, node_id: str | None) -> str | None:
"""Agent identity for the drive: app-bound agent, or the workflow node binding."""
if node_id:
return AgentComposerService.resolve_workflow_node_agent_id(
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id, session=db.session()
)
return app_model.bound_agent_id
@@ -184,7 +185,9 @@ class AgentDriveListByAgentApi(Resource):
query = query_params_from_request(AgentDriveListByAgentQuery)
resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix)
items = AgentDriveService().manifest(
tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]}
@@ -203,7 +206,7 @@ class AgentDriveSkillListByAgentApi(Resource):
def get(self, tenant_id: str, agent_id: UUID):
resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id))
items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id), session=db.session())
except AgentDriveError as exc:
return _handle(exc)
return {"items": items}
@@ -227,6 +230,7 @@ class AgentDriveSkillInspectByAgentApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
skill_path=skill_path,
session=db.session(),
)
)
except AgentDriveError as exc:
@@ -247,7 +251,9 @@ class AgentDrivePreviewByAgentApi(Resource):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
return AgentDriveService().preview(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
return AgentDriveService().preview(
tenant_id=tenant_id, agent_id=str(agent_id), key=query.key, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
@@ -266,7 +272,9 @@ class AgentDriveDownloadByAgentApi(Resource):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
url = AgentDriveService().download_url(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
url = AgentDriveService().download_url(
tenant_id=tenant_id, agent_id=str(agent_id), key=query.key, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
return {"url": url}
@@ -288,7 +296,9 @@ class AgentDriveListApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
items = AgentDriveService().manifest(tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=query.prefix)
items = AgentDriveService().manifest(
tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=query.prefix, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
# the inner manifest exposes file_id for agent-side pulls; the console
@@ -312,7 +322,9 @@ class AgentDriveSkillListApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
items = AgentDriveService().list_skills(tenant_id=app_model.tenant_id, agent_id=agent_id)
items = AgentDriveService().list_skills(
tenant_id=app_model.tenant_id, agent_id=agent_id, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
return {"items": items}
@@ -345,6 +357,7 @@ class AgentDriveSkillInspectApi(Resource):
tenant_id=app_model.tenant_id,
agent_id=agent_id,
skill_path=skill_path,
session=db.session(),
)
)
except AgentDriveError as exc:
@@ -367,7 +380,9 @@ class AgentDrivePreviewApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
return AgentDriveService().preview(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
return AgentDriveService().preview(
tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
@@ -388,7 +403,9 @@ class AgentDriveDownloadApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
url = AgentDriveService().download_url(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
url = AgentDriveService().download_url(
tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key, session=db.session()
)
except AgentDriveError as exc:
return _handle(exc)
return {"url": url}
+15 -10
View File
@@ -211,7 +211,7 @@ class AppAnnotationSettingDetailApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
def get(self, app_id: UUID):
result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id))
result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id), session=db.session())
return dump_response(AnnotationSettingResponse, result), 200
@@ -235,7 +235,7 @@ class AppAnnotationSettingUpdateApi(Resource):
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
result = AppAnnotationService.update_app_annotation_setting(
str(app_id), annotation_setting_id_str, setting_args
str(app_id), annotation_setting_id_str, setting_args, session=db.session()
)
return dump_response(AnnotationSettingResponse, result), 200
@@ -292,7 +292,9 @@ class AnnotationApi(Resource):
limit = args.limit
keyword = args.keyword
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(str(app_id), page, limit, keyword)
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
str(app_id), page, limit, keyword, session=db.session()
)
annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True)
return AnnotationList(
data=annotation_models, has_more=len(annotation_list) == limit, limit=limit, total=total, page=page
@@ -321,7 +323,9 @@ class AnnotationApi(Resource):
upsert_args["message_id"] = args.message_id
if args.question is not None:
upsert_args["question"] = args.question
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id))
annotation = AppAnnotationService.up_insert_app_annotation_from_message(
upsert_args, str(app_id), session=db.session()
)
return dump_response(Annotation, annotation), 201
@setup_required
@@ -345,11 +349,11 @@ class AnnotationApi(Resource):
}, 400
app_ref = _get_app_ref(str(app_id))
AppAnnotationService.delete_app_annotations_in_batch(app_ref, annotation_ids)
AppAnnotationService.delete_app_annotations_in_batch(app_ref, annotation_ids, session=db.session())
return "", 204
# If no annotation_ids are provided, handle clearing all annotations
else:
AppAnnotationService.clear_all_annotations(str(app_id))
AppAnnotationService.clear_all_annotations(str(app_id), session=db.session())
return "", 204
@@ -370,7 +374,7 @@ class AnnotationExportApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
def get(self, app_id: UUID):
annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id))
annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id), session=db.session())
annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True)
return (
AnnotationExportList(data=annotation_models).model_dump(mode="json"),
@@ -406,7 +410,7 @@ class AnnotationUpdateDeleteApi(Resource):
update_args["question"] = args.question
app_ref = _get_app_ref(str(app_id))
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, db.session)
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, db.session())
return Annotation.model_validate(annotation, from_attributes=True).model_dump(mode="json")
@setup_required
@@ -418,7 +422,7 @@ class AnnotationUpdateDeleteApi(Resource):
def delete(self, app_id: UUID, annotation_id: UUID):
app_ref = _get_app_ref(str(app_id))
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
AppAnnotationService.delete_app_annotation(annotation_ref, db.session)
AppAnnotationService.delete_app_annotation(annotation_ref, db.session())
return "", 204
@@ -477,7 +481,7 @@ class AnnotationBatchImportApi(Resource):
return dump_response(
AnnotationBatchImportResponse,
AppAnnotationService.batch_import_app_annotations(str(app_id), file),
AppAnnotationService.batch_import_app_annotations(str(app_id), file, session=db.session()),
)
@@ -538,6 +542,7 @@ class AnnotationHitHistoryListApi(Resource):
annotation_ref,
page,
limit,
session=db.session(),
)
history_models = TypeAdapter(list[AnnotationHitHistory]).validate_python(
annotation_hit_history_list, from_attributes=True
+16 -10
View File
@@ -584,6 +584,7 @@ class AppListApi(Resource):
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
str(current_tenant_id),
current_user_id,
session=db.session(),
)
if dify_config.RBAC_ENABLED:
access_filter = resolve_app_access_filter(
@@ -595,7 +596,7 @@ class AppListApi(Resource):
# get app list
app_service = AppService()
app_pagination = app_service.get_paginate_apps(current_user_id, current_tenant_id, params, db.session)
app_pagination = app_service.get_paginate_apps(current_user_id, current_tenant_id, params, session)
if not app_pagination:
response = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return response.model_dump(mode="json"), 200
@@ -643,11 +644,12 @@ class AppListApi(Resource):
)
app_service = AppService()
app = app_service.create_app(current_tenant_id, params, current_user)
app = app_service.create_app(current_tenant_id, params, current_user, session=db.session())
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
str(current_tenant_id),
current_user.id,
[str(app.id)],
session=db.session(),
)
app_detail = AppDetailWithSite.model_validate(app, from_attributes=True).model_copy(
update={"permission_keys": permission_keys_map.get(str(app.id), [])}
@@ -681,7 +683,7 @@ class StarredAppListApi(Resource):
is_created_by_me=args.is_created_by_me,
)
app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params, db.session)
app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params, session)
if not app_pagination:
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json"), 200
@@ -753,6 +755,7 @@ class AppApi(Resource):
str(current_tenant_id),
current_user.id,
app_id=str(app_model.id),
session=db.session(),
)
permission_keys_map = permissions.app.permission_keys_by_resource_ids([str(app_model.id)])
@@ -789,7 +792,7 @@ class AppApi(Resource):
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
}
app_model = app_service.update_app(app_model, args_dict)
app_model = app_service.update_app(app_model, args_dict, session=db.session())
return dump_response(AppDetailWithSite, app_model)
@console_ns.doc("delete_app")
@@ -806,7 +809,7 @@ class AppApi(Resource):
def delete(self, app_model: App):
"""Delete app"""
app_service = AppService()
app_service.delete_app(app_model)
app_service.delete_app(app_model, session=db.session())
return "", 204
@@ -835,7 +838,7 @@ class AppCopyApi(Resource):
with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session)
yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
yaml_content = import_service.export_dsl(app_model=app_model, session=session, include_secret=True)
result = import_service.import_app(
account=current_user,
import_mode=ImportMode.YAML_CONTENT,
@@ -877,6 +880,7 @@ class AppCopyApi(Resource):
str(current_tenant_id),
current_user.id,
[str(app.id)],
session=db.session(),
)
response_model = AppDetailWithSite.model_validate(app, from_attributes=True).model_copy(
update={"permission_keys": permission_keys_map.get(str(app.id), [])}
@@ -905,6 +909,7 @@ class AppExportApi(Resource):
response = AppExportResponse(
data=AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=args.include_secret,
workflow_id=args.workflow_id,
)
@@ -929,7 +934,7 @@ class AppPublishToCreatorsPlatformApi(Resource):
if not dify_config.CREATORS_PLATFORM_FEATURES_ENABLED:
return {"error": "Creators Platform features are not enabled"}, 403
dsl_content = AppDslService.export_dsl(app_model=app_model, include_secret=False)
dsl_content = AppDslService.export_dsl(app_model=app_model, session=db.session(), include_secret=False)
dsl_bytes = dsl_content.encode("utf-8")
claim_code = upload_dsl(dsl_bytes)
@@ -955,7 +960,7 @@ class AppNameApi(Resource):
args = AppNamePayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_name(app_model, args.name)
app_model = app_service.update_app_name(app_model, args.name, session=db.session())
return dump_response(AppDetail, app_model)
@@ -982,6 +987,7 @@ class AppIconApi(Resource):
args.icon or "",
args.icon_background or "",
args.icon_type,
session=db.session(),
)
return dump_response(AppDetail, app_model)
@@ -1004,7 +1010,7 @@ class AppSiteStatus(Resource):
args = AppSiteStatusPayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_site_status(app_model, args.enable_site)
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=db.session())
return dump_response(AppDetail, app_model)
@@ -1026,7 +1032,7 @@ class AppApiStatus(Resource):
args = AppApiStatusPayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_api_status(app_model, args.enable_api)
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=db.session())
return dump_response(AppDetail, app_model)
+1 -1
View File
@@ -161,7 +161,7 @@ class ChatMessageTextApi(Resource):
# response-contract:ignore
return AudioService.transcript_tts(
app_model=app_model,
session=db.session,
session=db.session(),
text=payload.text,
voice=payload.voice,
message_ref=message_ref,
@@ -124,6 +124,7 @@ Use only the current Build chat message history to identify changes that need to
validate old config unless the message history already shows that the old config is invalid.
Only update the build-draft config note when the current Build chat contains durable context that later runs need.
Write the config note in the language used by the message history.
Do not create, update, delete, inspect, or fill gaps in other Agent config resources, including config files, config
skills, config env, tools, models, knowledge, or prompt settings.
+2 -2
View File
@@ -200,7 +200,7 @@ class CompletionConversationDetailApi(Resource):
conversation_id_str = str(conversation_id)
try:
ConversationService.delete(app_model, conversation_id_str, current_user)
ConversationService.delete(app_model, conversation_id_str, current_user, session=db.session())
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@@ -354,7 +354,7 @@ class ChatConversationDetailApi(Resource):
conversation_id_str = str(conversation_id)
try:
ConversationService.delete(app_model, conversation_id_str, current_user)
ConversationService.delete(app_model, conversation_id_str, current_user, session=db.session())
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
+6 -1
View File
@@ -363,6 +363,7 @@ def _list_chat_messages(*, app_model: App, current_user: Account | None = None):
app_model=app_model,
conversation_id=args.conversation_id,
user=current_user,
session=db.session(),
)
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@@ -474,7 +475,11 @@ def _get_message_suggested_questions(*, current_user: Account, app_model: App, m
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model, message_id=message_id_str, user=current_user, invoke_from=InvokeFrom.DEBUGGER
app_model=app_model,
message_id=message_id_str,
user=current_user,
invoke_from=InvokeFrom.DEBUGGER,
session=db.session(),
)
except MessageNotExistsError:
raise NotFound("Message not found")
+13 -4
View File
@@ -17,6 +17,7 @@ from controllers.console.wraps import (
rbac_permission_required,
setup_required,
)
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.login import login_required
from models import App
@@ -78,7 +79,7 @@ class TraceAppConfigApi(Resource):
try:
trace_config = OpsService.get_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
)
if not trace_config:
return {"has_not_configured": True}
@@ -109,7 +110,10 @@ class TraceAppConfigApi(Resource):
try:
result = OpsService.create_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
session=db.session(),
)
if not result:
raise TracingConfigIsExist()
@@ -142,7 +146,10 @@ class TraceAppConfigApi(Resource):
try:
result = OpsService.update_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
session=db.session(),
)
if not result:
raise TracingConfigNotExist()
@@ -168,7 +175,9 @@ class TraceAppConfigApi(Resource):
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
try:
result = OpsService.delete_tracing_app_config(app_id=app_model.id, tracing_provider=args.tracing_provider)
result = OpsService.delete_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
)
if not result:
raise TracingConfigNotExist()
return "", 204
@@ -1,6 +1,9 @@
from extensions.ext_database import db
from services.enterprise import rbac_service as enterprise_rbac_service
def get_app_permission_keys(tenant_id: str, account_id: str | None, app_id: str) -> list[str]:
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(tenant_id, account_id, [app_id])
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
tenant_id, account_id, [app_id], session=db.session()
)
return permission_keys_map.get(app_id, [])
+47 -16
View File
@@ -2,7 +2,7 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, NotRequired, TypedDict, cast
from typing import Any, NotRequired, TypedDict
from flask import abort, request
from flask_restx import Resource, fields
@@ -522,7 +522,7 @@ class DraftWorkflowApi(Resource):
"""
# fetch draft workflow by app_model
workflow_service = WorkflowService()
workflow = workflow_service.get_draft_workflow(app_model=app_model)
workflow = workflow_service.get_draft_workflow(app_model=app_model, session=db.session())
if not workflow:
raise DraftWorkflowNotExist()
@@ -533,7 +533,7 @@ class DraftWorkflowApi(Resource):
# front-end can treat draft graph node data as the editing source.
response = WorkflowResponse.model_validate(workflow, from_attributes=True).model_dump(mode="json")
response["graph"] = WorkflowAgentPublishService.project_draft_bindings_to_graph(
session=cast(Session, db.session),
session=db.session(),
draft_workflow=workflow,
)
return response
@@ -602,6 +602,7 @@ class DraftWorkflowApi(Resource):
account=current_user,
environment_variables=environment_variables,
conversation_variables=conversation_variables,
session=db.session(),
)
except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync()
@@ -695,7 +696,12 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
try:
response = AppGenerateService.generate_single_iteration(
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
app_model=app_model,
user=current_user,
node_id=node_id,
args=args,
session=db.session(),
streaming=True,
)
return helper.compact_generate_response(response)
@@ -738,7 +744,12 @@ class WorkflowDraftRunIterationNodeApi(Resource):
try:
response = AppGenerateService.generate_single_iteration(
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
app_model=app_model,
user=current_user,
node_id=node_id,
args=args,
session=db.session(),
streaming=True,
)
return helper.compact_generate_response(response)
@@ -777,7 +788,12 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
try:
response = AppGenerateService.generate_single_loop(
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
app_model=app_model,
user=current_user,
node_id=node_id,
args=args,
session=db.session(),
streaming=True,
)
return helper.compact_generate_response(response)
@@ -820,7 +836,12 @@ class WorkflowDraftRunLoopNodeApi(Resource):
try:
response = AppGenerateService.generate_single_loop(
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
app_model=app_model,
user=current_user,
node_id=node_id,
args=args,
session=db.session(),
streaming=True,
)
return helper.compact_generate_response(response)
@@ -897,6 +918,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
account=current_user,
node_id=node_id,
inputs=inputs,
session=db.session(),
)
return jsonable_encoder(preview)
@@ -932,6 +954,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
form_inputs=args.form_inputs,
inputs=args.inputs,
action=args.action,
session=db.session(),
)
return jsonable_encoder(result)
@@ -963,6 +986,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
account=current_user,
node_id=node_id,
inputs=inputs,
session=db.session(),
)
return jsonable_encoder(preview)
@@ -998,6 +1022,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
form_inputs=args.form_inputs,
inputs=args.inputs,
action=args.action,
session=db.session(),
)
return jsonable_encoder(result)
@@ -1028,6 +1053,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource):
node_id=node_id,
delivery_method_id=args.delivery_method_id,
inputs=args.inputs,
session=db.session(),
)
return jsonable_encoder({})
@@ -1138,7 +1164,7 @@ class DraftWorkflowNodeRunApi(Resource):
workflow_srv = WorkflowService()
# fetch draft workflow by app_model
draft_workflow = workflow_srv.get_draft_workflow(app_model=app_model)
draft_workflow = workflow_srv.get_draft_workflow(app_model=app_model, session=db.session())
if not draft_workflow:
raise ValueError("Workflow not initialized")
files = _parse_file(draft_workflow, args.get("files"))
@@ -1181,7 +1207,7 @@ class PublishedWorkflowApi(Resource):
"""
# fetch published workflow by app_model
workflow_service = WorkflowService()
workflow = workflow_service.get_published_workflow(app_model=app_model)
workflow = workflow_service.get_published_workflow(app_model=app_model, session=db.session())
# return workflow, if not found, return None
if workflow is None:
@@ -1323,7 +1349,9 @@ class ConvertToWorkflowApi(Resource):
# convert to workflow mode
workflow_service = WorkflowService()
new_app_model = workflow_service.convert_to_workflow(app_model=app_model, account=current_user, args=args)
new_app_model = workflow_service.convert_to_workflow(
app_model=app_model, account=current_user, args=args, session=db.session()
)
# return app id
return {
@@ -1358,7 +1386,9 @@ class WorkflowFeaturesApi(Resource):
features = args.features.model_dump(mode="json", exclude_unset=True)
workflow_service = WorkflowService()
workflow_service.update_draft_workflow_features(app_model=app_model, features=features, account=current_user)
workflow_service.update_draft_workflow_features(
app_model=app_model, features=features, account=current_user, session=db.session()
)
return {"result": "success"}
@@ -1439,6 +1469,7 @@ class DraftWorkflowRestoreApi(Resource):
app_model=app_model,
workflow_id=workflow_id,
account=current_user,
session=db.session(),
)
except IsDraftWorkflowError as exc:
raise BadRequest(RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE) from exc
@@ -1553,7 +1584,7 @@ class DraftWorkflowNodeLastRunApi(Resource):
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, node_id: str):
srv = WorkflowService()
workflow = srv.get_draft_workflow(app_model)
workflow = srv.get_draft_workflow(app_model, session=db.session())
if not workflow:
raise NotFound("Workflow not found")
node_exec = srv.get_node_last_run(
@@ -1606,7 +1637,7 @@ class DraftWorkflowTriggerRunApi(Resource):
args = DraftWorkflowTriggerRunPayload.model_validate(console_ns.payload or {})
node_id = args.node_id
workflow_service = WorkflowService()
draft_workflow = workflow_service.get_draft_workflow(app_model)
draft_workflow = workflow_service.get_draft_workflow(app_model, session=db.session())
if not draft_workflow:
raise ValueError("Workflow not found")
@@ -1675,7 +1706,7 @@ class DraftWorkflowTriggerNodeApi(Resource):
"""
workflow_service = WorkflowService()
draft_workflow = workflow_service.get_draft_workflow(app_model)
draft_workflow = workflow_service.get_draft_workflow(app_model, session=db.session())
if not draft_workflow:
raise ValueError("Workflow not found")
@@ -1759,7 +1790,7 @@ class DraftWorkflowTriggerRunAllApi(Resource):
args = DraftWorkflowTriggerRunAllPayload.model_validate(console_ns.payload or {})
node_ids = args.node_ids
workflow_service = WorkflowService()
draft_workflow = workflow_service.get_draft_workflow(app_model)
draft_workflow = workflow_service.get_draft_workflow(app_model, session=db.session())
if not draft_workflow:
raise ValueError("Workflow not found")
@@ -1828,7 +1859,7 @@ class WorkflowOnlineUsersApi(Resource):
return {"data": []}
workflow_service = WorkflowService()
accessible_app_ids = workflow_service.get_accessible_app_ids(app_ids, current_tenant_id)
accessible_app_ids = workflow_service.get_accessible_app_ids(app_ids, current_tenant_id, session=db.session())
ordered_accessible_app_ids = [app_id for app_id in app_ids if app_id in accessible_app_ids]
users_json_by_app_id: dict[str, Any] = {}
@@ -490,7 +490,7 @@ class WorkflowCommentMentionUsersApi(Resource):
current_tenant = current_user.current_tenant # need the tenant object here
if current_tenant is None:
raise ValueError("current tenant is required")
members = TenantService.get_tenant_members(current_tenant, session=db.session)
members = TenantService.get_tenant_members(current_tenant, session=db.session())
users = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True)
response = WorkflowCommentMentionUsersPayload(users=users)
return response.model_dump(mode="json"), 200
@@ -337,7 +337,7 @@ class WorkflowVariableCollectionApi(Resource):
# fetch draft workflow by app_model
workflow_service = WorkflowService()
workflow_exist = workflow_service.is_workflow_exist(app_model=app_model)
workflow_exist = workflow_service.is_workflow_exist(app_model=app_model, session=db.session())
if not workflow_exist:
raise DraftWorkflowNotExist()
@@ -553,7 +553,7 @@ class VariableResetApi(Resource):
)
workflow_srv = WorkflowService()
draft_workflow = workflow_srv.get_draft_workflow(app_model)
draft_workflow = workflow_srv.get_draft_workflow(app_model, session=db.session())
if draft_workflow is None:
raise NotFoundError(
f"Draft workflow not found, app_id={app_model.id}",
@@ -606,7 +606,7 @@ class ConversationVariableCollectionApi(Resource):
# NOTE(QuantumGhost): Prefill conversation variables into the draft variables table
# so their IDs can be returned to the caller.
workflow_srv = WorkflowService()
draft_workflow = workflow_srv.get_draft_workflow(app_model)
draft_workflow = workflow_srv.get_draft_workflow(app_model, session=db.session())
if draft_workflow is None:
raise NotFoundError(description=f"draft workflow not found, id={app_model.id}")
draft_var_srv = WorkflowDraftVariableService(db.session())
@@ -646,6 +646,7 @@ class ConversationVariableCollectionApi(Resource):
app_model=app_model,
account=current_user,
conversation_variables=conversation_variables,
session=db.session(),
)
return {"result": "success"}
@@ -683,7 +684,7 @@ class EnvironmentVariableCollectionApi(Resource):
"""
# fetch draft workflow by app_model
workflow_service = WorkflowService()
workflow = workflow_service.get_draft_workflow(app_model=app_model)
workflow = workflow_service.get_draft_workflow(app_model=app_model, session=db.session())
if workflow is None:
raise DraftWorkflowNotExist()
@@ -740,6 +741,7 @@ class EnvironmentVariableCollectionApi(Resource):
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
session=db.session(),
)
return {"result": "success"}
@@ -41,6 +41,7 @@ from controllers.console.wraps import (
rbac_permission_required,
setup_required,
)
from extensions.ext_database import db
from libs.exception import BaseHTTPException
from libs.login import login_required
from models import App, AppMode
@@ -92,7 +93,9 @@ def _serve_snapshot(app_model: App, run_id: UUID) -> dict:
Flask request context.
"""
try:
snapshot = _service().snapshot_workflow_run(app_model=app_model, workflow_run_id=str(run_id))
snapshot = _service().snapshot_workflow_run(
app_model=app_model, workflow_run_id=str(run_id), session=db.session()
)
except NodeOutputInspectorError as error:
raise _InspectorNotFound(error) from error
return snapshot.model_dump(mode="json")
@@ -105,6 +108,7 @@ def _serve_node_detail(app_model: App, run_id: UUID, node_id: str) -> dict:
app_model=app_model,
workflow_run_id=str(run_id),
node_id=node_id,
session=db.session(),
)
except NodeOutputInspectorError as error:
raise _InspectorNotFound(error) from error
@@ -119,6 +123,7 @@ def _serve_output_preview(app_model: App, run_id: UUID, node_id: str, output_nam
workflow_run_id=str(run_id),
node_id=node_id,
output_name=output_name,
session=db.session(),
)
except NodeOutputInspectorError as error:
raise _InspectorNotFound(error) from error
@@ -245,7 +250,7 @@ def _stream_inspector_events(app_model: App, run_id: UUID) -> Iterator[str]:
# if the run is gone (raised before yielding any bytes, so Flask turns it
# into the normal HTTP 404 path).
try:
snapshot = service.snapshot_workflow_run(app_model=app_model, workflow_run_id=run_id_str)
snapshot = service.snapshot_workflow_run(app_model=app_model, workflow_run_id=run_id_str, session=db.session())
except NodeOutputInspectorError as error:
raise _InspectorNotFound(error) from error
@@ -308,6 +313,7 @@ def _stream_inspector_events(app_model: App, run_id: UUID) -> Iterator[str]:
app_model=app_model,
workflow_run_id=run_id_str,
node_id=message.node_id,
session=db.session(),
)
except NodeOutputInspectorError:
# Node may not appear in the graph yet (race with persistence); skip.
+4 -4
View File
@@ -90,7 +90,7 @@ class ActivateCheckApi(Resource):
token = args.token
invitation = RegisterService.get_invitation_with_case_fallback(
workspaceId, args.email, token, session=db.session
workspaceId, args.email, token, session=db.session()
)
if invitation:
data = invitation.get("data", {})
@@ -140,7 +140,7 @@ class ActivateApi(Resource):
normalized_request_email = args.email.lower() if args.email else None
invitation = RegisterService.get_invitation_with_case_fallback(
args.workspace_id, args.email, args.token, session=db.session
args.workspace_id, args.email, args.token, session=db.session()
)
if invitation is None:
raise AlreadyActivateError()
@@ -178,7 +178,7 @@ class ActivateApi(Resource):
RegisterService.revoke_token(args.workspace_id, normalized_request_email, args.token)
if membership_id is None:
TenantService.create_tenant_member(tenant, account, db.session, role=role)
TenantService.create_tenant_member(tenant, account, db.session(), role=role)
if setup_fields:
account.name = setup_fields[0]
@@ -188,6 +188,6 @@ class ActivateApi(Resource):
account.status = AccountStatus.ACTIVE
account.initialized_at = naive_utc_now()
TenantService.switch_tenant(account, tenant.id, session=db.session)
TenantService.switch_tenant(account, tenant.id, session=db.session())
return {"result": "success"}
@@ -101,7 +101,7 @@ class EmailRegisterSendEmailApi(Resource):
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
raise AccountInFreezeError()
account = AccountService.get_account_by_email_with_case_fallback(db.session, args.email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), args.email)
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
return {"result": "success", "data": token}
@@ -176,7 +176,7 @@ class EmailRegisterResetApi(Resource):
email = register_data.get("email", "")
normalized_email = email.lower()
account = AccountService.get_account_by_email_with_case_fallback(db.session, email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), email)
if account:
raise EmailAlreadyInUseError()
@@ -187,7 +187,7 @@ class EmailRegisterResetApi(Resource):
timezone=args.timezone,
language=args.language,
)
token_pair = AccountService.login(account=account, session=db.session, ip_address=extract_remote_ip(request))
token_pair = AccountService.login(account=account, session=db.session(), ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(normalized_email)
return {"result": "success", "data": token_pair.model_dump()}
@@ -206,7 +206,7 @@ class EmailRegisterResetApi(Resource):
password=password,
interface_language=get_valid_language(language),
timezone=timezone,
session=db.session,
session=db.session(),
)
except AccountRegisterError:
raise AccountInFreezeError()
@@ -82,7 +82,7 @@ class ForgotPasswordSendEmailApi(Resource):
else:
language = "en-US"
account = AccountService.get_account_by_email_with_case_fallback(db.session, args.email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), args.email)
token = AccountService.send_reset_password_email(
account=account,
@@ -180,7 +180,7 @@ class ForgotPasswordResetApi(Resource):
password_hashed = hash_password(args.new_password, salt)
email = reset_data.get("email", "")
account = AccountService.get_account_by_email_with_case_fallback(db.session, email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), email)
if account:
account = db.session.merge(account)
@@ -198,10 +198,10 @@ class ForgotPasswordResetApi(Resource):
# Create workspace if needed
if (
not TenantService.get_join_tenants(account, session=db.session)
not TenantService.get_join_tenants(account, session=db.session())
and FeatureService.get_system_features().is_allow_create_workspace
):
tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session)
TenantService.create_tenant_member(tenant, account, db.session, role="owner")
tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session())
TenantService.create_tenant_member(tenant, account, db.session(), role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
+54 -37
View File
@@ -9,7 +9,12 @@ from werkzeug.exceptions import Unauthorized
import services
from configs import dify_config
from constants.languages import get_valid_language
from controllers.common.fields import SimpleResultDataResponse, SimpleResultOptionalDataResponse, SimpleResultResponse
from controllers.common.fields import (
SimpleResultDataResponse,
SimpleResultMessageResponse,
SimpleResultOptionalDataResponse,
SimpleResultResponse,
)
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.auth.error import (
@@ -51,7 +56,7 @@ from models.account import Account
from services.account_service import AccountService, InvitationDetailDict, RegisterService, TenantService
from services.billing_service import BillingService
from services.entities.auth_entities import LoginFailureReason, LoginPayloadBase
from services.errors.account import AccountRegisterError
from services.errors.account import AccountRegisterError, RefreshTokenAccountNotFoundError, RefreshTokenNotFoundError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
from services.feature_service import FeatureService
@@ -87,6 +92,7 @@ register_schema_models(console_ns, LoginPayload, EmailPayload, EmailCodeLoginPay
register_response_schema_models(
console_ns,
SimpleResultDataResponse,
SimpleResultMessageResponse,
SimpleResultOptionalDataResponse,
SimpleResultResponse,
)
@@ -120,7 +126,7 @@ class LoginApi(Resource):
invitation_data: InvitationDetailDict | None = None
if invite_token:
invitation_data = RegisterService.get_invitation_with_case_fallback(
None, request_email, invite_token, session=db.session
None, request_email, invite_token, session=db.session()
)
if invitation_data is None:
invite_token = None
@@ -147,23 +153,26 @@ class LoginApi(Resource):
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.INVALID_CREDENTIALS)
raise AuthenticationFailedError() from exc
# SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account, session=db.session)
tenants = TenantService.get_join_tenants(account, session=db.session())
if len(tenants) == 0:
system_features = FeatureService.get_system_features()
if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available():
raise WorkspacesLimitExceeded()
else:
return {
"result": "fail",
"data": "workspace not found, please contact system admin to invite you to join in a workspace",
}
return SimpleResultOptionalDataResponse(
result="fail",
data="workspace not found, please contact system admin to invite you to join in a workspace",
).model_dump(mode="json")
token_pair = AccountService.login(account=account, session=db.session, ip_address=extract_remote_ip(request))
token_pair = AccountService.login(account=account, session=db.session(), ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(normalized_email)
# Create response with cookies instead of returning tokens in body
response = make_response({"result": "success"})
# response-contract:ignore cookie-bearing Flask response
response = make_response(
SimpleResultOptionalDataResponse(result="success").model_dump(mode="json", exclude_none=True)
)
set_access_token_to_cookie(request, response, token_pair.access_token)
set_refresh_token_to_cookie(request, response, token_pair.refresh_token)
@@ -178,12 +187,11 @@ class LogoutApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_user
def post(self, account: Account):
if isinstance(account, flask_login.AnonymousUserMixin):
response = make_response({"result": "success"})
else:
# response-contract:ignore cookie-bearing Flask response
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
if not isinstance(account, flask_login.AnonymousUserMixin):
AccountService.logout(account=account)
flask_login.logout_user()
response = make_response({"result": "success"})
# Clear cookies on logout
clear_access_token_from_cookie(response)
@@ -219,7 +227,7 @@ class ResetPasswordSendEmailApi(Resource):
is_allow_register=FeatureService.get_system_features().is_allow_register,
)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/email-code-login")
@@ -252,7 +260,7 @@ class EmailCodeLoginSendEmailApi(Resource):
else:
token = AccountService.send_email_code_login_email(account=account, language=language)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/email-code-login/validity")
@@ -293,7 +301,7 @@ class EmailCodeLoginApi(Resource):
_log_console_login_failure(email=user_email, reason=LoginFailureReason.ACCOUNT_IN_FREEZE)
raise AccountInFreezeError()
if account:
tenants = TenantService.get_join_tenants(account, session=db.session)
tenants = TenantService.get_join_tenants(account, session=db.session())
if not tenants:
workspaces = FeatureService.get_system_features().license.workspaces
if not workspaces.is_available():
@@ -301,8 +309,8 @@ class EmailCodeLoginApi(Resource):
if not FeatureService.get_system_features().is_allow_create_workspace:
raise NotAllowedCreateWorkspace()
else:
new_tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session)
TenantService.create_tenant_member(new_tenant, account, db.session, role="owner")
new_tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session())
TenantService.create_tenant_member(new_tenant, account, db.session(), role="owner")
account.current_tenant = new_tenant
tenant_was_created.send(new_tenant)
@@ -313,7 +321,7 @@ class EmailCodeLoginApi(Resource):
name=user_email,
interface_language=get_valid_language(language),
timezone=args.timezone,
session=db.session,
session=db.session(),
)
except WorkSpaceNotAllowedCreateError:
raise NotAllowedCreateWorkspace()
@@ -322,11 +330,12 @@ class EmailCodeLoginApi(Resource):
raise AccountInFreezeError()
except WorkspacesLimitExceededError:
raise WorkspacesLimitExceeded()
token_pair = AccountService.login(account, session=db.session, ip_address=extract_remote_ip(request))
token_pair = AccountService.login(account, session=db.session(), ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(user_email)
# Create response with cookies instead of returning tokens in body
response = make_response({"result": "success"})
# response-contract:ignore cookie-bearing Flask response
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
set_csrf_token_to_cookie(request, response, token_pair.csrf_token)
# Set HTTP-only secure cookies for tokens
@@ -338,45 +347,53 @@ class EmailCodeLoginApi(Resource):
@console_ns.route("/refresh-token")
class RefreshTokenApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(401, "Unauthorized", console_ns.models[SimpleResultMessageResponse.__name__])
def post(self):
# Get refresh token from cookie instead of request body
refresh_token = extract_refresh_token(request)
if not refresh_token:
return {"result": "fail", "message": "No refresh token provided"}, 401
return SimpleResultMessageResponse(result="fail", message="No refresh token provided").model_dump(
mode="json"
), 401
try:
new_token_pair = AccountService.refresh_token(refresh_token, session=db.session)
new_token_pair = AccountService.refresh_token(refresh_token, session=db.session())
except Unauthorized as exc:
return SimpleResultMessageResponse(result="fail", message=exc.description or "Unauthorized.").model_dump(
mode="json"
), 401
except (RefreshTokenNotFoundError, RefreshTokenAccountNotFoundError) as exc:
return SimpleResultMessageResponse(result="fail", message=str(exc)).model_dump(mode="json"), 401
# Create response with new cookies
response = make_response({"result": "success"})
# Create response with new cookies
# response-contract:ignore cookie-bearing Flask response
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
# Update cookies with new tokens
set_csrf_token_to_cookie(request, response, new_token_pair.csrf_token)
set_access_token_to_cookie(request, response, new_token_pair.access_token)
set_refresh_token_to_cookie(request, response, new_token_pair.refresh_token)
return response
except Exception as e:
return {"result": "fail", "message": str(e)}, 401
# Update cookies with new tokens
set_csrf_token_to_cookie(request, response, new_token_pair.csrf_token)
set_access_token_to_cookie(request, response, new_token_pair.access_token)
set_refresh_token_to_cookie(request, response, new_token_pair.refresh_token)
return response
def _get_account_with_case_fallback(email: str):
account = AccountService.get_user_through_email(email, session=db.session)
account = AccountService.get_user_through_email(email, session=db.session())
if account or email == email.lower():
return account
return AccountService.get_user_through_email(email.lower(), session=db.session)
return AccountService.get_user_through_email(email.lower(), session=db.session())
def _authenticate_account_with_case_fallback(
original_email: str, normalized_email: str, password: str, invite_token: str | None
):
try:
return AccountService.authenticate(original_email, password, invite_token, session=db.session)
return AccountService.authenticate(original_email, password, invite_token, session=db.session())
except services.errors.account.AccountPasswordError:
if original_email == normalized_email:
raise
return AccountService.authenticate(normalized_email, password, invite_token, session=db.session)
return AccountService.authenticate(normalized_email, password, invite_token, session=db.session())
def _log_console_login_failure(*, email: str, reason: LoginFailureReason) -> None:
+8 -8
View File
@@ -195,7 +195,7 @@ class OAuthCallback(Resource):
db.session.commit()
try:
TenantService.create_owner_tenant_if_not_exist(account, session=db.session)
TenantService.create_owner_tenant_if_not_exist(account, session=db.session())
except Unauthorized:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found.")
except WorkSpaceNotAllowedCreateError:
@@ -206,7 +206,7 @@ class OAuthCallback(Resource):
token_pair = AccountService.login(
account=account,
session=db.session,
session=db.session(),
ip_address=extract_remote_ip(request),
)
@@ -225,7 +225,7 @@ def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) ->
account: Account | None = Account.get_by_openid(provider, user_info.id)
if not account:
account = AccountService.get_account_by_email_with_case_fallback(db.session, user_info.email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), user_info.email)
return account
@@ -241,13 +241,13 @@ def _generate_account(
oauth_new_user = False
if account:
tenants = TenantService.get_join_tenants(account, session=db.session)
tenants = TenantService.get_join_tenants(account, session=db.session())
if not tenants:
if not FeatureService.get_system_features().is_allow_create_workspace:
raise WorkSpaceNotAllowedCreateError()
else:
new_tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session)
TenantService.create_tenant_member(new_tenant, account, db.session, role="owner")
new_tenant = TenantService.create_tenant(f"{account.name}'s Workspace", session=db.session())
TenantService.create_tenant_member(new_tenant, account, db.session(), role="owner")
account.current_tenant = new_tenant
tenant_was_created.send(new_tenant)
@@ -273,10 +273,10 @@ def _generate_account(
provider=provider,
language=interface_language,
timezone=timezone,
session=db.session,
session=db.session(),
)
# Link account
AccountService.link_account_integrate(provider, user_info.id, account, session=db.session)
AccountService.link_account_integrate(provider, user_info.id, account, session=db.session())
return account, oauth_new_user
+4 -1
View File
@@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from extensions.ext_database import db
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.login import login_required
from models import Account
@@ -131,7 +132,9 @@ def oauth_server_access_token_required[T, **P, R](
response.headers["WWW-Authenticate"] = "Bearer"
return response
account = OAuthServerService.validate_oauth_access_token(oauth_provider_app.client_id, access_token)
account = OAuthServerService.validate_oauth_access_token(
oauth_provider_app.client_id, access_token, db.session()
)
if not account:
response = jsonify({"error": "access_token or client_id is invalid"})
response.status_code = 401
+2 -2
View File
@@ -56,7 +56,7 @@ class Subscription(Resource):
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True))
BillingService.is_tenant_owner_or_admin(db.session, current_user)
BillingService.is_tenant_owner_or_admin(db.session(), current_user)
return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id)
@@ -70,7 +70,7 @@ class Invoices(Resource):
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
BillingService.is_tenant_owner_or_admin(db.session, current_user)
BillingService.is_tenant_owner_or_admin(db.session(), current_user)
return BillingService.get_invoices(current_user.email, current_tenant_id)
@@ -245,7 +245,7 @@ class DataSourceNotionListApi(Resource):
exist_page_ids = []
# import notion in the exist dataset
if query.dataset_id:
dataset = DatasetService.get_dataset(query.dataset_id, db.session)
dataset = DatasetService.get_dataset(query.dataset_id, db.session())
if not dataset:
raise NotFound("Dataset not found.")
if dataset.data_source_type != "notion_import":
@@ -400,11 +400,11 @@ class DataSourceNotionDatasetSyncApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
def get(self, dataset_id: UUID) -> tuple[dict[str, str], int]:
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
documents = DocumentService.get_document_by_dataset_id(dataset_id_str, db.session)
documents = DocumentService.get_document_by_dataset_id(dataset_id_str, db.session())
for document in documents:
document_indexing_sync_task.delay(dataset_id_str, document.id)
return {"result": "success"}, 200
@@ -420,11 +420,11 @@ class DataSourceNotionDocumentSyncApi(Resource):
def get(self, dataset_id: UUID, document_id: UUID) -> tuple[dict[str, str], int]:
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if document is None:
raise NotFound("Document not found.")
document_indexing_sync_task.delay(dataset_id_str, document_id_str)
+28 -24
View File
@@ -418,6 +418,7 @@ class DatasetListApi(Resource):
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
str(current_tenant_id),
current_user.id,
session=db.session(),
)
accessible_dataset_ids: list[str] | None = None
@@ -461,7 +462,7 @@ class DatasetListApi(Resource):
datasets, total = DatasetService.get_datasets(
query.page,
query.limit,
db.session,
db.session(),
current_tenant_id,
current_user,
query.keyword,
@@ -573,6 +574,7 @@ class DatasetListApi(Resource):
current_tenant_id,
current_user.id,
[dataset.id],
session=session,
)
item = DatasetDetailWithPartialMembersResponse.model_validate(dataset, from_attributes=True).model_dump(
@@ -602,17 +604,18 @@ class DatasetApi(Resource):
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
current_tenant_id,
current_user.id,
dataset_id=dataset_id_str,
session=db.session(),
)
permission_keys_map = permissions.dataset.permission_keys_by_resource_ids([dataset_id_str])
data = dump_response(DatasetDetailResponse, dataset)
@@ -622,7 +625,7 @@ class DatasetApi(Resource):
provider_id = ModelProviderID(dataset.embedding_model_provider)
data["embedding_model_provider"] = str(provider_id)
if data.get("permission") == "partial_members":
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session)
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session())
data.update({"partial_member_list": part_users_list})
# check embedding setting
@@ -666,7 +669,7 @@ class DatasetApi(Resource):
@with_session
def patch(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
@@ -697,6 +700,7 @@ class DatasetApi(Resource):
current_tenant_id,
current_user.id,
[dataset_id_str],
session=session,
)
result_data = dump_response(DatasetDetailResponse, dataset)
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
@@ -704,13 +708,13 @@ class DatasetApi(Resource):
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
DatasetPermissionService.update_partial_member_list(
tenant_id, dataset_id_str, payload.partial_member_list, db.session
tenant_id, dataset_id_str, payload.partial_member_list, db.session()
)
# clear partial member list when permission is only_me or all_team_members
elif payload.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
DatasetPermissionService.clear_partial_member_list(dataset_id_str, db.session)
DatasetPermissionService.clear_partial_member_list(dataset_id_str, db.session())
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session)
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session())
result_data.update({"partial_member_list": partial_member_list})
return result_data, 200
@@ -729,8 +733,8 @@ class DatasetApi(Resource):
raise Forbidden()
try:
if DatasetService.delete_dataset(dataset_id_str, current_user, db.session):
DatasetPermissionService.clear_partial_member_list(dataset_id_str, db.session)
if DatasetService.delete_dataset(dataset_id_str, current_user, db.session()):
DatasetPermissionService.clear_partial_member_list(dataset_id_str, db.session())
return "", 204
else:
raise NotFound("Dataset not found.")
@@ -755,7 +759,7 @@ class DatasetUseCheckApi(Resource):
def get(self, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset_is_using = DatasetService.dataset_use_check(dataset_id_str, db.session)
dataset_is_using = DatasetService.dataset_use_check(dataset_id_str, db.session())
return {"is_using": dataset_is_using}, 200
@@ -776,12 +780,12 @@ class DatasetQueryApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
def get(self, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -917,16 +921,16 @@ class DatasetRelatedAppListApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
def get(self, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
app_dataset_joins = DatasetService.get_related_apps(dataset.id, db.session)
app_dataset_joins = DatasetService.get_related_apps(dataset.id, db.session())
related_apps = []
for app_dataset_join in app_dataset_joins:
@@ -1101,7 +1105,7 @@ class DatasetEnableApiApi(Resource):
def post(self, dataset_id: UUID, status: str):
dataset_id_str = str(dataset_id)
DatasetService.update_dataset_api_status(dataset_id_str, status == "enable", db.session)
DatasetService.update_dataset_api_status(dataset_id_str, status == "enable", db.session())
return {"result": "success"}, 200
@@ -1170,10 +1174,10 @@ class DatasetErrorDocs(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
def get(self, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
results = DocumentService.get_error_documents_by_dataset_id(dataset_id_str, db.session)
results = DocumentService.get_error_documents_by_dataset_id(dataset_id_str, db.session())
return dump_response(ErrorDocsResponse, {"data": results, "total": len(results)}), 200
@@ -1197,15 +1201,15 @@ class DatasetPermissionUserListApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
def get(self, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
partial_members_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session)
partial_members_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, db.session())
return dump_response(PartialMemberListResponse, {"data": partial_members_list}), 200
@@ -1227,8 +1231,8 @@ class DatasetAutoDisableLogApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
def get(self, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
auto_disable_logs = DatasetService.get_dataset_auto_disable_logs(dataset_id_str, db.session)
auto_disable_logs = DatasetService.get_dataset_auto_disable_logs(dataset_id_str, db.session())
return dump_response(AutoDisableLogsResponse, auto_disable_logs), 200
@@ -183,16 +183,16 @@ class DocumentResource(Resource):
def get_document(
self, dataset_id: str, document_id: str, current_user: Account, current_tenant_id: str
) -> Document:
dataset = DatasetService.get_dataset(dataset_id, db.session)
dataset = DatasetService.get_dataset(dataset_id, db.session())
if not dataset:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
document = DocumentService.get_document(dataset_id, document_id, session=db.session)
document = DocumentService.get_document(dataset_id, document_id, session=db.session())
if not document:
raise NotFound("Document not found.")
@@ -203,16 +203,16 @@ class DocumentResource(Resource):
return document
def get_batch_documents(self, dataset_id: str, batch: str, current_user: Account) -> Sequence[Document]:
dataset = DatasetService.get_dataset(dataset_id, db.session)
dataset = DatasetService.get_dataset(dataset_id, db.session())
if not dataset:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
documents = DocumentService.get_batch_documents(dataset_id, batch, db.session)
documents = DocumentService.get_batch_documents(dataset_id, batch, db.session())
if not documents:
raise NotFound("Documents not found.")
@@ -243,13 +243,13 @@ class GetProcessRuleApi(Resource):
# get the latest process rule
document = db.get_or_404(Document, document_id)
dataset = DatasetService.get_dataset(document.dataset_id, db.session)
dataset = DatasetService.get_dataset(document.dataset_id, db.session())
if not dataset:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -319,12 +319,12 @@ class DatasetDocumentListApi(Resource):
)
except (ArgumentTypeError, ValueError, Exception):
fetch = False
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -376,6 +376,7 @@ class DatasetDocumentListApi(Resource):
documents=documents,
dataset=dataset,
tenant_id=current_tenant_id,
session=db.session(),
)
if fetch:
@@ -423,7 +424,7 @@ class DatasetDocumentListApi(Resource):
def post(self, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
@@ -433,7 +434,7 @@ class DatasetDocumentListApi(Resource):
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -447,9 +448,9 @@ class DatasetDocumentListApi(Resource):
try:
documents, batch = DocumentService.save_document_with_dataset_id(
dataset, knowledge_config, current_user, session=db.session
dataset, knowledge_config, current_user, session=db.session()
)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -468,7 +469,7 @@ class DatasetDocumentListApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def delete(self, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
# check user's model setting
@@ -477,7 +478,7 @@ class DatasetDocumentListApi(Resource):
try:
document_ids = request.args.getlist("document_id")
dataset_ref = DatasetRefService.create_dataset_ref(dataset)
DocumentService.delete_documents(dataset_ref, document_ids, dataset.doc_form, db.session)
DocumentService.delete_documents(dataset_ref, document_ids, dataset.doc_form, db.session())
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot delete document during indexing.")
@@ -536,7 +537,7 @@ class DatasetInitApi(Resource):
tenant_id=current_tenant_id,
knowledge_config=knowledge_config,
account=current_user,
session=db.session,
session=db.session(),
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -873,7 +874,7 @@ class DocumentApi(DocumentResource):
if metadata == "only":
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata_details}
elif metadata == "without":
dataset_process_rules = DatasetService.get_process_rules(dataset_id_str, db.session)
dataset_process_rules = DatasetService.get_process_rules(dataset_id_str, db.session())
document_process_rules = document.dataset_process_rule.to_dict() if document.dataset_process_rule else {}
response = {
"id": document.id,
@@ -907,7 +908,7 @@ class DocumentApi(DocumentResource):
"need_summary": document.need_summary if document.need_summary is not None else False,
}
else:
dataset_process_rules = DatasetService.get_process_rules(dataset_id_str, db.session)
dataset_process_rules = DatasetService.get_process_rules(dataset_id_str, db.session())
document_process_rules = document.dataset_process_rule.to_dict() if document.dataset_process_rule else {}
response = {
"id": document.id,
@@ -956,7 +957,7 @@ class DocumentApi(DocumentResource):
def delete(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
# check user's model setting
@@ -965,7 +966,7 @@ class DocumentApi(DocumentResource):
document = self.get_document(dataset_id_str, document_id_str, current_user, current_tenant_id)
try:
DocumentService.delete_document(document, db.session)
DocumentService.delete_document(document, db.session())
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot delete document during indexing.")
@@ -989,7 +990,7 @@ class DocumentDownloadApi(DocumentResource):
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID) -> dict[str, Any]:
# Reuse the shared permission/tenant checks implemented in DocumentResource.
document = self.get_document(str(dataset_id), str(document_id), current_user, current_tenant_id)
return {"url": DocumentService.get_document_download_url(document, db.session)}
return {"url": DocumentService.get_document_download_url(document, db.session())}
@console_ns.route("/datasets/<uuid:dataset_id>/documents/download-zip")
@@ -1019,7 +1020,7 @@ class DocumentBatchDownloadZipApi(DocumentResource):
document_ids=document_ids,
tenant_id=current_tenant_id,
current_user=current_user,
session=db.session,
session=db.session(),
)
# Delegate ZIP packing to FileService, but keep Flask response+cleanup in the route.
@@ -1168,7 +1169,7 @@ class DocumentStatusApi(DocumentResource):
self, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable", "archive", "un_archive"]
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
@@ -1180,12 +1181,12 @@ class DocumentStatusApi(DocumentResource):
DatasetService.check_dataset_model_setting(dataset)
# check user's permission
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
document_ids = request.args.getlist("document_id")
try:
DocumentService.batch_update_document_status(dataset, document_ids, action, current_user, db.session)
DocumentService.batch_update_document_status(dataset, document_ids, action, current_user, db.session())
except services.errors.document.DocumentIndexingError as e:
raise InvalidActionError(str(e))
except ValueError as e:
@@ -1209,11 +1210,11 @@ class DocumentPauseApi(DocumentResource):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session)
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session())
# 404 if document not found
if document is None:
@@ -1225,7 +1226,7 @@ class DocumentPauseApi(DocumentResource):
try:
# pause document
DocumentService.pause_document(document, db.session)
DocumentService.pause_document(document, db.session())
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot pause completed document.")
@@ -1244,10 +1245,10 @@ class DocumentRecoverApi(DocumentResource):
"""recover document."""
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session)
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session())
# 404 if document not found
if document is None:
@@ -1258,7 +1259,7 @@ class DocumentRecoverApi(DocumentResource):
raise ArchivedDocumentImmutableError()
try:
# pause document
DocumentService.recover_document(document, db.session)
DocumentService.recover_document(document, db.session())
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Document is not in paused status.")
@@ -1278,13 +1279,13 @@ class DocumentRetryApi(DocumentResource):
"""retry document."""
payload = DocumentRetryPayload.model_validate(console_ns.payload or {})
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
retry_documents = []
if not dataset:
raise NotFound("Dataset not found.")
for document_id in payload.document_ids:
try:
document = DocumentService.get_document(dataset.id, document_id, session=db.session)
document = DocumentService.get_document(dataset.id, document_id, session=db.session())
# 404 if document not found
if document is None:
@@ -1302,7 +1303,7 @@ class DocumentRetryApi(DocumentResource):
logger.exception("Failed to retry document, document id: %s", document_id)
continue
# retry document
DocumentService.retry_document(dataset_id_str, retry_documents, db.session)
DocumentService.retry_document(dataset_id_str, retry_documents, db.session())
return "", 204
@@ -1320,14 +1321,14 @@ class DocumentRenameApi(DocumentResource):
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
dataset = DatasetService.get_dataset(dataset_id, db.session)
dataset = DatasetService.get_dataset(dataset_id, db.session())
if not dataset:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_operator_permission(current_user, dataset, session=db.session)
DatasetService.check_dataset_operator_permission(current_user, dataset, session=db.session())
payload = DocumentRenamePayload.model_validate(console_ns.payload or {})
try:
document = DocumentService.rename_document(str(dataset_id), str(document_id), payload.name, db.session)
document = DocumentService.rename_document(str(dataset_id), str(document_id), payload.name, db.session())
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot delete document during indexing.")
@@ -1345,11 +1346,11 @@ class WebsiteDocumentSyncApi(DocumentResource):
def get(self, current_tenant_id: str, dataset_id: UUID, document_id: UUID):
"""sync website document."""
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
document_id_str = str(document_id)
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session)
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
if document.tenant_id != current_tenant_id:
@@ -1360,7 +1361,7 @@ class WebsiteDocumentSyncApi(DocumentResource):
if DocumentService.check_archived(document):
raise ArchivedDocumentImmutableError()
# sync document
DocumentService.sync_website_document(dataset_id_str, document, db.session)
DocumentService.sync_website_document(dataset_id_str, document, db.session())
return {"result": "success"}, 200
@@ -1380,10 +1381,10 @@ class DocumentPipelineExecutionLogApi(DocumentResource):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session)
document = DocumentService.get_document(dataset.id, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
log = db.session.scalar(
@@ -1438,7 +1439,7 @@ class DocumentGenerateSummaryApi(Resource):
dataset_id_str = str(dataset_id)
# Get dataset
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
@@ -1447,7 +1448,7 @@ class DocumentGenerateSummaryApi(Resource):
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -1472,7 +1473,7 @@ class DocumentGenerateSummaryApi(Resource):
raise ValueError("Summary index is not enabled for this dataset. Please enable it in the dataset settings.")
# Verify all documents exist and belong to the dataset
documents = DocumentService.get_documents_by_ids(dataset_id_str, document_list, db.session)
documents = DocumentService.get_documents_by_ids(dataset_id_str, document_list, db.session())
if len(documents) != len(document_list):
found_ids = {doc.id for doc in documents}
@@ -1488,7 +1489,7 @@ class DocumentGenerateSummaryApi(Resource):
DocumentService.update_documents_need_summary(
dataset_id=dataset_id_str,
document_ids=document_ids_to_update,
session=db.session,
session=db.session(),
need_summary=True,
)
@@ -1539,13 +1540,13 @@ class DocumentSummaryStatusApi(DocumentResource):
document_id_str = str(document_id)
# Get dataset
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# Check permissions
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -1555,7 +1556,7 @@ class DocumentSummaryStatusApi(DocumentResource):
result = SummaryIndexService.get_document_summary_status_detail(
document_id=document_id_str,
dataset_id=dataset_id_str,
session=db.session,
session=db.session(),
)
return result, 200
@@ -173,7 +173,7 @@ def _get_segment_for_document(
raise NotFound("Document not found.")
segment_ref = DatasetRefService.create_segment_ref(document_ref, segment_id)
segment = SegmentService.get_segment_by_ref(segment_ref)
segment = SegmentService.get_segment_by_ref(segment_ref, db.session())
if not segment:
raise NotFound("Segment not found.")
return segment_ref, segment
@@ -193,16 +193,16 @@ class DatasetDocumentSegmentListApi(Resource):
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
@@ -278,7 +278,7 @@ class DatasetDocumentSegmentListApi(Resource):
summaries: dict[str, str | None] = {}
if segment_ids:
summary_records = SummaryIndexService.get_segments_summaries(
segment_ids=segment_ids, dataset_id=dataset_id_str
segment_ids=segment_ids, dataset_id=dataset_id_str, session=db.session()
)
summaries = {chunk_id: summary.summary_content for chunk_id, summary in summary_records.items()}
@@ -303,14 +303,14 @@ class DatasetDocumentSegmentListApi(Resource):
def delete(self, current_user: Account, dataset_id: UUID, document_id: UUID):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
segment_ids = request.args.getlist("segment_id")
@@ -319,10 +319,10 @@ class DatasetDocumentSegmentListApi(Resource):
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
SegmentService.delete_segments(segment_ids, document, dataset, db.session)
SegmentService.delete_segments(segment_ids, document, dataset, db.session())
return "", 204
@@ -348,11 +348,11 @@ class DatasetDocumentSegmentApi(Resource):
action: Literal["enable", "disable"],
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# check user's model setting
@@ -362,7 +362,7 @@ class DatasetDocumentSegmentApi(Resource):
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY:
@@ -388,7 +388,7 @@ class DatasetDocumentSegmentApi(Resource):
if cache_result is not None:
raise InvalidActionError("Document is being indexed, please try again later")
try:
SegmentService.update_segments_status(segment_ids, action, dataset, document, db.session)
SegmentService.update_segments_status(segment_ids, action, dataset, document, db.session())
except Exception as e:
raise InvalidActionError(str(e))
return dump_response(SimpleResultResponse, {"result": "success"}), 200
@@ -411,12 +411,12 @@ class DatasetDocumentSegmentAddApi(Resource):
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
if not current_user.is_dataset_editor:
@@ -438,15 +438,20 @@ class DatasetDocumentSegmentAddApi(Resource):
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
payload = SegmentCreatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
segment = type_cast(DocumentSegment, SegmentService.create_segment(payload_dict, document, dataset, db.session))
summary = SummaryIndexService.get_segment_summary(segment_id=segment.id, dataset_id=dataset_id_str)
segment = type_cast(
DocumentSegment,
SegmentService.create_segment(payload_dict, document, dataset, db.session()),
)
summary = SummaryIndexService.get_segment_summary(
segment_id=segment.id, dataset_id=dataset_id_str, session=db.session()
)
response = {
"data": segment_response_with_summary(segment, summary.summary_content if summary else None),
"doc_form": document.doc_form,
@@ -472,21 +477,21 @@ class DatasetDocumentSegmentUpdateApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY:
@@ -518,9 +523,11 @@ class DatasetDocumentSegmentUpdateApi(Resource):
segment,
document,
dataset,
db.session,
db.session(),
)
summary = SummaryIndexService.get_segment_summary(
segment_id=segment.id, dataset_id=dataset_id_str, session=db.session()
)
summary = SummaryIndexService.get_segment_summary(segment_id=segment.id, dataset_id=dataset_id_str)
response = {
"data": segment_response_with_summary(segment, summary.summary_content if summary else None),
"doc_form": document.doc_form,
@@ -541,26 +548,26 @@ class DatasetDocumentSegmentUpdateApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(dataset, document, segment_id_str)
SegmentService.delete_segment(segment, document, dataset, db.session)
SegmentService.delete_segment(segment, document, dataset, db.session())
return "", 204
@@ -583,12 +590,12 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
@@ -658,18 +665,18 @@ class ChildChunkAddApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# check embedding model setting
@@ -693,7 +700,7 @@ class ChildChunkAddApi(Resource):
# validate args
try:
payload = ChildChunkCreatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, db.session)
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, db.session())
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200
@@ -709,14 +716,14 @@ class ChildChunkAddApi(Resource):
def get(self, current_tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
segment_id_str = str(segment_id)
@@ -759,21 +766,21 @@ class ChildChunkAddApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
segment_id_str = str(segment_id)
@@ -781,7 +788,7 @@ class ChildChunkAddApi(Resource):
# validate args
payload = ChildChunkBatchUpdatePayload.model_validate(console_ns.payload or {})
try:
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, db.session)
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, db.session())
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkBatchUpdateResponse, {"data": child_chunks}), 200
@@ -811,31 +818,31 @@ class ChildChunkUpdateApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
segment_id_str = str(segment_id)
segment_ref, _ = _get_segment_for_document(dataset, document, segment_id_str)
child_chunk_id_str = str(child_chunk_id)
child_chunk = SegmentService.get_child_chunk_by_segment_ref(child_chunk_id_str, segment_ref)
child_chunk = SegmentService.get_child_chunk_by_segment_ref(child_chunk_id_str, segment_ref, db.session())
if not child_chunk:
raise NotFound("Child chunk not found.")
try:
SegmentService.delete_child_chunk(child_chunk, dataset, db.session)
SegmentService.delete_child_chunk(child_chunk, dataset, db.session())
except ChildChunkDeleteIndexServiceError as e:
raise ChildChunkDeleteIndexError(str(e))
return "", 204
@@ -862,34 +869,34 @@ class ChildChunkUpdateApi(Resource):
):
# check dataset
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id_str = str(document_id)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session)
document = DocumentService.get_document(dataset_id_str, document_id_str, session=db.session())
if not document:
raise NotFound("Document not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
if not current_user.is_dataset_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
segment_id_str = str(segment_id)
segment_ref, segment = _get_segment_for_document(dataset, document, segment_id_str)
child_chunk_id_str = str(child_chunk_id)
child_chunk = SegmentService.get_child_chunk_by_segment_ref(child_chunk_id_str, segment_ref)
child_chunk = SegmentService.get_child_chunk_by_segment_ref(child_chunk_id_str, segment_ref, db.session())
if not child_chunk:
raise NotFound("Child chunk not found.")
# validate args
try:
payload = ChildChunkUpdatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.update_child_chunk(
payload.content, child_chunk, segment, document, dataset, db.session
payload.content, child_chunk, segment, document, dataset, db.session()
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
+7 -6
View File
@@ -299,7 +299,9 @@ class ExternalApiTemplateApi(Resource):
if not (current_user.has_edit_permission or current_user.is_dataset_operator):
raise Forbidden()
ExternalDatasetService.delete_external_knowledge_api(session, current_tenant_id, external_knowledge_api_id_str)
ExternalDatasetService.delete_external_knowledge_api(
current_tenant_id, external_knowledge_api_id_str, session=session
)
return "", 204
@@ -318,9 +320,7 @@ class ExternalApiUseCheckApi(Resource):
external_knowledge_api_id_str = str(external_knowledge_api_id)
external_knowledge_api_is_using, count = ExternalDatasetService.external_knowledge_api_use_check(
session,
external_knowledge_api_id_str,
current_tenant_id,
external_knowledge_api_id_str, current_tenant_id, session=session
)
return {"is_using": external_knowledge_api_is_using, "count": count}, 200
@@ -366,6 +366,7 @@ class ExternalDatasetCreateApi(Resource):
str(current_tenant_id),
current_user.id,
[dataset_id_str],
session=session,
)
item["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
@@ -393,12 +394,12 @@ class ExternalKnowledgeHitTestingApi(Resource):
@with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
@@ -86,12 +86,12 @@ class DatasetsHitTestingBase:
dataset_id: str, current_user: Account | None = None, current_tenant_id: str | None = None
) -> Dataset:
current_user, _ = resolve_account_fallback(current_user, current_tenant_id)
dataset = DatasetService.get_dataset(dataset_id, db.session)
dataset = DatasetService.get_dataset(dataset_id, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
+11 -11
View File
@@ -61,10 +61,10 @@ class DatasetMetadataCreateApi(Resource):
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
metadata = MetadataService.create_metadata(
db.session(), dataset_id_str, metadata_args, current_user, current_tenant_id
@@ -81,7 +81,7 @@ class DatasetMetadataCreateApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
def get(self, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
metadata = MetadataService.get_dataset_metadatas(db.session(), dataset)
@@ -105,10 +105,10 @@ class DatasetMetadataApi(Resource):
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
metadata = MetadataService.update_metadata_name(
db.session(), dataset_id_str, metadata_id_str, name, current_user, current_tenant_id
@@ -125,10 +125,10 @@ class DatasetMetadataApi(Resource):
def delete(self, current_user: Account, dataset_id: UUID, metadata_id: UUID):
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
MetadataService.delete_metadata(db.session(), dataset_id_str, metadata_id_str)
# Frontend callers only await success and invalidate metadata caches; no response body is consumed.
@@ -162,10 +162,10 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable"]):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
match action:
case "enable":
@@ -191,10 +191,10 @@ class DocumentMetadataEditApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, db.session)
dataset = DatasetService.get_dataset(dataset_id_str, db.session())
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, db.session)
DatasetService.check_dataset_permission(dataset, current_user, db.session())
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.common.fields import RedirectResponse, SimpleResultResponse
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
@@ -19,11 +19,14 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from core.entities.provider_entities import ProviderConfig
from core.plugin.entities.plugin_daemon import PluginOAuthAuthorizationUrlResponse
from core.plugin.impl.oauth import OAuthHandler
from core.tools.entities.common_entities import I18nObject
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import dump_response
from libs.login import login_required
from models import Account
from models.provider_ids import DatasourceProviderID
@@ -33,7 +36,9 @@ from services.plugin.oauth_service import OAuthProxyService
class DatasourceCredentialPayload(BaseModel):
name: str | None = Field(default=None, max_length=100)
credentials: dict[str, Any]
credentials: dict[str, Any] = Field(
description="Plugin-defined credential parameters. The schema is declared by the datasource provider."
)
class DatasourceCredentialDeletePayload(BaseModel):
@@ -43,11 +48,17 @@ class DatasourceCredentialDeletePayload(BaseModel):
class DatasourceCredentialUpdatePayload(BaseModel):
credential_id: str
name: str | None = Field(default=None, max_length=100)
credentials: dict[str, Any] | None = Field(default=None)
credentials: dict[str, Any] | None = Field(
default=None,
description="Plugin-defined credential parameters. The schema is declared by the datasource provider.",
)
class DatasourceCustomClientPayload(BaseModel):
client_params: dict[str, Any] | None = Field(default=None)
client_params: dict[str, Any] | None = Field(
default=None,
description="Plugin-defined OAuth client parameters. The schema is declared by the datasource provider.",
)
enable_oauth_custom_client: bool | None = None
@@ -71,8 +82,48 @@ class DatasourceOAuthCallbackQuery(BaseModel):
context_id: str | None = Field(default=None, description="OAuth proxy context ID")
class DatasourceCredentialsResponse(ResponseModel):
result: Any
class DatasourceCredentialResponse(ResponseModel):
credential: dict[str, Any] = Field(
description="Obfuscated plugin-defined credential parameters from the datasource provider."
)
type: str
name: str
avatar_url: str | None
id: str
is_default: bool
class DatasourceCredentialListResponse(ResponseModel):
result: list[DatasourceCredentialResponse]
class DatasourceOAuthSchemaResponse(ResponseModel):
client_schema: list[ProviderConfig]
credentials_schema: list[ProviderConfig]
oauth_custom_client_params: dict[str, Any] | None = Field(
description="Masked plugin-defined OAuth client parameters, when configured for the tenant."
)
is_oauth_custom_client_enabled: bool
is_system_oauth_params_exists: bool
redirect_uri: str
class DatasourceProviderAuthResponse(ResponseModel):
author: str
provider: str
plugin_id: str
plugin_unique_identifier: str
icon: str
name: str
label: I18nObject
description: I18nObject
credential_schema: list[ProviderConfig]
oauth_schema: DatasourceOAuthSchemaResponse | None
credentials_list: list[DatasourceCredentialResponse]
class DatasourceProviderAuthListResponse(ResponseModel):
result: list[DatasourceProviderAuthResponse]
register_schema_models(
@@ -88,9 +139,9 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
DatasourceCredentialsResponse,
DatasourceCredentialListResponse,
DatasourceProviderAuthListResponse,
PluginOAuthAuthorizationUrlResponse,
RedirectResponse,
SimpleResultResponse,
)
@@ -100,7 +151,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
@console_ns.doc(params=query_params_from_model(DatasourceOAuthAuthorizationQuery))
@console_ns.response(
200,
"Authorization URL retrieved successfully",
"Datasource OAuth authorization URL generated successfully",
console_ns.models[PluginOAuthAuthorizationUrlResponse.__name__],
)
@setup_required
@@ -140,7 +191,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
redirect_uri=redirect_uri,
system_credentials=oauth_config,
)
response = make_response(jsonable_encoder(authorization_url_response))
# response-contract:ignore cookie-bearing Flask response
response = make_response(dump_response(PluginOAuthAuthorizationUrlResponse, authorization_url_response))
response.set_cookie(
"context_id",
context_id,
@@ -154,11 +206,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
@console_ns.route("/oauth/plugin/<path:provider_id>/datasource/callback")
class DatasourceOAuthCallback(Resource):
@console_ns.doc(params=query_params_from_model(DatasourceOAuthCallbackQuery))
@console_ns.response(
302,
"Redirect to console OAuth callback page",
console_ns.models[RedirectResponse.__name__],
)
# response-contract:ignore redirect response
@console_ns.response(302, "Redirect to OAuth callback page")
@setup_required
def get(self, provider_id: str):
context_id = request.cookies.get("context_id") or request.args.get("context_id")
@@ -217,7 +266,9 @@ class DatasourceOAuthCallback(Resource):
@console_ns.route("/auth/plugin/datasource/<path:provider_id>")
class DatasourceAuth(Resource):
@console_ns.expect(console_ns.models[DatasourceCredentialPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
200, "Datasource credential created successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@@ -238,12 +289,16 @@ class DatasourceAuth(Resource):
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.response(
200,
"Datasource credentials retrieved successfully",
console_ns.models[DatasourceCredentialListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, user: Account, provider_id: str):
@@ -255,8 +310,9 @@ class DatasourceAuth(Resource):
provider=datasource_provider_id.provider_name,
plugin_id=datasource_provider_id.plugin_id,
user=user,
session=db.session(),
)
return {"result": datasources}, 200
return dump_response(DatasourceCredentialListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/delete")
@@ -281,14 +337,17 @@ class DatasourceAuthDeleteApi(Resource):
auth_id=payload.credential_id,
provider=provider_name,
plugin_id=plugin_id,
session=db.session(),
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update")
class DatasourceAuthUpdateApi(Resource):
@console_ns.expect(console_ns.models[DatasourceCredentialUpdatePayload.__name__])
@console_ns.response(201, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
201, "Datasource credential updated successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@@ -308,39 +367,53 @@ class DatasourceAuthUpdateApi(Resource):
credentials=payload.credentials or {},
name=payload.name,
)
return {"result": "success"}, 201
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
@console_ns.route("/auth/plugin/datasource/list")
class DatasourceAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@console_ns.response(
200,
"Datasource credentials retrieved successfully",
console_ns.models[DatasourceProviderAuthListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str):
datasource_provider_service = DatasourceProviderService()
datasources = datasource_provider_service.get_all_datasource_credentials(tenant_id=current_tenant_id)
return {"result": jsonable_encoder(datasources)}, 200
datasources = datasource_provider_service.get_all_datasource_credentials(
tenant_id=current_tenant_id, session=db.session()
)
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/default-list")
class DatasourceHardCodeAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@console_ns.response(
200,
"Default datasource credentials retrieved successfully",
console_ns.models[DatasourceProviderAuthListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str):
datasource_provider_service = DatasourceProviderService()
datasources = datasource_provider_service.get_hard_code_datasource_credentials(tenant_id=current_tenant_id)
return {"result": jsonable_encoder(datasources)}, 200
datasources = datasource_provider_service.get_hard_code_datasource_credentials(
tenant_id=current_tenant_id, session=db.session()
)
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/custom-client")
class DatasourceAuthOauthCustomClient(Resource):
@console_ns.expect(console_ns.models[DatasourceCustomClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
200, "Datasource OAuth custom client saved successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@@ -357,7 +430,7 @@ class DatasourceAuthOauthCustomClient(Resource):
client_params=payload.client_params or {},
enabled=payload.enable_oauth_custom_client or False,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@setup_required
@login_required
@@ -371,7 +444,7 @@ class DatasourceAuthOauthCustomClient(Resource):
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/default")
@@ -393,7 +466,7 @@ class DatasourceAuthDefaultApi(Resource):
datasource_provider_id=datasource_provider_id,
credential_id=payload.id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update-name")
@@ -416,4 +489,4 @@ class DatasourceUpdateProviderNameApi(Resource):
name=payload.name,
credential_id=payload.credential_id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -3,12 +3,13 @@ from typing import Any
from flask_restx import ( # type: ignore
Resource, # type: ignore
)
from pydantic import BaseModel, RootModel
from pydantic import BaseModel
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import register_schema_models
from controllers.console import console_ns
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from extensions.ext_database import db
from libs.login import login_required
from models import Account
from models.dataset import Pipeline
@@ -21,18 +22,13 @@ class Parser(BaseModel):
credential_id: str | None = None
class DataSourceContentPreviewResponse(RootModel[Any]):
root: Any
register_schema_models(console_ns, Parser)
register_response_schema_models(console_ns, DataSourceContentPreviewResponse)
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/datasource/nodes/<string:node_id>/preview")
class DataSourceContentPreviewApi(Resource):
@console_ns.expect(console_ns.models[Parser.__name__])
@console_ns.response(200, "Success", console_ns.models[DataSourceContentPreviewResponse.__name__])
@console_ns.response(200, "Success")
@setup_required
@login_required
@account_initialization_required
@@ -46,7 +42,7 @@ class DataSourceContentPreviewApi(Resource):
inputs = args.inputs
datasource_type = args.datasource_type
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
preview_content = rag_pipeline_service.run_datasource_node_preview(
pipeline=pipeline,
node_id=node_id,
@@ -108,7 +108,10 @@ class PipelineTemplateListApi(Resource):
query = PipelineTemplateListQuery.model_validate(request.args.to_dict(flat=True))
# get pipeline templates
pipeline_templates = RagPipelineService.get_pipeline_templates(
session, query.type, query.language, current_tenant_id
type=query.type,
language=query.language,
current_tenant_id=current_tenant_id,
session=session,
)
return dump_response(PipelineTemplateListResponse, pipeline_templates), 200
@@ -124,8 +127,11 @@ class PipelineTemplateDetailApi(Resource):
@with_session
def get(self, session: Session, template_id: str) -> JsonResponseWithStatus:
query = PipelineTemplateDetailQuery.model_validate(request.args.to_dict(flat=True))
rag_pipeline_service = RagPipelineService()
pipeline_template = rag_pipeline_service.get_pipeline_template_detail(session, template_id, query.type)
pipeline_template = RagPipelineService.get_pipeline_template_detail(
template_id,
type=query.type,
session=session,
)
if pipeline_template is None:
raise NotFound("Pipeline template not found from upstream service.")
return dump_response(PipelineTemplateDetailResponse, pipeline_template), 200
@@ -145,7 +151,7 @@ class CustomizedPipelineTemplateApi(Resource):
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(payload.model_dump())
RagPipelineService.update_customized_pipeline_template(
template_id, pipeline_template_info, current_user, current_tenant_id
template_id, pipeline_template_info, current_user, current_tenant_id, session=db.session()
)
return "", 204
@@ -156,7 +162,7 @@ class CustomizedPipelineTemplateApi(Resource):
@enterprise_license_required
@with_current_tenant_id
def delete(self, current_tenant_id: str, template_id: str) -> tuple[str, int]:
RagPipelineService.delete_customized_pipeline_template(template_id, current_tenant_id)
RagPipelineService.delete_customized_pipeline_template(template_id, current_tenant_id, session=db.session())
return "", 204
@setup_required
@@ -188,8 +194,8 @@ class PublishCustomizedPipelineTemplateApi(Resource):
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, pipeline_id: str) -> tuple[str, int]:
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
rag_pipeline_service.publish_customized_pipeline_template(
pipeline_id, payload.model_dump(), current_user, current_tenant_id
pipeline_id, payload.model_dump(), current_user, current_tenant_id, session=db.session()
)
return "", 204
@@ -65,7 +65,7 @@ class CreateRagPipelineDatasetApi(Resource):
yaml_content=payload.yaml_content,
)
try:
rag_pipeline_dsl_service = RagPipelineDslService(db.session)
rag_pipeline_dsl_service = RagPipelineDslService(db.session())
import_info = rag_pipeline_dsl_service.create_rag_pipeline_dataset(
tenant_id=current_tenant_id,
rag_pipeline_dataset_create_entity=rag_pipeline_dataset_create_entity,
@@ -75,7 +75,7 @@ class CreateRagPipelineDatasetApi(Resource):
current_tenant_id,
import_info["dataset_id"],
rag_pipeline_dataset_create_entity.partial_member_list,
db.session,
db.session(),
)
db.session.commit()
except services.errors.dataset.DatasetNameDuplicateError:
@@ -110,6 +110,6 @@ class CreateEmptyRagPipelineDatasetApi(Resource):
permission=DatasetPermissionEnum.ONLY_ME,
partial_member_list=None,
),
session=db.session,
session=db.session(),
)
return dump_response(DatasetDetailResponse, dataset), 201
@@ -98,7 +98,7 @@ class RagPipelineVariableCollectionApi(Resource):
query = PaginationQuery.model_validate(request.args.to_dict())
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow_exist = rag_pipeline_service.is_workflow_exist(pipeline=pipeline)
if not workflow_exist:
raise DraftWorkflowNotExist()
@@ -290,7 +290,7 @@ class RagPipelineVariableResetApi(Resource):
session=db.session(),
)
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
draft_workflow = rag_pipeline_service.get_draft_workflow(pipeline=pipeline)
if draft_workflow is None:
raise NotFoundError(
@@ -347,7 +347,7 @@ class RagPipelineEnvironmentVariableCollectionApi(Resource):
Get draft workflow
"""
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow = rag_pipeline_service.get_draft_workflow(pipeline=pipeline)
if workflow is None:
raise DraftWorkflowNotExist()
@@ -197,7 +197,7 @@ class DraftRagPipelineApi(Resource):
Get draft rag pipeline's workflow
"""
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow = rag_pipeline_service.get_draft_workflow(pipeline=pipeline)
if not workflow:
@@ -231,7 +231,7 @@ class DraftRagPipelineApi(Resource):
return {"message": "Invalid JSON data"}, 400
else:
abort(415)
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
try:
environment_variables_list = Workflow.normalize_environment_variable_mappings(
@@ -283,7 +283,7 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
try:
response = PipelineGenerateService.generate_single_iteration(
pipeline=pipeline, user=current_user, node_id=node_id, args=args, streaming=True
pipeline=pipeline, user=current_user, node_id=node_id, args=args, session=db.session(), streaming=True
)
return helper.compact_generate_response(response)
@@ -318,7 +318,7 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
try:
response = PipelineGenerateService.generate_single_loop(
pipeline=pipeline, user=current_user, node_id=node_id, args=args, streaming=True
pipeline=pipeline, user=current_user, node_id=node_id, args=args, session=db.session(), streaming=True
)
return helper.compact_generate_response(response)
@@ -419,7 +419,7 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
PipelineGenerator.convert_to_event_stream(
rag_pipeline_service.run_datasource_workflow_node(
@@ -452,7 +452,7 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
PipelineGenerator.convert_to_event_stream(
rag_pipeline_service.run_datasource_workflow_node(
@@ -490,7 +490,7 @@ class RagPipelineDraftNodeRunApi(Resource):
payload = NodeRunRequiredPayload.model_validate(console_ns.payload or {})
inputs = payload.inputs
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.run_draft_workflow_node(
pipeline=pipeline, node_id=node_id, user_inputs=inputs, account=current_user
)
@@ -543,7 +543,7 @@ class PublishedRagPipelineApi(Resource):
if not pipeline.is_published:
return None
# fetch published workflow by pipeline
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow = rag_pipeline_service.get_published_workflow(pipeline=pipeline)
# return workflow, if not found, return None
@@ -564,9 +564,9 @@ class PublishedRagPipelineApi(Resource):
"""
Publish workflow
"""
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow = rag_pipeline_service.publish_workflow(
session=db.session, # type: ignore[reportArgumentType,arg-type]
session=db.session(),
pipeline=pipeline,
account=current_user,
)
@@ -599,7 +599,7 @@ class DefaultRagPipelineBlockConfigsApi(Resource):
Get default block config
"""
# Get default block configs
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
return rag_pipeline_service.get_default_block_configs()
@@ -631,7 +631,7 @@ class DefaultRagPipelineBlockConfigApi(Resource):
raise ValueError("Invalid filters")
# Get default block configs
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
return rag_pipeline_service.get_default_block_config(node_type=block_type, filters=filters)
@@ -666,7 +666,7 @@ class PublishedAllRagPipelineApi(Resource):
if user_id != current_user.id:
raise Forbidden()
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
with sessionmaker(db.engine).begin() as session:
workflows, has_more = rag_pipeline_service.get_all_published_workflow(
session=session,
@@ -698,7 +698,7 @@ class RagPipelineDraftWorkflowRestoreApi(Resource):
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
try:
workflow = rag_pipeline_service.restore_published_workflow_to_draft(
@@ -743,7 +743,7 @@ class RagPipelineByIdApi(Resource):
if not update_data:
return {"message": "No valid fields to update"}, 400
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow_ref = WorkflowRefService.create_pipeline_workflow_ref(pipeline, workflow_id)
# Create a session and manage the transaction
@@ -809,7 +809,7 @@ class PublishedRagPipelineSecondStepApi(Resource):
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
"variables": variables,
@@ -832,7 +832,7 @@ class PublishedRagPipelineFirstStepApi(Resource):
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
"variables": variables,
@@ -855,7 +855,7 @@ class DraftRagPipelineFirstStepApi(Resource):
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
return {
"variables": variables,
@@ -879,7 +879,7 @@ class DraftRagPipelineSecondStepApi(Resource):
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
return {
"variables": variables,
@@ -913,7 +913,7 @@ class RagPipelineWorkflowRunListApi(Resource):
"limit": query.limit,
}
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
result = rag_pipeline_service.get_rag_pipeline_paginate_workflow_runs(pipeline=pipeline, args=args)
return WorkflowRunPaginationResponse.model_validate(result, from_attributes=True).model_dump(mode="json")
@@ -936,7 +936,7 @@ class RagPipelineWorkflowRunDetailApi(Resource):
"""
run_id_str = str(run_id)
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow_run = rag_pipeline_service.get_rag_pipeline_workflow_run(pipeline=pipeline, run_id=run_id_str)
if workflow_run is None:
raise NotFound("Workflow run not found")
@@ -962,7 +962,7 @@ class RagPipelineWorkflowRunNodeExecutionListApi(Resource):
"""
run_id_str = str(run_id)
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
user = cast("Account | EndUser", current_user)
node_executions = rag_pipeline_service.get_rag_pipeline_workflow_run_node_executions(
pipeline=pipeline,
@@ -998,7 +998,7 @@ class RagPipelineWorkflowLastRunApi(Resource):
@account_initialization_required
@get_rag_pipeline
def get(self, pipeline: Pipeline, node_id: str):
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow = rag_pipeline_service.get_draft_workflow(pipeline=pipeline)
if not workflow:
raise NotFound("Workflow not found")
@@ -1051,7 +1051,7 @@ class RagPipelineDatasourceVariableApi(Resource):
"""
args = DatasourceVariablesPayload.model_validate(console_ns.payload or {}).model_dump()
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.set_datasource_variables(
pipeline=pipeline,
args=args,
@@ -1074,6 +1074,6 @@ class RagPipelineRecommendedPluginApi(Resource):
def get(self, current_tenant_id: str, current_user: Account):
query = RagPipelineRecommendedPluginQuery.model_validate(request.args.to_dict())
rag_pipeline_service = RagPipelineService()
rag_pipeline_service = RagPipelineService(db.session())
recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type, current_user, current_tenant_id)
return recommended_plugins
+1 -1
View File
@@ -113,7 +113,7 @@ class ChatTextApi(InstalledAppResource):
response = AudioService.transcript_tts(
app_model=app_model,
session=db.session,
session=db.session(),
text=text,
voice=voice,
message_ref=message_ref,
@@ -111,7 +111,7 @@ class ConversationApi(InstalledAppResource):
conversation_id = str(c_id)
try:
ConversationService.delete(app_model, conversation_id, current_user)
ConversationService.delete(app_model, conversation_id, current_user, session=db.session())
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@@ -140,7 +140,7 @@ class ConversationRenameApi(InstalledAppResource):
try:
conversation = ConversationService.rename(
app_model, conversation_id, current_user, payload.name, payload.auto_generate
app_model, conversation_id, current_user, payload.name, payload.auto_generate, session=db.session()
)
return (
TypeAdapter(SimpleConversation)
@@ -169,7 +169,7 @@ class ConversationPinApi(InstalledAppResource):
conversation_id = str(c_id)
try:
WebConversationService.pin(app_model, conversation_id, current_user)
WebConversationService.pin(app_model, conversation_id, current_user, db.session())
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@@ -192,6 +192,6 @@ class ConversationUnPinApi(InstalledAppResource):
raise NotChatAppError()
conversation_id = str(c_id)
WebConversationService.unpin(app_model, conversation_id, current_user)
WebConversationService.unpin(app_model, conversation_id, current_user, db.session())
return ResultResponse(result="success").model_dump(mode="json")
@@ -181,7 +181,7 @@ class InstalledAppsListApi(Resource):
if current_user.current_tenant is None:
raise ValueError("current_user.current_tenant must not be None")
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant, session=db.session)
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant, session=db.session())
installed_app_list: list[dict[str, Any]] = []
for installed_app, app_model in installed_apps:
installed_app_list.append(
+8 -1
View File
@@ -27,6 +27,7 @@ from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user
from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from extensions.ext_database import db
from fields.conversation_fields import ResultResponse
from fields.message_fields import (
ExploreMessageInfiniteScrollPagination,
@@ -91,6 +92,7 @@ class MessageListApi(InstalledAppResource):
args.conversation_id,
args.first_id or None,
args.limit,
session=db.session(),
)
adapter = TypeAdapter(ExploreMessageListItem)
items = [adapter.validate_python(message, from_attributes=True) for message in pagination.data]
@@ -129,6 +131,7 @@ class MessageFeedbackApi(InstalledAppResource):
user=current_user,
rating=FeedbackRating(payload.rating) if payload.rating else None,
content=payload.content,
session=db.session(),
)
except MessageNotExistsError:
raise NotFound("Message Not Exists.")
@@ -207,7 +210,11 @@ class MessageSuggestedQuestionApi(InstalledAppResource):
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model, user=current_user, message_id=message_id_str, invoke_from=InvokeFrom.EXPLORE
app_model=app_model,
user=current_user,
message_id=message_id_str,
invoke_from=InvokeFrom.EXPLORE,
session=db.session(),
)
except MessageNotExistsError:
raise NotFound("Message not found")
+2 -1
View File
@@ -8,6 +8,7 @@ from controllers.console import console_ns
from controllers.console.app.error import AppUnavailableError
from controllers.console.explore.wraps import InstalledAppResource
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from extensions.ext_database import db
from models.model import AppMode, InstalledApp
from services.app_service import AppService
@@ -64,4 +65,4 @@ class ExploreAppMetaApi(InstalledAppResource):
app_model = installed_app.app
if not app_model:
raise ValueError("App not found")
return AppService().get_app_meta(app_model)
return AppService().get_app_meta(app_model, session=db.session())
@@ -120,7 +120,7 @@ class RecommendedAppListApi(Resource):
language_prefix = _resolve_language(args.language, current_user)
return RecommendedAppListResponse.model_validate(
RecommendedAppService.get_recommended_apps_and_categories(db.session, language_prefix),
RecommendedAppService.get_recommended_apps_and_categories(db.session(), language_prefix),
from_attributes=True,
).model_dump(mode="json")
@@ -137,7 +137,7 @@ class LearnDifyAppListApi(Resource):
language_prefix = _resolve_language(args.language, current_user)
return LearnDifyAppListResponse.model_validate(
RecommendedAppService.get_learn_dify_apps(db.session, language_prefix),
RecommendedAppService.get_learn_dify_apps(db.session(), language_prefix),
from_attributes=True,
).model_dump(mode="json")
@@ -148,4 +148,4 @@ class RecommendedAppApi(Resource):
@login_required
@account_initialization_required
def get(self, app_id: UUID):
return RecommendedAppService.get_recommend_app_detail(db.session, str(app_id))
return RecommendedAppService.get_recommend_app_detail(db.session(), str(app_id))
+17 -12
View File
@@ -13,7 +13,6 @@ import services
from controllers.common.fields import (
AudioBinaryResponse,
AudioTranscriptResponse,
GeneratedAppResponse,
SimpleResultResponse,
)
from controllers.common.fields import Parameters as ParametersResponse
@@ -391,7 +390,6 @@ register_response_schema_models(
ParametersResponse,
AudioBinaryResponse,
AudioTranscriptResponse,
GeneratedAppResponse,
SimpleResultResponse,
SiteResponse,
SuggestedQuestionsResponse,
@@ -406,7 +404,7 @@ simple_account_model = console_ns.models[TrialSimpleAccount.__name__]
class TrialAppWorkflowRunApi(TrialAppResource):
@trial_feature_enable
@console_ns.expect(console_ns.models[WorkflowRunRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, trial_app):
@@ -433,7 +431,8 @@ class TrialAppWorkflowRunApi(TrialAppResource):
invoke_from=InvokeFrom.EXPLORE,
streaming=True,
)
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
RecommendedAppService.add_trial_app_record(session, app_id, user_id)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -478,7 +477,7 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource):
class TrialChatApi(TrialAppResource):
@console_ns.expect(console_ns.models[ChatRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(200, "Success")
@trial_feature_enable
@with_current_user
@with_session
@@ -512,7 +511,8 @@ class TrialChatApi(TrialAppResource):
invoke_from=InvokeFrom.EXPLORE,
streaming=True,
)
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
RecommendedAppService.add_trial_app_record(session, app_id, user_id)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@@ -551,7 +551,11 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource):
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model, user=current_user, message_id=message_id, invoke_from=InvokeFrom.EXPLORE
app_model=app_model,
user=current_user,
message_id=message_id,
invoke_from=InvokeFrom.EXPLORE,
session=db.session(),
)
except MessageNotExistsError:
raise NotFound("Message not found")
@@ -589,7 +593,7 @@ class TrialChatAudioApi(TrialAppResource):
user_id = current_user.id
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=None)
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
RecommendedAppService.add_trial_app_record(db.session(), app_id, user_id)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logger.exception("App model config broken.")
@@ -645,12 +649,12 @@ class TrialChatTextApi(TrialAppResource):
response = AudioService.transcript_tts(
app_model=app_model,
session=db.session,
session=db.session(),
text=text,
voice=voice,
message_ref=message_ref,
)
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
RecommendedAppService.add_trial_app_record(db.session(), app_id, user_id)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logger.exception("App model config broken.")
@@ -680,7 +684,7 @@ class TrialChatTextApi(TrialAppResource):
class TrialCompletionApi(TrialAppResource):
@console_ns.expect(console_ns.models[CompletionRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(200, "Success")
@trial_feature_enable
@with_current_user
@with_session
@@ -709,7 +713,8 @@ class TrialCompletionApi(TrialAppResource):
streaming=streaming,
)
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
RecommendedAppService.add_trial_app_record(session, app_id, user_id)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
+1 -1
View File
@@ -50,7 +50,7 @@ def get_init_status() -> InitStatusResponse:
@only_edition_self_hosted
def validate_init_password(payload: InitValidatePayload) -> InitValidateResponse:
"""Validate initialization password."""
tenant_count = TenantService.get_tenant_count(session=db.session)
tenant_count = TenantService.get_tenant_count(session=db.session())
if tenant_count > 0:
raise AlreadySetupError()
+2 -2
View File
@@ -79,7 +79,7 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
if get_setup_status():
raise AlreadySetupError()
tenant_count = TenantService.get_tenant_count(session=db.session)
tenant_count = TenantService.get_tenant_count(session=db.session())
if tenant_count > 0:
raise AlreadySetupError()
@@ -94,7 +94,7 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
password=payload.password,
ip_address=extract_remote_ip(request),
language=payload.language,
session=db.session,
session=db.session(),
)
return SetupResponse(result="success")
+2 -2
View File
@@ -44,7 +44,7 @@ def socket_connect(sid, environ, auth):
return False
with sio.app.app_context():
user = AccountService.load_logged_in_account(account_id=user_id, session=db.session)
user = AccountService.load_logged_in_account(account_id=user_id, session=db.session())
if not user:
logging.warning("Socket connect rejected: user not found (user_id=%s, sid=%s)", user_id, sid)
return False
@@ -69,7 +69,7 @@ def handle_user_connect(sid, data):
if not workflow_id:
return {"msg": "workflow_id is required"}, 400
result = collaboration_service.authorize_and_join_workflow_room(workflow_id, sid)
result = collaboration_service.authorize_and_join_workflow_room(workflow_id, sid, session=db.session())
if not result:
return {"msg": "unauthorized"}, 401
+6 -6
View File
@@ -154,7 +154,7 @@ class TagListApi(Resource):
payload = TagBasePayload.model_validate(console_ns.payload or {})
_enforce_snippet_tag_rbac_if_needed(payload.type)
tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type), db.session)
tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type), db.session())
return dump_response(TagResponse, {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0}), 200
@@ -175,9 +175,9 @@ class TagUpdateDeleteApi(Resource):
payload = TagUpdateRequestPayload.model_validate(console_ns.payload or {})
_enforce_snippet_tag_rbac_by_tag_id(tag_id_str)
tag = TagService.update_tags(UpdateTagPayload(name=payload.name), tag_id_str, db.session)
tag = TagService.update_tags(UpdateTagPayload(name=payload.name), tag_id_str, db.session())
binding_count = TagService.get_tag_binding_count(tag_id_str, db.session)
binding_count = TagService.get_tag_binding_count(tag_id_str, db.session())
return (
dump_response(
@@ -196,7 +196,7 @@ class TagUpdateDeleteApi(Resource):
tag_id_str = str(tag_id)
_enforce_snippet_tag_rbac_by_tag_id(tag_id_str)
TagService.delete_tag(tag_id_str, db.session)
TagService.delete_tag(tag_id_str, db.session())
return "", 204
@@ -223,7 +223,7 @@ def _create_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]:
target_id=payload.target_id,
type=payload.type,
),
db.session,
db.session(),
)
return {"result": "success"}, 200
@@ -239,7 +239,7 @@ def _remove_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]:
target_id=payload.target_id,
type=payload.type,
),
db.session,
db.session(),
)
return {"result": "success"}, 200
@@ -8,7 +8,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from extensions.ext_database import db
from libs.login import current_account_with_tenant
from models.account import TenantPluginPermission
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
def plugin_permission_required(
@@ -40,22 +40,22 @@ def plugin_permission_required(
if install_required:
match permission.install_permission:
case TenantPluginPermission.InstallPermission.NOBODY:
case TenantPluginInstallPermission.NOBODY:
raise Forbidden()
case TenantPluginPermission.InstallPermission.ADMINS:
case TenantPluginInstallPermission.ADMINS:
if not user.is_admin_or_owner:
raise Forbidden()
case TenantPluginPermission.InstallPermission.EVERYONE:
case TenantPluginInstallPermission.EVERYONE:
pass
if debug_required:
match permission.debug_permission:
case TenantPluginPermission.DebugPermission.NOBODY:
case TenantPluginDebugPermission.NOBODY:
raise Forbidden()
case TenantPluginPermission.DebugPermission.ADMINS:
case TenantPluginDebugPermission.ADMINS:
if not user.is_admin_or_owner:
raise Forbidden()
case TenantPluginPermission.DebugPermission.EVERYONE:
case TenantPluginDebugPermission.EVERYONE:
pass
return view(*args, **kwargs)
+84 -94
View File
@@ -1,12 +1,13 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from http import HTTPStatus
from typing import Literal
import pytz
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel, field_validator, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy import select
from werkzeug.exceptions import NotFound
@@ -47,7 +48,7 @@ from controllers.console.wraps import (
)
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.member_fields import Account as AccountResponse
from fields.member_fields import AccountResponse
from graphon.file import helpers as file_helpers
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, dump_response, extract_remote_ip, timezone, to_timestamp
@@ -194,10 +195,6 @@ register_schema_models(
)
def _serialize_account(account) -> dict[str, Any]:
return AccountResponse.model_validate(account, from_attributes=True).model_dump(mode="json")
class AccountIntegrateResponse(ResponseModel):
provider: str
created_at: int | None = None
@@ -236,23 +233,15 @@ class EducationAutocompleteResponse(ResponseModel):
has_next: bool | None = None
class EducationActivateResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(
console_ns,
AccountIntegrateResponse,
AccountIntegrateListResponse,
EducationVerifyResponse,
EducationStatusResponse,
EducationAutocompleteResponse,
)
register_response_schema_models(
console_ns,
AccountResponse,
AccountIntegrateResponse,
AccountIntegrateListResponse,
AvatarUrlResponse,
EducationActivateResponse,
EducationVerifyResponse,
EducationStatusResponse,
EducationAutocompleteResponse,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
@@ -262,7 +251,7 @@ register_response_schema_models(
@console_ns.route("/account/init")
class AccountInitApi(Resource):
@console_ns.expect(console_ns.models[AccountInitPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@with_current_user
@@ -302,7 +291,7 @@ class AccountInitApi(Resource):
account.initialized_at = naive_utc_now()
db.session.commit()
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/profile")
@@ -310,11 +299,11 @@ class AccountProfileApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@enterprise_license_required
@with_current_user
def get(self, current_user: Account):
return _serialize_account(current_user)
return dump_response(AccountResponse, current_user)
@console_ns.route("/account/name")
@@ -323,14 +312,14 @@ class AccountNameApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountNamePayload.model_validate(payload)
updated_account = AccountService.update_account(current_user, session=db.session, name=args.name)
updated_account = AccountService.update_account(current_user, session=db.session(), name=args.name)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/avatar")
@@ -338,7 +327,7 @@ class AccountAvatarApi(Resource):
@console_ns.doc("get_account_avatar")
@console_ns.doc(description="Get account avatar url")
@console_ns.doc(params=query_params_from_model(AccountAvatarQuery))
@console_ns.response(200, "Success", console_ns.models[AvatarUrlResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AvatarUrlResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -349,7 +338,7 @@ class AccountAvatarApi(Resource):
avatar = args.avatar
if avatar.startswith(("http://", "https://")):
return dump_response(AvatarUrlResponse, {"avatar_url": avatar})
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")
upload_file = db.session.scalar(select(UploadFile).where(UploadFile.id == avatar).limit(1))
if upload_file is None:
@@ -362,21 +351,21 @@ class AccountAvatarApi(Resource):
raise NotFound("Avatar file not found")
avatar_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return dump_response(AvatarUrlResponse, {"avatar_url": avatar_url})
return AvatarUrlResponse(avatar_url=avatar_url).model_dump(mode="json")
@console_ns.expect(console_ns.models[AccountAvatarPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountAvatarPayload.model_validate(payload)
updated_account = AccountService.update_account(current_user, session=db.session, avatar=args.avatar)
updated_account = AccountService.update_account(current_user, session=db.session(), avatar=args.avatar)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/interface-language")
@@ -385,17 +374,17 @@ class AccountInterfaceLanguageApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountInterfaceLanguagePayload.model_validate(payload)
updated_account = AccountService.update_account(
current_user, session=db.session, interface_language=args.interface_language
current_user, session=db.session(), interface_language=args.interface_language
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/interface-theme")
@@ -404,17 +393,17 @@ class AccountInterfaceThemeApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountInterfaceThemePayload.model_validate(payload)
updated_account = AccountService.update_account(
current_user, session=db.session, interface_theme=args.interface_theme
current_user, session=db.session(), interface_theme=args.interface_theme
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/timezone")
@@ -423,15 +412,15 @@ class AccountTimezoneApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountTimezonePayload.model_validate(payload)
updated_account = AccountService.update_account(current_user, session=db.session, timezone=args.timezone)
updated_account = AccountService.update_account(current_user, session=db.session(), timezone=args.timezone)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/password")
@@ -440,7 +429,7 @@ class AccountPasswordApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@@ -448,11 +437,11 @@ class AccountPasswordApi(Resource):
try:
assert args.password is not None
AccountService.update_account_password(current_user, args.password, args.new_password, session=db.session)
AccountService.update_account_password(current_user, args.password, args.new_password, session=db.session())
except ServiceCurrentPasswordIncorrectError:
raise CurrentPasswordIncorrectError()
return _serialize_account(current_user)
return dump_response(AccountResponse, current_user)
@console_ns.route("/account/integrates")
@@ -460,7 +449,7 @@ class AccountIntegrateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
@with_current_user
def get(self, account: Account):
account_integrates = db.session.scalars(
@@ -471,33 +460,29 @@ class AccountIntegrateApi(Resource):
oauth_base_path = "/console/api/oauth/login"
providers = ["github", "google"]
integrate_data = []
integrate_data: list[AccountIntegrateResponse] = []
for provider in providers:
existing_integrate = next((ai for ai in account_integrates if ai.provider == provider), None)
if existing_integrate:
integrate_data.append(
{
"id": existing_integrate.id,
"provider": provider,
"created_at": existing_integrate.created_at,
"is_bound": True,
"link": None,
}
AccountIntegrateResponse(
provider=provider,
created_at=to_timestamp(existing_integrate.created_at),
is_bound=True,
link=None,
)
)
else:
integrate_data.append(
{
"id": None,
"provider": provider,
"created_at": None,
"is_bound": False,
"link": f"{base_url}{oauth_base_path}/{provider}",
}
AccountIntegrateResponse(
provider=provider,
created_at=None,
is_bound=False,
link=f"{base_url}{oauth_base_path}/{provider}",
)
)
return AccountIntegrateListResponse(
data=[AccountIntegrateResponse.model_validate(item) for item in integrate_data]
).model_dump(mode="json")
return AccountIntegrateListResponse(data=integrate_data).model_dump(mode="json")
@console_ns.route("/account/delete/verify")
@@ -505,19 +490,19 @@ class AccountDeleteVerifyApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@with_current_user
def get(self, account: Account):
token, code = AccountService.generate_account_deletion_verification_code(account)
AccountService.send_account_deletion_verification_email(account, code)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/account/delete")
class AccountDeleteApi(Resource):
@console_ns.expect(console_ns.models[AccountDeletePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -529,15 +514,15 @@ class AccountDeleteApi(Resource):
if not AccountService.verify_account_deletion_code(args.token, args.code):
raise InvalidAccountDeletionCodeError()
AccountService.delete_account(account)
AccountService.delete_account(account, session=db.session())
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/delete/feedback")
class AccountDeleteUpdateFeedbackApi(Resource):
@console_ns.expect(console_ns.models[AccountDeletionFeedbackPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
def post(self):
payload = console_ns.payload or {}
@@ -545,7 +530,7 @@ class AccountDeleteUpdateFeedbackApi(Resource):
BillingService.update_account_deletion_feedback(args.email, args.feedback)
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/education/verify")
@@ -555,18 +540,19 @@ class EducationVerifyApi(Resource):
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationVerifyResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationVerifyResponse.__name__])
@with_current_user
def get(self, account: Account):
return EducationVerifyResponse.model_validate(
BillingService.EducationIdentity.verify(account.id, account.email) or {}
).model_dump(mode="json")
return dump_response(
EducationVerifyResponse, BillingService.EducationIdentity.verify(account.id, account.email) or {}
)
@console_ns.route("/account/education")
class EducationApi(Resource):
@console_ns.expect(console_ns.models[EducationActivatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EducationActivateResponse.__name__])
# response-contract:ignore billing-service activation payload; TODO: model education activation result.
@console_ns.response(HTTPStatus.OK, "Success")
@setup_required
@login_required
@account_initialization_required
@@ -577,21 +563,22 @@ class EducationApi(Resource):
payload = console_ns.payload or {}
args = EducationActivatePayload.model_validate(payload)
return BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
result = BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
return result
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationStatusResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationStatusResponse.__name__])
@with_current_user
def get(self, account: Account):
res = BillingService.EducationIdentity.status(account.id) or {}
# convert expire_at to UTC timestamp from isoformat
if res and "expire_at" in res:
res["expire_at"] = datetime.fromisoformat(res["expire_at"]).astimezone(pytz.utc)
return EducationStatusResponse.model_validate(res).model_dump(mode="json")
return dump_response(EducationStatusResponse, res)
@console_ns.route("/account/education/autocomplete")
@@ -602,20 +589,21 @@ class EducationAutoCompleteApi(Resource):
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
def get(self):
payload = request.args.to_dict(flat=True)
args = EducationAutocompleteQuery.model_validate(payload)
return EducationAutocompleteResponse.model_validate(
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {}
).model_dump(mode="json")
return dump_response(
EducationAutocompleteResponse,
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {},
)
@console_ns.route("/account/change-email")
class ChangeEmailSendEmailApi(Resource):
@console_ns.expect(console_ns.models[ChangeEmailSendPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@enable_change_email
@setup_required
@login_required
@@ -669,13 +657,13 @@ class ChangeEmailSendEmailApi(Resource):
language=language,
phase=send_phase,
)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/account/change-email/validity")
class ChangeEmailCheckApi(Resource):
@console_ns.expect(console_ns.models[ChangeEmailValidityPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
@enable_change_email
@setup_required
@login_required
@@ -716,7 +704,9 @@ class ChangeEmailCheckApi(Resource):
new_token = AccountService.generate_change_email_token(refreshed_token_data, current_user)
AccountService.reset_change_email_error_rate_limit(user_email)
return {"is_valid": True, "email": normalized_token_email, "token": new_token}
return VerificationTokenResponse(is_valid=True, email=normalized_token_email, token=new_token).model_dump(
mode="json"
)
@console_ns.route("/account/change-email/reset")
@@ -726,7 +716,7 @@ class ChangeEmailResetApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@@ -736,7 +726,7 @@ class ChangeEmailResetApi(Resource):
if AccountService.is_account_in_freeze(normalized_new_email):
raise AccountInFreezeError()
if not AccountService.check_email_unique(normalized_new_email, session=db.session):
if not AccountService.check_email_unique(normalized_new_email, session=db.session()):
raise EmailAlreadyInUseError()
reset_data = AccountService.get_change_email_data(args.token)
@@ -761,20 +751,20 @@ class ChangeEmailResetApi(Resource):
AccountService.revoke_change_email_token(args.token)
updated_account = AccountService.update_account_email(
current_user, email=normalized_new_email, session=db.session
current_user, email=normalized_new_email, session=db.session()
)
AccountService.send_change_email_completed_notify_email(
email=normalized_new_email,
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/change-email/check-email-unique")
class CheckEmailUnique(Resource):
@console_ns.expect(console_ns.models[CheckEmailUniquePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
def post(self):
payload = console_ns.payload or {}
@@ -782,6 +772,6 @@ class CheckEmailUnique(Resource):
normalized_email = args.email.lower()
if AccountService.is_account_in_freeze(normalized_email):
raise AccountInFreezeError()
if not AccountService.check_email_unique(normalized_email, session=db.session):
if not AccountService.check_email_unique(normalized_email, session=db.session()):
raise EmailAlreadyInUseError()
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
+193 -133
View File
@@ -6,13 +6,17 @@ verb-based aliases stay available as deprecated resources so OpenAPI metadata
marks only the legacy paths as deprecated.
"""
from datetime import datetime
from enum import StrEnum
from http import HTTPStatus
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.fields import SuccessResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
RBACPermission,
@@ -24,8 +28,14 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user_id,
)
from core.entities.parameter_entities import (
AppSelectorScope,
ModelSelectorScope,
ToolSelectorScope,
)
from core.entities.provider_entities import ProviderConfigType
from core.plugin.impl.exc import PluginPermissionDeniedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from fields.base import ResponseModel
from libs.login import login_required
from services.plugin.endpoint_service import EndpointService
@@ -40,14 +50,17 @@ class EndpointIdPayload(BaseModel):
endpoint_id: str
class EndpointUpdatePayload(BaseModel):
class EndpointSettingsPayload(BaseModel):
settings: dict[str, Any]
name: str = Field(min_length=1)
class LegacyEndpointUpdatePayload(EndpointIdPayload):
settings: dict[str, Any]
name: str = Field(min_length=1)
class EndpointUpdatePayload(EndpointSettingsPayload):
pass
class LegacyEndpointUpdatePayload(EndpointIdPayload, EndpointSettingsPayload):
pass
class EndpointListQuery(BaseModel):
@@ -59,98 +72,158 @@ class EndpointListForPluginQuery(EndpointListQuery):
plugin_id: str
class EndpointCreateResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderConfigScope(StrEnum):
ALL = AppSelectorScope.ALL.value
CHAT = AppSelectorScope.CHAT.value
WORKFLOW = AppSelectorScope.WORKFLOW.value
COMPLETION = AppSelectorScope.COMPLETION.value
LLM = ModelSelectorScope.LLM.value
TEXT_EMBEDDING = ModelSelectorScope.TEXT_EMBEDDING.value
RERANK = ModelSelectorScope.RERANK.value
TTS = ModelSelectorScope.TTS.value
SPEECH2TEXT = ModelSelectorScope.SPEECH2TEXT.value
MODERATION = ModelSelectorScope.MODERATION.value
VISION = ModelSelectorScope.VISION.value
CUSTOM = ToolSelectorScope.CUSTOM.value
BUILTIN = ToolSelectorScope.BUILTIN.value
class EndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class EndpointProviderConfigI18nResponse(ResponseModel):
en_US: str
zh_Hans: str | None = None
pt_BR: str | None = None
ja_JP: str | None = None
class PluginEndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class EndpointProviderConfigOptionResponse(ResponseModel):
value: str
label: EndpointProviderConfigI18nResponse
class EndpointDeleteResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderConfigResponse(ResponseModel):
type: ProviderConfigType
name: str
scope: EndpointProviderConfigScope | None = None
required: bool = False
default: int | str | float | bool | None = None
options: list[EndpointProviderConfigOptionResponse] | None = None
multiple: bool = False
label: EndpointProviderConfigI18nResponse | None = None
help: EndpointProviderConfigI18nResponse | None = None
url: str | None = None
placeholder: EndpointProviderConfigI18nResponse | None = None
class EndpointUpdateResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointDeclarationResponse(ResponseModel):
path: str
method: str
hidden: bool = False
class EndpointEnableResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderDeclarationResponse(ResponseModel):
settings: list[EndpointProviderConfigResponse] = Field(default_factory=list)
endpoints: list[EndpointDeclarationResponse] | None = Field(default_factory=list)
class EndpointDisableResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointListItemResponse(ResponseModel):
id: str
created_at: datetime
updated_at: datetime
tenant_id: str
plugin_id: str
settings: dict[str, Any]
expired_at: datetime
declaration: EndpointProviderDeclarationResponse = Field(default_factory=EndpointProviderDeclarationResponse)
name: str
enabled: bool
url: str
hook_id: str
class EndpointListResponse(ResponseModel):
endpoints: list[EndpointListItemResponse] = Field(description="Endpoint information")
register_schema_models(
console_ns,
EndpointCreatePayload,
EndpointIdPayload,
EndpointSettingsPayload,
EndpointUpdatePayload,
LegacyEndpointUpdatePayload,
EndpointListQuery,
EndpointListForPluginQuery,
EndpointCreateResponse,
)
register_response_schema_models(
console_ns,
SuccessResponse,
EndpointProviderConfigOptionResponse,
EndpointProviderConfigResponse,
EndpointDeclarationResponse,
EndpointProviderDeclarationResponse,
EndpointListItemResponse,
EndpointListResponse,
PluginEndpointListResponse,
EndpointDeleteResponse,
EndpointUpdateResponse,
EndpointEnableResponse,
EndpointDisableResponse,
)
def _create_endpoint(tenant_id: str, user_id: str) -> dict[str, bool]:
def _create_endpoint(tenant_id: str, user_id: str) -> bool:
"""Create a plugin endpoint for the injected workspace and user."""
args = EndpointCreatePayload.model_validate(console_ns.payload)
try:
return {
"success": EndpointService.create_endpoint(
tenant_id=tenant_id,
user_id=user_id,
plugin_unique_identifier=args.plugin_unique_identifier,
name=args.name,
settings=args.settings,
)
}
return EndpointService.create_endpoint(
tenant_id=tenant_id,
user_id=user_id,
plugin_unique_identifier=args.plugin_unique_identifier,
name=args.name,
settings=args.settings,
)
except PluginPermissionDeniedError as e:
raise ValueError(e.description) from e
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
"""Update a plugin endpoint identified by the canonical path parameter."""
args = EndpointUpdatePayload.model_validate(console_ns.payload)
return {
"success": EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
name=args.name,
settings=args.settings,
)
}
return EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
name=args.name,
settings=args.settings,
)
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
def _legacy_update_endpoint(tenant_id: str, user_id: str) -> bool:
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
return EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=args.endpoint_id,
name=args.name,
settings=args.settings,
)
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
"""Delete a plugin endpoint identified by the canonical path parameter."""
return {
"success": EndpointService.delete_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
)
}
return EndpointService.delete_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
)
def _delete_endpoint_from_payload(tenant_id: str, user_id: str) -> bool:
args = EndpointIdPayload.model_validate(console_ns.payload)
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
def _set_endpoint_enabled(tenant_id: str, user_id: str, *, enabled: bool) -> bool:
args = EndpointIdPayload.model_validate(console_ns.payload)
action = EndpointService.enable_endpoint if enabled else EndpointService.disable_endpoint
return action(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
@console_ns.route("/workspaces/current/endpoints")
@@ -161,11 +234,11 @@ class EndpointCollectionApi(Resource):
@console_ns.doc(description="Create a new plugin endpoint")
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint created successfully",
console_ns.models[EndpointCreateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -174,7 +247,7 @@ class EndpointCollectionApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/create")
@@ -190,11 +263,11 @@ class DeprecatedEndpointCreateApi(Resource):
)
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint created successfully",
console_ns.models[EndpointCreateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -203,7 +276,7 @@ class DeprecatedEndpointCreateApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/list")
@@ -212,7 +285,7 @@ class EndpointListApi(Resource):
@console_ns.doc(description="List plugin endpoints with pagination")
@console_ns.doc(params=query_params_from_model(EndpointListQuery))
@console_ns.response(
200,
HTTPStatus.OK,
"Success",
console_ns.models[EndpointListResponse.__name__],
)
@@ -224,20 +297,15 @@ class EndpointListApi(Resource):
def get(self, tenant_id: str, user_id: str):
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
page_size = args.page_size
return jsonable_encoder(
{
"endpoints": EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=page,
page_size=page_size,
)
}
endpoints = EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=args.page,
page_size=args.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/list/plugin")
class EndpointListForSinglePluginApi(Resource):
@@ -245,9 +313,9 @@ class EndpointListForSinglePluginApi(Resource):
@console_ns.doc(description="List endpoints for a specific plugin")
@console_ns.doc(params=query_params_from_model(EndpointListForPluginQuery))
@console_ns.response(
200,
HTTPStatus.OK,
"Success",
console_ns.models[PluginEndpointListResponse.__name__],
console_ns.models[EndpointListResponse.__name__],
)
@setup_required
@login_required
@@ -257,22 +325,16 @@ class EndpointListForSinglePluginApi(Resource):
def get(self, tenant_id: str, user_id: str):
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
page_size = args.page_size
plugin_id = args.plugin_id
return jsonable_encoder(
{
"endpoints": EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=plugin_id,
page=page,
page_size=page_size,
)
}
endpoints = EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=args.plugin_id,
page=args.page,
page_size=args.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/<string:id>")
class EndpointItemApi(Resource):
@@ -282,11 +344,11 @@ class EndpointItemApi(Resource):
@console_ns.doc(description="Delete a plugin endpoint")
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint deleted successfully",
console_ns.models[EndpointDeleteResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -295,18 +357,20 @@ class EndpointItemApi(Resource):
@with_current_user_id
@with_current_tenant_id
def delete(self, tenant_id: str, user_id: str, id: str):
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
return SuccessResponse(
success=_delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
).model_dump(mode="json")
@console_ns.doc("update_endpoint")
@console_ns.doc(description="Update a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointUpdatePayload.__name__])
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint updated successfully",
console_ns.models[EndpointUpdateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -315,7 +379,9 @@ class EndpointItemApi(Resource):
@with_current_user_id
@with_current_tenant_id
def patch(self, tenant_id: str, user_id: str, id: str):
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
return SuccessResponse(
success=_update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/delete")
@@ -332,11 +398,11 @@ class DeprecatedEndpointDeleteApi(Resource):
)
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint deleted successfully",
console_ns.models[EndpointDeleteResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -345,8 +411,9 @@ class DeprecatedEndpointDeleteApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
return SuccessResponse(success=_delete_endpoint_from_payload(tenant_id=tenant_id, user_id=user_id)).model_dump(
mode="json"
)
@console_ns.route("/workspaces/current/endpoints/update")
@@ -363,11 +430,11 @@ class DeprecatedEndpointUpdateApi(Resource):
)
@console_ns.expect(console_ns.models[LegacyEndpointUpdatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint updated successfully",
console_ns.models[EndpointUpdateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -376,8 +443,9 @@ class DeprecatedEndpointUpdateApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
return SuccessResponse(success=_legacy_update_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(
mode="json"
)
@console_ns.route("/workspaces/current/endpoints/enable")
@@ -386,11 +454,11 @@ class EndpointEnableApi(Resource):
@console_ns.doc(description="Enable a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint enabled successfully",
console_ns.models[EndpointEnableResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -399,13 +467,9 @@ class EndpointEnableApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return {
"success": EndpointService.enable_endpoint(
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
)
}
return SuccessResponse(
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=True)
).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/disable")
@@ -414,11 +478,11 @@ class EndpointDisableApi(Resource):
@console_ns.doc(description="Disable a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint disabled successfully",
console_ns.models[EndpointDisableResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@@ -427,10 +491,6 @@ class EndpointDisableApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return {
"success": EndpointService.disable_endpoint(
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
)
}
return SuccessResponse(
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=False)
).model_dump(mode="json")
@@ -10,6 +10,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
@@ -69,6 +70,7 @@ class LoadBalancingCredentialsValidateApi(Resource):
model=payload.model,
model_type=payload.model_type,
credentials=payload.credentials,
session=db.session(),
)
except CredentialsValidateFailedError as ex:
result = False
@@ -118,6 +120,7 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
model=payload.model,
model_type=payload.model_type,
credentials=payload.credentials,
session=db.session(),
config_id=config_id,
)
except CredentialsValidateFailedError as ex:
+82 -81
View File
@@ -1,10 +1,12 @@
from http import HTTPStatus
from urllib import parse
from uuid import UUID
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, TypeAdapter
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, select
from werkzeug.exceptions import NotFound
import services
from configs import dify_config
@@ -30,8 +32,8 @@ from controllers.console.wraps import (
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from fields.base import ResponseModel
from fields.member_fields import AccountWithRole, AccountWithRoleList
from libs.helper import extract_remote_ip
from fields.member_fields import AccountWithRoleListResponse, AccountWithRoleResponse
from libs.helper import dump_response, extract_remote_ip
from libs.login import current_account_with_tenant, login_required
from models.account import Account, TenantAccountJoin, TenantAccountRole
from services.account_service import AccountService, RegisterService, TenantService
@@ -45,6 +47,11 @@ class MemberInvitePayload(BaseModel):
role: str
language: str | None = None
@field_validator("emails")
@classmethod
def normalize_emails(cls, emails: list[str]) -> list[str]:
return list(dict.fromkeys(email.lower() for email in emails))
class MemberRoleUpdatePayload(BaseModel):
role: str
@@ -70,14 +77,14 @@ class MemberInviteResultResponse(ResponseModel):
message: str | None = None
class MemberInviteResponse(ResponseModel):
class MemberActionResponse(ResponseModel):
result: str
invitation_results: list[MemberInviteResultResponse]
tenant_id: str
class MemberActionTenantResponse(ResponseModel):
class MemberInviteResponse(ResponseModel):
result: str
invitation_results: list[MemberInviteResultResponse]
tenant_id: str
@@ -92,13 +99,14 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
AccountWithRole,
AccountWithRoleList,
AccountWithRoleResponse,
AccountWithRoleListResponse,
MemberActionResponse,
MemberInviteResponse,
MemberInviteResultResponse,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
MemberInviteResponse,
MemberActionTenantResponse,
)
@@ -124,14 +132,10 @@ def _normalize_enum_value(value: object) -> str:
return str(normalized) if normalized is not None else ""
def _normalize_invitee_emails(emails: list[str]) -> list[str]:
return list(dict.fromkeys(email.lower() for email in emails))
def _count_new_member_invites(tenant_id: str, emails: list[str]) -> int:
new_member_count = 0
for email in emails:
account = AccountService.get_account_by_email_with_case_fallback(db.session, email)
account = AccountService.get_account_by_email_with_case_fallback(db.session(), email)
if not account:
new_member_count += 1
continue
@@ -179,14 +183,14 @@ class MemberListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
@with_current_user
def get(self, current_user: Account | None = None):
if current_user is None:
current_user, _ = current_account_with_tenant()
if not current_user.current_tenant:
raise ValueError("No current tenant")
members = TenantService.get_tenant_members(current_user.current_tenant, session=db.session)
members = TenantService.get_tenant_members(current_user.current_tenant, session=db.session())
if dify_config.RBAC_ENABLED:
member_ids = [member.id for member in members]
member_roles = enterprise_rbac_service.RBACService.MemberRoles.batch_get(
@@ -216,9 +220,7 @@ class MemberListApi(Resource):
}
)
member_models = TypeAdapter(list[AccountWithRole]).validate_python(serialized_members)
response = AccountWithRoleList(accounts=member_models)
return response.model_dump(mode="json"), 200
return dump_response(AccountWithRoleListResponse, {"accounts": serialized_members}), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/invite-email")
@@ -226,7 +228,7 @@ class MemberInviteEmailApi(Resource):
"""Invite a new member by email."""
@console_ns.expect(console_ns.models[MemberInvitePayload.__name__])
@console_ns.response(201, "Success", console_ns.models[MemberInviteResponse.__name__])
@console_ns.response(HTTPStatus.CREATED, "Success", console_ns.models[MemberInviteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -235,26 +237,26 @@ class MemberInviteEmailApi(Resource):
payload = console_ns.payload or {}
args = MemberInvitePayload.model_validate(payload)
invitee_emails = _normalize_invitee_emails(args.emails)
invitee_emails = args.emails
invitee_role = args.role
interface_language = args.language
if not dify_config.RBAC_ENABLED:
if not TenantAccountRole.is_valid_role(invitee_role):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
if not TenantAccountRole.is_non_owner_role(TenantAccountRole(invitee_role)):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
inviter = current_user
if not inviter.current_tenant:
raise ValueError("No current tenant")
if not _is_role_enabled(invitee_role, inviter.current_tenant.id):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
# Check workspace permission for member invitations
from libs.workspace_permission import check_workspace_member_invite_permission
check_workspace_member_invite_permission(inviter.current_tenant.id)
invitation_results = []
invitation_results: list[MemberInviteResultResponse] = []
console_web_url = dify_config.CONSOLE_WEB_URL
tenant_id = inviter.current_tenant.id
@@ -273,67 +275,69 @@ class MemberInviteEmailApi(Resource):
language=interface_language,
role=invitee_role,
inviter=inviter,
session=db.session,
session=db.session(),
)
encoded_invitee_email = parse.quote(invitee_email)
invitation_results.append(
{
"status": "success",
"email": invitee_email,
"url": f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
}
MemberInviteResultResponse(
status="success",
email=invitee_email,
url=f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
)
)
except AccountAlreadyInTenantError:
invitation_results.append(
{
"status": "already_member",
"email": invitee_email,
"message": "Account already in workspace.",
}
MemberInviteResultResponse(
status="already_member",
email=invitee_email,
message="Account already in workspace.",
)
)
except Exception as e:
invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})
invitation_results.append(
MemberInviteResultResponse(status="failed", email=invitee_email, message=str(e))
)
return {
"result": "success",
"invitation_results": invitation_results,
"tenant_id": str(inviter.current_tenant.id) if inviter.current_tenant else "",
}, 201
return MemberInviteResponse(
result="success",
invitation_results=invitation_results,
tenant_id=inviter.current_tenant.id if inviter.current_tenant else "",
).model_dump(mode="json"), HTTPStatus.CREATED
@console_ns.route("/workspaces/current/members/<uuid:member_id>")
class MemberCancelInviteApi(Resource):
"""Cancel an invitation by member id."""
@console_ns.response(200, "Success", console_ns.models[MemberActionTenantResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[MemberActionResponse.__name__])
@with_current_user
def delete(self, current_user: Account, member_id: UUID):
if not current_user.current_tenant:
raise ValueError("No current tenant")
member = db.session.get(Account, str(member_id))
if member is None:
abort(404)
abort(HTTPStatus.NOT_FOUND)
else:
try:
TenantService.remove_member_from_tenant(
current_user.current_tenant, member, current_user, session=db.session
current_user.current_tenant, member, current_user, session=db.session()
)
except services.errors.account.CannotOperateSelfError as e:
return {"code": "cannot-operate-self", "message": str(e)}, 400
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
except services.errors.account.NoPermissionError as e:
return {"code": "forbidden", "message": str(e)}, 403
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
except services.errors.account.MemberNotInTenantError as e:
return {"code": "member-not-found", "message": str(e)}, 404
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
except Exception as e:
raise ValueError(str(e))
return {
"result": "success",
"tenant_id": str(current_user.current_tenant.id) if current_user.current_tenant else "",
}, 200
return MemberActionResponse(
result="success",
tenant_id=current_user.current_tenant.id if current_user.current_tenant else "",
).model_dump(mode="json"), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/<uuid:member_id>/update-role")
@@ -341,7 +345,7 @@ class MemberUpdateRoleApi(Resource):
"""Update member role."""
@console_ns.expect(console_ns.models[MemberRoleUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -352,32 +356,32 @@ class MemberUpdateRoleApi(Resource):
new_role = args.role
if not TenantAccountRole.is_valid_role(new_role):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not _is_role_enabled(new_role, current_user.current_tenant.id):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
member = db.session.get(Account, str(member_id))
if not member:
abort(404)
abort(HTTPStatus.NOT_FOUND)
try:
assert member is not None, "Member not found"
TenantService.update_member_role(
current_user.current_tenant, member, new_role, current_user, session=db.session
current_user.current_tenant, member, new_role, current_user, session=db.session()
)
except services.errors.account.CannotOperateSelfError as e:
return {"code": "cannot-operate-self", "message": str(e)}, 400
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
except services.errors.account.NoPermissionError as e:
return {"code": "forbidden", "message": str(e)}, 403
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
except services.errors.account.MemberNotInTenantError as e:
return {"code": "member-not-found", "message": str(e)}, 404
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
except services.errors.account.RoleAlreadyAssignedError as e:
return {"code": "role-already-assigned", "message": str(e)}, 400
return {"code": "role-already-assigned", "message": str(e)}, HTTPStatus.BAD_REQUEST
except Exception as e:
raise ValueError(str(e))
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/workspaces/current/dataset-operators")
@@ -387,15 +391,13 @@ class DatasetOperatorMemberListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
@with_current_user
def get(self, current_user: Account):
if not current_user.current_tenant:
raise ValueError("No current tenant")
members = TenantService.get_dataset_operator_members(current_user.current_tenant, session=db.session)
member_models = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True)
response = AccountWithRoleList(accounts=member_models)
return response.model_dump(mode="json"), 200
members = TenantService.get_dataset_operator_members(current_user.current_tenant, session=db.session())
return dump_response(AccountWithRoleListResponse, {"accounts": members}), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/send-owner-transfer-confirm-email")
@@ -403,7 +405,7 @@ class SendOwnerTransferEmailApi(Resource):
"""Send owner transfer email."""
@console_ns.expect(console_ns.models[OwnerTransferEmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -418,7 +420,7 @@ class SendOwnerTransferEmailApi(Resource):
# check if the current user is the owner of the workspace
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session):
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session()):
raise NotOwnerError()
if args.language is not None and args.language == "zh-Hans":
@@ -435,13 +437,13 @@ class SendOwnerTransferEmailApi(Resource):
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/workspaces/current/members/owner-transfer-check")
class OwnerTransferCheckApi(Resource):
@console_ns.expect(console_ns.models[OwnerTransferCheckPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -453,7 +455,7 @@ class OwnerTransferCheckApi(Resource):
# check if the current user is the owner of the workspace
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session):
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session()):
raise NotOwnerError()
user_email = current_user.email
@@ -480,13 +482,13 @@ class OwnerTransferCheckApi(Resource):
_, new_token = AccountService.generate_owner_transfer_token(user_email, code=args.code, additional_data={})
AccountService.reset_owner_transfer_error_rate_limit(user_email)
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
return VerificationTokenResponse(is_valid=True, email=user_email, token=new_token).model_dump(mode="json")
@console_ns.route("/workspaces/current/members/<uuid:member_id>/owner-transfer")
class OwnerTransfer(Resource):
@console_ns.expect(console_ns.models[OwnerTransferPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -499,7 +501,7 @@ class OwnerTransfer(Resource):
# check if the current user is the owner of the workspace
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session):
if not TenantService.is_owner(current_user, current_user.current_tenant, session=db.session()):
raise NotOwnerError()
if current_user.id == str(member_id):
@@ -516,18 +518,17 @@ class OwnerTransfer(Resource):
member = db.session.get(Account, str(member_id))
if not member:
abort(404)
return # Never reached, but helps type checker
raise NotFound()
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not TenantService.is_member(member, current_user.current_tenant, session=db.session):
if not TenantService.is_member(member, current_user.current_tenant, session=db.session()):
raise MemberNotInTenantError()
try:
assert member is not None, "Member not found"
TenantService.update_member_role(
current_user.current_tenant, member, "owner", current_user, session=db.session
current_user.current_tenant, member, "owner", current_user, session=db.session()
)
AccountService.send_new_owner_transfer_notify_email(
@@ -546,4 +547,4 @@ class OwnerTransfer(Resource):
except Exception as e:
raise ValueError(str(e))
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -353,7 +353,7 @@ class ModelProviderPaymentCheckoutUrlApi(Resource):
def get(self, current_tenant_id: str, current_user: Account, provider: str):
if provider != "anthropic":
raise ValueError(f"provider name {provider} is invalid")
BillingService.is_tenant_owner_or_admin(db.session, current_user)
BillingService.is_tenant_owner_or_admin(db.session(), current_user)
data = BillingService.get_model_provider_payment_link(
provider_name=provider,
tenant_id=current_tenant_id,
@@ -24,6 +24,7 @@ from controllers.console.wraps import (
with_current_user,
)
from core.entities.provider_entities import CredentialConfiguration
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
@@ -297,6 +298,7 @@ class ModelProviderModelApi(Resource):
model_type=args.model_type,
configs=args.load_balancing.configs,
config_from=args.config_from or "",
session=db.session(),
)
if args.load_balancing.enabled:
@@ -356,6 +358,7 @@ class ModelProviderModelCredentialApi(Resource):
provider=provider,
model=args.model,
model_type=args.model_type,
session=db.session(),
config_from=args.config_from or "",
)
+44 -32
View File
@@ -38,11 +38,20 @@ from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.tool_manager import ToolManager
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import dump_response
from libs.login import login_required
from models.account import Account, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.account import (
Account,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategy,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginDebugPermission,
TenantPluginInstallPermission,
)
from models.provider_ids import ToolProviderID
from services.entities.model_provider_entities import ProviderEntityResponse
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
@@ -52,9 +61,9 @@ from services.tools.tools_transform_service import ToolTransformService
class AutoUpgradeSettingsResponse(TypedDict):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
strategy_setting: TenantPluginAutoUpgradeStrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
upgrade_mode: TenantPluginAutoUpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
@@ -127,8 +136,8 @@ class ParserUninstall(BaseModel):
class ParserPermissionChange(BaseModel):
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
class ParserDynamicOptions(BaseModel):
@@ -150,16 +159,14 @@ class ParserDynamicOptionsWithCredentials(BaseModel):
class PluginPermissionSettingsPayload(BaseModel):
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
class PluginAutoUpgradeSettingsPayload(BaseModel):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting = (
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
)
strategy_setting: TenantPluginAutoUpgradeStrategySetting = TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
upgrade_time_of_day: int = 0
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
upgrade_mode: TenantPluginAutoUpgradeMode = TenantPluginAutoUpgradeMode.EXCLUDE
exclude_plugins: list[str] = Field(default_factory=list)
include_plugins: list[str] = Field(default_factory=list)
@@ -170,15 +177,15 @@ class PluginAutoUpgradeChangeResponse(ResponseModel):
class PluginAutoUpgradeSettingsResponseModel(ResponseModel):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
strategy_setting: TenantPluginAutoUpgradeStrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
upgrade_mode: TenantPluginAutoUpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
class PluginAutoUpgradeFetchResponse(ResponseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
@@ -209,19 +216,19 @@ class PluginDeclarationResponse(ResponseModel):
class ParserAutoUpgradeChange(BaseModel):
model_config = ConfigDict(extra="forbid")
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
auto_upgrade: PluginAutoUpgradeSettingsPayload
class ParserAutoUpgradeFetch(BaseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
class ParserExcludePlugin(BaseModel):
model_config = ConfigDict(extra="forbid")
plugin_id: str
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
class ParserReadme(BaseModel):
@@ -339,8 +346,8 @@ class PluginTaskResponse(ResponseModel):
class PluginPermissionResponse(ResponseModel):
install_permission: TenantPluginPermission.InstallPermission
debug_permission: TenantPluginPermission.DebugPermission
install_permission: TenantPluginInstallPermission
debug_permission: TenantPluginDebugPermission
class PluginDynamicOptionsResponse(ResponseModel):
@@ -408,22 +415,22 @@ register_response_schema_models(
register_enum_models(
console_ns,
TenantPluginPermission.DebugPermission,
TenantPluginAutoUpgradeStrategy.PluginCategory,
TenantPluginAutoUpgradeStrategy.UpgradeMode,
TenantPluginAutoUpgradeStrategy.StrategySetting,
TenantPluginPermission.InstallPermission,
TenantPluginDebugPermission,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginInstallPermission,
)
def _default_auto_upgrade_settings(
tenant_id: str,
category: TenantPluginAutoUpgradeStrategy.PluginCategory,
category: TenantPluginAutoUpgradeCategory,
) -> AutoUpgradeSettingsResponse:
return {
"strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category),
"upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id),
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
}
@@ -967,7 +974,7 @@ class PluginChangePermissionApi(Resource):
args = ParserPermissionChange.model_validate(console_ns.payload)
set_permission_result = PluginPermissionService.change_permission(
tenant_id, args.install_permission, args.debug_permission
tenant_id, args.install_permission, args.debug_permission, session=db.session()
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
@@ -983,12 +990,12 @@ class PluginFetchPermissionApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
permission = PluginPermissionService.get_permission(tenant_id)
permission = PluginPermissionService.get_permission(tenant_id, session=db.session())
if not permission:
return jsonable_encoder(
{
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
"install_permission": TenantPluginInstallPermission.EVERYONE,
"debug_permission": TenantPluginDebugPermission.EVERYONE,
}
)
@@ -1088,6 +1095,7 @@ class PluginChangeAutoUpgradeApi(Resource):
auto_upgrade.exclude_plugins,
auto_upgrade.include_plugins,
category=args.category,
session=db.session(),
)
if not set_auto_upgrade_strategy_result:
return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"})
@@ -1105,7 +1113,7 @@ class PluginFetchAutoUpgradeApi(Resource):
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True))
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category)
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category, session=db.session())
auto_upgrade_dict = (
_auto_upgrade_settings_to_dict(auto_upgrade)
if auto_upgrade
@@ -1134,7 +1142,11 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
args = ParserExcludePlugin.model_validate(console_ns.payload)
return jsonable_encoder(
{"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id, args.category)}
{
"success": PluginAutoUpgradeService.exclude_plugin(
tenant_id, args.plugin_id, args.category, session=db.session()
)
}
)
+4 -1
View File
@@ -14,6 +14,7 @@ from controllers.console import console_ns
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
from core.db.session_factory import session_factory
from core.rbac import RBACResourceWhitelistScope
from extensions.ext_database import db
from libs.login import current_account_with_tenant, login_required
from models import Account
from services.enterprise import rbac_service as svc
@@ -564,6 +565,7 @@ class RBACMyPermissionsApi(Resource):
account_id,
app_id=request.args.get("app_id") or None,
dataset_id=request.args.get("dataset_id") or None,
session=db.session(),
)
)
@@ -902,7 +904,7 @@ class RBACMemberRolesApi(Resource):
@console_ns.response(200, "Success", console_ns.models[svc.MemberRolesResponse.__name__])
def get(self, member_id):
tenant_id, account_id = _current_ids()
return _dump(svc.RBACService.MemberRoles.get(tenant_id, account_id, str(member_id)))
return _dump(svc.RBACService.MemberRoles.get(tenant_id, account_id, str(member_id), session=db.session()))
@login_required
@console_ns.expect(console_ns.models[_ReplaceMemberRolesRequest.__name__])
@@ -916,6 +918,7 @@ class RBACMemberRolesApi(Resource):
account_id,
str(member_id),
role_ids=list(request.role_ids),
session=db.session(),
)
)
+17 -15
View File
@@ -4,7 +4,7 @@ from typing import Any
from urllib.parse import quote
from flask import Response, request
from flask_restx import Resource, marshal
from flask_restx import Resource
from pydantic import Field as PydanticField
from pydantic import field_validator
from sqlalchemy.orm import Session, sessionmaker
@@ -37,8 +37,7 @@ from controllers.console.wraps import (
from core.plugin.entities.plugin import PluginDependency
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.snippet_fields import snippet_fields, snippet_list_fields
from libs.helper import to_timestamp
from libs.helper import dump_response, to_timestamp
from libs.login import login_required
from models import Account
from models.snippet import SnippetType
@@ -189,7 +188,7 @@ class CustomizedSnippetsApi(Resource):
snippet_service = _snippet_service()
snippets, total, has_more = snippet_service.get_snippets(
tenant_id=current_tenant_id,
session=db.session,
session=db.session(),
page=query.page,
limit=query.limit,
keyword=query.keyword,
@@ -198,13 +197,16 @@ class CustomizedSnippetsApi(Resource):
tag_ids=query.tag_ids,
)
return {
"data": marshal(snippets, snippet_list_fields),
"page": query.page,
"limit": query.limit,
"total": total,
"has_more": has_more,
}, 200
return dump_response(
SnippetPaginationResponse,
{
"data": snippets,
"page": query.page,
"limit": query.limit,
"total": total,
"has_more": has_more,
},
), 200
@console_ns.doc("create_customized_snippet")
@console_ns.expect(console_ns.models.get(CreateSnippetPayload.__name__))
@@ -245,7 +247,7 @@ class CustomizedSnippetsApi(Resource):
except ValueError as e:
return {"message": str(e)}, 400
return marshal(snippet, snippet_fields), 201
return dump_response(SnippetResponse, snippet), 201
@console_ns.route("/workspaces/current/customized-snippets/<uuid:snippet_id>")
@@ -268,7 +270,7 @@ class CustomizedSnippetDetailApi(Resource):
if not snippet:
raise NotFound("Snippet not found")
return marshal(snippet, snippet_fields), 200
return dump_response(SnippetResponse, snippet), 200
@console_ns.doc("update_customized_snippet")
@console_ns.expect(console_ns.models.get(UpdateSnippetPayload.__name__))
@@ -317,7 +319,7 @@ class CustomizedSnippetDetailApi(Resource):
except ValueError as e:
return {"message": str(e)}, 400
return marshal(snippet, snippet_fields), 200
return dump_response(SnippetResponse, snippet), 200
@console_ns.doc("delete_customized_snippet")
@console_ns.response(204, "Snippet deleted successfully")
@@ -533,4 +535,4 @@ class CustomizedSnippetUseCountIncrementApi(Resource):
session.commit()
session.refresh(snippet)
return {"result": "success", "use_count": snippet.use_count}, 200
return SnippetUseCountResponse(result="success", use_count=snippet.use_count).model_dump(mode="json"), 200
@@ -459,6 +459,7 @@ class ToolBuiltinProviderGetCredentialsApi(Resource):
BuiltinToolManageService.get_builtin_tool_provider_credentials(
tenant_id=tenant_id,
provider_name=provider,
session=db.session(),
user=user,
include_credential_ids=query.include_credential_ids or None,
)
@@ -1064,6 +1065,7 @@ class ToolBuiltinProviderGetCredentialInfoApi(Resource):
BuiltinToolManageService.get_builtin_tool_provider_credential_info(
tenant_id=tenant_id,
provider=provider,
session=db.session(),
user=user,
include_credential_ids=query.include_credential_ids or None,
)
+66 -77
View File
@@ -1,8 +1,9 @@
import logging
from datetime import datetime
from http import HTTPStatus
from flask import request
from flask_restx import Resource, fields, marshal
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from werkzeug.exceptions import Unauthorized
@@ -16,7 +17,12 @@ from controllers.common.errors import (
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.schema import (
query_params_from_model,
query_params_from_request,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.admin import admin_required
from controllers.console.error import AccountNotLinkTenantError
@@ -31,7 +37,7 @@ from controllers.console.wraps import (
from enums.cloud_plan import CloudPlan
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp
from libs.helper import dump_response, to_timestamp
from libs.login import login_required
from libs.pagination import paginate_query
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
@@ -133,7 +139,7 @@ class WorkspaceListItemResponse(ResponseModel):
@field_validator("status", mode="before")
@classmethod
def _normalize_status(cls, value):
def _normalize_enum_like(cls, value):
if value is None:
return None
if isinstance(value, str):
@@ -146,7 +152,7 @@ class WorkspaceListItemResponse(ResponseModel):
return to_timestamp(value)
class WorkspaceListResponse(ResponseModel):
class WorkspacePaginationResponse(ResponseModel):
data: list[WorkspaceListItemResponse]
has_more: bool
limit: int
@@ -159,7 +165,7 @@ class SwitchWorkspaceResponse(ResponseModel):
new_tenant: TenantInfoResponse
class WorkspaceMutationResponse(ResponseModel):
class WorkspaceTenantResultResponse(ResponseModel):
result: str
tenant: TenantInfoResponse
@@ -174,6 +180,16 @@ class WorkspacePermissionResponse(ResponseModel):
allow_owner_transfer: bool
WORKSPACE_LOGO_UPLOAD_PARAMS = {
"file": {
"in": "formData",
"type": "file",
"required": True,
"description": "Workspace web app logo file. Only SVG and PNG files are supported.",
}
}
register_schema_models(
console_ns,
WorkspaceListQuery,
@@ -184,53 +200,21 @@ register_schema_models(
register_response_schema_models(
console_ns,
TenantInfoResponse,
TenantListItemResponse,
TenantListResponse,
WorkspaceListResponse,
SwitchWorkspaceResponse,
WorkspaceMutationResponse,
WorkspaceLogoUploadResponse,
WorkspaceCustomConfigResponse,
WorkspaceListItemResponse,
WorkspacePaginationResponse,
SwitchWorkspaceResponse,
WorkspaceTenantResultResponse,
WorkspaceLogoUploadResponse,
WorkspacePermissionResponse,
)
provider_fields = {
"provider_name": fields.String,
"provider_type": fields.String,
"is_valid": fields.Boolean,
"token_is_set": fields.Boolean,
}
tenant_fields = {
"id": fields.String,
"name": fields.String,
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"role": fields.String,
"in_trial": fields.Boolean,
"trial_end_reason": fields.String,
"custom_config": fields.Raw(attribute="custom_config"),
"trial_credits": fields.Integer,
"trial_credits_used": fields.Integer,
"next_credit_reset_date": fields.Integer,
}
tenants_fields = {
"id": fields.String,
"name": fields.String,
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"last_opened_at": OptionalTimestampField,
"current": fields.Boolean,
}
workspace_fields = {"id": fields.String, "name": fields.String, "status": fields.String, "created_at": TimestampField}
@console_ns.route("/workspaces")
class TenantListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TenantListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -239,7 +223,7 @@ class TenantListApi(Resource):
def get(self, current_tenant_id: str, current_user: Account):
tenant_rows: list[tuple[Tenant, TenantAccountJoin]] = [
(tenant, membership)
for tenant, membership in TenantService.get_workspaces_for_account(db.session, current_user.id)
for tenant, membership in TenantService.get_workspaces_for_account(db.session(), current_user.id)
if tenant.status == TenantStatus.NORMAL
]
tenants = [tenant for tenant, _ in tenant_rows]
@@ -281,18 +265,17 @@ class TenantListApi(Resource):
tenant_dicts.append(tenant_dict)
return {"workspaces": marshal(tenant_dicts, tenants_fields)}, 200
return dump_response(TenantListResponse, {"workspaces": tenant_dicts}), HTTPStatus.OK
@console_ns.route("/all-workspaces")
class WorkspaceListApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceListQuery))
@console_ns.response(200, "Success", console_ns.models[WorkspaceListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePaginationResponse.__name__])
@setup_required
@admin_required
def get(self):
payload = request.args.to_dict(flat=True)
args = WorkspaceListQuery.model_validate(payload)
args = query_params_from_request(WorkspaceListQuery)
stmt = select(Tenant).order_by(Tenant.created_at.desc())
tenants = paginate_query(stmt, page=args.page, per_page=args.limit)
@@ -301,13 +284,9 @@ class WorkspaceListApi(Resource):
if tenants.has_next:
has_more = True
return {
"data": marshal(tenants.items, workspace_fields),
"has_more": has_more,
"limit": args.limit,
"page": args.page,
"total": tenants.total,
}, 200
return WorkspacePaginationResponse(
data=tenants.items, has_more=has_more, limit=args.limit, page=args.page, total=tenants.total or 0
).model_dump(mode="json"), HTTPStatus.OK
@console_ns.route("/workspaces/current", endpoint="workspaces_current")
@@ -316,7 +295,7 @@ class TenantApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[TenantInfoResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantInfoResponse.__name__])
@with_current_user
def post(self, current_user: Account):
if request.path == "/info":
@@ -327,22 +306,25 @@ class TenantApi(Resource):
raise ValueError("No current tenant")
if tenant.status == TenantStatus.ARCHIVE:
tenants = TenantService.get_join_tenants(current_user, session=db.session)
tenants = TenantService.get_join_tenants(current_user, session=db.session())
# if there is any tenant, switch to the first one
if len(tenants) > 0:
TenantService.switch_tenant(current_user, tenants[0].id, session=db.session)
TenantService.switch_tenant(current_user, tenants[0].id, session=db.session())
tenant = tenants[0]
# else, raise Unauthorized
else:
raise Unauthorized("workspace is archived")
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), 200
return (
dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant, session=db.session())),
HTTPStatus.OK,
)
@console_ns.route("/workspaces/switch")
class SwitchWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[SwitchWorkspacePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -351,9 +333,9 @@ class SwitchWorkspaceApi(Resource):
payload = console_ns.payload or {}
args = SwitchWorkspacePayload.model_validate(payload)
# check if tenant_id is valid, 403 if not
# Check whether the tenant_id belongs to the current account.
try:
TenantService.switch_tenant(current_user, args.tenant_id, session=db.session)
TenantService.switch_tenant(current_user, args.tenant_id, session=db.session())
except Exception:
raise AccountNotLinkTenantError("Account not link tenant")
@@ -361,13 +343,15 @@ class SwitchWorkspaceApi(Resource):
if new_tenant is None:
raise ValueError("Tenant not found")
return {"result": "success", "new_tenant": marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)}
return SwitchWorkspaceResponse(
result="success", new_tenant=WorkspaceService.get_tenant_info(new_tenant, session=db.session())
).model_dump(mode="json")
@console_ns.route("/workspaces/custom-config")
class CustomConfigWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -390,12 +374,15 @@ class CustomConfigWorkspaceApi(Resource):
tenant.custom_config_dict = custom_config_dict
db.session.commit()
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
return WorkspaceTenantResultResponse(
result="success", tenant=WorkspaceService.get_tenant_info(tenant, session=db.session())
).model_dump(mode="json")
@console_ns.route("/workspaces/custom-config/webapp-logo/upload")
class WebappLogoWorkspaceApi(Resource):
@console_ns.response(201, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
@console_ns.doc(consumes=["multipart/form-data"], params=WORKSPACE_LOGO_UPLOAD_PARAMS)
@console_ns.response(HTTPStatus.CREATED, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -431,13 +418,13 @@ class WebappLogoWorkspaceApi(Resource):
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
return {"id": upload_file.id}, 201
return WorkspaceLogoUploadResponse(id=upload_file.id).model_dump(mode="json"), HTTPStatus.CREATED
@console_ns.route("/workspaces/info")
class WorkspaceInfoApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceInfoPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -453,14 +440,16 @@ class WorkspaceInfoApi(Resource):
tenant.name = args.name
db.session.commit()
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
return WorkspaceTenantResultResponse(
result="success", tenant=WorkspaceService.get_tenant_info(tenant, session=db.session())
).model_dump(mode="json")
@console_ns.route("/workspaces/current/permission")
class WorkspacePermissionApi(Resource):
"""Get workspace permissions for the current workspace."""
@console_ns.response(200, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -477,8 +466,8 @@ class WorkspacePermissionApi(Resource):
# Get workspace permissions from enterprise service
permission = EnterpriseService.WorkspacePermissionService.get_permission(current_tenant_id)
return {
"workspace_id": permission.workspace_id,
"allow_member_invite": permission.allow_member_invite,
"allow_owner_transfer": permission.allow_owner_transfer,
}, 200
return WorkspacePermissionResponse(
workspace_id=permission.workspace_id,
allow_member_invite=permission.allow_member_invite,
allow_owner_transfer=permission.allow_owner_transfer,
).model_dump(mode="json"), HTTPStatus.OK
@@ -8,6 +8,7 @@ from werkzeug.exceptions import Forbidden, NotFound
from controllers.common.file_response import enforce_download_for_html
from controllers.common.schema import register_schema_models
from controllers.files import files_ns
from extensions.ext_database import db
from models.agent import AgentDriveFileKind
from services.agent_drive_service import AgentDriveError, AgentDriveService
@@ -54,6 +55,7 @@ class AgentDriveArchiveMemberApi(Resource):
archive_file_kind=args.archive_file_kind,
archive_file_id=args.archive_file_id,
member_path=args.member_path,
session=db.session(),
)
except AgentDriveError as exc:
raise NotFound(exc.message) from exc
+1
View File
@@ -98,6 +98,7 @@ class EnterpriseAppDSLExport(Resource):
data = AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=include_secret,
)
@@ -17,6 +17,7 @@ from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.plugin.wraps import get_user
from controllers.inner_api.wraps import plugin_inner_api_only
from extensions.ext_database import db
from services.agent_drive_service import (
AgentDriveError,
AgentDriveService,
@@ -53,6 +54,7 @@ class AgentDriveManifestApi(Resource):
agent_id=agent_id,
prefix=request.args.get("prefix", ""),
include_download_url=include_download_url,
session=db.session(),
)
except AgentDriveError as exc:
return _error_response(exc)
@@ -71,7 +73,7 @@ class AgentDriveSkillsApi(Resource):
tenant_id = (request.args.get("tenant_id") or "").strip()
if not tenant_id:
raise AgentDriveError("missing_tenant_id", "tenant_id is required", status_code=400)
items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=agent_id)
items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=agent_id, session=db.session())
except AgentDriveError as exc:
return _error_response(exc)
return {"items": items}
@@ -96,6 +98,7 @@ class AgentDriveCommitApi(Resource):
user_id=user.id,
agent_id=agent_id,
items=body.items,
session=db.session(),
)
except AgentDriveError as exc:
return _error_response(exc)
@@ -47,8 +47,8 @@ class EnterpriseWorkspace(Resource):
if account is None:
return {"message": "owner account not found."}, 404
tenant = TenantService.create_tenant(args.name, is_from_dashboard=True, session=db.session)
TenantService.create_tenant_member(tenant, account, db.session, role="owner")
tenant = TenantService.create_tenant(args.name, is_from_dashboard=True, session=db.session())
TenantService.create_tenant_member(tenant, account, db.session(), role="owner")
tenant_was_created.send(tenant)
@@ -84,7 +84,7 @@ class EnterpriseWorkspaceNoOwnerEmail(Resource):
def post(self):
args = WorkspaceOwnerlessPayload.model_validate(inner_api_ns.payload or {})
tenant = TenantService.create_tenant(args.name, is_from_dashboard=True, session=db.session)
tenant = TenantService.create_tenant(args.name, is_from_dashboard=True, session=db.session())
tenant_was_created.send(tenant)
+47 -6
View File
@@ -1,6 +1,6 @@
from typing import Any, Union
from flask import Response
from flask import Response, request
from flask_restx import Resource
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy import select
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session, sessionmaker
from controllers.common.schema import register_schema_model
from controllers.mcp import mcp_ns
from core.mcp import types as mcp_types
from core.mcp.server.streamable_http import handle_mcp_request
from core.mcp.server.streamable_http import handle_mcp_request, negotiate_protocol_version
from extensions.ext_database import db
from graphon.variables.input_entities import VariableEntity, VariableEntityType
from libs import helper
@@ -68,6 +68,17 @@ class MCPAppApi(Resource):
request_id: Union[int, str] | None = args.id
mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True))
# Resolve the negotiated protocol version from the MCP-Protocol-Version header.
is_initialize = isinstance(mcp_request.root, mcp_types.InitializeRequest)
header_value = request.headers.get("MCP-Protocol-Version")
protocol_version = negotiate_protocol_version(header_value, is_initialize)
if protocol_version is None:
# A notification never receives a response, even with an unsupported header.
if isinstance(mcp_request, mcp_types.ClientNotification):
protocol_version = mcp_types.DEFAULT_NEGOTIATED_VERSION
else:
return self._protocol_version_error_response(request_id, header_value)
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Get MCP server and app
mcp_server, app = self._get_mcp_server_and_app(server_code, session)
@@ -77,7 +88,28 @@ class MCPAppApi(Resource):
user_input_form = self._get_user_input_form(app)
# Handle notification vs request differently
return self._process_mcp_message(mcp_request, request_id, app, mcp_server, user_input_form, session)
return self._process_mcp_message(
mcp_request, request_id, app, mcp_server, user_input_form, session, protocol_version
)
def _protocol_version_error_response(
self, request_id: Union[int, str] | None, header_value: str | None
) -> Response:
"""Return a JSON-RPC error for an unsupported MCP-Protocol-Version header.
Per JSON-RPC 2.0, an error whose request id is unknown uses a null id, so we echo the
offending request's id directly (None -> null) instead of fabricating a placeholder.
"""
error_data = mcp_types.ErrorData(
code=mcp_types.INVALID_REQUEST,
message=f"Unsupported MCP-Protocol-Version: {header_value}",
)
error_response = {
"jsonrpc": "2.0",
"id": request_id,
"error": error_data.model_dump(by_alias=True, mode="json", exclude_none=True),
}
return helper.compact_generate_response(error_response)
def _get_mcp_server_and_app(self, server_code: str, session: Session) -> tuple[AppMCPServer, App]:
"""Get and validate MCP server and app in one query session"""
@@ -104,12 +136,15 @@ class MCPAppApi(Resource):
mcp_server: AppMCPServer,
user_input_form: list[VariableEntity],
session: Session,
protocol_version: str,
) -> Response:
"""Process MCP message (notification or request)"""
if isinstance(mcp_request, mcp_types.ClientNotification):
return self._handle_notification(mcp_request)
else:
return self._handle_request(mcp_request, request_id, app, mcp_server, user_input_form, session)
return self._handle_request(
mcp_request, request_id, app, mcp_server, user_input_form, session, protocol_version
)
def _handle_notification(self, mcp_request: mcp_types.ClientNotification) -> Response:
"""Handle MCP notification"""
@@ -127,12 +162,15 @@ class MCPAppApi(Resource):
mcp_server: AppMCPServer,
user_input_form: list[VariableEntity],
session: Session,
protocol_version: str,
) -> Response:
"""Handle MCP request"""
if request_id is None:
raise MCPRequestError(mcp_types.INVALID_REQUEST, "Request ID is required")
result = self._handle_mcp_request(app, mcp_server, mcp_request, user_input_form, session, request_id)
result = self._handle_mcp_request(
app, mcp_server, mcp_request, user_input_form, session, request_id, protocol_version
)
if result is None:
# This shouldn't happen for requests, but handle gracefully
raise MCPRequestError(mcp_types.INTERNAL_ERROR, "No response generated for request")
@@ -229,6 +267,7 @@ class MCPAppApi(Resource):
user_input_form: list[VariableEntity],
session: Session,
request_id: Union[int, str],
protocol_version: str,
) -> mcp_types.JSONRPCResponse | mcp_types.JSONRPCError | None:
"""Handle MCP request and return response"""
end_user = self._retrieve_end_user(mcp_server.tenant_id, mcp_server.id)
@@ -238,4 +277,6 @@ class MCPAppApi(Resource):
client_name = f"{client_info.name}@{client_info.version}"
end_user = self._create_end_user(client_name, app.tenant_id, app.id, mcp_server.id, session)
return handle_mcp_request(session, app, mcp_request, user_input_form, mcp_server, end_user, request_id)
return handle_mcp_request(
session, app, mcp_request, user_input_form, mcp_server, end_user, request_id, protocol_version
)
+1
View File
@@ -49,6 +49,7 @@ class OpenApiErrorCode(StrEnum):
# domain codes (must match the error_code attribute of the exception
# classes raised on the openapi surface)
APP_UNAVAILABLE = "app_unavailable"
AGENT_NOT_PUBLISHED = "agent_not_published"
CONVERSATION_COMPLETED = "conversation_completed"
PROVIDER_NOT_INITIALIZE = "provider_not_initialize"
PROVIDER_QUOTA_EXCEEDED = "provider_quota_exceeded"
+6 -6
View File
@@ -45,8 +45,8 @@ class AccountApi(Resource):
enforce(LIMIT_ME_PER_ACCOUNT, key=f"account:{auth_data.account_id}")
account_id_str = str(auth_data.account_id) if auth_data.account_id else None
account = AccountService.get_account_by_id(db.session, account_id_str) if account_id_str else None
memberships = TenantService.get_account_memberships(db.session, account_id_str) if account_id_str else []
account = AccountService.get_account_by_id(db.session(), account_id_str) if account_id_str else None
memberships = TenantService.get_account_memberships(db.session(), account_id_str) if account_id_str else []
default_ws_id = _pick_default_workspace(memberships)
return AccountResponse(
@@ -63,7 +63,7 @@ class AccountSessionsSelfApi(Resource):
@auth_router.guard(scope=Scope.FULL, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}))
@returns(200, RevokeResponse, description="Session revoked")
def delete(self, *, auth_data: AuthData):
revoke_oauth_token(db.session, redis_client, str(auth_data.token_id))
revoke_oauth_token(db.session(), redis_client, str(auth_data.token_id))
return RevokeResponse(status="revoked")
@@ -81,7 +81,7 @@ class AccountSessionsApi(Resource):
page = query.page
limit = query.limit
all_rows = list_active_sessions(db.session, ctx, now)
all_rows = list_active_sessions(db.session(), ctx, now)
total = len(all_rows)
sliced = all_rows[(page - 1) * limit : page * limit]
@@ -117,10 +117,10 @@ class AccountSessionByIdApi(Resource):
# 404 (not 403) on cross-subject so the endpoint doesn't leak
# token IDs that belong to other subjects.
if not token_belongs_to_subject(db.session, session_id, ctx):
if not token_belongs_to_subject(db.session(), session_id, ctx):
raise NotFound("session not found")
revoke_oauth_token(db.session, redis_client, session_id)
revoke_oauth_token(db.session(), redis_client, session_id)
return RevokeResponse(status="revoked")

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