Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
868eff9427 | ||
|
|
d0ea5a5e0d | ||
|
|
4767d90e1a | ||
|
|
e32b778606 | ||
|
|
bdb3469ca0 | ||
|
|
c47663d77b | ||
|
|
1247fa28f1 | ||
|
|
64d72c6fb9 | ||
|
|
78ca1a8a76 | ||
|
|
5aae5c15b3 | ||
|
|
b1724073fc | ||
|
|
904fadde20 | ||
|
|
77ae583b44 | ||
|
|
c3b1508712 | ||
|
|
8208b786ee | ||
|
|
d9c99daf29 | ||
|
|
93eb6d32b5 | ||
|
|
de5af6c2da | ||
|
|
586c8de1a0 |
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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")
|
||||
if not agent.active_config_snapshot_id or not agent.active_config_is_published:
|
||||
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)
|
||||
@@ -108,14 +108,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 +219,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
|
||||
@@ -322,7 +316,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 (
|
||||
@@ -87,6 +92,7 @@ register_schema_models(console_ns, LoginPayload, EmailPayload, EmailCodeLoginPay
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SimpleResultDataResponse,
|
||||
SimpleResultMessageResponse,
|
||||
SimpleResultOptionalDataResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
@@ -154,16 +160,19 @@ class LoginApi(Resource):
|
||||
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))
|
||||
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")
|
||||
@@ -326,7 +334,8 @@ class EmailCodeLoginApi(Resource):
|
||||
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,18 +347,22 @@ 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)
|
||||
|
||||
# Create response with new cookies
|
||||
response = make_response({"result": "success"})
|
||||
# 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)
|
||||
@@ -357,7 +370,7 @@ class RefreshTokenApi(Resource):
|
||||
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
|
||||
return SimpleResultMessageResponse(result="fail", message=str(e)).model_dump(mode="json"), 401
|
||||
|
||||
|
||||
def _get_account_with_case_fallback(email: str):
|
||||
|
||||
@@ -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,13 @@ 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 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 +35,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 +47,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 +81,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 +138,9 @@ register_schema_models(
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
DatasourceCredentialsResponse,
|
||||
DatasourceCredentialListResponse,
|
||||
DatasourceProviderAuthListResponse,
|
||||
PluginOAuthAuthorizationUrlResponse,
|
||||
RedirectResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
|
||||
@@ -100,7 +150,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 +190,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 +205,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 +265,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 +288,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):
|
||||
@@ -256,7 +310,7 @@ class DatasourceAuth(Resource):
|
||||
plugin_id=datasource_provider_id.plugin_id,
|
||||
user=user,
|
||||
)
|
||||
return {"result": datasources}, 200
|
||||
return dump_response(DatasourceCredentialListResponse, {"result": datasources}), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/delete")
|
||||
@@ -282,13 +336,15 @@ class DatasourceAuthDeleteApi(Resource):
|
||||
provider=provider_name,
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
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,12 +364,16 @@ 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
|
||||
@@ -321,12 +381,16 @@ class DatasourceAuthListApi(Resource):
|
||||
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
|
||||
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
|
||||
@@ -334,13 +398,15 @@ class DatasourceHardCodeAuthListApi(Resource):
|
||||
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
|
||||
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 +423,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 +437,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 +459,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 +482,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,9 +3,9 @@ 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
|
||||
@@ -21,18 +21,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
|
||||
|
||||
@@ -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):
|
||||
@@ -434,6 +432,7 @@ class TrialAppWorkflowRunApi(TrialAppResource):
|
||||
streaming=True,
|
||||
)
|
||||
RecommendedAppService.add_trial_app_record(db.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
|
||||
@@ -513,6 +512,7 @@ class TrialChatApi(TrialAppResource):
|
||||
streaming=True,
|
||||
)
|
||||
RecommendedAppService.add_trial_app_record(db.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.")
|
||||
@@ -680,7 +680,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
|
||||
@@ -710,6 +710,7 @@ class TrialCompletionApi(TrialAppResource):
|
||||
)
|
||||
|
||||
RecommendedAppService.add_trial_app_record(db.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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
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,13 +351,13 @@ 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 {}
|
||||
@@ -376,7 +365,7 @@ class AccountAvatarApi(Resource):
|
||||
|
||||
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,7 +374,7 @@ 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 {}
|
||||
@@ -395,7 +384,7 @@ class AccountInterfaceLanguageApi(Resource):
|
||||
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,7 +393,7 @@ 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 {}
|
||||
@@ -414,7 +403,7 @@ class AccountInterfaceThemeApi(Resource):
|
||||
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,7 +412,7 @@ 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 {}
|
||||
@@ -431,7 +420,7 @@ class AccountTimezoneApi(Resource):
|
||||
|
||||
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 {}
|
||||
@@ -452,7 +441,7 @@ class AccountPasswordApi(Resource):
|
||||
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
|
||||
@@ -531,13 +516,13 @@ class AccountDeleteApi(Resource):
|
||||
|
||||
AccountService.delete_account(account)
|
||||
|
||||
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 {}
|
||||
@@ -768,13 +758,13 @@ class ChangeEmailResetApi(Resource):
|
||||
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 {}
|
||||
@@ -784,4 +774,4 @@ class CheckEmailUnique(Resource):
|
||||
raise AccountInFreezeError()
|
||||
if not AccountService.check_email_unique(normalized_email, session=db.session):
|
||||
raise EmailAlreadyInUseError()
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,10 +132,6 @@ 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:
|
||||
@@ -179,7 +183,7 @@ 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:
|
||||
@@ -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
|
||||
@@ -277,63 +279,65 @@ class MemberInviteEmailApi(Resource):
|
||||
)
|
||||
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
|
||||
)
|
||||
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,14 +356,14 @@ 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"
|
||||
@@ -367,17 +371,17 @@ class MemberUpdateRoleApi(Resource):
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -516,8 +518,7 @@ 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")
|
||||
@@ -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")
|
||||
|
||||
@@ -42,7 +42,15 @@ 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 +60,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 +135,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 +158,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 +176,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 +215,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 +345,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 +414,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": [],
|
||||
}
|
||||
@@ -987,8 +993,8 @@ class PluginFetchPermissionApi(Resource):
|
||||
if not permission:
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
"install_permission": TenantPluginInstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginDebugPermission.EVERYONE,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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":
|
||||
@@ -336,13 +315,13 @@ class TenantApi(Resource):
|
||||
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)), 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,7 +330,7 @@ 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)
|
||||
except Exception:
|
||||
@@ -361,13 +340,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)
|
||||
).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 +371,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)
|
||||
).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 +415,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 +437,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)
|
||||
).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 +463,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,19 +2,16 @@ from typing import Any, cast
|
||||
|
||||
from flask_restx import Resource
|
||||
from pydantic import Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from controllers.common.agent_app_parameters import get_published_agent_app_feature_dict_and_user_input_form
|
||||
from controllers.common.fields import Parameters
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.app.error import AppUnavailableError
|
||||
from controllers.service_api.app.error import AgentNotPublishedError, AppUnavailableError
|
||||
from controllers.service_api.wraps import validate_app_token
|
||||
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
|
||||
from core.app.apps.agent_app.app_variable_projection import agent_app_variables_to_user_input_form
|
||||
from extensions.ext_database import db
|
||||
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
from fields.base import ResponseModel
|
||||
from models.agent import Agent, AgentConfigSnapshot, AgentScope, AgentSource, AgentStatus
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
from models.model import App, AppMode
|
||||
from services.app_service import AppService
|
||||
|
||||
@@ -35,38 +32,13 @@ register_response_schema_models(service_api_ns, Parameters, AppMetaResponse, App
|
||||
|
||||
|
||||
def _get_agent_app_feature_dict_and_user_input_form(app_model: App) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
app_model_config = app_model.app_model_config
|
||||
features_dict = cast(dict[str, Any], app_model_config.to_dict()) if app_model_config is not None else {}
|
||||
|
||||
agent = db.session.scalar(
|
||||
select(Agent)
|
||||
.where(
|
||||
Agent.tenant_id == app_model.tenant_id,
|
||||
Agent.app_id == app_model.id,
|
||||
Agent.scope == AgentScope.ROSTER,
|
||||
Agent.source == AgentSource.AGENT_APP,
|
||||
Agent.status == AgentStatus.ACTIVE,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if agent is None or not agent.active_config_snapshot_id:
|
||||
try:
|
||||
return get_published_agent_app_feature_dict_and_user_input_form(app_model)
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except AgentAppGeneratorError:
|
||||
raise AppUnavailableError()
|
||||
|
||||
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 AppUnavailableError()
|
||||
|
||||
agent_soul = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
|
||||
return features_dict, agent_app_variables_to_user_input_form(agent_soul.app_variables)
|
||||
|
||||
|
||||
@service_api_ns.route("/parameters")
|
||||
class AppParameterApi(Resource):
|
||||
|
||||
@@ -15,6 +15,7 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.app.error import (
|
||||
AgentNotPublishedError,
|
||||
AppUnavailableError,
|
||||
CompletionRequestError,
|
||||
ConversationCompletedError,
|
||||
@@ -31,6 +32,7 @@ from controllers.service_api.schema import (
|
||||
)
|
||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.errors.error import (
|
||||
ModelCurrentlyNotSupportError,
|
||||
@@ -248,6 +250,8 @@ class CompletionApi(Resource):
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
@@ -403,6 +407,8 @@ class ChatApi(Resource):
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
|
||||
@@ -7,6 +7,12 @@ class AppUnavailableError(BaseHTTPException):
|
||||
code = 400
|
||||
|
||||
|
||||
class AgentNotPublishedError(BaseHTTPException):
|
||||
error_code = "agent_not_published"
|
||||
description = "Agent has not been published. Please publish the Agent before using the API."
|
||||
code = 400
|
||||
|
||||
|
||||
class NotCompletionAppError(BaseHTTPException):
|
||||
error_code = "not_completion_app"
|
||||
description = "Please check if your Completion app mode matches the right API route."
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
from pydantic import BaseModel, Field, RootModel, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
@@ -13,24 +14,20 @@ from controllers.common.errors import FilenameNotExistsError, NoFileUploadedErro
|
||||
from controllers.common.fields import GeneratedAppResponse
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
query_params_from_request,
|
||||
register_response_schema_models,
|
||||
register_schema_model,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.dataset.error import PipelineRunError
|
||||
from controllers.service_api.dataset.rag_pipeline.serializers import serialize_upload_file
|
||||
from controllers.service_api.schema import (
|
||||
event_stream_response,
|
||||
json_or_event_stream_response,
|
||||
multipart_file_params,
|
||||
)
|
||||
from controllers.service_api.schema import event_stream_response, json_or_event_stream_response, multipart_file_params
|
||||
from controllers.service_api.wraps import DatasetApiResource
|
||||
from core.app.apps.pipeline.pipeline_generator import PipelineGenerator
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from fields.base import ResponseModel
|
||||
from libs import helper
|
||||
from libs.helper import dump_response
|
||||
from libs.login import current_user
|
||||
from models import Account
|
||||
from models.dataset import Dataset, Pipeline
|
||||
@@ -84,7 +81,7 @@ class DatasourcePluginResponse(ResponseModel):
|
||||
datasource_type: str | None = None
|
||||
title: str | None = None
|
||||
user_input_variables: list[dict[str, Any]] = Field(default_factory=list)
|
||||
credentials: list[DatasourceCredentialInfoResponse]
|
||||
credentials: list[DatasourceCredentialInfoResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DatasourcePluginListResponse(RootModel[list[DatasourcePluginResponse]]):
|
||||
@@ -100,14 +97,22 @@ class PipelineUploadFileResponse(ResponseModel):
|
||||
created_by: str
|
||||
created_at: str | None = None
|
||||
|
||||
@field_validator("created_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_created_at(cls, value: datetime | str | None) -> str | None:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
register_schema_model(service_api_ns, DatasourceNodeRunPayload)
|
||||
register_schema_model(service_api_ns, DatasourcePluginsQuery)
|
||||
register_schema_model(service_api_ns, PipelineRunApiEntity)
|
||||
register_schema_models(service_api_ns, DatasourcePluginsQuery)
|
||||
register_response_schema_models(
|
||||
service_api_ns,
|
||||
DatasourceCredentialInfoResponse,
|
||||
DatasourcePluginResponse,
|
||||
DatasourcePluginListResponse,
|
||||
GeneratedAppResponse,
|
||||
PipelineUploadFileResponse,
|
||||
)
|
||||
|
||||
@@ -119,8 +124,8 @@ class DatasourcePluginsApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="List Datasource Plugins",
|
||||
description=(
|
||||
"List the datasource nodes configured in the knowledge pipeline. Each node includes the "
|
||||
"plugin it uses plus the metadata needed to run it."
|
||||
"List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses "
|
||||
"plus the metadata needed to run it."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -152,14 +157,13 @@ class DatasourcePluginsApi(DatasetApiResource):
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
# Get query parameter to determine published or draft
|
||||
is_published: bool = request.args.get("is_published", default=True, type=bool)
|
||||
query = query_params_from_request(DatasourcePluginsQuery)
|
||||
|
||||
rag_pipeline_service: RagPipelineService = RagPipelineService()
|
||||
datasource_plugins: list[dict[Any, Any]] = rag_pipeline_service.get_datasource_plugins(
|
||||
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=is_published
|
||||
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=query.is_published
|
||||
)
|
||||
return datasource_plugins, 200
|
||||
return dump_response(DatasourcePluginListResponse, datasource_plugins), 200
|
||||
|
||||
|
||||
@service_api_ns.route("/datasets/<uuid:dataset_id>/pipeline/datasource/nodes/<string:node_id>/run")
|
||||
@@ -169,8 +173,8 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="Run Datasource Node",
|
||||
description=(
|
||||
"Execute a single datasource node within the knowledge pipeline. Returns a streaming "
|
||||
"response with the node execution results."
|
||||
"Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the "
|
||||
"node execution results."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -189,11 +193,6 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
}
|
||||
)
|
||||
@service_api_ns.expect(service_api_ns.models[DatasourceNodeRunPayload.__name__])
|
||||
@service_api_ns.response(
|
||||
200,
|
||||
"Datasource node run successfully",
|
||||
service_api_ns.models[GeneratedAppResponse.__name__],
|
||||
)
|
||||
def post(self, tenant_id: str, dataset_id: UUID, node_id: str):
|
||||
"""Resource for getting datasource plugins."""
|
||||
dataset_id_str = str(dataset_id)
|
||||
@@ -210,10 +209,11 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
datasource_node_run_api_entity = DatasourceNodeRunApiEntity.model_validate(
|
||||
{
|
||||
**payload.model_dump(exclude_none=True),
|
||||
"pipeline_id": str(pipeline.id),
|
||||
"pipeline_id": pipeline.id,
|
||||
"node_id": node_id,
|
||||
}
|
||||
)
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(
|
||||
PipelineGenerator.convert_to_event_stream(
|
||||
rag_pipeline_service.run_datasource_workflow_node(
|
||||
@@ -236,8 +236,8 @@ class PipelineRunApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="Run Pipeline",
|
||||
description=(
|
||||
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and "
|
||||
"blocking response modes."
|
||||
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response "
|
||||
"modes."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -293,6 +293,7 @@ class PipelineRunApi(DatasetApiResource):
|
||||
streaming=payload.response_mode == "streaming",
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except Exception as ex:
|
||||
raise PipelineRunError(description=str(ex))
|
||||
@@ -368,4 +369,4 @@ class KnowledgebasePipelineFileUploadApi(DatasetApiResource):
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
return serialize_upload_file(upload_file), 201
|
||||
return dump_response(PipelineUploadFileResponse, upload_file), 201
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""
|
||||
Serialization helpers for Service API knowledge pipeline endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models.model import UploadFile
|
||||
|
||||
|
||||
class UploadFileDict(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
size: int
|
||||
extension: str
|
||||
mime_type: str | None
|
||||
created_by: str
|
||||
created_at: str | None
|
||||
|
||||
|
||||
def serialize_upload_file(upload_file: UploadFile) -> UploadFileDict:
|
||||
return {
|
||||
"id": upload_file.id,
|
||||
"name": upload_file.name,
|
||||
"size": upload_file.size,
|
||||
"extension": upload_file.extension,
|
||||
"mime_type": upload_file.mime_type,
|
||||
"created_by": upload_file.created_by,
|
||||
"created_at": upload_file.created_at.isoformat() if upload_file.created_at else None,
|
||||
}
|
||||
@@ -8,8 +8,10 @@ from werkzeug.exceptions import Unauthorized
|
||||
|
||||
from constants import HEADER_NAME_APP_CODE
|
||||
from controllers.common import fields
|
||||
from controllers.common.agent_app_parameters import get_published_agent_app_feature_dict_and_user_input_form
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
|
||||
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_webapp_passport
|
||||
from models.model import App, AppMode, EndUser
|
||||
@@ -19,7 +21,7 @@ from services.feature_service import FeatureService
|
||||
from services.webapp_auth_service import WebAppAuthService
|
||||
|
||||
from . import web_ns
|
||||
from .error import AppUnavailableError
|
||||
from .error import AgentNotPublishedError, AppUnavailableError
|
||||
from .wraps import WebApiResource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -74,12 +76,21 @@ class AppParameterApi(WebApiResource):
|
||||
@web_ns.response(200, "Success", web_ns.models[fields.Parameters.__name__])
|
||||
def get(self, app_model: App, end_user: EndUser):
|
||||
"""Retrieve app parameters."""
|
||||
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||
features_dict: dict[str, Any]
|
||||
user_input_form: list[dict[str, Any]]
|
||||
if app_model.mode == AppMode.AGENT:
|
||||
try:
|
||||
features_dict, user_input_form = get_published_agent_app_feature_dict_and_user_input_form(app_model)
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except AgentAppGeneratorError:
|
||||
raise AppUnavailableError()
|
||||
elif app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||
workflow = app_model.workflow
|
||||
if workflow is None:
|
||||
raise AppUnavailableError()
|
||||
|
||||
features_dict: dict[str, Any] = workflow.features_dict
|
||||
features_dict = workflow.features_dict
|
||||
user_input_form = workflow.user_input_form(to_old_structure=True)
|
||||
else:
|
||||
app_model_config = app_model.app_model_config
|
||||
|
||||
@@ -11,6 +11,7 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import (
|
||||
AgentNotPublishedError,
|
||||
AppUnavailableError,
|
||||
CompletionRequestError,
|
||||
ConversationCompletedError,
|
||||
@@ -22,6 +23,7 @@ from controllers.web.error import (
|
||||
)
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.errors.error import (
|
||||
ModelCurrentlyNotSupportError,
|
||||
@@ -138,6 +140,8 @@ class CompletionApi(WebApiResource):
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
@@ -235,6 +239,8 @@ class ChatApi(WebApiResource):
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
except AgentAppNotPublishedError:
|
||||
raise AgentNotPublishedError()
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
|
||||
@@ -7,6 +7,12 @@ class AppUnavailableError(BaseHTTPException):
|
||||
code = 400
|
||||
|
||||
|
||||
class AgentNotPublishedError(BaseHTTPException):
|
||||
error_code = "agent_not_published"
|
||||
description = "Agent has not been published. Please publish the Agent before using the web app."
|
||||
code = 400
|
||||
|
||||
|
||||
class NotCompletionAppError(BaseHTTPException):
|
||||
error_code = "not_completion_app"
|
||||
description = "Please check if your Completion app mode matches the right API route."
|
||||
|
||||
@@ -9,6 +9,7 @@ from werkzeug.exceptions import Unauthorized
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import (
|
||||
AccessTokenData,
|
||||
AccessTokenResultResponse,
|
||||
LoginStatusResponse,
|
||||
SimpleResultDataResponse,
|
||||
@@ -115,9 +116,10 @@ class LoginApi(Resource):
|
||||
raise AuthenticationFailedError()
|
||||
|
||||
token = WebAppAuthService.login(account=account)
|
||||
response = make_response({"result": "success", "data": {"access_token": token}})
|
||||
# set_access_token_to_cookie(request, response, token, samesite="None", httponly=False)
|
||||
return response
|
||||
return AccessTokenResultResponse(result="success", data=AccessTokenData(access_token=token)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
# this api helps frontend to check whether user is authenticated
|
||||
@@ -136,14 +138,12 @@ class LoginStatusApi(Resource):
|
||||
)
|
||||
@web_ns.response(200, "Login status", web_ns.models[LoginStatusResponse.__name__])
|
||||
def get(self):
|
||||
app_code = request.args.get("app_code")
|
||||
user_id = request.args.get("user_id")
|
||||
query = LoginStatusQuery.model_validate(request.args.to_dict(flat=True))
|
||||
app_code = query.app_code
|
||||
user_id = query.user_id
|
||||
token = extract_webapp_access_token(request)
|
||||
if not app_code:
|
||||
return {
|
||||
"logged_in": bool(token),
|
||||
"app_logged_in": False,
|
||||
}
|
||||
return LoginStatusResponse(logged_in=bool(token), app_logged_in=False).model_dump(mode="json")
|
||||
app_id = AppService.get_app_id_by_code(app_code)
|
||||
is_public = not dify_config.ENTERPRISE_ENABLED or not WebAppAuthService.is_app_require_permission_check(
|
||||
app_id=app_id
|
||||
@@ -165,10 +165,7 @@ class LoginStatusApi(Resource):
|
||||
except Exception:
|
||||
app_logged_in = False
|
||||
|
||||
return {
|
||||
"logged_in": user_logged_in,
|
||||
"app_logged_in": app_logged_in,
|
||||
}
|
||||
return LoginStatusResponse(logged_in=user_logged_in, app_logged_in=app_logged_in).model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/logout")
|
||||
@@ -183,7 +180,8 @@ class LogoutApi(Resource):
|
||||
)
|
||||
@web_ns.response(200, "Logout successful", web_ns.models[SimpleResultResponse.__name__])
|
||||
def post(self):
|
||||
response = make_response({"result": "success"})
|
||||
# response-contract:ignore hand-crafted response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
# enterprise SSO sets same site to None in https deployment
|
||||
# so we need to logout by calling api
|
||||
clear_webapp_access_token_from_cookie(response, samesite="None")
|
||||
@@ -216,9 +214,8 @@ class EmailCodeLoginSendEmailApi(Resource):
|
||||
account = WebAppAuthService.get_user_through_email(payload.email)
|
||||
if account is None:
|
||||
raise AuthenticationFailedError()
|
||||
else:
|
||||
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
|
||||
return {"result": "success", "data": token}
|
||||
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/email-code-login/validity")
|
||||
@@ -277,9 +274,10 @@ class EmailCodeLoginApi(Resource):
|
||||
|
||||
token = WebAppAuthService.login(account=account)
|
||||
AccountService.reset_login_error_rate_limit(user_email)
|
||||
response = make_response({"result": "success", "data": {"access_token": token}})
|
||||
# set_access_token_to_cookie(request, response, token, samesite="None", httponly=False)
|
||||
return response
|
||||
return AccessTokenResultResponse(result="success", data=AccessTokenData(access_token=token)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
def _log_web_login_failure(*, email: str, reason: LoginFailureReason) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from flask import make_response, request
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
@@ -10,11 +10,12 @@ from werkzeug.exceptions import NotFound, Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
from constants import HEADER_NAME_APP_CODE
|
||||
from controllers.common.fields import AccessTokenData
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import WebAppAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import dump_response
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_webapp_access_token
|
||||
from models.enums import EndUserType
|
||||
@@ -28,7 +29,13 @@ class PassportQuery(BaseModel):
|
||||
|
||||
|
||||
register_schema_models(web_ns, PassportQuery)
|
||||
register_response_schema_models(web_ns, AccessTokenData)
|
||||
|
||||
|
||||
class PassportAccessTokenResponse(ResponseModel):
|
||||
access_token: str
|
||||
|
||||
|
||||
register_response_schema_models(web_ns, PassportAccessTokenResponse)
|
||||
|
||||
|
||||
@web_ns.route("/passport")
|
||||
@@ -45,7 +52,7 @@ class PassportResource(Resource):
|
||||
404: "Application or user not found",
|
||||
}
|
||||
)
|
||||
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[AccessTokenData.__name__])
|
||||
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[PassportAccessTokenResponse.__name__])
|
||||
def get(self):
|
||||
system_features = FeatureService.get_system_features()
|
||||
app_code = request.headers.get(HEADER_NAME_APP_CODE)
|
||||
@@ -59,8 +66,11 @@ class PassportResource(Resource):
|
||||
if app_auth_type != WebAppAuthType.PUBLIC:
|
||||
if not enterprise_user_decoded:
|
||||
raise WebAppAuthRequiredError()
|
||||
return exchange_token_for_existing_web_user(
|
||||
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
|
||||
return dump_response(
|
||||
PassportAccessTokenResponse,
|
||||
exchange_token_for_existing_web_user(
|
||||
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
|
||||
),
|
||||
)
|
||||
|
||||
# get site from db and check if it is normal
|
||||
@@ -110,12 +120,7 @@ class PassportResource(Resource):
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
response = make_response(
|
||||
{
|
||||
"access_token": tk,
|
||||
}
|
||||
)
|
||||
return response
|
||||
return dump_response(PassportAccessTokenResponse, {"access_token": tk})
|
||||
|
||||
|
||||
def decode_enterprise_webapp_user_id(jwt_token: str | None) -> dict[str, Any] | None:
|
||||
@@ -206,12 +211,7 @@ def exchange_token_for_existing_web_user(
|
||||
"exp": exp,
|
||||
}
|
||||
token: str = PassportService().issue(payload)
|
||||
resp = make_response(
|
||||
{
|
||||
"access_token": token,
|
||||
}
|
||||
)
|
||||
return resp
|
||||
return {"access_token": token}
|
||||
|
||||
|
||||
def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
@@ -244,12 +244,7 @@ def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
resp = make_response(
|
||||
{
|
||||
"access_token": tk,
|
||||
}
|
||||
)
|
||||
return resp
|
||||
return {"access_token": tk}
|
||||
|
||||
|
||||
def generate_session_id():
|
||||
|
||||
@@ -133,6 +133,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
||||
stop=app_generate_entity.model_conf.stop,
|
||||
stream=True,
|
||||
callbacks=[],
|
||||
request_metadata={"app_id": self.app_config.app_id},
|
||||
)
|
||||
|
||||
usage_dict: dict[str, LLMUsage | None] = {}
|
||||
|
||||
@@ -101,6 +101,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
||||
stop=app_generate_entity.model_conf.stop,
|
||||
stream=self.stream_tool_call,
|
||||
callbacks=[],
|
||||
request_metadata={"app_id": self.app_config.app_id},
|
||||
)
|
||||
|
||||
tool_calls: list[tuple[str, str, dict[str, Any]]] = []
|
||||
|
||||
@@ -61,10 +61,17 @@ _sensitive_word_avoidance_adapter: TypeAdapter[SensitiveWordAvoidanceConfig] = T
|
||||
|
||||
def _normalize_raw(raw: Any) -> Any:
|
||||
if isinstance(raw, dict):
|
||||
if raw.get("enabled") is None:
|
||||
enabled = raw.get("enabled")
|
||||
if enabled is None:
|
||||
raw = {**raw, "enabled": False}
|
||||
elif raw.get("enabled") is True and raw.get("config") is None:
|
||||
raw = {**raw, "config": {}}
|
||||
elif enabled is True:
|
||||
if raw.get("config") is None:
|
||||
raw = {**raw, "config": {}}
|
||||
else:
|
||||
# enabled is False or any falsy value —
|
||||
# drop extra fields (type, config) so they don't
|
||||
# violate SensitiveWordAvoidanceDisabledConfig.extra="forbid"
|
||||
raw = {"enabled": False}
|
||||
return raw
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
An Agent App has no legacy ``app_model_config``: its model / prompt live in the
|
||||
bound Agent Soul snapshot. To ride the existing chat message + SSE pipeline we
|
||||
synthesize an ``app_model_config``-shaped dict from the Soul (model + system
|
||||
prompt) plus any app-level feature flags (opening statement, follow-up, …)
|
||||
stored on ``app_model_config`` when present, then reuse the same sub-managers
|
||||
the chat app type uses.
|
||||
prompt) plus app-level feature flags from Agent Soul, while preserving any
|
||||
legacy ``app_model_config`` feature flags when present. Then we reuse the same
|
||||
sub-managers the chat app type uses.
|
||||
"""
|
||||
|
||||
from typing import Any, cast
|
||||
@@ -21,6 +21,7 @@ from core.app.app_config.entities import (
|
||||
EasyUIBasedAppModelConfigFrom,
|
||||
PromptTemplateEntity,
|
||||
)
|
||||
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 models.agent_config_entities import AgentSoulConfig
|
||||
from models.model import App, AppMode, AppModelConfig, AppModelConfigDict, Conversation
|
||||
@@ -79,12 +80,11 @@ class AgentAppConfigManager(BaseAppConfigManager):
|
||||
) -> dict[str, Any]:
|
||||
"""Shape a Soul + feature flags into an ``app_model_config``-style dict.
|
||||
|
||||
Feature flags (opening statement / follow-up / tts / stt / citations /
|
||||
moderation / annotation) come from ``app_model_config`` when present
|
||||
(Q3: stored there), otherwise defaults; model + prompt always come from
|
||||
Feature flags come from Agent Soul and fill gaps in the legacy
|
||||
``app_model_config`` when one exists; model + prompt always come from
|
||||
the Agent Soul (the single source of truth for those).
|
||||
"""
|
||||
base: dict[str, Any] = dict(app_model_config.to_dict()) if app_model_config else {}
|
||||
base = merge_agent_app_features(agent_soul=agent_soul, app_model_config=app_model_config)
|
||||
|
||||
model = agent_soul.model
|
||||
if model is not None:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from typing import Any
|
||||
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
|
||||
|
||||
def merge_agent_app_features(
|
||||
*,
|
||||
agent_soul: AgentSoulConfig,
|
||||
app_model_config: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Project public Agent App features from legacy config plus Agent Soul.
|
||||
|
||||
The hidden backing app may still carry legacy presentation fields such as
|
||||
opening statements. Agent Soul is the source of truth for Agent-owned
|
||||
features like file upload, so Soul fields override same-named legacy keys.
|
||||
"""
|
||||
features: dict[str, Any] = dict(app_model_config.to_dict()) if app_model_config else {}
|
||||
soul_features = agent_soul.app_features.model_dump(mode="json", exclude_none=True)
|
||||
features.update(soul_features)
|
||||
return features
|
||||
|
||||
|
||||
__all__ = ["merge_agent_app_features"]
|
||||
@@ -32,6 +32,7 @@ from constants import UUID_NIL
|
||||
from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter
|
||||
from core.app.apps.agent_app.app_config_manager import AgentAppConfigManager
|
||||
from core.app.apps.agent_app.app_runner import AgentAppRunner
|
||||
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
from core.app.apps.agent_app.generate_response_converter import AgentAppGenerateResponseConverter
|
||||
from core.app.apps.agent_app.runtime_request_builder import AgentAppRuntimeRequestBuilder
|
||||
from core.app.apps.agent_app.session_store import AgentAppRuntimeSessionStore
|
||||
@@ -64,10 +65,6 @@ from services.conversation_service import ConversationService
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentAppGeneratorError(ValueError):
|
||||
"""Raised when an Agent App turn cannot be set up."""
|
||||
|
||||
|
||||
def _append_prompt_file_mappings(query: str, prompt_file_mappings: Sequence[JsonValue]) -> str:
|
||||
"""Append raw request file references to the backend user prompt."""
|
||||
if not prompt_file_mappings:
|
||||
@@ -614,6 +611,8 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
"build_draft" if draft.draft_type == AgentConfigDraftType.DEBUG_BUILD else "draft"
|
||||
)
|
||||
return agent, draft.id, config_version_kind, agent_soul
|
||||
if not agent.active_config_snapshot_id or not agent.active_config_is_published:
|
||||
raise AgentAppNotPublishedError("Agent has not been published")
|
||||
_, snapshot, agent_soul = self._resolve_agent_by_id(
|
||||
tenant_id=app_model.tenant_id,
|
||||
agent_id=agent.id,
|
||||
@@ -709,4 +708,4 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
return agent, draft, agent_soul
|
||||
|
||||
|
||||
__all__ = ["AgentAppGenerator", "AgentAppGeneratorError"]
|
||||
__all__ = ["AgentAppGenerator", "AgentAppGeneratorError", "AgentAppNotPublishedError"]
|
||||
|
||||
@@ -372,14 +372,14 @@ class _AgentProcessRecorder:
|
||||
row = MessageAgentThought(
|
||||
message_id=self._message_id,
|
||||
message_chain_id=None,
|
||||
thought=thought,
|
||||
tool=tool,
|
||||
thought=thought or "",
|
||||
tool=tool or "",
|
||||
tool_labels_str=_tool_labels(tool),
|
||||
tool_meta_str="{}",
|
||||
tool_input=tool_input,
|
||||
observation=None,
|
||||
tool_input=tool_input or "",
|
||||
observation="",
|
||||
tool_process_data=None,
|
||||
message=None,
|
||||
message="",
|
||||
message_token=0,
|
||||
message_unit_price=Decimal(0),
|
||||
message_price_unit=Decimal("0.001"),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AgentAppGeneratorError(ValueError):
|
||||
"""Raised when an Agent App turn cannot be set up."""
|
||||
|
||||
|
||||
class AgentAppNotPublishedError(AgentAppGeneratorError):
|
||||
"""Raised when a public Agent App runtime is requested before publish."""
|
||||
@@ -5,6 +5,8 @@ from collections.abc import Generator, Mapping, Sequence
|
||||
from mimetypes import guess_extension
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||
from core.app.apps.exc import GenerateTaskStoppedError
|
||||
@@ -423,7 +425,9 @@ class AppRunner:
|
||||
_logger.exception("Failed to save image file")
|
||||
return
|
||||
|
||||
# Create MessageFile record
|
||||
# Create MessageFile record.
|
||||
# Use an independent session so this side-effect write does not
|
||||
# commit or close the caller's request-scoped session.
|
||||
message_file = MessageFile(
|
||||
message_id=message_id,
|
||||
type=FileType.IMAGE,
|
||||
@@ -437,9 +441,8 @@ class AppRunner:
|
||||
created_by=user_id,
|
||||
)
|
||||
|
||||
db.session.add(message_file)
|
||||
db.session.commit()
|
||||
db.session.refresh(message_file)
|
||||
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
|
||||
session.add(message_file)
|
||||
|
||||
# Publish QueueMessageFileEvent
|
||||
queue_manager.publish(
|
||||
|
||||
@@ -232,6 +232,7 @@ class ChatAppRunner(AppRunner):
|
||||
model_parameters=application_generate_entity.model_conf.parameters,
|
||||
stop=stop,
|
||||
stream=application_generate_entity.stream,
|
||||
request_metadata={"app_id": app_config.app_id},
|
||||
)
|
||||
|
||||
# handle invoke result
|
||||
|
||||
@@ -193,6 +193,7 @@ class CompletionAppRunner(AppRunner):
|
||||
model_parameters=application_generate_entity.model_conf.parameters,
|
||||
stop=stop,
|
||||
stream=application_generate_entity.stream,
|
||||
request_metadata={"app_id": app_config.app_id},
|
||||
)
|
||||
|
||||
# handle invoke result
|
||||
|
||||
@@ -122,7 +122,7 @@ class MessageStreamResponse(StreamResponse):
|
||||
event: StreamEvent = StreamEvent.MESSAGE
|
||||
id: str
|
||||
answer: str
|
||||
from_variable_selector: list[str] | None = None
|
||||
from_variable_selector: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MessageAudioStreamResponse(StreamResponse):
|
||||
@@ -151,7 +151,7 @@ class MessageEndStreamResponse(StreamResponse):
|
||||
event: StreamEvent = StreamEvent.MESSAGE_END
|
||||
id: str
|
||||
metadata: Mapping[str, object] = Field(default_factory=dict)
|
||||
files: Sequence[Mapping[str, Any]] | None = None
|
||||
files: Sequence[Mapping[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MessageFileStreamResponse(StreamResponse):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from threading import Thread
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -44,7 +44,7 @@ from core.app.entities.task_entities import (
|
||||
)
|
||||
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
|
||||
from core.app.task_pipeline.message_cycle_manager import MessageCycleManager
|
||||
from core.app.task_pipeline.message_file_utils import prepare_file_dict
|
||||
from core.app.task_pipeline.message_file_utils import MessageFileInfoDict, prepare_file_dict
|
||||
from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk
|
||||
from core.model_manager import ModelInstance
|
||||
from core.ops.entities.trace_entity import TraceTaskName
|
||||
@@ -466,10 +466,10 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline[EasyUIAppGenerat
|
||||
:return:
|
||||
"""
|
||||
self._task_state.metadata.usage = self._task_state.llm_result.usage
|
||||
metadata_dict = self._task_state.metadata.model_dump()
|
||||
metadata_dict = self._task_state.metadata.model_dump(exclude_none=True)
|
||||
|
||||
# Fetch files associated with this message
|
||||
files = None
|
||||
files: list[MessageFileInfoDict] = []
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
message_files = session.scalars(select(MessageFile).where(MessageFile.message_id == self._message_id)).all()
|
||||
|
||||
@@ -492,13 +492,13 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline[EasyUIAppGenerat
|
||||
file_dict = prepare_file_dict(message_file, upload_files_map)
|
||||
files_list.append(file_dict)
|
||||
|
||||
files = files_list or None
|
||||
files = files_list
|
||||
|
||||
return MessageEndStreamResponse(
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
id=self._message_id,
|
||||
metadata=metadata_dict,
|
||||
files=files,
|
||||
files=cast(Sequence[Mapping[str, Any]], files),
|
||||
)
|
||||
|
||||
def _agent_message_to_stream_response(self, answer: str, message_id: str) -> AgentMessageStreamResponse:
|
||||
@@ -528,11 +528,11 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline[EasyUIAppGenerat
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
id=agent_thought.id,
|
||||
position=agent_thought.position,
|
||||
thought=agent_thought.thought,
|
||||
observation=agent_thought.observation,
|
||||
tool=agent_thought.tool,
|
||||
thought=agent_thought.thought or "",
|
||||
observation=agent_thought.observation or "",
|
||||
tool=agent_thought.tool or "",
|
||||
tool_labels=agent_thought.tool_labels,
|
||||
tool_input=agent_thought.tool_input,
|
||||
tool_input=agent_thought.tool_input or "",
|
||||
message_files=agent_thought.files,
|
||||
)
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ class MessageCycleManager:
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
id=message_id,
|
||||
answer=answer,
|
||||
from_variable_selector=from_variable_selector,
|
||||
from_variable_selector=from_variable_selector or [],
|
||||
event=event_type or StreamEvent.MESSAGE,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
@@ -10,6 +10,7 @@ from core.app.entities.queue_entities import QueueRetrieverResourcesEvent
|
||||
from core.rag.entities import RetrievalSourceMetadata
|
||||
from core.rag.index_processor.constant.index_type import IndexStructureType
|
||||
from core.rag.models.document import Document
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import ChildChunk, DatasetQuery, DocumentSegment
|
||||
from models.dataset import Document as DatasetDocument
|
||||
from models.enums import CreatorUserRole, DatasetQuerySource
|
||||
@@ -46,47 +47,52 @@ class DatasetIndexToolCallbackHandler:
|
||||
created_by=self._user_id,
|
||||
)
|
||||
|
||||
session.add(dataset_query)
|
||||
session.commit()
|
||||
# Use an independent session so this audit-log side effect does
|
||||
# not commit or close the caller's request-scoped session.
|
||||
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as independent_session:
|
||||
independent_session.add(dataset_query)
|
||||
|
||||
def on_tool_end(self, documents: list[Document], session: scoped_session):
|
||||
"""Handle tool end."""
|
||||
for document in documents:
|
||||
if document.metadata is not None:
|
||||
document_id = document.metadata["document_id"]
|
||||
dataset_document_stmt = select(DatasetDocument).where(DatasetDocument.id == document_id)
|
||||
dataset_document = session.scalar(dataset_document_stmt)
|
||||
if not dataset_document:
|
||||
_logger.warning(
|
||||
"Expected DatasetDocument record to exist, but none was found, document_id=%s",
|
||||
document_id,
|
||||
)
|
||||
continue
|
||||
if dataset_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX:
|
||||
child_chunk_stmt = select(ChildChunk).where(
|
||||
ChildChunk.index_node_id == document.metadata["doc_id"],
|
||||
ChildChunk.dataset_id == dataset_document.dataset_id,
|
||||
ChildChunk.document_id == dataset_document.id,
|
||||
)
|
||||
child_chunk = session.scalar(child_chunk_stmt)
|
||||
if child_chunk:
|
||||
session.execute(
|
||||
update(DocumentSegment)
|
||||
.where(DocumentSegment.id == child_chunk.segment_id)
|
||||
.values(hit_count=DocumentSegment.hit_count + 1)
|
||||
# Use an independent session so hit-count updates do not
|
||||
# interfere with the caller's request-scoped session.
|
||||
with Session(db.engine, expire_on_commit=False) as independent_session:
|
||||
for document in documents:
|
||||
if document.metadata is not None:
|
||||
document_id = document.metadata["document_id"]
|
||||
dataset_document_stmt = select(DatasetDocument).where(DatasetDocument.id == document_id)
|
||||
dataset_document = independent_session.scalar(dataset_document_stmt)
|
||||
if not dataset_document:
|
||||
_logger.warning(
|
||||
"Expected DatasetDocument record to exist, but none was found, document_id=%s",
|
||||
document_id,
|
||||
)
|
||||
else:
|
||||
conditions = [DocumentSegment.index_node_id == document.metadata["doc_id"]]
|
||||
continue
|
||||
if dataset_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX:
|
||||
child_chunk_stmt = select(ChildChunk).where(
|
||||
ChildChunk.index_node_id == document.metadata["doc_id"],
|
||||
ChildChunk.dataset_id == dataset_document.dataset_id,
|
||||
ChildChunk.document_id == dataset_document.id,
|
||||
)
|
||||
child_chunk = independent_session.scalar(child_chunk_stmt)
|
||||
if child_chunk:
|
||||
independent_session.execute(
|
||||
update(DocumentSegment)
|
||||
.where(DocumentSegment.id == child_chunk.segment_id)
|
||||
.values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
else:
|
||||
conditions = [DocumentSegment.index_node_id == document.metadata["doc_id"]]
|
||||
|
||||
if "dataset_id" in document.metadata:
|
||||
conditions.append(DocumentSegment.dataset_id == document.metadata["dataset_id"])
|
||||
if "dataset_id" in document.metadata:
|
||||
conditions.append(DocumentSegment.dataset_id == document.metadata["dataset_id"])
|
||||
|
||||
# add hit count to document segment
|
||||
session.execute(
|
||||
update(DocumentSegment).where(*conditions).values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
# add hit count to document segment
|
||||
independent_session.execute(
|
||||
update(DocumentSegment).where(*conditions).values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
independent_session.commit()
|
||||
|
||||
# TODO(-LAN-): Improve type check
|
||||
def return_retriever_resource_info(self, resource: Sequence[RetrievalSourceMetadata]):
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
from core.datasource.__base.datasource_plugin import DatasourcePlugin
|
||||
from core.datasource.entities.datasource_entities import DatasourceProviderEntityWithPlugin, DatasourceProviderType
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.plugin.impl.tool import PluginToolManager
|
||||
from core.tools.errors import ToolProviderCredentialValidationError
|
||||
|
||||
@@ -78,11 +78,11 @@ class DatasourcePluginProviderController(ABC):
|
||||
if not credential_schema.required and credentials[credential_name] is None:
|
||||
continue
|
||||
|
||||
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
|
||||
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
elif credential_schema.type == ProviderConfig.Type.SELECT:
|
||||
elif credential_schema.type == ProviderConfigType.SELECT:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
@@ -107,9 +107,9 @@ class DatasourcePluginProviderController(ABC):
|
||||
default_value = credential_schema.default
|
||||
# parse default value into the correct type
|
||||
if credential_schema.type in {
|
||||
ProviderConfig.Type.SECRET_INPUT,
|
||||
ProviderConfig.Type.TEXT_INPUT,
|
||||
ProviderConfig.Type.SELECT,
|
||||
ProviderConfigType.SECRET_INPUT,
|
||||
ProviderConfigType.TEXT_INPUT,
|
||||
ProviderConfigType.SELECT,
|
||||
}:
|
||||
default_value = str(default_value)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from urllib.parse import urlparse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from configs import dify_config
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.helper import encrypter
|
||||
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||
from core.mcp.types import OAuthClientInformation, OAuthClientMetadata, OAuthTokens
|
||||
@@ -315,7 +315,7 @@ class MCPProviderEntity(BaseModel):
|
||||
return data
|
||||
|
||||
# Create dynamic config only for encrypted fields
|
||||
config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in encrypted_fields]
|
||||
config = [BasicProviderConfig(type=ProviderConfigType.SECRET_INPUT, name=key) for key in encrypted_fields]
|
||||
|
||||
encrypter_instance, _ = create_provider_encrypter(
|
||||
tenant_id=self.tenant_id,
|
||||
|
||||
@@ -165,34 +165,35 @@ class ModelSettings(BaseModel):
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
class ProviderConfigType(StrEnum):
|
||||
SECRET_INPUT = CommonParameterType.SECRET_INPUT
|
||||
TEXT_INPUT = CommonParameterType.TEXT_INPUT
|
||||
SELECT = CommonParameterType.SELECT
|
||||
BOOLEAN = CommonParameterType.BOOLEAN
|
||||
APP_SELECTOR = CommonParameterType.APP_SELECTOR
|
||||
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
|
||||
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
|
||||
|
||||
@classmethod
|
||||
def value_of(cls, value: str) -> ProviderConfigType:
|
||||
"""
|
||||
Get value of given mode.
|
||||
|
||||
:param value: mode value
|
||||
:return: mode
|
||||
"""
|
||||
for mode in cls:
|
||||
if mode.value == value:
|
||||
return mode
|
||||
raise ValueError(f"invalid mode value {value}")
|
||||
|
||||
|
||||
class BasicProviderConfig(BaseModel):
|
||||
"""
|
||||
Base model class for common provider settings like credentials
|
||||
"""
|
||||
|
||||
class Type(StrEnum):
|
||||
SECRET_INPUT = CommonParameterType.SECRET_INPUT
|
||||
TEXT_INPUT = CommonParameterType.TEXT_INPUT
|
||||
SELECT = CommonParameterType.SELECT
|
||||
BOOLEAN = CommonParameterType.BOOLEAN
|
||||
APP_SELECTOR = CommonParameterType.APP_SELECTOR
|
||||
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
|
||||
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
|
||||
|
||||
@classmethod
|
||||
def value_of(cls, value: str) -> ProviderConfig.Type:
|
||||
"""
|
||||
Get value of given mode.
|
||||
|
||||
:param value: mode value
|
||||
:return: mode
|
||||
"""
|
||||
for mode in cls:
|
||||
if mode.value == value:
|
||||
return mode
|
||||
raise ValueError(f"invalid mode value {value}")
|
||||
|
||||
type: Type = Field(..., description="The type of the credentials")
|
||||
type: ProviderConfigType = Field(..., description="The type of the credentials")
|
||||
name: str = Field(..., description="The name of the credentials")
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from typing import Any, Protocol
|
||||
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.helper import encrypter
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
encrypted = encrypter.encrypt_token(self.tenant_id, data[field_name] or "")
|
||||
data[field_name] = encrypted
|
||||
@@ -81,7 +81,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
if len(data[field_name]) > 6:
|
||||
data[field_name] = (
|
||||
@@ -112,7 +112,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
with contextlib.suppress(Exception):
|
||||
# if the value is None or empty string, skip decrypt
|
||||
|
||||
@@ -124,6 +124,7 @@ class ModelInstance:
|
||||
stop: list[str] | None = None,
|
||||
stream: Literal[True] = True,
|
||||
callbacks: list[Callback] | None = None,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Generator: ...
|
||||
|
||||
@overload
|
||||
@@ -135,6 +136,7 @@ class ModelInstance:
|
||||
stop: list[str] | None = None,
|
||||
stream: Literal[False] = False,
|
||||
callbacks: list[Callback] | None = None,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> LLMResult: ...
|
||||
|
||||
@overload
|
||||
@@ -146,6 +148,7 @@ class ModelInstance:
|
||||
stop: list[str] | None = None,
|
||||
stream: bool = True,
|
||||
callbacks: list[Callback] | None = None,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Union[LLMResult, Generator]: ...
|
||||
|
||||
def invoke_llm(
|
||||
@@ -156,6 +159,7 @@ class ModelInstance:
|
||||
stop: Sequence[str] | None = None,
|
||||
stream: bool = True,
|
||||
callbacks: list[Callback] | None = None,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Union[LLMResult, Generator]:
|
||||
"""
|
||||
Invoke large language model
|
||||
@@ -166,6 +170,7 @@ class ModelInstance:
|
||||
:param stop: stop words
|
||||
:param stream: is stream response
|
||||
:param callbacks: callbacks
|
||||
:param request_metadata: optional request metadata
|
||||
:return: full response or stream response chunk generator result
|
||||
"""
|
||||
if not isinstance(self.model_type_instance, LargeLanguageModel):
|
||||
@@ -182,6 +187,7 @@ class ModelInstance:
|
||||
stop=list(stop) if stop else None,
|
||||
stream=stream,
|
||||
callbacks=callbacks,
|
||||
request_metadata=request_metadata,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +78,11 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
|
||||
if not user_id:
|
||||
user = EndUserService.get_or_create_end_user(app)
|
||||
else:
|
||||
user = cls._get_user(user_id, app)
|
||||
try:
|
||||
user = cls._get_user(user_id, app)
|
||||
except ValueError:
|
||||
# Plugins such as WeCom Bot pass external sender IDs rather than EndUser UUIDs.
|
||||
user = EndUserService.get_or_create_end_user(app, user_id=user_id)
|
||||
|
||||
conversation_id = conversation_id or ""
|
||||
|
||||
@@ -232,6 +236,13 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
|
||||
EndUser.app_id == app.id,
|
||||
)
|
||||
user = session.scalar(stmt)
|
||||
if not user:
|
||||
stmt = select(EndUser).where(
|
||||
EndUser.session_id == user_id,
|
||||
EndUser.tenant_id == app.tenant_id,
|
||||
EndUser.app_id == app.id,
|
||||
)
|
||||
user = session.scalar(stmt)
|
||||
if not user:
|
||||
stmt = select(Account).where(
|
||||
Account.id == user_id,
|
||||
|
||||
@@ -5,12 +5,13 @@ from pydantic import BaseModel
|
||||
from core.plugin.entities.plugin import PluginDeclaration, PluginInstallationSource
|
||||
|
||||
|
||||
class PluginBundleDependency(BaseModel):
|
||||
class Type(StrEnum):
|
||||
Github = PluginInstallationSource.Github.value
|
||||
Marketplace = PluginInstallationSource.Marketplace.value
|
||||
Package = PluginInstallationSource.Package.value
|
||||
class PluginBundleDependencyType(StrEnum):
|
||||
Github = PluginInstallationSource.Github.value
|
||||
Marketplace = PluginInstallationSource.Marketplace.value
|
||||
Package = PluginInstallationSource.Package.value
|
||||
|
||||
|
||||
class PluginBundleDependency(BaseModel):
|
||||
class Github(BaseModel):
|
||||
repo_address: str
|
||||
repo: str
|
||||
@@ -26,5 +27,5 @@ class PluginBundleDependency(BaseModel):
|
||||
unique_identifier: str
|
||||
manifest: PluginDeclaration
|
||||
|
||||
type: Type
|
||||
type: PluginBundleDependencyType
|
||||
value: Github | Marketplace | Package
|
||||
|
||||
@@ -32,6 +32,7 @@ class EndpointEntity(BasePluginEntity):
|
||||
entity of an endpoint
|
||||
"""
|
||||
|
||||
# TODO: Confirm daemon masks secret-input settings before endpoint list responses expose them.
|
||||
settings: dict[str, Any]
|
||||
tenant_id: str
|
||||
plugin_id: str
|
||||
|
||||
@@ -57,11 +57,12 @@ class MCPServerParameterType(StrEnum):
|
||||
OBJECT = auto()
|
||||
|
||||
|
||||
class PluginParameterAutoGenerate(BaseModel):
|
||||
class Type(StrEnum):
|
||||
PROMPT_INSTRUCTION = auto()
|
||||
class PluginParameterAutoGenerateType(StrEnum):
|
||||
PROMPT_INSTRUCTION = auto()
|
||||
|
||||
type: Type
|
||||
|
||||
class PluginParameterAutoGenerate(BaseModel):
|
||||
type: PluginParameterAutoGenerateType
|
||||
|
||||
|
||||
class PluginParameterTemplate(BaseModel):
|
||||
|
||||
@@ -166,12 +166,13 @@ class PluginEntity(PluginInstallation):
|
||||
return self
|
||||
|
||||
|
||||
class PluginDependency(BaseModel):
|
||||
class Type(StrEnum):
|
||||
Github = PluginInstallationSource.Github
|
||||
Marketplace = PluginInstallationSource.Marketplace
|
||||
Package = PluginInstallationSource.Package
|
||||
class PluginDependencyType(StrEnum):
|
||||
Github = PluginInstallationSource.Github
|
||||
Marketplace = PluginInstallationSource.Marketplace
|
||||
Package = PluginInstallationSource.Package
|
||||
|
||||
|
||||
class PluginDependency(BaseModel):
|
||||
class Github(BaseModel):
|
||||
repo: str
|
||||
version: str
|
||||
@@ -194,7 +195,7 @@ class PluginDependency(BaseModel):
|
||||
plugin_unique_identifier: str
|
||||
version: str | None = None
|
||||
|
||||
type: Type
|
||||
type: PluginDependencyType
|
||||
value: Github | Marketplace | Package
|
||||
current_identifier: str | None = None
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ _POLLING_UNSUPPORTED_ERROR_MESSAGE = "does not support polling"
|
||||
|
||||
class PluginModelClient(BasePluginClient):
|
||||
@staticmethod
|
||||
def _dispatch_payload(*, user_id: str | None, data: dict[str, Any]) -> dict[str, Any]:
|
||||
def _dispatch_payload(*, user_id: str | None, data: dict[str, Any], app_id: str | None = None) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"data": data}
|
||||
if user_id is not None:
|
||||
payload["user_id"] = user_id
|
||||
if app_id is not None:
|
||||
payload["app_id"] = app_id
|
||||
return payload
|
||||
|
||||
def fetch_model_providers(self, tenant_id: str) -> Sequence[PluginModelProviderEntity]:
|
||||
@@ -166,6 +168,7 @@ class PluginModelClient(BasePluginClient):
|
||||
tools: list[PromptMessageTool] | None = None,
|
||||
stop: list[str] | None = None,
|
||||
stream: bool = True,
|
||||
app_id: str | None = None,
|
||||
) -> Generator[LLMResultChunk, None, None]:
|
||||
"""
|
||||
Invoke llm
|
||||
@@ -188,6 +191,7 @@ class PluginModelClient(BasePluginClient):
|
||||
"stop": stop,
|
||||
"stream": stream,
|
||||
},
|
||||
app_id=app_id,
|
||||
)
|
||||
),
|
||||
headers={
|
||||
|
||||
@@ -317,21 +317,39 @@ class PluginModelRuntime(ModelRuntime):
|
||||
stream: bool,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> LLMResult | Generator[LLMResultChunk, None, None]:
|
||||
del request_metadata
|
||||
app_id = request_metadata.get("app_id") if request_metadata else None
|
||||
if not isinstance(app_id, str):
|
||||
app_id = None
|
||||
plugin_id, provider_name = self._split_provider(provider)
|
||||
result = self.client.invoke_llm(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.user_id,
|
||||
plugin_id=plugin_id,
|
||||
provider=provider_name,
|
||||
model=model,
|
||||
credentials=credentials,
|
||||
model_parameters=model_parameters,
|
||||
prompt_messages=list(prompt_messages),
|
||||
tools=tools,
|
||||
stop=list(stop) if stop else None,
|
||||
stream=stream,
|
||||
)
|
||||
if app_id is None:
|
||||
result = self.client.invoke_llm(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.user_id,
|
||||
plugin_id=plugin_id,
|
||||
provider=provider_name,
|
||||
model=model,
|
||||
credentials=credentials,
|
||||
model_parameters=model_parameters,
|
||||
prompt_messages=list(prompt_messages),
|
||||
tools=tools,
|
||||
stop=list(stop) if stop else None,
|
||||
stream=stream,
|
||||
)
|
||||
else:
|
||||
result = self.client.invoke_llm(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.user_id,
|
||||
plugin_id=plugin_id,
|
||||
provider=provider_name,
|
||||
model=model,
|
||||
credentials=credentials,
|
||||
model_parameters=model_parameters,
|
||||
prompt_messages=list(prompt_messages),
|
||||
tools=tools,
|
||||
stop=list(stop) if stop else None,
|
||||
stream=stream,
|
||||
app_id=app_id,
|
||||
)
|
||||
if stream:
|
||||
return result
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.entities.tool_entities import (
|
||||
ToolProviderEntity,
|
||||
@@ -71,11 +71,11 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
|
||||
if not credential_schema.required and credentials[credential_name] is None:
|
||||
continue
|
||||
|
||||
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
|
||||
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
elif credential_schema.type == ProviderConfig.Type.SELECT:
|
||||
elif credential_schema.type == ProviderConfigType.SELECT:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
@@ -100,9 +100,9 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
|
||||
default_value = credential_schema.default
|
||||
# parse default value into the correct type
|
||||
if credential_schema.type in {
|
||||
ProviderConfig.Type.SECRET_INPUT,
|
||||
ProviderConfig.Type.TEXT_INPUT,
|
||||
ProviderConfig.Type.SELECT,
|
||||
ProviderConfigType.SECRET_INPUT,
|
||||
ProviderConfigType.TEXT_INPUT,
|
||||
ProviderConfigType.SELECT,
|
||||
}:
|
||||
default_value = str(default_value)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import override
|
||||
from pydantic import Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool_provider import ToolProviderController
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.custom_tool.tool import ApiTool
|
||||
@@ -41,7 +41,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
ProviderConfig(
|
||||
name="auth_type",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
options=[
|
||||
ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="无")),
|
||||
ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")),
|
||||
@@ -60,20 +60,20 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
name="api_key_header",
|
||||
required=False,
|
||||
default="Authorization",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"),
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_value",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SECRET_INPUT,
|
||||
type=ProviderConfigType.SECRET_INPUT,
|
||||
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_header_prefix",
|
||||
required=False,
|
||||
default="basic",
|
||||
type=ProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
help=I18nObject(en_US="The prefix of the api key header", zh_Hans="api key header 的前缀"),
|
||||
options=[
|
||||
ProviderConfig.Option(value="basic", label=I18nObject(en_US="Basic", zh_Hans="Basic")),
|
||||
@@ -89,7 +89,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
name="api_key_query_param",
|
||||
required=False,
|
||||
default="key",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
help=I18nObject(
|
||||
en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称"
|
||||
),
|
||||
@@ -97,7 +97,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
ProviderConfig(
|
||||
name="api_key_value",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SECRET_INPUT,
|
||||
type=ProviderConfigType.SECRET_INPUT,
|
||||
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Union, override
|
||||
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.helper.provider_cache import ProviderCredentialsCache
|
||||
from core.helper.provider_encryption import ProviderConfigCache, ProviderConfigEncrypter, create_provider_encrypter
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
@@ -142,7 +142,7 @@ def masked_credentials(
|
||||
if not config:
|
||||
masked_credentials[key] = value
|
||||
continue
|
||||
if config.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if config.type == ProviderConfigType.SECRET_INPUT:
|
||||
if len(value) <= 4:
|
||||
masked_credentials[key] = "*" * len(value)
|
||||
else:
|
||||
|
||||
@@ -549,7 +549,11 @@ class DifyNodeFactory(NodeFactory):
|
||||
"credentials_provider": self._llm_credentials_provider,
|
||||
"model_factory": self._llm_model_factory,
|
||||
"model_instance": (
|
||||
self._wrap_model_instance_for_node(node_data=validated_node_data, model_instance=model_instance)
|
||||
self._wrap_model_instance_for_node(
|
||||
node_data=validated_node_data,
|
||||
model_instance=model_instance,
|
||||
request_metadata={"app_id": self._dify_context.app_id},
|
||||
)
|
||||
if wrap_model_instance
|
||||
else model_instance
|
||||
),
|
||||
@@ -581,13 +585,14 @@ class DifyNodeFactory(NodeFactory):
|
||||
*,
|
||||
node_data: LLMCompatibleNodeData,
|
||||
model_instance: ModelInstance,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> DifyPreparedLLM:
|
||||
# Only graphon's LLM node consumes the polling protocol. Keep classifier
|
||||
# and extractor nodes on the existing wrapper even if the same model
|
||||
# advertises polling support.
|
||||
if node_data.type == BuiltinNodeTypes.LLM and DifyNodeFactory._supports_plugin_llm_polling(model_instance):
|
||||
return DifyPreparedPollingLLM(model_instance)
|
||||
return DifyPreparedLLM(model_instance)
|
||||
return DifyPreparedPollingLLM(model_instance, request_metadata=request_metadata)
|
||||
return DifyPreparedLLM(model_instance, request_metadata=request_metadata)
|
||||
|
||||
@staticmethod
|
||||
def _supports_plugin_llm_polling(model_instance: ModelInstance) -> bool:
|
||||
|
||||
@@ -150,8 +150,9 @@ class DifyFileReferenceFactory(FileReferenceFactoryProtocol):
|
||||
class DifyPreparedLLM(LLMProtocol):
|
||||
"""Workflow-layer adapter that hides the full `ModelInstance` API from `graphon` nodes."""
|
||||
|
||||
def __init__(self, model_instance: ModelInstance) -> None:
|
||||
def __init__(self, model_instance: ModelInstance, request_metadata: Mapping[str, object] | None = None) -> None:
|
||||
self._model_instance = model_instance
|
||||
self._request_metadata = request_metadata
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -230,6 +231,7 @@ class DifyPreparedLLM(LLMProtocol):
|
||||
tools=list(tools or []),
|
||||
stop=list(stop or []),
|
||||
stream=stream,
|
||||
request_metadata=self._request_metadata,
|
||||
)
|
||||
|
||||
@overload
|
||||
@@ -283,10 +285,10 @@ class DifyPreparedLLM(LLMProtocol):
|
||||
class DifyPreparedPollingLLM(DifyPreparedLLM, LLMPollingCapableProtocol):
|
||||
"""Prepared workflow LLM adapter that exposes Graphon's polling protocol."""
|
||||
|
||||
def __init__(self, model_instance: ModelInstance) -> None:
|
||||
def __init__(self, model_instance: ModelInstance, request_metadata: Mapping[str, object] | None = None) -> None:
|
||||
from core.plugin.impl.model_runtime import PluginModelRuntime
|
||||
|
||||
super().__init__(model_instance)
|
||||
super().__init__(model_instance, request_metadata=request_metadata)
|
||||
model_type_instance = model_instance.model_type_instance
|
||||
if not isinstance(model_type_instance, LargeLanguageModel):
|
||||
raise TypeError("Polling wrapper requires a large-language-model instance.")
|
||||
|
||||
@@ -48,7 +48,7 @@ from clients.agent_backend import (
|
||||
)
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text, get_system_value
|
||||
from graphon.file import File, FileTransferMethod
|
||||
from graphon.variables.segments import Segment
|
||||
from models.agent import Agent, AgentConfigSnapshot, WorkflowAgentNodeBinding
|
||||
@@ -354,17 +354,22 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
) -> str:
|
||||
lines: list[str] = []
|
||||
query = get_system_text(context.variable_pool, SystemVariableKey.QUERY)
|
||||
uploaded_files = self._summarize_uploaded_workflow_files(context.variable_pool)
|
||||
resolved_outputs = self._resolve_previous_node_outputs(
|
||||
context.variable_pool,
|
||||
node_job.previous_node_output_refs,
|
||||
)
|
||||
if not query and not resolved_outputs:
|
||||
if not query and uploaded_files is None and not resolved_outputs:
|
||||
return ""
|
||||
|
||||
lines.append("Workflow context loaded for this run:")
|
||||
if query:
|
||||
lines.append(f"- User query: {query}")
|
||||
|
||||
if uploaded_files is not None:
|
||||
lines.append("- Uploaded workflow files:")
|
||||
lines.append(f" - sys.files: {uploaded_files}")
|
||||
|
||||
if resolved_outputs:
|
||||
lines.append("- Previous node outputs:")
|
||||
for item in resolved_outputs:
|
||||
@@ -373,6 +378,14 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
lines.append("The above workflow context is run-specific. Do not treat it as Agent Soul or persistent memory.")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _summarize_uploaded_workflow_files(self, variable_pool: VariablePoolReader) -> str | None:
|
||||
files = get_system_value(variable_pool, SystemVariableKey.FILES)
|
||||
if files is None:
|
||||
return None
|
||||
if isinstance(files, list | tuple) and not files:
|
||||
return None
|
||||
return self._summarize_value(files)
|
||||
|
||||
def _build_workflow_task_prompt(
|
||||
self,
|
||||
context: WorkflowAgentRuntimeBuildContext,
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import json
|
||||
from typing import override
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
from fields.workflow_fields import workflow_partial_fields
|
||||
from libs.helper import AppIconUrlField, TimestampField
|
||||
|
||||
|
||||
class JsonStringField(fields.Raw):
|
||||
@override
|
||||
def format(self, value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
|
||||
class StringListRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
|
||||
class ObjectListRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "array", "items": {"type": "object"}}
|
||||
|
||||
|
||||
app_detail_kernel_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
}
|
||||
|
||||
related_app_list = {
|
||||
"data": fields.List(fields.Nested(app_detail_kernel_fields)),
|
||||
"total": fields.Integer,
|
||||
}
|
||||
|
||||
model_config_fields = {
|
||||
"opening_statement": fields.String,
|
||||
"suggested_questions": StringListRawField(attribute="suggested_questions_list"),
|
||||
"suggested_questions_after_answer": OpaqueRawField(attribute="suggested_questions_after_answer_dict"),
|
||||
"speech_to_text": OpaqueRawField(attribute="speech_to_text_dict"),
|
||||
"text_to_speech": OpaqueRawField(attribute="text_to_speech_dict"),
|
||||
"retriever_resource": OpaqueRawField(attribute="retriever_resource_dict"),
|
||||
"annotation_reply": OpaqueRawField(attribute="annotation_reply_dict"),
|
||||
"more_like_this": OpaqueRawField(attribute="more_like_this_dict"),
|
||||
"sensitive_word_avoidance": OpaqueRawField(attribute="sensitive_word_avoidance_dict"),
|
||||
"external_data_tools": ObjectListRawField(attribute="external_data_tools_list"),
|
||||
"model": OpaqueRawField(attribute="model_dict"),
|
||||
"user_input_form": ObjectListRawField(attribute="user_input_form_list"),
|
||||
"dataset_query_variable": fields.String,
|
||||
"pre_prompt": fields.String,
|
||||
"agent_mode": OpaqueRawField(attribute="agent_mode_dict"),
|
||||
"prompt_type": fields.String,
|
||||
"chat_prompt_config": OpaqueRawField(attribute="chat_prompt_config_dict"),
|
||||
"completion_prompt_config": OpaqueRawField(attribute="completion_prompt_config_dict"),
|
||||
"dataset_configs": OpaqueRawField(attribute="dataset_configs_dict"),
|
||||
"file_upload": OpaqueRawField(attribute="file_upload_dict"),
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String}
|
||||
|
||||
app_detail_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"enable_site": fields.Boolean,
|
||||
"enable_api": fields.Boolean,
|
||||
"model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"tracing": OpaqueRawField,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
prompt_config_fields = {
|
||||
"prompt_template": fields.String,
|
||||
}
|
||||
|
||||
model_config_partial_fields = {
|
||||
"model": OpaqueRawField(attribute="model_dict"),
|
||||
"pre_prompt": fields.String,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
app_partial_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"max_active_requests": OpaqueRawField(),
|
||||
"description": fields.String(attribute="desc_or_prompt"),
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"access_mode": fields.String,
|
||||
"create_user_name": fields.String,
|
||||
"author_name": fields.String,
|
||||
"has_draft_trigger": fields.Boolean,
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
|
||||
app_pagination_fields = {
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer(attribute="per_page"),
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean(attribute="has_next"),
|
||||
"data": fields.List(fields.Nested(app_partial_fields), attribute="items"),
|
||||
}
|
||||
|
||||
template_fields = {
|
||||
"name": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String,
|
||||
"model_config": fields.Nested(model_config_fields),
|
||||
}
|
||||
|
||||
template_list_fields = {
|
||||
"data": fields.List(fields.Nested(template_fields)),
|
||||
}
|
||||
|
||||
site_fields = {
|
||||
"access_token": fields.String(attribute="code"),
|
||||
"code": fields.String,
|
||||
"title": fields.String,
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"description": fields.String,
|
||||
"default_language": fields.String,
|
||||
"chat_color_theme": fields.String,
|
||||
"chat_color_theme_inverted": fields.Boolean,
|
||||
"customize_domain": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"customize_token_strategy": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
"app_base_url": fields.String,
|
||||
"show_workflow_steps": fields.Boolean,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
deleted_tool_fields = {
|
||||
"type": fields.String,
|
||||
"tool_name": fields.String,
|
||||
"provider_id": fields.String,
|
||||
}
|
||||
|
||||
app_detail_fields_with_site = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"enable_site": fields.Boolean,
|
||||
"enable_api": fields.Boolean,
|
||||
"model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"api_base_url": fields.String,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"max_active_requests": fields.Integer,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"deleted_tools": fields.List(fields.Nested(deleted_tool_fields)),
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
"site": fields.Nested(site_fields),
|
||||
}
|
||||
|
||||
|
||||
app_site_fields = {
|
||||
"app_id": fields.String,
|
||||
"access_token": fields.String(attribute="code"),
|
||||
"code": fields.String,
|
||||
"title": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"description": fields.String,
|
||||
"default_language": fields.String,
|
||||
"customize_domain": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"customize_token_strategy": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
"show_workflow_steps": fields.Boolean,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
}
|
||||
|
||||
leaked_dependency_fields = {"type": fields.String, "value": OpaqueRawField, "current_identifier": fields.String}
|
||||
|
||||
app_import_fields = {
|
||||
"id": fields.String,
|
||||
"status": fields.String,
|
||||
"app_id": fields.String,
|
||||
"app_mode": fields.String,
|
||||
"current_dsl_version": fields.String,
|
||||
"imported_dsl_version": fields.String,
|
||||
"error": fields.String,
|
||||
}
|
||||
|
||||
app_import_check_dependencies_fields = {
|
||||
"leaked_dependencies": fields.List(fields.Nested(leaked_dependency_fields)),
|
||||
}
|
||||
|
||||
app_server_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"server_code": fields.String,
|
||||
"description": fields.String,
|
||||
"status": fields.String,
|
||||
"parameters": JsonStringField,
|
||||
"created_at": TimestampField,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
@@ -2,18 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask_restx import fields
|
||||
from pydantic import Field, computed_field, field_validator
|
||||
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import build_avatar_url, to_timestamp
|
||||
|
||||
simple_account_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"email": fields.String,
|
||||
}
|
||||
|
||||
|
||||
class SimpleAccountResponse(ResponseModel):
|
||||
id: str
|
||||
|
||||
@@ -1,61 +1,74 @@
|
||||
from typing import override
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask_restx import fields
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from fields.member_fields import simple_account_fields
|
||||
from libs.helper import TimestampField
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import SimpleAccountResponse
|
||||
from libs.helper import to_timestamp
|
||||
from models.snippet import SnippetType
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
class SnippetTagResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
|
||||
|
||||
tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String}
|
||||
class SnippetListItemResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str | None
|
||||
type: SnippetType
|
||||
version: int
|
||||
use_count: int
|
||||
is_published: bool
|
||||
icon_info: dict[str, Any] | None
|
||||
tags: list[SnippetTagResponse]
|
||||
created_by: str | None
|
||||
author_name: str | None
|
||||
created_at: int
|
||||
updated_by: str | None
|
||||
updated_at: int
|
||||
|
||||
# Snippet list item fields (lightweight for list display)
|
||||
snippet_list_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"type": fields.String,
|
||||
"version": fields.Integer,
|
||||
"use_count": fields.Integer,
|
||||
"is_published": fields.Boolean,
|
||||
"icon_info": OpaqueRawField,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"created_by": fields.String,
|
||||
"author_name": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
|
||||
timestamp = to_timestamp(value)
|
||||
if timestamp is None:
|
||||
raise ValueError("timestamp is required")
|
||||
return timestamp
|
||||
|
||||
# Full snippet fields (includes creator info and graph data)
|
||||
snippet_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"type": fields.String,
|
||||
"version": fields.Integer,
|
||||
"use_count": fields.Integer,
|
||||
"is_published": fields.Boolean,
|
||||
"icon_info": OpaqueRawField,
|
||||
"graph": OpaqueRawField(attribute="graph_dict"),
|
||||
"input_fields": OpaqueRawField(attribute="input_fields_list"),
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account", allow_null=True),
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
# Pagination response fields
|
||||
snippet_pagination_fields = {
|
||||
"data": fields.List(fields.Nested(snippet_list_fields)),
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer,
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean,
|
||||
}
|
||||
class SnippetResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str | None
|
||||
type: SnippetType
|
||||
version: int
|
||||
use_count: int
|
||||
is_published: bool
|
||||
icon_info: dict[str, Any] | None
|
||||
graph: dict[str, Any] = Field(validation_alias="graph_dict")
|
||||
input_fields: list[dict[str, Any]] = Field(validation_alias="input_fields_list")
|
||||
tags: list[SnippetTagResponse]
|
||||
created_by: SimpleAccountResponse | None = Field(validation_alias="created_by_account")
|
||||
created_at: int
|
||||
updated_by: SimpleAccountResponse | None = Field(validation_alias="updated_by_account")
|
||||
updated_at: int
|
||||
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
|
||||
timestamp = to_timestamp(value)
|
||||
if timestamp is None:
|
||||
raise ValueError("timestamp is required")
|
||||
return timestamp
|
||||
|
||||
|
||||
class SnippetPaginationResponse(ResponseModel):
|
||||
data: list[SnippetListItemResponse]
|
||||
page: int
|
||||
limit: int
|
||||
total: int
|
||||
has_more: bool
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
from typing import override
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
from core.helper import encrypter
|
||||
from fields.member_fields import simple_account_fields
|
||||
from graphon.variables import SecretVariable, SegmentType, VariableBase
|
||||
from libs.helper import TimestampField
|
||||
|
||||
from ._value_type_serializer import serialize_value_type
|
||||
|
||||
ENVIRONMENT_VARIABLE_SUPPORTED_TYPES = (SegmentType.STRING, SegmentType.NUMBER, SegmentType.SECRET)
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
|
||||
class JsonValueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
{"type": "number"},
|
||||
{"type": "boolean"},
|
||||
{"type": "object", "additionalProperties": True},
|
||||
{"type": "array", "items": {}},
|
||||
{"type": "null"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class EnvironmentVariableField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
@override
|
||||
def format(self, value):
|
||||
# Mask secret variables values in environment_variables
|
||||
if isinstance(value, SecretVariable):
|
||||
return {
|
||||
"id": value.id,
|
||||
"name": value.name,
|
||||
"value": encrypter.full_mask_token(),
|
||||
"value_type": value.value_type.value,
|
||||
"description": value.description,
|
||||
}
|
||||
if isinstance(value, VariableBase):
|
||||
return {
|
||||
"id": value.id,
|
||||
"name": value.name,
|
||||
"value": value.value,
|
||||
"value_type": str(value.value_type.exposed_type()),
|
||||
"description": value.description,
|
||||
}
|
||||
if isinstance(value, dict):
|
||||
value_type_str = value.get("value_type")
|
||||
if not isinstance(value_type_str, str):
|
||||
raise TypeError(
|
||||
f"unexpected type for value_type field, value={value_type_str}, type={type(value_type_str)}"
|
||||
)
|
||||
value_type = SegmentType(value_type_str).exposed_type()
|
||||
if value_type not in ENVIRONMENT_VARIABLE_SUPPORTED_TYPES:
|
||||
raise ValueError(f"Unsupported environment variable value type: {value_type}")
|
||||
return value
|
||||
|
||||
|
||||
conversation_variable_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"value_type": fields.String(attribute=serialize_value_type),
|
||||
"value": JsonValueRawField,
|
||||
"description": fields.String,
|
||||
}
|
||||
|
||||
pipeline_variable_fields = {
|
||||
"label": fields.String,
|
||||
"variable": fields.String,
|
||||
"type": fields.String,
|
||||
"belong_to_node_id": fields.String,
|
||||
"max_length": fields.Integer,
|
||||
"required": fields.Boolean,
|
||||
"unit": fields.String,
|
||||
"default_value": JsonValueRawField,
|
||||
"options": fields.List(fields.String),
|
||||
"placeholder": fields.String,
|
||||
"tooltips": fields.String,
|
||||
"allowed_file_types": fields.List(fields.String),
|
||||
"allow_file_extension": fields.List(fields.String),
|
||||
"allow_file_upload_methods": fields.List(fields.String),
|
||||
}
|
||||
|
||||
workflow_fields = {
|
||||
"id": fields.String,
|
||||
"graph": OpaqueRawField(attribute="graph_dict"),
|
||||
"features": OpaqueRawField(attribute="features_dict"),
|
||||
"hash": fields.String(attribute="unique_hash"),
|
||||
"version": fields.String,
|
||||
"marked_name": fields.String,
|
||||
"marked_comment": fields.String,
|
||||
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account"),
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
|
||||
"updated_at": TimestampField,
|
||||
"tool_published": fields.Boolean,
|
||||
"environment_variables": fields.List(EnvironmentVariableField()),
|
||||
"conversation_variables": fields.List(fields.Nested(conversation_variable_fields)),
|
||||
"rag_pipeline_variables": fields.List(fields.Nested(pipeline_variable_fields)),
|
||||
}
|
||||
|
||||
workflow_partial_fields = {
|
||||
"id": fields.String,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
workflow_pagination_fields = {
|
||||
"items": fields.List(fields.Nested(workflow_fields), attribute="items"),
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer(attribute="limit"),
|
||||
"has_more": fields.Boolean(attribute="has_more"),
|
||||
}
|
||||
+52
-41
@@ -369,17 +369,19 @@ class InvitationCode(TypeBase):
|
||||
)
|
||||
|
||||
|
||||
class TenantPluginInstallPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
|
||||
class TenantPluginDebugPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
|
||||
class TenantPluginPermission(TypeBase):
|
||||
class InstallPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
class DebugPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
__tablename__ = "account_plugin_permissions"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="account_plugin_permission_pkey"),
|
||||
@@ -390,36 +392,42 @@ class TenantPluginPermission(TypeBase):
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
install_permission: Mapped[InstallPermission] = mapped_column(
|
||||
EnumText(InstallPermission, length=16),
|
||||
install_permission: Mapped[TenantPluginInstallPermission] = mapped_column(
|
||||
EnumText(TenantPluginInstallPermission, length=16),
|
||||
nullable=False,
|
||||
server_default="everyone",
|
||||
default=InstallPermission.EVERYONE,
|
||||
default=TenantPluginInstallPermission.EVERYONE,
|
||||
)
|
||||
debug_permission: Mapped[DebugPermission] = mapped_column(
|
||||
EnumText(DebugPermission, length=16), nullable=False, server_default="noone", default=DebugPermission.NOBODY
|
||||
debug_permission: Mapped[TenantPluginDebugPermission] = mapped_column(
|
||||
EnumText(TenantPluginDebugPermission, length=16),
|
||||
nullable=False,
|
||||
server_default="noone",
|
||||
default=TenantPluginDebugPermission.NOBODY,
|
||||
)
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeCategory(enum.StrEnum):
|
||||
TOOL = "tool"
|
||||
MODEL = "model"
|
||||
EXTENSION = "extension"
|
||||
AGENT_STRATEGY = "agent-strategy"
|
||||
DATASOURCE = "datasource"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategySetting(enum.StrEnum):
|
||||
DISABLED = "disabled"
|
||||
FIX_ONLY = "fix_only"
|
||||
LATEST = "latest"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeMode(enum.StrEnum):
|
||||
ALL = "all"
|
||||
PARTIAL = "partial"
|
||||
EXCLUDE = "exclude"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
class PluginCategory(enum.StrEnum):
|
||||
TOOL = "tool"
|
||||
MODEL = "model"
|
||||
EXTENSION = "extension"
|
||||
AGENT_STRATEGY = "agent-strategy"
|
||||
DATASOURCE = "datasource"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
class StrategySetting(enum.StrEnum):
|
||||
DISABLED = "disabled"
|
||||
FIX_ONLY = "fix_only"
|
||||
LATEST = "latest"
|
||||
|
||||
class UpgradeMode(enum.StrEnum):
|
||||
ALL = "all"
|
||||
PARTIAL = "partial"
|
||||
EXCLUDE = "exclude"
|
||||
|
||||
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
|
||||
@@ -431,20 +439,23 @@ class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
category: Mapped[PluginCategory] = mapped_column(
|
||||
EnumText(PluginCategory, length=32),
|
||||
category: Mapped[TenantPluginAutoUpgradeCategory] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeCategory, length=32),
|
||||
nullable=False,
|
||||
server_default="tool",
|
||||
default=PluginCategory.TOOL,
|
||||
default=TenantPluginAutoUpgradeCategory.TOOL,
|
||||
)
|
||||
strategy_setting: Mapped[StrategySetting] = mapped_column(
|
||||
EnumText(StrategySetting, length=16),
|
||||
strategy_setting: Mapped[TenantPluginAutoUpgradeStrategySetting] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeStrategySetting, length=16),
|
||||
nullable=False,
|
||||
server_default="fix_only",
|
||||
default=StrategySetting.FIX_ONLY,
|
||||
default=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
)
|
||||
upgrade_mode: Mapped[UpgradeMode] = mapped_column(
|
||||
EnumText(UpgradeMode, length=16), nullable=False, server_default="exclude", default=UpgradeMode.EXCLUDE
|
||||
upgrade_mode: Mapped[TenantPluginAutoUpgradeMode] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeMode, length=16),
|
||||
nullable=False,
|
||||
server_default="exclude",
|
||||
default=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
)
|
||||
exclude_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)
|
||||
include_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, field_validat
|
||||
|
||||
from core.rag.entities.metadata_entities import ConditionValue, SupportedComparisonOperator
|
||||
from core.workflow.file_reference import is_canonical_file_reference
|
||||
from graphon.file import FileTransferMethod
|
||||
from graphon.file import FileTransferMethod, FileType
|
||||
|
||||
|
||||
class AgentKnowledgeQueryMode(StrEnum):
|
||||
@@ -314,8 +314,9 @@ class AgentKnowledgeQueryConfig(BaseModel):
|
||||
|
||||
Agent v2 stores knowledge as explicit ``knowledge.sets`` rather than the
|
||||
legacy flat ``datasets`` / ``query_mode`` / ``query_config`` shape. Each
|
||||
set owns its own query policy, so ``user_query`` must carry an explicit
|
||||
``value`` while ``generated_query`` leaves that value empty.
|
||||
set owns its own query policy. Mode-dependent completeness, such as
|
||||
requiring ``value`` for ``user_query``, is enforced by composer publish
|
||||
validation so draft saves can persist partially configured knowledge sets.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -323,12 +324,6 @@ class AgentKnowledgeQueryConfig(BaseModel):
|
||||
mode: AgentKnowledgeQueryMode
|
||||
value: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_query(self) -> Self:
|
||||
if self.mode == AgentKnowledgeQueryMode.USER_QUERY and not (self.value or "").strip():
|
||||
raise ValueError("knowledge query.value is required for user_query mode")
|
||||
return self
|
||||
|
||||
|
||||
class AgentKnowledgeModelConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -356,8 +351,9 @@ class AgentKnowledgeRetrievalConfig(BaseModel):
|
||||
"""Per-set retrieval policy for Agent v2 knowledge retrieval.
|
||||
|
||||
Retrieval settings now live on each knowledge set instead of one shared
|
||||
flat config. A set may use either ``multiple`` retrieval with ``top_k`` or
|
||||
``single`` retrieval with a required model config.
|
||||
flat config. Mode-dependent completeness, such as requiring ``top_k`` for
|
||||
``multiple`` or a model for ``single``, is enforced by composer publish
|
||||
validation so draft saves can persist partially configured knowledge sets.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -371,14 +367,6 @@ class AgentKnowledgeRetrievalConfig(BaseModel):
|
||||
weights: AgentKnowledgeWeightedScoreConfig | None = None
|
||||
model: AgentKnowledgeModelConfig | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_mode_fields(self) -> Self:
|
||||
if self.mode == "multiple" and self.top_k is None:
|
||||
raise ValueError("knowledge retrieval.top_k is required for multiple mode")
|
||||
if self.mode == "single" and self.model is None:
|
||||
raise ValueError("knowledge retrieval.model is required for single mode")
|
||||
return self
|
||||
|
||||
|
||||
class AgentKnowledgeMetadataCondition(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -401,6 +389,8 @@ class AgentKnowledgeMetadataFilteringConfig(BaseModel):
|
||||
The Python attribute uses ``metadata_model_config`` for clarity because the
|
||||
model belongs to metadata filtering specifically, while the external API and
|
||||
generated schema keep the historical ``model_config`` field name via alias.
|
||||
Mode-dependent completeness is enforced by composer publish validation so
|
||||
draft saves can persist partially configured metadata filters.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
@@ -410,14 +400,6 @@ class AgentKnowledgeMetadataFilteringConfig(BaseModel):
|
||||
metadata_model_config: AgentKnowledgeModelConfig | None = Field(default=None, alias="model_config")
|
||||
conditions: AgentKnowledgeMetadataConditions | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_mode_fields(self) -> Self:
|
||||
if self.mode == "automatic" and self.metadata_model_config is None:
|
||||
raise ValueError("metadata_filtering.model_config is required for automatic mode")
|
||||
if self.mode == "manual" and (self.conditions is None or not self.conditions.conditions):
|
||||
raise ValueError("metadata_filtering.conditions is required for manual mode")
|
||||
return self
|
||||
|
||||
|
||||
class AgentKnowledgeSetConfig(BaseModel):
|
||||
"""One explicit knowledge set in Agent v2.
|
||||
@@ -547,6 +529,23 @@ class AgentSensitiveWordAvoidanceFeatureConfig(AgentFeatureToggleConfig):
|
||||
config: AgentModerationProviderConfig | None = None
|
||||
|
||||
|
||||
class AgentFileUploadImageFeatureConfig(AgentFeatureToggleConfig):
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class AgentFileUploadFeatureConfig(AgentFeatureToggleConfig):
|
||||
enabled: bool = True
|
||||
allowed_file_extensions: list[str] = Field(default_factory=lambda: ["JPG", "JPEG", "PNG", "GIF", "WEBP", "SVG"])
|
||||
allowed_file_types: list[FileType] = Field(
|
||||
default_factory=lambda: [FileType.DOCUMENT, FileType.IMAGE, FileType.AUDIO, FileType.VIDEO]
|
||||
)
|
||||
allowed_file_upload_methods: list[FileTransferMethod] = Field(
|
||||
default_factory=lambda: [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]
|
||||
)
|
||||
image: AgentFileUploadImageFeatureConfig = Field(default_factory=AgentFileUploadImageFeatureConfig)
|
||||
number_limits: int = 3
|
||||
|
||||
|
||||
class AgentSoulAppFeaturesConfig(AgentFlexibleConfig):
|
||||
opening_statement: str | None = None
|
||||
suggested_questions: list[str] | None = None
|
||||
@@ -555,6 +554,7 @@ class AgentSoulAppFeaturesConfig(AgentFlexibleConfig):
|
||||
text_to_speech: AgentTextToSpeechFeatureConfig | None = None
|
||||
retriever_resource: AgentFeatureToggleConfig | None = None
|
||||
sensitive_word_avoidance: AgentSensitiveWordAvoidanceFeatureConfig | None = None
|
||||
file_upload: AgentFileUploadFeatureConfig = Field(default_factory=AgentFileUploadFeatureConfig)
|
||||
|
||||
|
||||
class WorkflowPreviousNodeOutputRef(AgentFlexibleConfig):
|
||||
|
||||
@@ -141,9 +141,9 @@ Get account avatar url
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [EducationActivateResponse](#educationactivateresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [GET] /account/education/autocomplete
|
||||
#### Parameters
|
||||
@@ -1268,7 +1268,7 @@ Read a text/binary preview file in an Agent App conversation sandbox
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [POST] /agent/{agent_id}/sandbox/files/upload
|
||||
Upload one Agent App sandbox file as a Dify ToolFile mapping
|
||||
Upload one Agent App sandbox file and return a signed download URL
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -1411,7 +1411,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceListResponse](#workspacelistresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspacePaginationResponse](#workspacepaginationresponse)<br> |
|
||||
|
||||
### [GET] /api-based-extension
|
||||
Get all API-based extensions for current tenant
|
||||
@@ -3777,7 +3777,7 @@ Read a text/binary preview file in a workflow Agent node sandbox
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [POST] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/upload
|
||||
Upload one workflow Agent sandbox file as a Dify ToolFile mapping
|
||||
Upload one workflow Agent sandbox file and return a signed download URL
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -5106,14 +5106,14 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Default datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
|
||||
|
||||
### [GET] /auth/plugin/datasource/list
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
|
||||
|
||||
### [GET] /auth/plugin/datasource/{provider_id}
|
||||
#### Parameters
|
||||
@@ -5126,7 +5126,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceCredentialListResponse](#datasourcecredentiallistresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}
|
||||
#### Parameters
|
||||
@@ -5145,7 +5145,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 200 | Datasource credential created successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [DELETE] /auth/plugin/datasource/{provider_id}/custom-client
|
||||
#### Parameters
|
||||
@@ -5177,7 +5177,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 200 | Datasource OAuth custom client saved successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}/default
|
||||
#### Parameters
|
||||
@@ -5234,7 +5234,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 201 | Datasource credential updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}/update-name
|
||||
#### Parameters
|
||||
@@ -7638,9 +7638,9 @@ Initiate OAuth login process
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 302 | Redirect to console OAuth callback page | **application/json**: [RedirectResponse](#redirectresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 302 | Redirect to OAuth callback page |
|
||||
|
||||
### [GET] /oauth/plugin/{provider_id}/datasource/get-authorization-url
|
||||
#### Parameters
|
||||
@@ -7654,7 +7654,7 @@ Initiate OAuth login process
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Authorization URL retrieved successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
|
||||
| 200 | Datasource OAuth authorization URL generated successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
|
||||
|
||||
### [GET] /oauth/plugin/{provider}/tool/authorization-url
|
||||
#### Parameters
|
||||
@@ -8477,9 +8477,9 @@ Initiate OAuth login process
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DataSourceContentPreviewResponse](#datasourcecontentpreviewresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/run
|
||||
**Run rag pipeline datasource**
|
||||
@@ -8617,6 +8617,7 @@ Initiate OAuth login process
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 401 | Unauthorized | **application/json**: [SimpleResultMessageResponse](#simpleresultmessageresponse)<br> |
|
||||
|
||||
### [POST] /remote-files/upload
|
||||
#### Request Body
|
||||
@@ -9432,9 +9433,9 @@ Bedrock retrieval test (internal use only)
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /trial-apps/{app_id}/completion-messages
|
||||
#### Parameters
|
||||
@@ -9451,9 +9452,9 @@ Bedrock retrieval test (internal use only)
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [GET] /trial-apps/{app_id}/datasets
|
||||
#### Parameters
|
||||
@@ -9568,9 +9569,9 @@ Returns the site configuration for the application including theme, icons, and t
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /trial-apps/{app_id}/workflows/tasks/{task_id}/stop
|
||||
**Stop workflow task**
|
||||
@@ -9983,7 +9984,7 @@ Create a new plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
|
||||
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/create~~
|
||||
@@ -10002,7 +10003,7 @@ Deprecated legacy alias for creating a plugin endpoint. Use POST /workspaces/cur
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
|
||||
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/delete~~
|
||||
@@ -10021,7 +10022,7 @@ Deprecated legacy alias for deleting a plugin endpoint. Use DELETE /workspaces/c
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [POST] /workspaces/current/endpoints/disable
|
||||
@@ -10037,7 +10038,7 @@ Disable a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint disabled successfully | **application/json**: [EndpointDisableResponse](#endpointdisableresponse)<br> |
|
||||
| 200 | Endpoint disabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [POST] /workspaces/current/endpoints/enable
|
||||
@@ -10053,7 +10054,7 @@ Enable a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint enabled successfully | **application/json**: [EndpointEnableResponse](#endpointenableresponse)<br> |
|
||||
| 200 | Endpoint enabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [GET] /workspaces/current/endpoints/list
|
||||
@@ -10087,7 +10088,7 @@ List endpoints for a specific plugin
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [PluginEndpointListResponse](#pluginendpointlistresponse)<br> |
|
||||
| 200 | Success | **application/json**: [EndpointListResponse](#endpointlistresponse)<br> |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/update~~
|
||||
|
||||
@@ -10105,7 +10106,7 @@ Deprecated legacy alias for updating a plugin endpoint. Use PATCH /workspaces/cu
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [DELETE] /workspaces/current/endpoints/{id}
|
||||
@@ -10121,7 +10122,7 @@ Delete a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [PATCH] /workspaces/current/endpoints/{id}
|
||||
@@ -10143,7 +10144,7 @@ Update a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [GET] /workspaces/current/members
|
||||
@@ -10203,7 +10204,7 @@ Update a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [MemberActionTenantResponse](#memberactiontenantresponse)<br> |
|
||||
| 200 | Success | **application/json**: [MemberActionResponse](#memberactionresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/members/{member_id}/owner-transfer
|
||||
#### Parameters
|
||||
@@ -12471,9 +12472,15 @@ Returns permission flags that control workspace features like member invitations
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/custom-config/webapp-logo/upload
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
@@ -12491,7 +12498,7 @@ Returns permission flags that control workspace features like member invitations
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/switch
|
||||
#### Request Body
|
||||
@@ -13747,6 +13754,23 @@ Stable Agent Soul reference to one normalized skill archive.
|
||||
| upload_file_id | string | | No |
|
||||
| url | string | | No |
|
||||
|
||||
#### AgentFileUploadFeatureConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| allowed_file_extensions | [ string ] | | No |
|
||||
| allowed_file_types | [ [FileType](#filetype) ] | | No |
|
||||
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
|
||||
| enabled | boolean, <br>**Default:** true | | No |
|
||||
| image | [AgentFileUploadImageFeatureConfig](#agentfileuploadimagefeatureconfig) | | No |
|
||||
| number_limits | integer, <br>**Default:** 3 | | No |
|
||||
|
||||
#### AgentFileUploadImageFeatureConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| enabled | boolean, <br>**Default:** true | | No |
|
||||
|
||||
#### AgentHumanContactConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -13890,6 +13914,8 @@ Per-set metadata filtering policy.
|
||||
The Python attribute uses ``metadata_model_config`` for clarity because the
|
||||
model belongs to metadata filtering specifically, while the external API and
|
||||
generated schema keep the historical ``model_config`` field name via alias.
|
||||
Mode-dependent completeness is enforced by composer publish validation so
|
||||
draft saves can persist partially configured metadata filters.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -13912,8 +13938,9 @@ Per-set query policy for Agent v2 knowledge retrieval.
|
||||
|
||||
Agent v2 stores knowledge as explicit ``knowledge.sets`` rather than the
|
||||
legacy flat ``datasets`` / ``query_mode`` / ``query_config`` shape. Each
|
||||
set owns its own query policy, so ``user_query`` must carry an explicit
|
||||
``value`` while ``generated_query`` leaves that value empty.
|
||||
set owns its own query policy. Mode-dependent completeness, such as
|
||||
requiring ``value`` for ``user_query``, is enforced by composer publish
|
||||
validation so draft saves can persist partially configured knowledge sets.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -13938,8 +13965,9 @@ set owns its own query policy, so ``user_query`` must carry an explicit
|
||||
Per-set retrieval policy for Agent v2 knowledge retrieval.
|
||||
|
||||
Retrieval settings now live on each knowledge set instead of one shared
|
||||
flat config. A set may use either ``multiple`` retrieval with ``top_k`` or
|
||||
``single`` retrieval with a required model config.
|
||||
flat config. Mode-dependent completeness, such as requiring ``top_k`` for
|
||||
``multiple`` or a model for ``single``, is enforced by composer publish
|
||||
validation so draft saves can persist partially configured knowledge sets.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -14338,6 +14366,7 @@ Visibility and lifecycle scope of an Agent record.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| file_upload | [AgentFileUploadFeatureConfig](#agentfileuploadfeatureconfig) | | No |
|
||||
| opening_statement | string | | No |
|
||||
| retriever_resource | [AgentFeatureToggleConfig](#agentfeaturetoggleconfig) | | No |
|
||||
| sensitive_word_avoidance | [AgentSensitiveWordAvoidanceFeatureConfig](#agentsensitivewordavoidancefeatureconfig) | | No |
|
||||
@@ -16103,12 +16132,6 @@ Model class for provider custom model configuration.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| info_list | [InfoList](#infolist) | | Yes |
|
||||
|
||||
#### DataSourceContentPreviewResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| DataSourceContentPreviewResponse | | | |
|
||||
|
||||
#### DataSourceIntegrateIconResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -16629,32 +16652,43 @@ Model class for provider custom model configuration.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credential_id | string | | Yes |
|
||||
|
||||
#### DatasourceCredentialListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
|
||||
|
||||
#### DatasourceCredentialPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credentials | object | | Yes |
|
||||
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | Yes |
|
||||
| name | string | | No |
|
||||
|
||||
#### DatasourceCredentialResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| avatar_url | string | | Yes |
|
||||
| credential | object | Obfuscated plugin-defined credential parameters from the datasource provider. | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_default | boolean | | Yes |
|
||||
| name | string | | Yes |
|
||||
| type | string | | Yes |
|
||||
|
||||
#### DatasourceCredentialUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credential_id | string | | Yes |
|
||||
| credentials | object | | No |
|
||||
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | No |
|
||||
| name | string | | No |
|
||||
|
||||
#### DatasourceCredentialsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | | | Yes |
|
||||
|
||||
#### DatasourceCustomClientPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| client_params | object | | No |
|
||||
| client_params | object | Plugin-defined OAuth client parameters. The schema is declared by the datasource provider. | No |
|
||||
| enable_oauth_custom_client | boolean | | No |
|
||||
|
||||
#### DatasourceDefaultPayload
|
||||
@@ -16686,6 +16720,39 @@ Model class for provider custom model configuration.
|
||||
| error | string | Error message from OAuth provider | No |
|
||||
| state | string | OAuth state parameter | No |
|
||||
|
||||
#### DatasourceOAuthSchemaResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| client_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| credentials_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| is_oauth_custom_client_enabled | boolean | | Yes |
|
||||
| is_system_oauth_params_exists | boolean | | Yes |
|
||||
| oauth_custom_client_params | object | Masked plugin-defined OAuth client parameters, when configured for the tenant. | Yes |
|
||||
| redirect_uri | string | | Yes |
|
||||
|
||||
#### DatasourceProviderAuthListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | [ [DatasourceProviderAuthResponse](#datasourceproviderauthresponse) ] | | Yes |
|
||||
|
||||
#### DatasourceProviderAuthResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| author | string | | Yes |
|
||||
| credential_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| credentials_list | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
|
||||
| description | [I18nObject](#i18nobject) | | Yes |
|
||||
| icon | string | | Yes |
|
||||
| label | [I18nObject](#i18nobject) | | Yes |
|
||||
| name | string | | Yes |
|
||||
| oauth_schema | [DatasourceOAuthSchemaResponse](#datasourceoauthschemaresponse) | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
| plugin_unique_identifier | string | | Yes |
|
||||
| provider | string | | Yes |
|
||||
|
||||
#### DatasourceUpdateNamePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -16702,12 +16769,6 @@ Model class for provider custom model configuration.
|
||||
| start_node_id | string | | Yes |
|
||||
| start_node_title | string | | Yes |
|
||||
|
||||
#### DebugPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| DebugPermission | string | | |
|
||||
|
||||
#### DeclaredArrayItem
|
||||
|
||||
Per-item shape for an ``array``-typed declared output.
|
||||
@@ -17034,12 +17095,6 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| role | string | | Yes |
|
||||
| token | string | | Yes |
|
||||
|
||||
#### EducationActivateResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| EducationActivateResponse | object | | |
|
||||
|
||||
#### EducationAutocompleteQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -17149,29 +17204,13 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| plugin_unique_identifier | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointCreateResponse
|
||||
#### EndpointDeclarationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointDisableResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointEnableResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
| hidden | boolean | | No |
|
||||
| method | string | | Yes |
|
||||
| path | string | | Yes |
|
||||
|
||||
#### EndpointIdPayload
|
||||
|
||||
@@ -17187,6 +17226,23 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| page_size | integer | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
|
||||
#### EndpointListItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | dateTime | | Yes |
|
||||
| declaration | [EndpointProviderDeclarationResponse](#endpointproviderdeclarationresponse) | | No |
|
||||
| enabled | boolean | | Yes |
|
||||
| expired_at | dateTime | | Yes |
|
||||
| hook_id | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
| tenant_id | string | | Yes |
|
||||
| updated_at | dateTime | | Yes |
|
||||
| url | string | | Yes |
|
||||
|
||||
#### EndpointListQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -17198,7 +17254,59 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ object ] | Endpoint information | Yes |
|
||||
| endpoints | [ [EndpointListItemResponse](#endpointlistitemresponse) ] | Endpoint information | Yes |
|
||||
|
||||
#### EndpointProviderConfigI18nResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| en_US | string | | Yes |
|
||||
| ja_JP | string | | No |
|
||||
| pt_BR | string | | No |
|
||||
| zh_Hans | string | | No |
|
||||
|
||||
#### EndpointProviderConfigOptionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | Yes |
|
||||
| value | string | | Yes |
|
||||
|
||||
#### EndpointProviderConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| default | integer<br>string<br>number<br>boolean | | No |
|
||||
| help | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| multiple | boolean | | No |
|
||||
| name | string | | Yes |
|
||||
| options | [ [EndpointProviderConfigOptionResponse](#endpointproviderconfigoptionresponse) ] | | No |
|
||||
| placeholder | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| required | boolean | | No |
|
||||
| scope | [EndpointProviderConfigScope](#endpointproviderconfigscope) | | No |
|
||||
| type | [ProviderConfigType](#providerconfigtype) | | Yes |
|
||||
| url | string | | No |
|
||||
|
||||
#### EndpointProviderConfigScope
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| EndpointProviderConfigScope | string | | |
|
||||
|
||||
#### EndpointProviderDeclarationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ [EndpointDeclarationResponse](#endpointdeclarationresponse) ] | | No |
|
||||
| settings | [ [EndpointProviderConfigResponse](#endpointproviderconfigresponse) ] | | No |
|
||||
|
||||
#### EndpointSettingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| name | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointUpdatePayload
|
||||
|
||||
@@ -17207,12 +17315,6 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| name | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointUpdateResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EnvSuggestion
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -18010,12 +18112,6 @@ Input field definition for snippet parameters.
|
||||
| required | boolean | | No |
|
||||
| type | string | | No |
|
||||
|
||||
#### InstallPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| InstallPermission | string | | |
|
||||
|
||||
#### InstalledAppCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -18321,7 +18417,7 @@ Enum class for large language model mode.
|
||||
| marketplace_plugin_unique_identifier | string | | Yes |
|
||||
| version | string | | No |
|
||||
|
||||
#### MemberActionTenantResponse
|
||||
#### MemberActionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -19092,13 +19188,13 @@ Enum class for parameter type.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### ParserAutoUpgradeFetch
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### ParserCreateCredential
|
||||
|
||||
@@ -19196,7 +19292,7 @@ Enum class for parameter type.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
|
||||
#### ParserGetCredentials
|
||||
@@ -19284,8 +19380,8 @@ Enum class for parameter type.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | No |
|
||||
| install_permission | [InstallPermission](#installpermission) | | No |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
|
||||
|
||||
#### ParserPluginIdentifierQuery
|
||||
|
||||
@@ -19494,7 +19590,7 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsResponseModel](#pluginautoupgradesettingsresponsemodel) | | Yes |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### PluginAutoUpgradeSettingsPayload
|
||||
|
||||
@@ -19502,8 +19598,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| exclude_plugins | [ string ] | | No |
|
||||
| include_plugins | [ string ] | | No |
|
||||
| strategy_setting | [StrategySetting](#strategysetting) | | No |
|
||||
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
|
||||
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | No |
|
||||
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | No |
|
||||
| upgrade_time_of_day | integer | | No |
|
||||
|
||||
#### PluginAutoUpgradeSettingsResponseModel
|
||||
@@ -19512,8 +19608,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| exclude_plugins | [ string ] | | Yes |
|
||||
| include_plugins | [ string ] | | Yes |
|
||||
| strategy_setting | [StrategySetting](#strategysetting) | | Yes |
|
||||
| upgrade_mode | [UpgradeMode](#upgrademode) | | Yes |
|
||||
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | Yes |
|
||||
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | Yes |
|
||||
| upgrade_time_of_day | integer | | Yes |
|
||||
|
||||
#### PluginCategory
|
||||
@@ -19635,21 +19731,21 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| current_identifier | string | | No |
|
||||
| type | [Type](#type) | | Yes |
|
||||
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
|
||||
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
|
||||
|
||||
#### PluginDependencyType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginDependencyType | string | | |
|
||||
|
||||
#### PluginDynamicOptionsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| options | | | Yes |
|
||||
|
||||
#### PluginEndpointListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ object ] | Endpoint information | Yes |
|
||||
|
||||
#### PluginInstallationItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -19730,7 +19826,13 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| type | [core__plugin__entities__parameters__PluginParameterAutoGenerate__Type](#core__plugin__entities__parameters__pluginparameterautogenerate__type) | | Yes |
|
||||
| type | [PluginParameterAutoGenerateType](#pluginparameterautogeneratetype) | | Yes |
|
||||
|
||||
#### PluginParameterAutoGenerateType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginParameterAutoGenerateType | string | | |
|
||||
|
||||
#### PluginParameterOption
|
||||
|
||||
@@ -19750,15 +19852,15 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | Yes |
|
||||
| install_permission | [InstallPermission](#installpermission) | | Yes |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | Yes |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | Yes |
|
||||
|
||||
#### PluginPermissionSettingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | No |
|
||||
| install_permission | [InstallPermission](#installpermission) | | No |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
|
||||
|
||||
#### PluginReadmeResponse
|
||||
|
||||
@@ -19840,9 +19942,15 @@ Model class for common provider settings like credentials
|
||||
| placeholder | [I18nObject](#i18nobject) | | No |
|
||||
| required | boolean | | No |
|
||||
| scope | [AppSelectorScope](#appselectorscope)<br>[ModelSelectorScope](#modelselectorscope)<br>[ToolSelectorScope](#toolselectorscope) | | No |
|
||||
| type | [core__entities__provider_entities__BasicProviderConfig__Type](#core__entities__provider_entities__basicproviderconfig__type) | The type of the credentials | Yes |
|
||||
| type | [ProviderConfigType](#providerconfigtype) | The type of the credentials | Yes |
|
||||
| url | string | | No |
|
||||
|
||||
#### ProviderConfigType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| ProviderConfigType | string | | |
|
||||
|
||||
#### ProviderCredentialResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -20437,19 +20545,11 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs.
|
||||
| text | string | | No |
|
||||
| truncated | boolean | | Yes |
|
||||
|
||||
#### SandboxToolFileResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| reference | string | | Yes |
|
||||
| transfer_method | string, <br>**Default:** tool_file | | No |
|
||||
|
||||
#### SandboxUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| file | [SandboxToolFileResponse](#sandboxtoolfileresponse) | | Yes |
|
||||
| path | string | | Yes |
|
||||
| url | string | | Yes |
|
||||
|
||||
#### SavedMessageCreatePayload
|
||||
|
||||
@@ -20998,12 +21098,6 @@ Query parameters for listing snippet published workflows.
|
||||
| paused | integer | | Yes |
|
||||
| success | integer | | Yes |
|
||||
|
||||
#### StrategySetting
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| StrategySetting | string | | |
|
||||
|
||||
#### StringListSource
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21258,6 +21352,36 @@ Tag type
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| workspaces | [ [TenantListItemResponse](#tenantlistitemresponse) ] | | Yes |
|
||||
|
||||
#### TenantPluginAutoUpgradeCategory
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeCategory | string | | |
|
||||
|
||||
#### TenantPluginAutoUpgradeMode
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeMode | string | | |
|
||||
|
||||
#### TenantPluginAutoUpgradeStrategySetting
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeStrategySetting | string | | |
|
||||
|
||||
#### TenantPluginDebugPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginDebugPermission | string | | |
|
||||
|
||||
#### TenantPluginInstallPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginInstallPermission | string | | |
|
||||
|
||||
#### TextContentResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21764,12 +21888,6 @@ Enum class for tool provider
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TriggerSubscriptionListResponse | array | | |
|
||||
|
||||
#### Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| Type | string | | |
|
||||
|
||||
#### UnaddedModelConfiguration
|
||||
|
||||
Model class for provider unadded model configuration.
|
||||
@@ -21810,12 +21928,6 @@ Payload for updating a snippet.
|
||||
| icon_info | [IconInfo](#iconinfo) | | No |
|
||||
| name | string | | No |
|
||||
|
||||
#### UpgradeMode
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| UpgradeMode | string | | |
|
||||
|
||||
#### UploadConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22946,7 +23058,13 @@ Workflow tool configuration
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### WorkspaceListResponse
|
||||
#### WorkspaceLogoUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### WorkspacePaginationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -22956,19 +23074,6 @@ Workflow tool configuration
|
||||
| page | integer | | Yes |
|
||||
| total | integer | | Yes |
|
||||
|
||||
#### WorkspaceLogoUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### WorkspaceMutationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
|
||||
|
||||
#### WorkspacePermissionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22983,6 +23088,13 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| permission_keys | [ string ] | | No |
|
||||
|
||||
#### WorkspaceTenantResultResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
|
||||
|
||||
#### _AccessControlLanguageQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -23051,18 +23163,6 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| scope | [RBACResourceWhitelistScope](#rbacresourcewhitelistscope) | | Yes |
|
||||
|
||||
#### core__entities__provider_entities__BasicProviderConfig__Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| core__entities__provider_entities__BasicProviderConfig__Type | string | | |
|
||||
|
||||
#### core__plugin__entities__parameters__PluginParameterAutoGenerate__Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| core__plugin__entities__parameters__PluginParameterAutoGenerate__Type | string | | |
|
||||
|
||||
#### core__tools__entities__common_entities__I18nObject
|
||||
|
||||
Model class for i18n object.
|
||||
|
||||
@@ -941,9 +941,15 @@ Strict (extra='forbid').
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| current_identifier | string | | No |
|
||||
| type | [Type](#type) | | Yes |
|
||||
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
|
||||
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
|
||||
|
||||
#### PluginDependencyType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginDependencyType | string | | |
|
||||
|
||||
#### RevokeResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -1024,12 +1030,6 @@ types it as a required `'success'` rather than an optional field.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
|
||||
#### Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| Type | string | | |
|
||||
|
||||
#### UsageInfo
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -1046,12 +1046,12 @@ Execute a single datasource node within the knowledge pipeline. Returns a stream
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Streaming response with node execution events. | **text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| 401 | Unauthorized - invalid API token | |
|
||||
| 403 | Forbidden - dataset API access or workspace access denied | |
|
||||
| 404 | `not_found` : Dataset not found. | |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Streaming response with node execution events. |
|
||||
| 401 | Unauthorized - invalid API token |
|
||||
| 403 | Forbidden - dataset API access or workspace access denied |
|
||||
| 404 | `not_found` : Dataset not found. |
|
||||
|
||||
### [POST] /datasets/{dataset_id}/pipeline/run
|
||||
**Run Pipeline**
|
||||
@@ -2960,7 +2960,7 @@ Enum class for custom configuration status.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | Yes |
|
||||
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | No |
|
||||
| datasource_type | string | | No |
|
||||
| node_id | string | | No |
|
||||
| plugin_id | string | | No |
|
||||
|
||||
@@ -600,7 +600,7 @@ Get authentication passport for web application access
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Passport retrieved successfully | **application/json**: [AccessTokenData](#accesstokendata)<br> |
|
||||
| 200 | Passport retrieved successfully | **application/json**: [PassportAccessTokenResponse](#passportaccesstokenresponse)<br> |
|
||||
| 401 | Unauthorized - missing app code or invalid authentication | |
|
||||
| 404 | Application or user not found | |
|
||||
|
||||
@@ -1430,6 +1430,12 @@ Form input definition.
|
||||
| text_to_speech | [JSONObject](#jsonobject) | | Yes |
|
||||
| user_input_form | [ [JSONObject](#jsonobject) ] | | Yes |
|
||||
|
||||
#### PassportAccessTokenResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_token | string | | Yes |
|
||||
|
||||
#### PassportQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy import select
|
||||
import app
|
||||
from core.helper.marketplace import fetch_global_plugin_manifest
|
||||
from extensions.ext_database import db
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginAutoUpgradeStrategySetting
|
||||
from tasks import process_tenant_plugin_autoupgrade_check_task as check_task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,8 +34,7 @@ def check_upgradable_plugin_task():
|
||||
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
|
||||
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
|
||||
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
|
||||
TenantPluginAutoUpgradeStrategy.strategy_setting
|
||||
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
|
||||
TenantPluginAutoUpgradeStrategy.strategy_setting != TenantPluginAutoUpgradeStrategySetting.DISABLED,
|
||||
)
|
||||
).all()
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ from models.account import (
|
||||
Tenant,
|
||||
TenantAccountJoin,
|
||||
TenantAccountRole,
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantStatus,
|
||||
)
|
||||
@@ -1257,13 +1259,13 @@ class TenantService:
|
||||
session.add(tenant)
|
||||
session.commit()
|
||||
|
||||
for category in TenantPluginAutoUpgradeStrategy.PluginCategory:
|
||||
for category in TenantPluginAutoUpgradeCategory:
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
category=category,
|
||||
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=[],
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ from models.workflow import Workflow
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import (
|
||||
AgentModelNotConfiguredError,
|
||||
AgentNameConflictError,
|
||||
AgentNotFoundError,
|
||||
AgentVersionConflictError,
|
||||
@@ -168,7 +169,8 @@ class AgentComposerService:
|
||||
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
if payload.save_strategy in _PUBLISH_SAVE_STRATEGIES:
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
workflow = cls._get_draft_workflow(tenant_id=tenant_id, app_id=app_id)
|
||||
binding = cls._get_workflow_binding(tenant_id=tenant_id, workflow_id=workflow.id, node_id=node_id)
|
||||
|
||||
@@ -357,7 +359,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
|
||||
agent = cls._get_agent_app_agent(tenant_id=tenant_id, app_id=app_id)
|
||||
if not agent:
|
||||
@@ -401,7 +402,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id)
|
||||
return cls._save_agent_composer_for_agent(
|
||||
tenant_id=tenant_id,
|
||||
@@ -511,6 +511,8 @@ class AgentComposerService:
|
||||
version_note=version_note,
|
||||
)
|
||||
)
|
||||
if not agent_soul_has_model(agent_soul):
|
||||
raise AgentModelNotConfiguredError()
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=agent_soul)
|
||||
version = cls._create_config_version(
|
||||
tenant_id=tenant_id,
|
||||
@@ -591,7 +593,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
ComposerConfigValidator.validate_draft_save_payload(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id)
|
||||
build_draft = cls._save_agent_draft(
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from models.agent_config_entities import AgentKnowledgeQueryMode
|
||||
from services.agent.errors import AgentSoulLockedError, InvalidComposerConfigError, PlaintextSecretNotAllowedError
|
||||
from services.agent.prompt_mentions import (
|
||||
MAX_MENTIONS_PER_PROMPT,
|
||||
@@ -228,9 +229,40 @@ class ComposerConfigValidator:
|
||||
@classmethod
|
||||
def validate_agent_soul(cls, agent_soul: AgentSoulConfig) -> None:
|
||||
dumped = agent_soul.model_dump(mode="json")
|
||||
cls._validate_knowledge_runtime_config(agent_soul)
|
||||
cls._reject_plaintext_secrets(dumped, path="agent_soul")
|
||||
cls._validate_shell_config(dumped)
|
||||
|
||||
@classmethod
|
||||
def _validate_knowledge_runtime_config(cls, agent_soul: AgentSoulConfig) -> None:
|
||||
"""Validate knowledge settings that are required only for publish/run.
|
||||
|
||||
Draft composer saves must be able to persist partially configured
|
||||
knowledge sets while a user is still editing the panel. These checks
|
||||
stay in the publish validator so invalid runtime configs are still
|
||||
blocked before a version can be published or executed.
|
||||
"""
|
||||
for knowledge_set in agent_soul.knowledge.sets:
|
||||
if (
|
||||
knowledge_set.query.mode == AgentKnowledgeQueryMode.USER_QUERY
|
||||
and not (knowledge_set.query.value or "").strip()
|
||||
):
|
||||
raise InvalidComposerConfigError("knowledge query.value is required for user_query mode")
|
||||
|
||||
retrieval = knowledge_set.retrieval
|
||||
if retrieval.mode == "multiple" and retrieval.top_k is None:
|
||||
raise InvalidComposerConfigError("knowledge retrieval.top_k is required for multiple mode")
|
||||
if retrieval.mode == "single" and retrieval.model is None:
|
||||
raise InvalidComposerConfigError("knowledge retrieval.model is required for single mode")
|
||||
|
||||
metadata_filtering = knowledge_set.metadata_filtering
|
||||
if metadata_filtering.mode == "automatic" and metadata_filtering.metadata_model_config is None:
|
||||
raise InvalidComposerConfigError("metadata_filtering.model_config is required for automatic mode")
|
||||
if metadata_filtering.mode == "manual" and (
|
||||
metadata_filtering.conditions is None or not metadata_filtering.conditions.conditions
|
||||
):
|
||||
raise InvalidComposerConfigError("metadata_filtering.conditions is required for manual mode")
|
||||
|
||||
@classmethod
|
||||
def validate_node_job(cls, node_job: WorkflowNodeJobConfig) -> None:
|
||||
cls._reject_plaintext_secrets(node_job.model_dump(mode="json"), path="node_job")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
||||
|
||||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
class AgentNotFoundError(NotFound):
|
||||
description = "Agent not found."
|
||||
@@ -21,6 +23,12 @@ class AgentVersionConflictError(Conflict):
|
||||
description = "Agent config version changed. Please reload and try again."
|
||||
|
||||
|
||||
class AgentModelNotConfiguredError(BaseHTTPException):
|
||||
error_code = "agent_model_not_configured"
|
||||
description = "Agent App requires the Agent Soul model to be configured."
|
||||
code = 400
|
||||
|
||||
|
||||
class AgentSoulLockedError(BadRequest):
|
||||
description = "Agent Soul is locked for this workflow node."
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
These services keep product-facing locators (conversation, workflow run, node)
|
||||
on the API boundary and translate them into the agent backend's
|
||||
``SandboxLocator`` using persisted non-sensitive runtime layer specs plus the
|
||||
saved Agenton session snapshot.
|
||||
saved Agenton session snapshot. Upload responses stay console-facing here: the
|
||||
agent backend still returns a canonical ToolFile mapping, while this API layer
|
||||
re-resolves that mapping into a signed browser download URL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import urllib.parse
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from agenton.compositor import CompositorSessionSnapshot
|
||||
from dify_agent.client import Client
|
||||
@@ -18,7 +22,10 @@ from sqlalchemy import select
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.apps.agent_app.session_store import AgentAppRuntimeSessionStore
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
|
||||
from core.db.session_factory import session_factory
|
||||
from factories import file_factory
|
||||
from models.agent import AgentRuntimeSessionOwnerType, WorkflowAgentRuntimeSession, WorkflowAgentRuntimeSessionStatus
|
||||
|
||||
_RUNTIME_LAYER_SPECS_ADAPTER: TypeAdapter[list[RuntimeLayerSpec]] = TypeAdapter(list[RuntimeLayerSpec])
|
||||
@@ -45,6 +52,12 @@ class AgentSandboxInfo(BaseModel):
|
||||
workspace_cwd: str
|
||||
|
||||
|
||||
class AgentSandboxUploadDownload(BaseModel):
|
||||
"""Signed browser download URL for one sandbox upload result."""
|
||||
|
||||
url: str
|
||||
|
||||
|
||||
class AgentAppSandboxService:
|
||||
"""Inspect and proxy file access for an Agent App conversation sandbox."""
|
||||
|
||||
@@ -77,9 +90,15 @@ class AgentAppSandboxService:
|
||||
locator = self._resolve_locator(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
||||
return self._client_factory().read_sandbox_file_sync(locator, path)
|
||||
|
||||
def upload_file(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str):
|
||||
def upload_file(
|
||||
self, *, tenant_id: str, app_id: str, conversation_id: str, path: str
|
||||
) -> AgentSandboxUploadDownload:
|
||||
locator = self._resolve_locator(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
||||
return self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
uploaded = self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
return _upload_download_response(
|
||||
tenant_id=tenant_id,
|
||||
file_mapping=uploaded.file.model_dump(mode="python"),
|
||||
)
|
||||
|
||||
def _resolve_locator(self, *, tenant_id: str, app_id: str, conversation_id: str) -> SandboxLocator:
|
||||
stored = self._session_store.load_active_session_for_conversation(
|
||||
@@ -153,7 +172,7 @@ class WorkflowAgentSandboxService:
|
||||
node_id: str,
|
||||
node_execution_id: str | None,
|
||||
path: str,
|
||||
):
|
||||
) -> AgentSandboxUploadDownload:
|
||||
locator = self._resolve_locator(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
@@ -161,7 +180,11 @@ class WorkflowAgentSandboxService:
|
||||
node_id=node_id,
|
||||
node_execution_id=node_execution_id,
|
||||
)
|
||||
return self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
uploaded = self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
return _upload_download_response(
|
||||
tenant_id=tenant_id,
|
||||
file_mapping=uploaded.file.model_dump(mode="python"),
|
||||
)
|
||||
|
||||
def _resolve_locator(
|
||||
self,
|
||||
@@ -246,6 +269,41 @@ def _deserialize_runtime_layer_specs(value: str | None) -> list[RuntimeLayerSpec
|
||||
return _RUNTIME_LAYER_SPECS_ADAPTER.validate_json(value)
|
||||
|
||||
|
||||
def _upload_download_response(*, tenant_id: str, file_mapping: dict[str, Any]) -> AgentSandboxUploadDownload:
|
||||
"""Resolve one uploaded ToolFile mapping into a signed external download URL."""
|
||||
|
||||
controller = DatabaseFileAccessController()
|
||||
runtime = DifyWorkflowFileRuntime(file_access_controller=controller)
|
||||
try:
|
||||
file = file_factory.build_from_mapping(
|
||||
mapping=file_mapping,
|
||||
tenant_id=tenant_id,
|
||||
access_controller=controller,
|
||||
)
|
||||
url = runtime.resolve_file_url(file=file, for_external=True)
|
||||
except ValueError as exc:
|
||||
raise AgentSandboxInspectorError(
|
||||
"sandbox_upload_download_unavailable",
|
||||
"uploaded sandbox file could not be converted to a download URL",
|
||||
status_code=502,
|
||||
) from exc
|
||||
|
||||
if not url:
|
||||
raise AgentSandboxInspectorError(
|
||||
"sandbox_upload_download_unavailable",
|
||||
"uploaded sandbox file does not support download URL generation",
|
||||
status_code=502,
|
||||
)
|
||||
return AgentSandboxUploadDownload(url=_with_as_attachment(url))
|
||||
|
||||
|
||||
def _with_as_attachment(url: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
|
||||
query.append(("as_attachment", "true"))
|
||||
return urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
|
||||
|
||||
|
||||
def _default_client_factory() -> Client:
|
||||
base_url = dify_config.AGENT_BACKEND_BASE_URL
|
||||
if not base_url:
|
||||
@@ -257,4 +315,10 @@ def _default_client_factory() -> Client:
|
||||
return Client(base_url=base_url)
|
||||
|
||||
|
||||
__all__ = ["AgentAppSandboxService", "AgentSandboxInfo", "AgentSandboxInspectorError", "WorkflowAgentSandboxService"]
|
||||
__all__ = [
|
||||
"AgentAppSandboxService",
|
||||
"AgentSandboxInfo",
|
||||
"AgentSandboxInspectorError",
|
||||
"AgentSandboxUploadDownload",
|
||||
"WorkflowAgentSandboxService",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
from configs import dify_config
|
||||
from core.helper import marketplace
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType, PluginInstallationSource
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from models.provider_ids import ModelProviderID, ToolProviderID
|
||||
|
||||
@@ -55,7 +55,7 @@ class DependenciesAnalysisService:
|
||||
unique_identifier = dependency.value.plugin_unique_identifier
|
||||
if unique_identifier in missing_plugin_unique_identifiers:
|
||||
# Extract version for Marketplace dependencies
|
||||
if dependency.type == PluginDependency.Type.Marketplace:
|
||||
if dependency.type == PluginDependencyType.Marketplace:
|
||||
version_match = _VERSION_REGEX.search(unique_identifier)
|
||||
if version_match:
|
||||
dependency.value.version = version_match.group("version")
|
||||
@@ -84,7 +84,7 @@ class DependenciesAnalysisService:
|
||||
if plugin.source == PluginInstallationSource.Github:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Github,
|
||||
type=PluginDependencyType.Github,
|
||||
value=PluginDependency.Github(
|
||||
repo=plugin.meta["repo"],
|
||||
version=plugin.meta["version"],
|
||||
@@ -96,7 +96,7 @@ class DependenciesAnalysisService:
|
||||
elif plugin.source == PluginInstallationSource.Marketplace:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(
|
||||
marketplace_plugin_unique_identifier=plugin.plugin_unique_identifier
|
||||
),
|
||||
@@ -105,7 +105,7 @@ class DependenciesAnalysisService:
|
||||
elif plugin.source == PluginInstallationSource.Package:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Package,
|
||||
type=PluginDependencyType.Package,
|
||||
value=PluginDependency.Package(plugin_unique_identifier=plugin.plugin_unique_identifier),
|
||||
)
|
||||
)
|
||||
@@ -130,7 +130,7 @@ class DependenciesAnalysisService:
|
||||
deps = marketplace.batch_fetch_plugin_manifests(dependencies)
|
||||
return [
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=dep.latest_package_identifier),
|
||||
)
|
||||
for dep in deps
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user