Compare commits

..

1 Commits

Author SHA1 Message Date
Joel
116e8bf6c9 fix: offset legacy app error toasts below model controls 2026-08-05 18:35:25 +08:00
1303 changed files with 33078 additions and 50974 deletions

View File

@@ -1,17 +1,17 @@
title: 'General Discussion'
title: "General Discussion"
body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:

View File

@@ -1,17 +1,17 @@
title: 'Help'
title: "Help"
body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:

View File

@@ -3,15 +3,15 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:

View File

@@ -1,4 +1,4 @@
name: '🕷️ Bug report'
name: "🕷️ Bug report"
description: Report errors or unexpected behavior
labels:
- bug
@@ -6,7 +6,7 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -18,7 +18,7 @@ body:
required: true
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: input

View File

@@ -1,13 +1,13 @@
blank_issues_enabled: false
contact_links:
- name: "\U0001F510 Security Vulnerabilities"
url: 'https://github.com/langgenius/dify/security/advisories/new'
url: "https://github.com/langgenius/dify/security/advisories/new"
about: Report security vulnerabilities through GitHub Security Advisories to ensure responsible disclosure. 💡 Please do not report security vulnerabilities in public issues.
- name: "\U0001F4A1 Model Providers & Plugins"
url: 'https://github.com/langgenius/dify-official-plugins/issues/new/choose'
url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose"
about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details.
- name: "\U0001F4AC Documentation Issues"
url: 'https://github.com/langgenius/dify-docs/issues/new'
url: "https://github.com/langgenius/dify-docs/issues/new"
about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue.
- name: "\U0001F4E7 Discussions"
url: https://github.com/langgenius/dify/discussions/categories/general

View File

@@ -1,4 +1,4 @@
name: '⭐ Feature or enhancement request'
name: "⭐ Feature or enhancement request"
description: Propose something new.
labels:
- enhancement
@@ -6,7 +6,7 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -14,7 +14,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report, otherwise it will be closed.
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:

View File

@@ -1,11 +1,11 @@
name: '✨ Refactor or Chore'
name: "✨ Refactor or Chore"
description: Refactor existing code or perform maintenance chores to improve readability and reliability.
title: '[Refactor/Chore] '
title: "[Refactor/Chore] "
body:
- type: checkboxes
attributes:
label: Self Checks
description: 'To make sure we get to you in time, please check the following :)'
description: "To make sure we get to you in time, please check the following :)"
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -17,26 +17,26 @@ body:
required: true
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
required: true
- label: 'Please do not modify this template :) and fill in all the required fields.'
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
id: description
attributes:
label: Description
placeholder: 'Describe the refactor or chore you are proposing.'
placeholder: "Describe the refactor or chore you are proposing."
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
placeholder: 'Explain why this refactor or chore is necessary.'
placeholder: "Explain why this refactor or chore is necessary."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
placeholder: 'Add any other context or screenshots about the request here.'
placeholder: "Add any other context or screenshots about the request here."
validations:
required: false

View File

@@ -5,12 +5,13 @@ runs:
using: composite
steps:
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
with:
run_install: false
- name: Setup Vite+
uses: voidzero-dev/setup-vp@143f5f385f39b1b753ffed1a01ad443811855c8b # v1.16.1
uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1.10.0
with:
version: 0.2.6
node-version-file: .nvmrc
cache: true
run-install: true

328
.github/dependabot.yml vendored
View File

@@ -1,223 +1,223 @@
version: 2
updates:
- package-ecosystem: 'uv'
directory: '/api'
- package-ecosystem: "uv"
directory: "/api"
open-pull-requests-limit: 10
schedule:
interval: 'weekly'
interval: "weekly"
groups:
flask:
patterns:
- 'flask'
- 'flask-*'
- 'werkzeug'
- 'gunicorn'
- "flask"
- "flask-*"
- "werkzeug"
- "gunicorn"
google:
patterns:
- 'google-*'
- 'googleapis-*'
- "google-*"
- "googleapis-*"
opentelemetry:
patterns:
- 'opentelemetry-*'
- "opentelemetry-*"
pydantic:
patterns:
- 'pydantic'
- 'pydantic-*'
- "pydantic"
- "pydantic-*"
llm:
patterns:
- 'langfuse'
- 'langsmith'
- 'litellm'
- 'mlflow*'
- 'opik'
- 'weave*'
- 'arize*'
- 'tiktoken'
- 'transformers'
- "langfuse"
- "langsmith"
- "litellm"
- "mlflow*"
- "opik"
- "weave*"
- "arize*"
- "tiktoken"
- "transformers"
database:
patterns:
- 'sqlalchemy'
- 'psycopg2*'
- 'psycogreen'
- 'redis*'
- 'alembic*'
- "sqlalchemy"
- "psycopg2*"
- "psycogreen"
- "redis*"
- "alembic*"
storage:
patterns:
- 'boto3*'
- 'botocore*'
- 'azure-*'
- 'bce-*'
- 'cos-python-*'
- 'esdk-obs-*'
- 'google-cloud-storage'
- 'opendal'
- 'oss2'
- 'supabase*'
- 'tos*'
- "boto3*"
- "botocore*"
- "azure-*"
- "bce-*"
- "cos-python-*"
- "esdk-obs-*"
- "google-cloud-storage"
- "opendal"
- "oss2"
- "supabase*"
- "tos*"
vdb:
patterns:
- 'alibabacloud*'
- 'chromadb'
- 'clickhouse-*'
- 'clickzetta-*'
- 'couchbase'
- 'elasticsearch'
- 'opensearch-py'
- 'oracledb'
- 'pgvect*'
- 'pymilvus'
- 'pymochow'
- 'pyobvector'
- 'qdrant-client'
- 'intersystems-*'
- 'tablestore'
- 'tcvectordb'
- 'tidb-vector'
- 'upstash-*'
- 'volcengine-*'
- 'weaviate-*'
- 'xinference-*'
- 'mo-vector'
- 'mysql-connector-*'
- "alibabacloud*"
- "chromadb"
- "clickhouse-*"
- "clickzetta-*"
- "couchbase"
- "elasticsearch"
- "opensearch-py"
- "oracledb"
- "pgvect*"
- "pymilvus"
- "pymochow"
- "pyobvector"
- "qdrant-client"
- "intersystems-*"
- "tablestore"
- "tcvectordb"
- "tidb-vector"
- "upstash-*"
- "volcengine-*"
- "weaviate-*"
- "xinference-*"
- "mo-vector"
- "mysql-connector-*"
dev:
patterns:
- 'coverage'
- 'dotenv-linter'
- 'faker'
- 'lxml-stubs'
- 'basedpyright'
- 'ruff'
- 'pytest*'
- 'types-*'
- 'boto3-stubs'
- 'hypothesis'
- 'pandas-stubs'
- 'scipy-stubs'
- 'import-linter'
- 'celery-types'
- 'mypy*'
- 'pyrefly'
- "coverage"
- "dotenv-linter"
- "faker"
- "lxml-stubs"
- "basedpyright"
- "ruff"
- "pytest*"
- "types-*"
- "boto3-stubs"
- "hypothesis"
- "pandas-stubs"
- "scipy-stubs"
- "import-linter"
- "celery-types"
- "mypy*"
- "pyrefly"
python-packages:
patterns:
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
- "*"
- package-ecosystem: "github-actions"
directory: "/"
open-pull-requests-limit: 5
schedule:
interval: 'weekly'
interval: "weekly"
groups:
github-actions-dependencies:
patterns:
- '*'
- package-ecosystem: 'uv'
directory: '/api'
target-branch: 'lts/1.13.x'
- "*"
- package-ecosystem: "uv"
directory: "/api"
target-branch: "lts/1.13.x"
open-pull-requests-limit: 10
schedule:
interval: 'weekly'
interval: "weekly"
groups:
flask:
patterns:
- 'flask'
- 'flask-*'
- 'werkzeug'
- 'gunicorn'
- "flask"
- "flask-*"
- "werkzeug"
- "gunicorn"
google:
patterns:
- 'google-*'
- 'googleapis-*'
- "google-*"
- "googleapis-*"
opentelemetry:
patterns:
- 'opentelemetry-*'
- "opentelemetry-*"
pydantic:
patterns:
- 'pydantic'
- 'pydantic-*'
- "pydantic"
- "pydantic-*"
llm:
patterns:
- 'langfuse'
- 'langsmith'
- 'litellm'
- 'mlflow*'
- 'opik'
- 'weave*'
- 'arize*'
- 'tiktoken'
- 'transformers'
- "langfuse"
- "langsmith"
- "litellm"
- "mlflow*"
- "opik"
- "weave*"
- "arize*"
- "tiktoken"
- "transformers"
database:
patterns:
- 'sqlalchemy'
- 'psycopg2*'
- 'psycogreen'
- 'redis*'
- 'alembic*'
- "sqlalchemy"
- "psycopg2*"
- "psycogreen"
- "redis*"
- "alembic*"
storage:
patterns:
- 'boto3*'
- 'botocore*'
- 'azure-*'
- 'bce-*'
- 'cos-python-*'
- 'esdk-obs-*'
- 'google-cloud-storage'
- 'opendal'
- 'oss2'
- 'supabase*'
- 'tos*'
- "boto3*"
- "botocore*"
- "azure-*"
- "bce-*"
- "cos-python-*"
- "esdk-obs-*"
- "google-cloud-storage"
- "opendal"
- "oss2"
- "supabase*"
- "tos*"
vdb:
patterns:
- 'alibabacloud*'
- 'chromadb'
- 'clickhouse-*'
- 'clickzetta-*'
- 'couchbase'
- 'elasticsearch'
- 'opensearch-py'
- 'oracledb'
- 'pgvect*'
- 'pymilvus'
- 'pymochow'
- 'pyobvector'
- 'qdrant-client'
- 'intersystems-*'
- 'tablestore'
- 'tcvectordb'
- 'tidb-vector'
- 'upstash-*'
- 'volcengine-*'
- 'weaviate-*'
- 'xinference-*'
- 'mo-vector'
- 'mysql-connector-*'
- "alibabacloud*"
- "chromadb"
- "clickhouse-*"
- "clickzetta-*"
- "couchbase"
- "elasticsearch"
- "opensearch-py"
- "oracledb"
- "pgvect*"
- "pymilvus"
- "pymochow"
- "pyobvector"
- "qdrant-client"
- "intersystems-*"
- "tablestore"
- "tcvectordb"
- "tidb-vector"
- "upstash-*"
- "volcengine-*"
- "weaviate-*"
- "xinference-*"
- "mo-vector"
- "mysql-connector-*"
dev:
patterns:
- 'coverage'
- 'dotenv-linter'
- 'faker'
- 'lxml-stubs'
- 'basedpyright'
- 'ruff'
- 'pytest*'
- 'types-*'
- 'boto3-stubs'
- 'hypothesis'
- 'pandas-stubs'
- 'scipy-stubs'
- 'import-linter'
- 'celery-types'
- 'mypy*'
- 'pyrefly'
- "coverage"
- "dotenv-linter"
- "faker"
- "lxml-stubs"
- "basedpyright"
- "ruff"
- "pytest*"
- "types-*"
- "boto3-stubs"
- "hypothesis"
- "pandas-stubs"
- "scipy-stubs"
- "import-linter"
- "celery-types"
- "mypy*"
- "pyrefly"
python-packages:
patterns:
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
target-branch: 'lts/1.13.x'
- "*"
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "lts/1.13.x"
open-pull-requests-limit: 5
schedule:
interval: 'weekly'
interval: "weekly"
groups:
github-actions-dependencies:
patterns:
- '*'
- "*"

View File

@@ -1 +1 @@
failure-threshold: 'error'
failure-threshold: "error"

View File

@@ -1,4 +1,5 @@
---
extends: default
rules:

View File

@@ -1,22 +1,22 @@
{
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"^web/public/vs/",
"^web/public/pdf.worker.min.mjs$",
"web/app/components/base/icons/src/vender/"
],
"AllowedContentTypes": [],
"PassedFiles": [],
"Disable": {
"EndOfLine": false,
"Indentation": false,
"IndentSize": true,
"InsertFinalNewline": false,
"TrimTrailingWhitespace": false,
"MaxLineLength": false
}
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"^web/public/vs/",
"^web/public/pdf.worker.min.mjs$",
"web/app/components/base/icons/src/vender/"
],
"AllowedContentTypes": [],
"PassedFiles": [],
"Disable": {
"EndOfLine": false,
"Indentation": false,
"IndentSize": true,
"InsertFinalNewline": false,
"TrimTrailingWhitespace": false,
"MaxLineLength": false
}
}

View File

@@ -12,8 +12,8 @@
## Screenshots
| Before | After |
| ------ | ----- |
| ... | ... |
|--------|-------|
| ... | ... |
## Checklist

View File

@@ -8,31 +8,31 @@ const headSha = process.env.HEAD_SHA || ''
const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean)
const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json'
const englishPath = (fileStem) => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
const readCurrentJson = (fileStem) => {
const filePath = englishPath(fileStem)
if (!fs.existsSync(filePath)) return null
if (!fs.existsSync(filePath))
return null
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
}
const readBaseJson = (fileStem) => {
if (!baseSha) return null
if (!baseSha)
return null
try {
const relativePath = `web/i18n/en-US/${fileStem}.json`
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], {
encoding: 'utf8',
})
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' })
return JSON.parse(content)
} catch {
}
catch {
return null
}
}
const compareJson = (beforeValue, afterValue) =>
JSON.stringify(beforeValue) === JSON.stringify(afterValue)
const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue)
const changes = {}
@@ -59,7 +59,8 @@ for (const fileStem of files) {
}
for (const key of Object.keys(beforeJson)) {
if (!(key in afterJson)) deleted.push(key)
if (!(key in afterJson))
deleted.push(key)
}
changes[fileStem] = {
@@ -77,5 +78,5 @@ fs.writeFileSync(
headSha,
files,
changes,
}),
})
)

View File

@@ -1,101 +0,0 @@
name: Manual WCAG Accessibility Audit
run-name: WCAG ${{ inputs.level }} · ${{ inputs.page }}
on:
workflow_dispatch:
inputs:
page:
description: Page to scan
required: true
default: home
type: choice
options:
- home
- sign-in
- studio
- agents
- knowledge
- integrations
- all
level:
description: WCAG level to scan
required: true
default: aa
type: choice
options:
- aa
- a
permissions:
contents: read
concurrency:
group: accessibility-e2e-${{ github.ref }}-${{ inputs.page }}-${{ inputs.level }}
cancel-in-progress: true
jobs:
accessibility:
name: WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} · ${{ inputs.page }}
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 120
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup web dependencies
uses: ./.github/actions/setup-web
- name: Setup UV and Python
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
cache-dependency-glob: |
api/uv.lock
- name: Install API dependencies
run: uv sync --project api --dev
- name: Install Chromium for accessibility E2E
timeout-minutes: 15
working-directory: ./e2e
run: vp run e2e:install:ci:chromium
- name: Run automated WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} checks
working-directory: ./e2e
env:
E2E_ADMIN_EMAIL: e2e-admin@example.com
E2E_ADMIN_NAME: E2E Admin
E2E_ADMIN_PASSWORD: E2eAdmin12345
E2E_FORCE_WEB_BUILD: '1'
E2E_INIT_PASSWORD: E2eInit12345
WCAG_PAGE: ${{ inputs.page }}
run: |
if [[ "$WCAG_PAGE" == "all" ]]; then
vp run e2e:accessibility:${{ inputs.level }}
else
pnpm exec tsx ./scripts/run-cucumber.ts --full -- --tags "@axe and @wcag-${{ inputs.level }} and @wcag-page-$WCAG_PAGE"
fi
- name: Upload accessibility Cucumber report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cucumber-report-accessibility-${{ inputs.level }}-${{ inputs.page }}
path: e2e/cucumber-report
retention-days: 7
- name: Upload accessibility E2E logs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-logs-accessibility-${{ inputs.level }}-${{ inputs.page }}
path: e2e/.logs/*.log
include-hidden-files: true
retention-days: 7

View File

@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
python-version:
- '3.12'
- "3.12"
steps:
- name: Checkout code
@@ -84,7 +84,7 @@ jobs:
strategy:
matrix:
python-version:
- '3.12'
- "3.12"
steps:
- name: Checkout code
@@ -148,7 +148,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
python-version: "3.12"
cache-dependency-glob: api/uv.lock
- name: Install dependencies

View File

@@ -1,12 +1,12 @@
name: autofix.ci
on:
pull_request:
branches: ['main']
branches: ["main"]
merge_group:
branches: ['main']
branches: ["main"]
types: [checks_requested]
push:
branches: ['main']
branches: ["main"]
permissions:
contents: read
@@ -53,7 +53,8 @@ jobs:
oxlint-suppressions.json
eslint-suppressions.json
.vscode/**
.github/**
.github/workflows/autofix.yml
.github/workflows/style.yml
- name: Check api inputs
if: github.event_name != 'merge_group'
id: api-changes
@@ -85,7 +86,7 @@ jobs:
- if: github.event_name != 'merge_group'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
python-version: "3.11"
- if: github.event_name != 'merge_group'
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

View File

@@ -3,13 +3,13 @@ name: Build and Push API & Web
on:
push:
branches:
- 'main'
- 'deploy/**'
- 'build/**'
- 'release/e-*'
- 'hotfix/**'
- "main"
- "deploy/**"
- "build/**"
- "release/e-*"
- "hotfix/**"
tags:
- '*'
- "*"
concurrency:
group: build-push-${{ github.head_ref || github.run_id }}
@@ -33,60 +33,60 @@ jobs:
strategy:
matrix:
include:
- service_name: 'build-api-amd64'
image_name_env: 'DIFY_API_IMAGE_NAME'
artifact_context: 'api'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: "build-api-amd64"
image_name_env: "DIFY_API_IMAGE_NAME"
artifact_context: "api"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-api-arm64'
image_name_env: 'DIFY_API_IMAGE_NAME'
artifact_context: 'api'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: "build-api-arm64"
image_name_env: "DIFY_API_IMAGE_NAME"
artifact_context: "api"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-web-amd64'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
artifact_context: 'web'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: "build-web-amd64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
artifact_context: "web"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-web-arm64'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
artifact_context: 'web'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: "build-web-arm64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
artifact_context: "web"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-agent-amd64'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
artifact_context: 'agent'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
- service_name: "build-agent-amd64"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
artifact_context: "agent"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-agent-arm64'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
artifact_context: 'agent'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
- service_name: "build-agent-arm64"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
artifact_context: "agent"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-agent-local-sandbox-amd64'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
artifact_context: 'local-sandbox'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: "build-agent-local-sandbox-amd64"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
artifact_context: "local-sandbox"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: 'build-agent-local-sandbox-arm64'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
artifact_context: 'local-sandbox'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: "build-agent-local-sandbox-arm64"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
artifact_context: "local-sandbox"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
@@ -146,18 +146,18 @@ jobs:
strategy:
matrix:
include:
- service_name: 'validate-api-amd64'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'validate-web-amd64'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'validate-agent-amd64'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
- service_name: 'validate-agent-local-sandbox-amd64'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: "validate-api-amd64"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "validate-web-amd64"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "validate-agent-amd64"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
- service_name: "validate-agent-local-sandbox-amd64"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
@@ -178,18 +178,18 @@ jobs:
strategy:
matrix:
include:
- service_name: 'merge-api-images'
image_name_env: 'DIFY_API_IMAGE_NAME'
context: 'api'
- service_name: 'merge-web-images'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
context: 'web'
- service_name: 'merge-agent-images'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
context: 'agent'
- service_name: 'merge-agent-local-sandbox-images'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
context: 'local-sandbox'
- service_name: "merge-api-images"
image_name_env: "DIFY_API_IMAGE_NAME"
context: "api"
- service_name: "merge-web-images"
image_name_env: "DIFY_WEB_IMAGE_NAME"
context: "web"
- service_name: "merge-agent-images"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
context: "agent"
- service_name: "merge-agent-local-sandbox-images"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
context: "local-sandbox"
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

View File

@@ -4,19 +4,19 @@ on:
workflow_dispatch:
inputs:
cli_ref:
description: 'Git ref (default: current branch)'
description: "Git ref (default: current branch)"
type: string
required: false
edition:
description: 'Dify edition'
description: "Dify edition"
type: choice
required: false
default: ee
options: [ee, ce]
test_scope:
description: 'smoke = [P0] only / full = all cases'
description: "smoke = [P0] only / full = all cases"
type: choice
required: false
default: full
@@ -24,23 +24,23 @@ on:
# ── Suite on/off ────────────────────────────────────────────────────────
suite_framework_output_error:
description: 'framework + output + error-handling suites'
description: "framework + output + error-handling suites"
type: boolean
default: true
suite_discovery:
description: 'discovery suite (get app / describe app)'
description: "discovery suite (get app / describe app)"
type: boolean
default: true
suite_run:
description: 'run suite (basic / streaming / conversation / file / hitl)'
description: "run suite (basic / streaming / conversation / file / hitl)"
type: boolean
default: true
suite_auth:
description: 'auth suite (login / status / whoami / use / devices / logout)'
description: "auth suite (login / status / whoami / use / devices / logout)"
type: boolean
default: true
suite_agent:
description: 'agent suite'
description: "agent suite"
type: boolean
default: true
@@ -51,31 +51,32 @@ permissions:
# Each job reads DIFY_E2E_TOKEN + app IDs from the provision job outputs,
# so global-setup skips minting and finds existing apps in < 10 s.
env:
DIFY_E2E_NO_KEYRING: '1' # Linux CI has no keychain; skip probe
VITEST_RETRY: '2' # Retry flaky staging responses
DIFY_E2E_NO_KEYRING: "1" # Linux CI has no keychain; skip probe
VITEST_RETRY: "2" # Retry flaky staging responses
jobs:
# ════════════════════════════════════════════════════════════════════════════
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
# ════════════════════════════════════════════════════════════════════════════
provision:
name: 'Provision: mint token + DSL apps'
name: "Provision: mint token + DSL apps"
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
@@ -87,7 +88,7 @@ jobs:
with:
bun-version: latest
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
package_json_field: packageManager
run_install: false
@@ -100,18 +101,18 @@ jobs:
id: out
working-directory: cli
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
run: bun scripts/e2e-provision.ts
# ════════════════════════════════════════════════════════════════════════════
# 1-B. framework + output + error-handling (parallel with run/discovery)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-B. framework + output + error-handling (parallel with run/discovery)
# ════════════════════════════════════════════════════════════════════════════
suite-framework-output-error:
name: 'Suite: framework + output + error-handling'
name: "Suite: framework + output + error-handling"
if: ${{ inputs.suite_framework_output_error != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -130,23 +131,23 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run framework + output + error-handling
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts'
DIFY_E2E_INCLUDE: "test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts"
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -154,11 +155,11 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 1-C. Discovery (parallel)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-C. Discovery (parallel)
# ════════════════════════════════════════════════════════════════════════════
suite-discovery:
name: 'Suite: discovery'
name: "Suite: discovery"
if: ${{ inputs.suite_discovery != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -177,24 +178,24 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run discovery suite
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/discovery/**/*.e2e.ts'
DIFY_E2E_INCLUDE: "test/e2e/suites/discovery/**/*.e2e.ts"
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -202,11 +203,11 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 1-D. Run suite — 5 files in matrix (parallel)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-D. Run suite — 5 files in matrix (parallel)
# ════════════════════════════════════════════════════════════════════════════
suite-run:
name: 'Suite: run / ${{ matrix.name }}'
name: "Suite: run / ${{ matrix.name }}"
if: ${{ inputs.suite_run != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -240,30 +241,30 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: 'Run run/${{ matrix.name }}'
- name: "Run run/${{ matrix.name }}"
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/run/${{ matrix.file }}'
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/run/${{ matrix.file }}"
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -279,11 +280,11 @@ jobs:
path: cli/test-results/
retention-days: 3
# ════════════════════════════════════════════════════════════════════════════
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
# ════════════════════════════════════════════════════════════════════════════
suite-auth-safe:
name: 'Suite: auth (login / status / whoami)'
name: "Suite: auth (login / status / whoami)"
if: ${{ inputs.suite_auth != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -302,22 +303,22 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run auth/login + status + whoami
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts'
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts"
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -325,13 +326,13 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
# Must wait for ALL parallel suites to finish to avoid token revocation
# invalidating other in-flight requests.
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
# Must wait for ALL parallel suites to finish to avoid token revocation
# invalidating other in-flight requests.
# ════════════════════════════════════════════════════════════════════════════
suite-last:
name: 'Suite: auth-use + devices + logout + agent (last, serial)'
name: "Suite: auth-use + devices + logout + agent (last, serial)"
# Runs when auth is selected; also runs after all parallel jobs finish
if: ${{ inputs.suite_auth != 'false' || inputs.suite_agent != 'false' }}
needs:
@@ -358,27 +359,27 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run use / devices / logout / agent (serial)
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
run: |
# Collect files in safe order: use → devices → logout (revokes last) → agent
FILES=()

View File

@@ -36,7 +36,7 @@ jobs:
uses: ./.github/actions/setup-web
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
with:
bun-version-file: cli/.bun-version

View File

@@ -114,7 +114,7 @@ jobs:
run: node scripts/release-naming.mjs github-env >> "$GITHUB_ENV"
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
with:
bun-version-file: cli/.bun-version

View File

@@ -4,11 +4,11 @@ on:
workflow_dispatch:
inputs:
dify_version:
description: 'Dify image tag to test against (e.g. 1.7.0)'
description: "Dify image tag to test against (e.g. 1.7.0)"
type: string
required: true
cli_ref:
description: 'Git ref to build the cli from (default: current branch)'
description: "Git ref to build the cli from (default: current branch)"
type: string
required: false

View File

@@ -22,7 +22,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
python-version: "3.12"
cache-dependency-glob: api/uv.lock
- name: Install dependencies
@@ -72,7 +72,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
python-version: "3.12"
cache-dependency-glob: api/uv.lock
- name: Install dependencies

View File

@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ['Build and Push API & Web']
workflows: ["Build and Push API & Web"]
branches:
- 'deploy/agent'
- "deploy/agent"
types:
- completed

View File

@@ -2,9 +2,9 @@ name: Deploy Dev
on:
workflow_run:
workflows: ['Build and Push API & Web']
workflows: ["Build and Push API & Web"]
branches:
- 'deploy/dev'
- "deploy/dev"
types:
- completed

View File

@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ['Build and Push API & Web']
workflows: ["Build and Push API & Web"]
branches:
- 'deploy/enterprise'
- "deploy/enterprise"
types:
- completed

View File

@@ -6,9 +6,9 @@ permissions:
on:
workflow_run:
workflows: ['Build and Push API & Web']
workflows: ["Build and Push API & Web"]
branches:
- 'deploy/konwledge'
- "deploy/konwledge"
types:
- completed

View File

@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ['Build and Push API & Web']
workflows: ["Build and Push API & Web"]
branches:
- 'deploy/saas'
- "deploy/saas"
types:
- completed

View File

@@ -3,7 +3,7 @@ name: Build docker image
on:
pull_request:
branches:
- 'main'
- "main"
paths:
- api/Dockerfile
- api/Dockerfile.dockerignore
@@ -33,36 +33,36 @@ jobs:
strategy:
matrix:
include:
- service_name: 'api-amd64'
- service_name: "api-amd64"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'api-arm64'
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "api-arm64"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'web-amd64'
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "web-amd64"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'web-arm64'
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "web-arm64"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'local-sandbox-amd64'
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "local-sandbox-amd64"
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: 'local-sandbox-arm64'
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: "local-sandbox-arm64"
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
steps:
- name: Set up Depot CLI
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
@@ -85,15 +85,15 @@ jobs:
strategy:
matrix:
include:
- service_name: 'api-amd64'
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'web-amd64'
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'local-sandbox-amd64'
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: "api-amd64"
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "web-amd64"
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "local-sandbox-amd64"
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

View File

@@ -1,4 +1,4 @@
name: 'Pull Request Labeler'
name: "Pull Request Labeler"
on:
pull_request_target:

View File

@@ -2,9 +2,9 @@ name: Main CI Pipeline
on:
pull_request:
branches: ['main']
branches: ["main"]
merge_group:
branches: ['main']
branches: ["main"]
types: [checks_requested]
permissions:
@@ -48,7 +48,7 @@ jobs:
sandbox-runtime-changed: ${{ steps.changes.outputs.sandbox-runtime }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: |

View File

@@ -2,7 +2,7 @@ name: Post-Merge Checks
on:
push:
branches: ['main']
branches: ["main"]
permissions:
contents: read
@@ -19,7 +19,7 @@ jobs:
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: |

View File

@@ -57,7 +57,7 @@ jobs:
cache-dependency-path: dify-agent-runtime/go.sum
- name: Run golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v6.5.0
with:
working-directory: dify-agent-runtime
version: latest

View File

@@ -8,7 +8,7 @@ on:
- reopened
- synchronize
merge_group:
branches: ['main']
branches: ["main"]
types: [checks_requested]
jobs:

View File

@@ -11,6 +11,7 @@ on:
jobs:
stale:
runs-on: depot-ubuntu-24.04
permissions:
issues: write
@@ -22,8 +23,8 @@ jobs:
days-before-issue-stale: 15
days-before-issue-close: 3
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
stale-pr-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
any-of-labels: '🌚 invalid,🙋‍♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'

View File

@@ -44,7 +44,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: false
python-version: '3.12'
python-version: "3.12"
cache-dependency-glob: api/uv.lock
- name: Install dependencies
@@ -105,8 +105,6 @@ jobs:
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
knip.config.ts
scripts/check-web-production-unused-after-knip-fix.mjs
.nvmrc
.github/workflows/style.yml
.github/actions/setup-web/**
@@ -123,17 +121,14 @@ jobs:
- name: Web dead code check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip
- name: Web dead code check production
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip:production
- name: Web production unused declarations check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip:production-unused-check
ts-common-style:
@@ -169,7 +164,9 @@ jobs:
oxlint-suppressions.json
eslint-suppressions.json
.vscode/**
.github/**
.github/workflows/autofix.yml
.github/workflows/style.yml
.github/actions/setup-web/**
- name: Setup web environment
if: steps.changed-files.outputs.any_changed == 'true'

View File

@@ -158,7 +158,7 @@ jobs:
- name: Run Claude Code for Translation Sync
if: steps.context.outputs.CHANGED_FILES != ''
uses: anthropics/claude-code-action@c038e4dcdedfbbca18dfb17df35a17e40ded4ddc # v1.0.186
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
python-version:
- '3.12'
- "3.12"
steps:
- name: Checkout code
@@ -48,16 +48,16 @@ jobs:
- name: Install dependencies
run: uv sync --project api --dev
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/compose-action@v2.0.2
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/compose-action@v2.0.2
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
- name: Test Vector Stores
run: |

View File

@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
python-version:
- '3.12'
- "3.12"
steps:
- name: Checkout code
@@ -45,16 +45,16 @@ jobs:
- name: Install dependencies
run: uv sync --project api --dev
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/compose-action@v2.0.2
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/compose-action@v2.0.2
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
- name: Test Vector Stores
run: |

View File

@@ -26,11 +26,6 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web dependencies
uses: ./.github/actions/setup-web
@@ -38,7 +33,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
python-version: "3.12"
cache-dependency-glob: |
api/uv.lock
dify-agent/uv.lock
@@ -70,7 +65,7 @@ jobs:
E2E_ADMIN_EMAIL: e2e-admin@example.com
E2E_ADMIN_NAME: E2E Admin
E2E_ADMIN_PASSWORD: E2eAdmin12345
E2E_FORCE_WEB_BUILD: '1'
E2E_FORCE_WEB_BUILD: "1"
E2E_INIT_PASSWORD: E2eInit12345
run: vp run e2e:full
@@ -130,7 +125,7 @@ jobs:
E2E_AGENT_DECISION_MODEL_NAME: ${{ vars.E2E_AGENT_DECISION_MODEL_NAME || 'gpt-5.5' }}
E2E_AGENT_DECISION_MODEL_PROVIDER: ${{ vars.E2E_AGENT_DECISION_MODEL_PROVIDER || 'openai' }}
E2E_AGENT_DECISION_MODEL_TYPE: ${{ vars.E2E_AGENT_DECISION_MODEL_TYPE || 'llm' }}
E2E_FORCE_WEB_BUILD: '1'
E2E_FORCE_WEB_BUILD: "1"
E2E_INIT_PASSWORD: E2eInit12345
E2E_MARKETPLACE_API_URL: ${{ vars.E2E_MARKETPLACE_API_URL }}
E2E_MARKETPLACE_PLUGIN_IDS: ${{ vars.E2E_MARKETPLACE_PLUGIN_IDS }}

View File

@@ -101,11 +101,6 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web environment
uses: ./.github/actions/setup-web
@@ -140,11 +135,6 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web environment
uses: ./.github/actions/setup-web

3
.gitignore vendored
View File

@@ -140,9 +140,6 @@ dmypy.json
pyrightconfig.json
!api/pyrightconfig.json
# import-linter
.import_linter_cache/
# Pyre type checker
.pyre/
.idea/'

View File

@@ -209,11 +209,12 @@ At the same time, please consider supporting Dify by sharing it on social media
## Star History
<a href="https://star-history.dera.page/#langgenius/dify&type=date&legend=top-left">
<!-- GitHub token name: star-history -->
<a href="https://www.star-history.com/?repos=langgenius%2Fdify&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&theme=dark&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
</picture>
</a>

View File

@@ -39,9 +39,6 @@ ENABLE_COLLABORATION_MODE=true
# Learn app feature toggle
ENABLE_LEARN_APP=true
# Show the license expiry countdown badge in the console (enterprise only)
ENABLE_LICENSE_EXPIRY_NOTICE=true
# Access token expiration time in minutes
ACCESS_TOKEN_EXPIRE_MINUTES=60
@@ -120,19 +117,6 @@ SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
STORAGE_TYPE=opendal
# Key provider configuration, used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)
# key provider type: local, azure-keyvault
KEY_PROVIDER_TYPE=local
# Azure Key Vault configuration, required when KEY_PROVIDER_TYPE=azure-keyvault
# authentication uses DefaultAzureCredential (managed identity, environment variables, or Azure CLI login)
AZURE_KEYVAULT_VAULT_URL=https://<your-vault-name>.vault.azure.net
AZURE_KEYVAULT_KEY_SIZE=2048
# optional: auto-rotate each tenant's key every N days. Leave empty to manage rotation manually.
# rotation is safe because old key versions are never given an expiry and stay usable forever --
# do NOT set a rotation policy on this key in the Azure portal that expires old versions.
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS=
# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal
OPENDAL_SCHEME=fs
OPENDAL_FS_ROOT=storage
@@ -342,7 +326,6 @@ TIDB_VECTOR_PORT=4000
TIDB_VECTOR_USER=xxx.root
TIDB_VECTOR_PASSWORD=xxxxxx
TIDB_VECTOR_DATABASE=dify
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH=false
# Tidb on qdrant configuration
TIDB_ON_QDRANT_URL=http://127.0.0.1
@@ -350,7 +333,6 @@ TIDB_ON_QDRANT_API_KEY=dify
TIDB_ON_QDRANT_CLIENT_TIMEOUT=20
TIDB_ON_QDRANT_GRPC_ENABLED=false
TIDB_ON_QDRANT_GRPC_PORT=6334
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB=sandbox:60,professional:6400,team:25600
TIDB_PUBLIC_KEY=dify
TIDB_PRIVATE_KEY=dify
TIDB_API_URL=http://127.0.0.1
@@ -450,7 +432,6 @@ OPENGAUSS_MAX_CONNECTION=5
# Upload configuration
UPLOAD_FILE_SIZE_LIMIT=15
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN=15
UPLOAD_FILE_BATCH_LIMIT=5
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100

View File

@@ -15,29 +15,6 @@ root_packages =
services
include_external_packages = True
[importlinter:contract:no-direct-rsa-imports]
# Note: `libs` itself is deliberately excluded from source_modules -- import-linter's
# `forbidden` contract cannot check a package against its own descendant (libs.rsa lives
# inside libs). Sibling modules under libs/ importing libs.rsa are not covered by this
# contract; the realistic risk this guards against is application/service code reaching
# past the key provider abstraction, which is fully covered below.
name = Only the key provider abstraction may import libs.rsa directly
type = forbidden
source_modules =
core
constants
context
configs
controllers
extensions
factories
models
tasks
services
forbidden_modules =
libs.rsa
allow_indirect_imports = True
[importlinter:contract:machinery-framework-boundary]
name = API machinery is framework neutral
type = forbidden
@@ -64,64 +41,3 @@ forbidden_modules =
repositories
sqlalchemy
werkzeug
[importlinter:contract:feature-query-service-boundary]
name = Feature query application service is framework and persistence neutral
type = forbidden
source_modules =
services.feature_query_service
forbidden_modules =
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:workspace-member-query-service-boundary]
name = Workspace member query application service is framework and persistence neutral
type = forbidden
source_modules =
services.workspace_member_query_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
services.enterprise
services.workspace_member_role_resolver
sqlalchemy
werkzeug
[importlinter:contract:setup-service-boundary]
name = Setup application service is framework and persistence neutral
type = forbidden
source_modules =
services.setup_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:schema-definition-service-boundary]
name = Schema definition application service is framework and persistence neutral
type = forbidden
source_modules =
services.schema_definition_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug

View File

@@ -16,7 +16,7 @@ from core.logging.context import init_request_context
from dify_app import DifyApp
from extensions.ext_socketio import sio
from services.enterprise.enterprise_service import EnterpriseService
from services.entities.feature_entities import LicenseStatus
from services.feature_service import LicenseStatus
logger = logging.getLogger(__name__)
@@ -184,7 +184,6 @@ def initialize_extensions(app: DifyApp):
ext_forward_refs,
ext_hosting_provider,
ext_import_modules,
ext_key_provider,
ext_logging,
ext_login,
ext_logstore,
@@ -220,7 +219,6 @@ def initialize_extensions(app: DifyApp):
ext_migrate,
ext_redis,
ext_storage,
ext_key_provider, # Initialize after storage, since RSAKeyProvider reads private keys from it
ext_set_secretkey,
ext_logstore, # Initialize logstore after storage, before celery
ext_celery,

View File

@@ -10,8 +10,6 @@ from __future__ import annotations
from typing import Any
from dify_agent.protocol import RunFailureType
class AgentBackendError(Exception):
"""Base error for API-side Agent backend integration failures."""
@@ -56,7 +54,6 @@ class AgentBackendRunFailedError(AgentBackendError):
run_id: str
detail: Any
error_type: RunFailureType | None
reason: str | None
source_event_id: str | None
@@ -66,13 +63,11 @@ class AgentBackendRunFailedError(AgentBackendError):
detail: Any,
*,
message: str | None = None,
error_type: RunFailureType | None = None,
reason: str | None = None,
source_event_id: str | None = None,
) -> None:
self.run_id = run_id
self.detail = detail
self.error_type = error_type
self.reason = reason
self.source_event_id = source_event_id
display_message = message or f"Agent backend run failed: {run_id}"

View File

@@ -22,7 +22,6 @@ from dify_agent.protocol import (
RunCancelledEvent,
RunEvent,
RunFailedEvent,
RunFailureType,
RunStartedEvent,
RunSucceededEvent,
)
@@ -97,7 +96,6 @@ class AgentBackendRunFailedInternalEvent(AgentBackendInternalEventBase):
type: Literal[AgentBackendInternalEventType.RUN_FAILED] = AgentBackendInternalEventType.RUN_FAILED
error: str
error_type: RunFailureType | None = None
reason: str | None = None
@@ -182,7 +180,6 @@ class AgentBackendRunEventAdapter:
run_id=event.run_id,
source_event_id=event.id,
error=event.data.error,
error_type=event.data.error_type,
reason=event.data.reason,
)
]

View File

@@ -159,12 +159,8 @@ class AgentBackendModelConfig(BaseModel):
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
# config only receives the keys the runtime contract accepts directly; anything
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
# provider-specific parameters (see
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
# only receives the keys the runtime contract accepts.
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"temperature",
"top_p",
@@ -172,7 +168,6 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"frequency_penalty",
"max_tokens",
)
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
@@ -182,15 +177,6 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
stop = settings.get("stop")
if isinstance(stop, list) and stop:
sanitized["stop_sequences"] = stop
extra_body: dict[str, JsonValue] = {
key: value
for key, value in settings.items()
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
}
if extra_body:
sanitized["extra_body"] = extra_body
return sanitized or None

View File

@@ -11,7 +11,7 @@ from events.app_event import app_was_created
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.db_migration_lock import DbMigrationAutoRenewLock
from libs.key_providers import generate_key_pair
from libs.rsa import generate_key_pair
from models import Tenant
from models.model import App, AppMode, Conversation
from models.provider import Provider, ProviderModel

View File

@@ -25,12 +25,6 @@ class EnterpriseFeatureConfig(BaseSettings):
default=False,
)
ENABLE_LICENSE_EXPIRY_NOTICE: bool = Field(
description="Show the license expiry countdown badge in the console when the license is expiring. "
"Disable to hide the badge; license status and all enforcement remain unaffected.",
default=True,
)
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
)

View File

@@ -450,11 +450,6 @@ class FileUploadConfig(BaseSettings):
default=15,
)
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN: NonNegativeInt = Field(
description="Maximum allowed file size for knowledge uploads on paid cloud plans in megabytes",
default=15,
)
UPLOAD_FILE_BATCH_LIMIT: NonNegativeInt = Field(
description="Maximum number of files allowed in a single upload batch",
default=5,

View File

@@ -7,7 +7,6 @@ from pydantic_settings import BaseSettings
from .cache.redis_config import RedisConfig
from .cache.redis_pubsub_config import RedisPubSubConfig
from .key_provider.azure_keyvault_config import AzureKeyVaultConfig
from .storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
from .storage.amazon_s3_storage_config import S3StorageConfig
from .storage.azure_blob_storage_config import AzureBlobStorageConfig
@@ -84,22 +83,6 @@ class StorageConfig(BaseSettings):
)
_VALID_KEY_PROVIDER_TYPE = Literal[
"local",
"azure-keyvault",
]
class KeyProviderConfig(BaseSettings):
KEY_PROVIDER_TYPE: _VALID_KEY_PROVIDER_TYPE = Field(
description="Key provider used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)."
" Options: 'local' (per-tenant RSA key pair, private key kept in the STORAGE_TYPE backend),"
" 'azure-keyvault' (per-tenant RSA key kept in Azure Key Vault, private key never leaves the vault)."
" Default is 'local'.",
default=cast(_VALID_KEY_PROVIDER_TYPE, "local"),
)
class VectorStoreConfig(BaseSettings):
VECTOR_STORE: str | None = Field(
description="Type of vector store to use for efficient similarity search."
@@ -376,9 +359,6 @@ class MiddlewareConfig(
KeywordStoreConfig,
RedisConfig,
RedisPubSubConfig,
# configs of the tenant credential encryption key provider
KeyProviderConfig,
AzureKeyVaultConfig,
# configs of storage and storage providers
StorageConfig,
AliyunOSSStorageConfig,

View File

@@ -1,38 +0,0 @@
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class AzureKeyVaultConfig(BaseSettings):
"""
Configuration settings for Azure Key Vault, used as a tenant credential encryption key provider
"""
AZURE_KEYVAULT_VAULT_URL: str | None = Field(
description="URL of the Azure Key Vault instance (e.g., 'https://<your-vault-name>.vault.azure.net')."
" Required when KEY_PROVIDER_TYPE is set to 'azure-keyvault'. Authentication uses"
" DefaultAzureCredential (managed identity, environment variables, or Azure CLI login).",
default=None,
)
AZURE_KEYVAULT_KEY_SIZE: int = Field(
description="RSA key size (in bits) used when Dify provisions a new per-tenant key in Azure Key Vault.",
default=2048,
)
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS: int | None = Field(
description="If set, Dify configures each newly-created per-tenant Key Vault key to auto-rotate every"
" N days (using a 'time after create' trigger, with no expiry set on generated versions)."
" Old key versions are kept forever and remain usable for decrypting credentials encrypted"
" before the rotation, so rotation requires no manual re-encryption. Leave unset (default) to"
" not configure a rotation policy and manage rotation manually in Azure. Must be at least 7 days.",
default=None,
ge=7,
)
@field_validator("AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS", mode="before")
@classmethod
def _empty_string_to_none_for_rotation_interval(cls, v):
"""Allow empty string in env/.env (e.g. an unfilled template value) to mean 'unset'."""
if isinstance(v, str) and v.strip() == "":
return None
return v

View File

@@ -32,11 +32,6 @@ class TidbOnQdrantConfig(BaseSettings):
default=6334,
)
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB: str = Field(
description="Cloud pre-write thresholds for projected TiDB vector storage usage, in plan:MB pairs.",
default="sandbox:60,professional:6400,team:25600",
)
TIDB_PUBLIC_KEY: str | None = Field(
description="Tidb account public key",
default=None,

View File

@@ -31,8 +31,3 @@ class TiDBVectorConfig(BaseSettings):
description="Name of the TiDB Vector database to connect to",
default=None,
)
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH: bool = Field(
description="Enable TiDB Vector full-text and hybrid search features",
default=False,
)

View File

@@ -88,9 +88,8 @@ default_app_templates: Mapping[AppMode, Mapping] = {
AppMode.AGENT: {
"app": {
"mode": AppMode.AGENT,
# Public access is enabled atomically by the first successful publish.
"enable_site": False,
"enable_api": False,
"enable_site": True,
"enable_api": True,
},
},
}

View File

@@ -3,7 +3,6 @@ from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
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
@@ -35,7 +34,9 @@ def get_published_agent_app_feature_dict_and_user_input_form(
)
if agent is None:
raise AgentAppGeneratorError("Agent App has no bound Agent")
if not agent_has_workflow_callable_active_snapshot(session=session, agent=agent):
# active_config_is_published means the draft has no unpublished edits; the public app
# can still read parameters from the active snapshot while a newer draft is pending.
if not agent.active_config_snapshot_id:
raise AgentAppNotPublishedError("Agent has not been published")
snapshot = session.scalar(

View File

@@ -1,5 +1,6 @@
from uuid import UUID
from flask import request
from flask_restx import Resource
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
@@ -13,7 +14,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -64,16 +64,8 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
def get(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -82,7 +74,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
snapshot_id=req_data.snapshot_id,
snapshot_id=query.snapshot_id,
),
)
@@ -99,16 +91,8 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
def put(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -117,7 +101,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=req_data,
payload=payload,
),
)
@@ -139,16 +123,8 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -157,9 +133,9 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
),
)
@@ -176,16 +152,16 @@ class WorkflowAgentComposerValidateApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
ComposerConfigValidator.validate_publish_payload(req_data)
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=req_data,
payload=payload,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session, tenant_id=tenant_id, app_id=app_model.id, node_id=node_id
),
@@ -228,9 +204,9 @@ class WorkflowAgentComposerImpactApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -259,16 +235,8 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -277,7 +245,7 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=req_data,
payload=payload,
),
)
@@ -302,16 +270,8 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
def get(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -320,7 +280,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
snapshot_id=req_data.snapshot_id,
snapshot_id=query.snapshot_id,
),
)
@@ -336,16 +296,8 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
def put(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -354,7 +306,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=req_data,
payload=payload,
),
)
@@ -375,16 +327,8 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -393,9 +337,9 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
),
)
@@ -411,17 +355,17 @@ class SnippetAgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
app_id = _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
ComposerConfigValidator.validate_publish_payload(req_data)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=req_data,
payload=payload,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session,
tenant_id=tenant_id,
@@ -465,10 +409,10 @@ class SnippetAgentComposerImpactApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -500,16 +444,8 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -518,7 +454,7 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=req_data,
payload=payload,
),
)
@@ -548,8 +484,8 @@ class AgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
@model_validate(ComposerSavePayload)
def put(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
def put(self, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response(
AgentAppComposerResponse,
AgentComposerService.save_agent_composer(
@@ -557,7 +493,7 @@ class AgentComposerApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=account_id,
payload=req_data,
payload=payload,
),
)
@@ -573,17 +509,17 @@ class AgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, agent_id: UUID):
def post(self, session: Session, tenant_id: str, agent_id: UUID):
AgentComposerService.load_agent_composer(session=session, tenant_id=tenant_id, agent_id=str(agent_id))
ComposerConfigValidator.validate_publish_payload(req_data)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=req_data,
payload=payload,
agent_id=str(agent_id),
)
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})

View File

@@ -38,13 +38,11 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
with_current_user,
)
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
from fields.agent_fields import (
AgentConfigDraftSummaryResponse,
AgentConfigSnapshotDetailResponse,
@@ -141,7 +139,6 @@ class AgentApiStatusPayload(BaseModel):
class AgentApiAccessResponse(BaseModel):
access_ready: bool
enabled: bool
service_api_base_url: str
streaming_only: bool = True
@@ -260,7 +257,6 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite):
debug_conversation_has_messages: bool = False
debug_conversation_message_count: int = 0
role: str | None = None
access_ready: bool = False
class AgentDebugConversationRefreshResponse(BaseModel):
@@ -404,7 +400,6 @@ def _serialize_agent_app_detail(
payload["debug_conversation_has_messages"] = message_count > 0
payload["debug_conversation_message_count"] = message_count
payload["role"] = agent.role or ""
payload["access_ready"] = agent_has_workflow_callable_active_snapshot(session=session, agent=agent)
return payload
@@ -496,20 +491,10 @@ def _agent_api_key_count(session: Session, app_id: str) -> int:
)
def _agent_app_access_ready(session: Session, app_model: App) -> bool:
agent = _agent_roster_service(session).get_app_backing_agent(
tenant_id=app_model.tenant_id,
app_id=str(app_model.id),
)
return bool(agent and agent_has_workflow_callable_active_snapshot(session=session, agent=agent))
def _serialize_agent_api_access(session: Session, app_model: App) -> dict:
base_url = app_model.api_base_url
access_ready = _agent_app_access_ready(session, app_model)
response = AgentApiAccessResponse(
access_ready=access_ready,
enabled=bool(app_model.enable_api and access_ready),
enabled=bool(app_model.enable_api),
service_api_base_url=base_url,
chat_endpoint=f"{base_url}/chat-messages",
stop_endpoint=f"{base_url}/chat-messages/{{task_id}}/stop",
@@ -608,16 +593,16 @@ class AgentAppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentAppCreatePayload)
def post(self, req_data: AgentAppCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
def post(self, session: Session, current_tenant_id: str, current_user: Account):
args = AgentAppCreatePayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=req_data.name,
description=req_data.description,
name=args.name,
description=args.description,
mode="agent",
agent_role=req_data.role or "",
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
agent_role=args.role or "",
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
)
app = AppService().create_app(current_tenant_id, params, current_user, session=session)
@@ -650,25 +635,18 @@ class AgentAppApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentAppUpdatePayload)
def put(
self,
req_data: AgentAppUpdatePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
"role": req_data.role,
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
}
updated = AppService().update_app(app_model, args_dict, session=session)
return _serialize_agent_app_detail(session, updated, current_user=current_user)
@@ -729,21 +707,14 @@ class AgentPublishApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentPublishPayload)
def post(
self,
req_data: AgentPublishPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentPublishPayload.model_validate(console_ns.payload or {})
return AgentComposerService.publish_agent_app_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
version_note=req_data.version_note,
version_note=args.version_note,
)
@@ -759,21 +730,14 @@ class AgentBuildDraftCheckoutApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
@model_validate(AgentBuildDraftCheckoutPayload)
def post(
self,
req_data: AgentBuildDraftCheckoutPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentBuildDraftCheckoutPayload.model_validate(console_ns.payload or {})
return AgentComposerService.checkout_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
force=req_data.force,
force=args.force,
)
@@ -805,21 +769,14 @@ class AgentBuildDraftApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return AgentComposerService.save_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
payload=req_data,
payload=payload,
)
@console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__])
@@ -873,25 +830,18 @@ class AgentAppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentAppCopyPayload)
def post(
self,
req_data: AgentAppCopyPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentAppCopyPayload.model_validate(console_ns.payload or {})
copied_app = _agent_roster_service(session).duplicate_agent_app(
tenant_id=tenant_id,
agent_id=str(agent_id),
account=current_user,
name=req_data.name,
description=req_data.description,
role=req_data.role,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
name=args.name,
description=args.description,
role=args.role,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
)
return _serialize_agent_app_detail(session, copied_app, current_user=current_user), 201
@@ -923,10 +873,10 @@ class AgentApiStatusApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
@with_current_tenant_id
@with_session
@model_validate(AgentApiStatusPayload)
def post(self, req_data: AgentApiStatusPayload, session: Session, tenant_id: str, agent_id: UUID):
def post(self, session: Session, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
app_model = AppService().update_app_api_status(app_model, req_data.enable_api, session=session)
args = AgentApiStatusPayload.model_validate(console_ns.payload)
app_model = AppService().update_app_api_status(app_model, args.enable_api, session=session)
return _serialize_agent_api_access(session, app_model)
@@ -994,16 +944,16 @@ class AgentInviteOptionsApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
@model_validate(AgentInviteOptionsQuery)
def get(self, req_data: AgentInviteOptionsQuery, session: Session, tenant_id: str):
def get(self, session: Session, tenant_id: str):
query = AgentInviteOptionsQuery.model_validate(request.args.to_dict(flat=True))
return dump_response(
AgentInviteOptionsResponse,
_agent_roster_service(session).list_invite_options(
tenant_id=tenant_id,
page=req_data.page,
limit=req_data.limit,
keyword=req_data.keyword,
app_id=req_data.app_id,
page=query.page,
limit=query.limit,
keyword=query.keyword,
app_id=query.app_id,
),
)
@@ -1118,23 +1068,16 @@ class AgentStatisticsSummaryApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
@model_validate(AgentStatisticsQuery)
def get(
self,
req_data: AgentStatisticsQuery,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def get(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = _resolve_agent_runtime_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True))
timezone = current_user.timezone or "UTC"
start, end = _parse_observability_time_range(req_data.start, req_data.end, current_user)
start, end = _parse_observability_time_range(query.start, query.end, current_user)
try:
payload = _agent_observability_service(session).get_statistics_summary(
app=app_model,
agent_id=str(agent_id),
params=AgentStatisticsQueryParams(source=req_data.source, start=start, end=end, timezone=timezone),
params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone),
)
except ValueError as exc:
abort(400, description=str(exc))

View File

@@ -21,7 +21,6 @@ from models.dataset import Dataset
from models.enums import ApiTokenType
from models.model import ApiToken, App
from services.api_token_service import ApiTokenCache
from services.app_service import AppService
from . import console_ns
from .wraps import (
@@ -104,9 +103,7 @@ class BaseApiKeyListResource(Resource):
def _create_api_key(self, resource_id: str, current_tenant_id: str, *, session: Session) -> ApiToken:
assert self.resource_id_field is not None, "resource_id_field must be set"
resource = _get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
if isinstance(resource, App):
AppService.ensure_agent_app_access_ready(resource, session=session)
_get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
current_key_count: int = (
session.scalar(
select(func.count(ApiToken.id)).where(

View File

@@ -1,5 +1,6 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -9,7 +10,7 @@ from controllers.common.schema import (
register_response_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
from controllers.console.wraps import account_initialization_required, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
@@ -48,12 +49,12 @@ class AdvancedPromptTemplateList(Resource):
@setup_required
@login_required
@account_initialization_required
@model_validate(AdvancedPromptTemplateQuery)
def get(self, req_data: AdvancedPromptTemplateQuery):
def get(self):
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
prompt_args: AdvancedPromptTemplateArgs = {
"app_mode": req_data.app_mode,
"model_mode": req_data.model_mode,
"model_name": req_data.model_name,
"has_context": req_data.has_context,
"app_mode": args.app_mode,
"model_mode": args.model_mode,
"model_name": args.model_name,
"has_context": args.has_context,
}
return AdvancedPromptTemplateService.get_prompt(prompt_args)

View File

@@ -21,7 +21,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -212,12 +211,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(session, app_model, node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = session.scalar(
select(UploadFile).where(
@@ -342,11 +341,11 @@ class AgentLogApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT])
@model_validate(AgentLogQuery)
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
def get(self, session: Session, app_model: App):
"""Get agent logs"""
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")

View File

@@ -25,7 +25,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -88,21 +87,14 @@ class AgentAppFeatureConfigResource(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentAppFeaturesPayload)
def post(
self,
req_data: AgentAppFeaturesPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
new_app_model_config = AgentAppFeatureConfigService.update_features(
app_model=app_model,
account=current_user,
config=req_data.model_dump(exclude_none=True),
config=args.model_dump(exclude_none=True),
session=session,
)

View File

@@ -1,8 +1,8 @@
"""Console routes for Agent App and workflow Agent sandbox file access.
The API accepts product-facing Conversation, Build Draft, or Workflow Node
Execution locators and proxies list/read/download to the agent backend's
``/execution-bindings/files`` contract.
Execution locators and proxies list/read/upload to the agent backend's
``/sandbox`` contract.
"""
from __future__ import annotations
@@ -11,8 +11,10 @@ from typing import Literal
from uuid import UUID
from dify_agent.client import DifyAgentClientError, DifyAgentHTTPError, DifyAgentTimeoutError
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from controllers.common.schema import (
query_params_from_model,
@@ -20,12 +22,12 @@ from controllers.common.schema import (
register_response_schema_models,
register_schema_models,
)
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.app.error import AppNotFoundError
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -41,17 +43,11 @@ from services.agent_app_sandbox_service import (
WorkflowAgentSandboxService,
)
_BINDING_PATH_DESCRIPTION = (
"Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; "
"`~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside "
"Workspace are governed by backend isolation, not a Workspace-root restriction"
)
class AgentSandboxListQuery(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
class AgentSandboxInfoQuery(BaseModel):
@@ -62,28 +58,28 @@ class AgentSandboxInfoQuery(BaseModel):
class AgentSandboxFileQuery(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
class AgentSandboxDownloadPayload(BaseModel):
class AgentSandboxUploadPayload(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
class WorkflowAgentSandboxListQuery(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
class WorkflowAgentSandboxFileQuery(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
class WorkflowAgentSandboxDownloadPayload(BaseModel):
class WorkflowAgentSandboxUploadPayload(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
class SandboxFileEntryResponse(ResponseModel):
@@ -111,21 +107,21 @@ class SandboxReadResponse(ResponseModel):
text: str | None = None
class SandboxDownloadResponse(ResponseModel):
class SandboxUploadResponse(ResponseModel):
url: str
register_schema_models(
console_ns,
AgentSandboxDownloadPayload,
WorkflowAgentSandboxDownloadPayload,
AgentSandboxUploadPayload,
WorkflowAgentSandboxUploadPayload,
)
register_response_schema_models(
console_ns,
SandboxInfoResponse,
SandboxListResponse,
SandboxReadResponse,
SandboxDownloadResponse,
SandboxUploadResponse,
)
@@ -156,14 +152,14 @@ class AgentAppSandboxInfoResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
query = query_params_from_request(AgentSandboxInfoQuery)
try:
result = service.get_info(
result = AgentAppSandboxService().get_info(
tenant_id=tenant_id,
app_id=app_id,
app_id=app_model.id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -185,14 +181,14 @@ class AgentAppSandboxListResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
query = query_params_from_request(AgentSandboxListQuery)
try:
result = service.list_files(
result = AgentAppSandboxService().list_files(
tenant_id=tenant_id,
app_id=app_id,
app_id=app_model.id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -215,14 +211,14 @@ class AgentAppSandboxReadResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
query = query_params_from_request(AgentSandboxFileQuery)
try:
result = service.read_file(
result = AgentAppSandboxService().read_file(
tenant_id=tenant_id,
app_id=app_id,
app_id=app_model.id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -234,36 +230,30 @@ class AgentAppSandboxReadResource(Resource):
return result.model_dump()
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/download")
class AgentAppSandboxDownloadResource(Resource):
@console_ns.doc("download_agent_app_sandbox_file")
@console_ns.doc(description="Create a ToolFile from one Agent App Binding file and return its download URL")
@console_ns.expect(console_ns.models[AgentSandboxDownloadPayload.__name__])
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
@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 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
@login_required
@account_initialization_required
@with_current_tenant_id
@with_current_user
@model_validate(AgentSandboxDownloadPayload)
def post(
self,
req_data: AgentSandboxDownloadPayload,
current_user: Account,
tenant_id: str,
agent_id: UUID,
):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
@with_session(write=False)
def post(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
try:
result = service.download_file(
result = AgentAppSandboxService().upload_file(
tenant_id=tenant_id,
app_id=app_id,
app_id=app_model.id,
agent_id=str(agent_id),
caller_type=req_data.caller_type,
caller_id=req_data.caller_id,
caller_type=payload.caller_type,
caller_id=payload.caller_id,
account_id=current_user.id,
path=req_data.path,
path=payload.path,
)
except Exception as exc:
return _handle(exc)
@@ -343,41 +333,29 @@ class WorkflowAgentSandboxReadResource(Resource):
@console_ns.route(
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/download"
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/upload"
)
class WorkflowAgentSandboxDownloadResource(Resource):
@console_ns.doc("download_workflow_agent_sandbox_file")
@console_ns.doc(description="Create a ToolFile from one workflow Agent Binding file and return its download URL")
@console_ns.expect(console_ns.models[WorkflowAgentSandboxDownloadPayload.__name__])
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
class WorkflowAgentSandboxUploadResource(Resource):
@console_ns.doc("upload_workflow_agent_sandbox_file")
@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
@login_required
@account_initialization_required
@with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_tenant_id
@model_validate(WorkflowAgentSandboxDownloadPayload)
def post(
self,
req_data: WorkflowAgentSandboxDownloadPayload,
tenant_id: str,
current_user: Account,
app_id: UUID,
workflow_run_id: UUID,
node_id: str,
):
service = WorkflowAgentSandboxService()
resolved_app_id = service.resolve_app_id(tenant_id=tenant_id, app_id=str(app_id))
if resolved_app_id is None:
raise AppNotFoundError()
def post(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str):
payload = WorkflowAgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
try:
result = service.download_file(
result = WorkflowAgentSandboxService().upload_file(
tenant_id=tenant_id,
app_id=resolved_app_id,
app_id=app_model.id,
workflow_run_id=str(workflow_run_id),
node_id=node_id,
node_execution_id=req_data.node_execution_id,
account_id=current_user.id,
path=req_data.path,
node_execution_id=payload.node_execution_id,
path=payload.path,
session=db.session(),
)
except Exception as exc:
return _handle(exc)

View File

@@ -30,7 +30,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -811,21 +810,14 @@ class AgentConfigFilesByAgentApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(AgentConfigFileUploadPayload)
def post(
self,
req_data: AgentConfigFileUploadPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
return _with_agent_route_target(
session=session,
tenant_id=tenant_id,
agent_id=agent_id,
current_user=current_user,
action=lambda target: _file_upload_response(target, req_data),
action=lambda target: _file_upload_response(target, payload),
)
@@ -857,13 +849,13 @@ class AgentConfigFilesApi(Resource):
@with_current_user
@with_session
@get_app_model(mode=_WORKFLOW_APP_MODES)
@model_validate(AgentConfigFileUploadPayload)
def post(self, req_data: AgentConfigFileUploadPayload, session: Session, current_user: Account, app_model: App):
def post(self, session: Session, current_user: Account, app_model: App):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
return _with_app_route_target(
session=session,
app_model=app_model,
current_user=current_user,
action=lambda target: _file_upload_response(target, req_data),
action=lambda target: _file_upload_response(target, payload),
)

View File

@@ -20,7 +20,6 @@ from controllers.console.wraps import (
annotation_import_rate_limit,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -181,14 +180,14 @@ class AnnotationReplyActionApi(Resource):
@cloud_edition_billing_resource_check("annotation")
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@model_validate(AnnotationReplyPayload)
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
args = AnnotationReplyPayload.model_validate(console_ns.payload)
match action:
case "enable":
enable_args: EnableAnnotationArgs = {
"score_threshold": req_data.score_threshold,
"embedding_provider_name": req_data.embedding_provider_name,
"embedding_model_name": req_data.embedding_model_name,
"score_threshold": args.score_threshold,
"embedding_provider_name": args.embedding_provider_name,
"embedding_model_name": args.embedding_model_name,
}
result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id))
case "disable":
@@ -232,17 +231,12 @@ class AppAnnotationSettingUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
@model_validate(AnnotationSettingUpdatePayload)
def post(
self,
req_data: AnnotationSettingUpdatePayload,
session: Session,
app_id: UUID,
annotation_setting_id: UUID,
):
def post(self, session: Session, app_id: UUID, annotation_setting_id: UUID):
annotation_setting_id_str = str(annotation_setting_id)
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": req_data.score_threshold}
args = AnnotationSettingUpdatePayload.model_validate(console_ns.payload)
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
result = AppAnnotationService.update_app_annotation_setting(
str(app_id), annotation_setting_id_str, setting_args, session
)
@@ -296,11 +290,11 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@model_validate(AnnotationListQuery)
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
page = req_data.page
limit = req_data.limit
keyword = req_data.keyword
def get(self, session: Session, app_id: UUID):
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
limit = args.limit
keyword = args.keyword
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
str(app_id), page, limit, keyword, session
@@ -323,17 +317,17 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
@model_validate(CreateAnnotationPayload)
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
def post(self, session: Session, app_id: UUID):
args = CreateAnnotationPayload.model_validate(console_ns.payload)
upsert_args: UpsertAnnotationArgs = {}
if req_data.answer is not None:
upsert_args["answer"] = req_data.answer
if req_data.content is not None:
upsert_args["content"] = req_data.content
if req_data.message_id is not None:
upsert_args["message_id"] = req_data.message_id
if req_data.question is not None:
upsert_args["question"] = req_data.question
if args.answer is not None:
upsert_args["answer"] = args.answer
if args.content is not None:
upsert_args["content"] = args.content
if args.message_id is not None:
upsert_args["message_id"] = args.message_id
if args.question is not None:
upsert_args["question"] = args.question
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id), session)
return dump_response(Annotation, annotation), 201
@@ -413,13 +407,13 @@ class AnnotationUpdateDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
@model_validate(UpdateAnnotationPayload)
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
def post(self, session: Session, app_id: UUID, annotation_id: UUID):
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
update_args: UpdateAnnotationArgs = {}
if req_data.answer is not None:
update_args["answer"] = req_data.answer
if req_data.question is not None:
update_args["question"] = req_data.question
if args.answer is not None:
update_args["answer"] = args.answer
if args.question is not None:
update_args["question"] = args.question
app_ref = _get_app_ref(session, str(app_id))
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, session)

View File

@@ -4,6 +4,7 @@ from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
from sqlalchemy import select
@@ -32,7 +33,6 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -697,16 +697,16 @@ class AppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(CreateAppPayload)
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
def post(self, session: Session, current_tenant_id: str, current_user: Account):
"""Create app"""
args = CreateAppPayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=req_data.name,
description=req_data.description,
mode=req_data.mode,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
name=args.name,
description=args.description,
mode=args.mode,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
)
app_service = AppService()
@@ -908,20 +908,20 @@ class AppApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
@model_validate(UpdateAppPayload)
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
def put(self, session: Session, app_model: App):
"""Update app"""
args = UpdateAppPayload.model_validate(console_ns.payload)
app_service = AppService()
args_dict: AppService.ArgsDict = {
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
}
app_model = app_service.update_app(app_model, args_dict, session=session)
return AppDetailWithSite.model_validate(
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model(mode=None)
@model_validate(CopyAppPayload)
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
"""Copy app"""
# The role of the current user in the ta table must be admin, owner, or editor
args = CopyAppPayload.model_validate(console_ns.payload or {})
with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session)
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
account=current_user,
import_mode=ImportMode.YAML_CONTENT,
yaml_content=yaml_content,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
@agent_manage_required_for_agent_app
@get_app_model
@model_validate(AppExportQuery)
def get(self, req_data: AppExportQuery, app_model: App):
def get(self, app_model: App):
"""Export app"""
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
response = AppExportResponse(
data=AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=req_data.include_secret,
workflow_id=req_data.workflow_id,
include_secret=args.include_secret,
workflow_id=args.workflow_id,
)
)
return response.model_dump(mode="json")
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
@model_validate(AppNamePayload)
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
def post(self, session: Session, app_model: App):
args = AppNamePayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
app_model = app_service.update_app_name(app_model, args.name, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
@model_validate(AppIconPayload)
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
def post(self, session: Session, app_model: App):
args = AppIconPayload.model_validate(console_ns.payload or {})
app_service = AppService()
app_model = app_service.update_app_icon(
app_model,
req_data.icon or "",
req_data.icon_background or "",
req_data.icon_type,
args.icon or "",
args.icon_background or "",
args.icon_type,
session=session,
)
return AppDetail.model_validate(
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
@model_validate(AppSiteStatusPayload)
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
def post(self, session: Session, app_model: App):
args = AppSiteStatusPayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
@model_validate(AppApiStatusPayload)
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
def post(self, session: Session, app_model: App):
args = AppApiStatusPayload.model_validate(console_ns.payload)
app_service = AppService()
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
@model_validate(AppTracePayload)
def post(self, req_data: AppTracePayload, app_model: App):
def post(self, app_model: App):
# add app trace
args = AppTracePayload.model_validate(console_ns.payload)
OpsTraceManager.update_app_tracing_config(
app_id=app_model.id,
enabled=req_data.enabled,
tracing_provider=req_data.tracing_provider,
enabled=args.enabled,
tracing_provider=args.tracing_provider,
)
return SimpleResultResponse(result="success").model_dump(mode="json")

View File

@@ -12,7 +12,6 @@ from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -84,8 +83,8 @@ class AppImportApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
@with_current_user
@model_validate(AppImportPayload)
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
def post(self, current_user: Account | None = None):
args = AppImportPayload.model_validate(console_ns.payload)
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
# AppDslService performs internal commits for some creation paths, so use a plain
@@ -97,15 +96,15 @@ class AppImportApi(Resource):
try:
result = import_service.import_app(
account=account,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
app_id=req_data.app_id,
import_mode=args.mode,
yaml_content=args.yaml_content,
yaml_url=args.yaml_url,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
app_id=args.app_id,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -114,7 +113,7 @@ class AppImportApi(Resource):
else:
session.commit()
is_created_app = req_data.app_id is None and result.status in {
is_created_app = args.app_id is None and result.status in {
ImportStatus.COMPLETED,
ImportStatus.COMPLETED_WITH_WARNINGS,
}

View File

@@ -31,7 +31,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -277,15 +276,15 @@ class ChatMessageTextApi(Resource):
@login_required
@account_initialization_required
@get_app_model
@model_validate(TextToSpeechPayload)
def post(self, req_data: TextToSpeechPayload, app_model: App):
def post(self, app_model: App):
try:
payload = TextToSpeechPayload.model_validate(console_ns.payload)
message_ref = None
if req_data.message_id:
if payload.message_id:
app_ref = AppRefService.create_app_ref(app_model)
message_ref = AppRefService.create_message_ref(
app_ref,
req_data.message_id,
payload.message_id,
account_id=current_user.id,
)
@@ -293,8 +292,8 @@ class ChatMessageTextApi(Resource):
return AudioService.transcript_tts(
app_model=app_model,
session=db.session(),
text=req_data.text,
voice=req_data.voice,
text=payload.text,
voice=payload.voice,
message_ref=message_ref,
is_draft=True,
)
@@ -340,12 +339,13 @@ class TextModesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
@model_validate(TextToSpeechVoiceQuery)
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
def get(self, app_model: App):
try:
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
response = AudioService.transcript_tts_voices(
tenant_id=app_model.tenant_id,
language=req_data.language,
language=args.language,
)
return dump_response(TextToSpeechVoiceListResponse, response)

View File

@@ -29,7 +29,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -161,11 +160,11 @@ class CompletionMessageApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
@with_session
@get_app_model(mode=AppMode.COMPLETION)
@model_validate(CompletionMessagePayload)
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
args = req_data.model_dump(exclude_none=True, by_alias=True)
def post(self, session: Session, current_user: Account, app_model: App):
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
streaming = req_data.response_mode != "blocking"
streaming = args_model.response_mode != "blocking"
args["auto_generate_name"] = False
try:

View File

@@ -2,7 +2,7 @@ from typing import Literal
from uuid import UUID
import sqlalchemy as sa
from flask import abort
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, or_
@@ -18,7 +18,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -109,17 +108,17 @@ class CompletionConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=AppMode.COMPLETION)
@model_validate(CompletionConversationQuery)
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
def get(self, session: Session, current_user: Account, app_model: App):
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
query = sa.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
if req_data.keyword:
if args.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(req_data.keyword)
escaped_keyword = escape_like_pattern(args.keyword)
query = query.join(Message, Message.conversation_id == Conversation.id).where(
or_(
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
@@ -131,7 +130,7 @@ class CompletionConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -143,7 +142,7 @@ class CompletionConversationApi(Resource):
query = query.where(Conversation.created_at < end_datetime_utc)
# FIXME, the type ignore in this file
if req_data.annotation_status == "annotated":
if args.annotation_status == "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
.join( # type: ignore
@@ -151,7 +150,7 @@ class CompletionConversationApi(Resource):
)
.group_by(Conversation.id)
)
elif req_data.annotation_status == "not_annotated":
elif args.annotation_status == "not_annotated":
query = (
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
.group_by(Conversation.id)
@@ -160,7 +159,7 @@ class CompletionConversationApi(Resource):
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
return dump_response(
ConversationPaginationResponse,
@@ -239,8 +238,8 @@ class ChatConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@model_validate(ChatConversationQuery)
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
def get(self, session: Session, current_user: Account, app_model: App):
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
subquery = (
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
@@ -250,10 +249,10 @@ class ChatConversationApi(Resource):
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
if req_data.keyword:
if args.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(req_data.keyword)
escaped_keyword = escape_like_pattern(args.keyword)
keyword_filter = f"%{escaped_keyword}%"
query = (
query.join(
@@ -277,12 +276,12 @@ class ChatConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
if start_datetime_utc:
match req_data.sort_by:
match args.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at >= start_datetime_utc)
case "created_at" | "-created_at" | _:
@@ -290,13 +289,13 @@ class ChatConversationApi(Resource):
if end_datetime_utc:
end_datetime_utc = end_datetime_utc.replace(second=59)
match req_data.sort_by:
match args.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at <= end_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at <= end_datetime_utc)
match req_data.annotation_status:
match args.annotation_status:
case "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
@@ -317,7 +316,7 @@ class ChatConversationApi(Resource):
if app_model.mode == AppMode.ADVANCED_CHAT:
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
match req_data.sort_by:
match args.sort_by:
case "created_at":
query = query.order_by(Conversation.created_at.asc())
case "-created_at":
@@ -329,7 +328,7 @@ class ChatConversationApi(Resource):
case _:
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
return dump_response(
ConversationWithSummaryPaginationResponse,

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -15,7 +16,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=AppMode.ADVANCED_CHAT)
@model_validate(ConversationVariablesQuery)
def get(self, req_data: ConversationVariablesQuery, app_model: App):
def get(self, app_model: App):
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
stmt = (
select(ConversationVariable)
.where(ConversationVariable.app_id == app_model.id)
.order_by(ConversationVariable.created_at)
)
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
# NOTE: This is a temporary solution to avoid performance issues.
page = 1

View File

@@ -17,12 +17,7 @@ from controllers.console.app.error import (
ProviderQuotaExceededError,
)
from controllers.console.app.wraps import with_session
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
)
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from core.app.app_config.entities import ModelConfig
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.helper.code_executor.code_node_provider import CodeNodeProvider
@@ -243,11 +238,11 @@ class RuleGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(RuleGeneratePayload)
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = RuleGeneratePayload.model_validate(console_ns.payload)
try:
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
@@ -272,13 +267,13 @@ class RuleCodeGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(RuleCodeGeneratePayload)
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
try:
code_result = LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=req_data,
args=args,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -304,13 +299,13 @@ class RuleStructuredOutputGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(RuleStructuredOutputPayload)
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
try:
structured_output = LLMGenerator.generate_structured_output(
tenant_id=current_tenant_id,
args=req_data,
args=args,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -337,36 +332,36 @@ class InstructionGenerateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
@model_validate(InstructionGeneratePayload)
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
def post(self, session: Session, current_tenant_id: str):
args = InstructionGeneratePayload.model_validate(console_ns.payload)
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] | None = next(
(p for p in providers if p.is_accept_language(req_data.language)), None
(p for p in providers if p.is_accept_language(args.language)), None
)
code_template = code_provider.get_default_code() if code_provider else ""
try:
# Generate from nothing for a workflow node
if (req_data.current in (code_template, "")) and req_data.node_id != "":
if (args.current in (code_template, "")) and args.node_id != "":
app = session.scalar(
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
)
if not app:
return {"error": f"app {req_data.flow_id} not found"}, 400
return {"error": f"app {args.flow_id} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
if not workflow:
return {"error": f"workflow {req_data.flow_id} not found"}, 400
return {"error": f"workflow {args.flow_id} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == req_data.node_id]
node = [node for node in nodes if node["id"] == args.node_id]
if len(node) == 0:
return {"error": f"node {req_data.node_id} not found"}, 400
return {"error": f"node {args.node_id} not found"}, 400
node_type = node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=req_data.instruction,
model_config=req_data.model_config_data,
instruction=args.instruction,
model_config=args.model_config_data,
no_variable=True,
),
)
@@ -374,8 +369,8 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=req_data.instruction,
model_config=req_data.model_config_data,
instruction=args.instruction,
model_config=args.model_config_data,
no_variable=True,
),
)
@@ -383,31 +378,31 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=RuleCodeGeneratePayload(
instruction=req_data.instruction,
model_config=req_data.model_config_data,
code_language=req_data.language,
instruction=args.instruction,
model_config=args.model_config_data,
code_language=args.language,
),
)
case _:
return {"error": f"invalid node type: {node_type}"}
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
if args.node_id == "" and args.current != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_tenant_id,
flow_id=req_data.flow_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
flow_id=args.flow_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
)
if req_data.node_id != "" and req_data.current != "": # For workflow node
if args.node_id != "" and args.current != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_tenant_id,
flow_id=req_data.flow_id,
node_id=req_data.node_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
flow_id=args.flow_id,
node_id=args.node_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
workflow_service=WorkflowService(),
)
return {"error": "incompatible parameters"}, 400
@@ -431,9 +426,9 @@ class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@model_validate(InstructionTemplatePayload)
def post(self, req_data: InstructionTemplatePayload):
match req_data.type:
def post(self):
args = InstructionTemplatePayload.model_validate(console_ns.payload)
match args.type:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
@@ -443,7 +438,7 @@ class InstructionGenerationTemplateApi(Resource):
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
case _:
raise ValueError(f"Invalid type: {req_data.type}")
raise ValueError(f"Invalid type: {args.type}")
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
@@ -497,24 +492,24 @@ class WorkflowGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
# Reject empty / over-length instructions at the boundary (shared with
# the streaming endpoint) before spending a planner+builder roundtrip.
guard = _workflow_instruction_guard(req_data)
guard = _workflow_instruction_guard(args)
if guard is not None:
return guard
try:
result = WorkflowGeneratorService.generate_workflow_graph(
tenant_id=current_tenant_id,
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
if args.current_graph
else None,
)
except ProviderTokenNotInitError as ex:
@@ -552,13 +547,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(WorkflowInstructionSuggestionsPayload)
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
tenant_id=current_tenant_id,
mode=req_data.mode,
language=req_data.language,
count=req_data.count,
mode=args.mode,
language=args.language,
count=args.count,
)
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
@@ -588,12 +583,12 @@ class WorkflowGenerateStreamApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
# Same boundary guards as the blocking endpoint — return a normal 400
# JSON for these BEFORE opening the stream.
guard = _workflow_instruction_guard(req_data)
guard = _workflow_instruction_guard(args)
if guard is not None:
return guard
@@ -601,14 +596,12 @@ class WorkflowGenerateStreamApi(Resource):
try:
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
tenant_id=current_tenant_id,
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
current_graph=(
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
),
):
body = {"event": event_name, **payload}

View File

@@ -15,7 +15,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -110,16 +109,17 @@ class AppMCPServerController(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_tenant_id
@get_app_model
@model_validate(MCPServerCreatePayload)
def post(self, req_data: MCPServerCreatePayload, current_tenant_id: str, app_model: App):
description = req_data.description
def post(self, current_tenant_id: str, app_model: App):
payload = MCPServerCreatePayload.model_validate(console_ns.payload or {})
description = payload.description
if not description:
description = app_model.description or ""
server = AppMCPServer(
name=app_model.name,
description=description,
parameters=json.dumps(req_data.parameters, ensure_ascii=False),
parameters=json.dumps(payload.parameters, ensure_ascii=False),
status=AppMCPServerStatus.ACTIVE,
app_id=app_model.id,
tenant_id=current_tenant_id,
@@ -144,10 +144,10 @@ class AppMCPServerController(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@get_app_model
@model_validate(MCPServerUpdatePayload)
def put(self, req_data: MCPServerUpdatePayload, app_model: App):
def put(self, app_model: App):
payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {})
app_ref = AppRefService.create_app_ref(app_model)
server_ref = AppRefService.create_mcp_server_ref(app_ref, req_data.id)
server_ref = AppRefService.create_mcp_server_ref(app_ref, payload.id)
server = db.session.scalar(
select(AppMCPServer)
.where(
@@ -160,7 +160,7 @@ class AppMCPServerController(Resource):
if not server:
raise NotFound()
description = req_data.description
description = payload.description
if description is None or not description:
server.description = app_model.description or ""
else:
@@ -168,10 +168,10 @@ class AppMCPServerController(Resource):
server.name = app_model.name
server.parameters = json.dumps(req_data.parameters, ensure_ascii=False)
if req_data.status:
server.parameters = json.dumps(payload.parameters, ensure_ascii=False)
if payload.status:
try:
server.status = AppMCPServerStatus(req_data.status)
server.status = AppMCPServerStatus(payload.status)
except ValueError:
raise ValueError("Invalid status")
db.session.commit()

View File

@@ -28,7 +28,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -321,8 +320,8 @@ class MessageFeedbackExportApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
@model_validate(FeedbackExportQuery)
def get(self, req_data: FeedbackExportQuery, app_model: App):
def get(self, app_model: App):
args = FeedbackExportQuery.model_validate(request.args.to_dict())
# Import the service function
from services.feedback_service import FeedbackService
@@ -331,12 +330,12 @@ class MessageFeedbackExportApi(Resource):
export_data = FeedbackService.export_feedbacks(
app_model.id,
session=db.session(),
from_source=req_data.from_source,
rating=req_data.rating,
has_comment=req_data.has_comment,
start_date=req_data.start_date,
end_date=req_data.end_date,
format_type=req_data.format,
from_source=args.from_source,
rating=args.rating,
has_comment=args.has_comment,
start_date=args.start_date,
end_date=args.end_date,
format_type=args.format,
)
return export_data

View File

@@ -1,5 +1,6 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import BadRequest
@@ -13,7 +14,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -74,11 +74,12 @@ class TraceAppConfigApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
@model_validate(TraceProviderQuery)
def get(self, req_data: TraceProviderQuery, app_model: App):
def get(self, app_model: App):
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
try:
trace_config = OpsService.get_tracing_app_config(
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
)
if not trace_config:
return {"has_not_configured": True}
@@ -103,14 +104,15 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
@model_validate(TraceConfigPayload)
def post(self, req_data: TraceConfigPayload, app_model: App):
def post(self, app_model: App):
"""Create a new trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.create_tracing_app_config(
app_id=app_model.id,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
session=db.session(),
)
if not result:
@@ -138,14 +140,15 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
@model_validate(TraceConfigPayload)
def patch(self, req_data: TraceConfigPayload, app_model: App):
def patch(self, app_model: App):
"""Update an existing trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.update_tracing_app_config(
app_id=app_model.id,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
session=db.session(),
)
if not result:
@@ -167,12 +170,13 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
@model_validate(TraceProviderQuery)
def delete(self, req_data: TraceProviderQuery, app_model: App):
def delete(self, app_model: App):
"""Delete an existing trace app configuration"""
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
try:
result = OpsService.delete_tracing_app_config(
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
)
if not result:
raise TracingConfigNotExist()

View File

@@ -17,7 +17,6 @@ from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -99,8 +98,8 @@ class AppSite(Resource):
@with_current_user
@with_session
@get_app_model
@model_validate(AppSiteUpdatePayload)
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
def post(self, session: Session, current_user: Account, app_model: App):
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
if not site:
raise NotFound
@@ -124,7 +123,7 @@ class AppSite(Resource):
"show_workflow_steps",
"use_icon_as_answer_icon",
]:
value = getattr(req_data, attr_name)
value = getattr(args, attr_name)
if value is not None:
setattr(site, attr_name, value)

View File

@@ -1,7 +1,7 @@
from decimal import Decimal
import sqlalchemy as sa
from flask import abort
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -12,7 +12,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -158,8 +157,8 @@ class DailyMessageStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -178,7 +177,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -218,8 +217,8 @@ class DailyConversationStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -238,7 +237,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -277,8 +276,8 @@ class DailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -297,7 +296,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -337,8 +336,8 @@ class DailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -358,7 +357,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -400,8 +399,8 @@ class AverageSessionInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("c.created_at")
sql_query = f"""SELECT
@@ -428,7 +427,7 @@ FROM
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -479,8 +478,8 @@ class UserSatisfactionRateStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("m.created_at")
sql_query = f"""SELECT
@@ -503,7 +502,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -548,8 +547,8 @@ class AverageResponseTimeStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=AppMode.COMPLETION)
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -568,7 +567,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -608,8 +607,8 @@ class TokensPerSecondStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -631,7 +630,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -2,6 +2,7 @@ from datetime import datetime
from typing import Any
from dateutil.parser import isoparse
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -13,7 +14,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
def get(self, app_model: App):
"""
Get workflow app logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
# get paginate workflow app logs
workflow_app_service = WorkflowAppService()
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
session=session,
app_model=app_model,
keyword=req_data.keyword,
status=req_data.status,
created_at_before=req_data.created_at__before,
created_at_after=req_data.created_at__after,
page=req_data.page,
limit=req_data.limit,
detail=req_data.detail,
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
created_by_account=req_data.created_by_account,
keyword=args.keyword,
status=args.status,
created_at_before=args.created_at__before,
created_at_after=args.created_at__after,
page=args.page,
limit=args.limit,
detail=args.detail,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
)
return WorkflowAppLogPaginationResponse.model_validate(
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
def get(self, app_model: App):
"""
Get workflow archived logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
workflow_app_service = WorkflowAppService()
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
session=session,
app_model=app_model,
page=req_data.page,
limit=req_data.limit,
page=args.page,
limit=args.limit,
)
return WorkflowArchivedLogPaginationResponse.model_validate(

View File

@@ -10,7 +10,6 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -240,24 +239,18 @@ class WorkflowCommentListApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
@model_validate(WorkflowCommentCreatePayload)
def post(
self,
req_data: WorkflowCommentCreatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
):
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
"""Create a new workflow comment."""
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
created_by=current_user.id,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
)
return dump_response(WorkflowCommentCreate, result), 201
@@ -296,26 +289,19 @@ class WorkflowCommentDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
@model_validate(WorkflowCommentUpdatePayload)
def put(
self,
req_data: WorkflowCommentUpdatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
"""Update a workflow comment."""
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.update_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
user_id=current_user.id,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
)
return dump_response(WorkflowCommentUpdate, result)
@@ -386,26 +372,20 @@ class WorkflowCommentReplyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
@model_validate(WorkflowCommentReplyPayload)
def post(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
"""Add a reply to a workflow comment."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_reply(
comment_id=comment_id,
content=req_data.content,
content=payload.content,
created_by=current_user.id,
mentioned_user_ids=req_data.mentioned_user_ids,
mentioned_user_ids=payload.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyCreate, result), 201
@@ -427,30 +407,23 @@ class WorkflowCommentReplyDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
@model_validate(WorkflowCommentReplyPayload)
def put(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
reply_id: str,
):
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
"""Update a comment reply."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
reply = WorkflowCommentService.update_reply(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
reply_id=reply_id,
user_id=current_user.id,
content=req_data.content,
mentioned_user_ids=req_data.mentioned_user_ids,
content=payload.content,
mentioned_user_ids=payload.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyUpdate, reply)

View File

@@ -4,7 +4,7 @@ from functools import wraps
from typing import Any, Concatenate, Self, TypedDict, override
from uuid import UUID
from flask import Response
from flask import Response, request
from flask_restx import Resource, fields, marshal, marshal_with
from pydantic import BaseModel, ConfigDict, Field, model_validator
from sqlalchemy.orm import sessionmaker
@@ -22,7 +22,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -360,11 +359,11 @@ class WorkflowVariableCollectionApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@model_validate(WorkflowDraftVariableListQuery)
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
def get(self, current_user: Account, app_model: App):
"""
Get draft workflow
"""
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
# fetch draft workflow by app_model
workflow_service = WorkflowService()
@@ -379,8 +378,8 @@ class WorkflowVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=app_model.id,
page=req_data.page,
limit=req_data.limit,
page=args.page,
limit=args.limit,
user_id=current_user.id,
)
@@ -480,14 +479,7 @@ class VariableApi(Resource):
@console_ns.response(404, "Variable not found")
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
@model_validate(WorkflowDraftVariableUpdatePayload)
def patch(
self,
req_data: WorkflowDraftVariableUpdatePayload,
current_user: Account,
app_model: App,
variable_id: UUID,
):
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
# Request payload for file types:
#
# Local File:
@@ -512,6 +504,7 @@ class VariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
variable_id_str = str(variable_id)
variable = ensure_variable_access(
@@ -521,8 +514,8 @@ class VariableApi(Resource):
current_user_id=current_user.id,
)
new_name = req_data.name
raw_value = req_data.value
new_name = args_model.name
raw_value = args_model.value
if new_name is None and raw_value is None:
return variable
@@ -667,13 +660,13 @@ class ConversationVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=AppMode.ADVANCED_CHAT)
@model_validate(ConversationVariableUpdatePayload)
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
def post(self, current_user: Account, app_model: App):
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
workflow_service = WorkflowService()
conversation_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
]
conversation_variables = [
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
@@ -762,24 +755,24 @@ class EnvironmentVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(EnvironmentVariableUpdatePayload)
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
def post(self, current_user: Account, app_model: App):
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
workflow_service = WorkflowService()
environment_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
]
environment_variables = [
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
]
if req_data.patch:
if payload.patch:
workflow_service.patch_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
deleted_environment_variable_ids=payload.deleted_environment_variable_ids,
session=db.session(),
)
else:

View File

@@ -2,6 +2,7 @@ from datetime import UTC, datetime, timedelta
from typing import Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -16,7 +17,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
def get(self, app_model: App):
"""
Get advanced chat app workflow run list
"""
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
def get(self, app_model: App):
"""
Get advanced chat workflow runs count statistics
"""
args = req_data.model_dump(exclude_none=True)
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
def get(self, app_model: App):
"""
Get workflow run list
"""
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
def get(self, app_model: App):
"""
Get workflow runs count statistics
"""
args = req_data.model_dump(exclude_none=True)
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)

View File

@@ -1,4 +1,4 @@
from flask import abort, jsonify
from flask import abort, jsonify, request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -10,7 +10,6 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -105,13 +104,13 @@ class WorkflowDailyRunsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -149,13 +148,13 @@ class WorkflowDailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -193,13 +192,13 @@ class WorkflowDailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -237,13 +236,13 @@ class WorkflowAverageAppInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -1,6 +1,7 @@
import logging
from datetime import datetime
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, field_validator
from sqlalchemy import select
@@ -24,7 +25,6 @@ from ..wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -102,11 +102,11 @@ class WebhookTriggerApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__])
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model(mode=AppMode.WORKFLOW)
@model_validate(Parser)
def get(self, req_data: Parser, app_model: App):
def get(self, app_model: App):
"""Get webhook trigger for a node"""
args = Parser.model_validate(request.args.to_dict(flat=True))
node_id = req_data.node_id
node_id = args.node_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Get webhook trigger for this app and node
@@ -175,11 +175,11 @@ class AppTriggerEnableApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
@with_current_tenant_id
@get_app_model(mode=AppMode.WORKFLOW)
@model_validate(ParserEnable)
def post(self, req_data: ParserEnable, current_tenant_id: str, app_model: App):
def post(self, current_tenant_id: str, app_model: App):
"""Update app trigger (enable/disable)"""
args = ParserEnable.model_validate(console_ns.payload)
trigger_id = req_data.trigger_id
trigger_id = args.trigger_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Find the trigger using select
trigger = session.execute(
@@ -194,7 +194,7 @@ class AppTriggerEnableApi(Resource):
raise NotFound("Trigger not found")
# Update status based on enable_trigger boolean
trigger.status = AppTriggerStatus.ENABLED if req_data.enable_trigger else AppTriggerStatus.DISABLED
trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED
# Add computed icon field
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"

View File

@@ -9,7 +9,6 @@ from controllers.common.schema import query_params_from_model, register_schema_m
from controllers.console import console_ns
from controllers.console.auth.error import InvitationAccountMismatchError
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
from controllers.console.wraps import model_validate
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, timezone
@@ -87,14 +86,14 @@ class ActivateCheckApi(Resource):
"Success",
console_ns.models[ActivationCheckResponse.__name__],
)
@model_validate(ActivateCheckQuery)
def get(self, req_data: ActivateCheckQuery):
def get(self):
args = ActivateCheckQuery.model_validate(request.args.to_dict(flat=True))
workspaceId = req_data.workspace_id
token = req_data.token
workspaceId = args.workspace_id
token = args.token
invitation = RegisterService.get_invitation_with_case_fallback(
workspaceId, req_data.email, token, session=db.session()
workspaceId, args.email, token, session=db.session()
)
if invitation:
data = invitation.get("data", {})
@@ -139,18 +138,18 @@ class ActivateApi(Resource):
console_ns.models[ActivationResponse.__name__],
)
@console_ns.response(400, "Already activated or invalid token")
@model_validate(ActivatePayload)
def post(self, req_data: ActivatePayload):
def post(self):
"""Accept an invitation without letting an existing session act for another account.
Token-only activation remains available for legacy clients. When the request already
carries a console session, that session must belong to the account encoded in the
invitation before the token is consumed or tenant membership is changed.
"""
args = ActivatePayload.model_validate(console_ns.payload)
normalized_request_email = req_data.email.lower() if req_data.email else None
normalized_request_email = args.email.lower() if args.email else None
invitation = RegisterService.get_invitation_with_case_fallback(
req_data.workspace_id, req_data.email, req_data.token, session=db.session()
args.workspace_id, args.email, args.token, session=db.session()
)
if invitation is None:
raise AlreadyActivateError()
@@ -186,11 +185,11 @@ class ActivateApi(Resource):
setup_fields: tuple[str, str, str] | None = None
if requires_setup:
if not req_data.name or not req_data.interface_language or not req_data.timezone:
if not args.name or not args.interface_language or not args.timezone:
raise AlreadyActivateError()
setup_fields = (req_data.name, req_data.interface_language, req_data.timezone)
setup_fields = (args.name, args.interface_language, args.timezone)
RegisterService.revoke_token(req_data.workspace_id, normalized_request_email, req_data.token)
RegisterService.revoke_token(args.workspace_id, normalized_request_email, args.token)
if membership_id is None:
TenantService.create_tenant_member(tenant, account, db.session(), role=role)

View File

@@ -17,7 +17,6 @@ from ..wraps import (
RBACResourceScope,
account_initialization_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -88,10 +87,10 @@ class ApiKeyAuthDataSourceBinding(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__])
@with_current_tenant_id
@model_validate(ApiKeyAuthBindingPayload)
def post(self, req_data: ApiKeyAuthBindingPayload, current_tenant_id: str):
def post(self, current_tenant_id: str):
# The role of the current user in the table must be admin or owner
data = req_data.model_dump()
payload = ApiKeyAuthBindingPayload.model_validate(console_ns.payload)
data = payload.model_dump()
ApiKeyAuthService.validate_api_key_auth_args(data)
try:
ApiKeyAuthService.create_provider_auth(current_tenant_id, data, session=db.session())

View File

@@ -26,7 +26,7 @@ from services.billing_service import BillingService
from services.errors.account import AccountRegisterError, SeatsLimitExceededError
from ..error import AccountInFreezeError, EmailSendIpLimitError, SeatsLimitExceeded
from ..wraps import email_password_login_enabled, email_register_enabled, model_validate, setup_required
from ..wraps import email_password_login_enabled, email_register_enabled, setup_required
class EmailRegisterSendPayload(BaseModel):
@@ -87,21 +87,21 @@ class EmailRegisterSendEmailApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterSendPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@model_validate(EmailRegisterSendPayload)
def post(self, req_data: EmailRegisterSendPayload):
normalized_email = req_data.email.lower()
def post(self):
args = EmailRegisterSendPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
language = "en-US"
if req_data.language is not None and req_data.language in languages:
language = req_data.language
if args.language is not None and args.language in languages:
language = args.language
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
raise AccountInFreezeError()
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=db.session())
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
return {"result": "success", "data": token}
@@ -113,16 +113,16 @@ class EmailRegisterCheckApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterValidityPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
@model_validate(EmailRegisterValidityPayload)
def post(self, req_data: EmailRegisterValidityPayload):
def post(self):
args = EmailRegisterValidityPayload.model_validate(console_ns.payload)
user_email = req_data.email.lower()
user_email = args.email.lower()
is_email_register_error_rate_limit = AccountService.is_email_register_error_rate_limit(user_email)
if is_email_register_error_rate_limit:
raise EmailRegisterLimitError()
token_data = AccountService.get_email_register_data(req_data.token)
token_data = AccountService.get_email_register_data(args.token)
if token_data is None:
raise InvalidTokenError()
@@ -132,16 +132,16 @@ class EmailRegisterCheckApi(Resource):
if user_email != normalized_token_email:
raise InvalidEmailError()
if req_data.code != token_data.get("code"):
if args.code != token_data.get("code"):
AccountService.add_email_register_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_email_register_token(req_data.token)
AccountService.revoke_email_register_token(args.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_email_register_token(
user_email, code=req_data.code, additional_data={"phase": "register"}
user_email, code=args.code, additional_data={"phase": "register"}
)
AccountService.reset_email_register_error_rate_limit(user_email)
@@ -155,15 +155,15 @@ class EmailRegisterResetApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterResetPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EmailRegisterResetResponse.__name__])
@model_validate(EmailRegisterResetPayload)
def post(self, req_data: EmailRegisterResetPayload):
def post(self):
args = EmailRegisterResetPayload.model_validate(console_ns.payload)
# Validate passwords match
if req_data.new_password != req_data.password_confirm:
if args.new_password != args.password_confirm:
raise PasswordMismatchError()
# Validate token and get register data
register_data = AccountService.get_email_register_data(req_data.token)
register_data = AccountService.get_email_register_data(args.token)
if not register_data:
raise InvalidTokenError()
# Must use token in reset phase
@@ -171,7 +171,7 @@ class EmailRegisterResetApi(Resource):
raise InvalidTokenError()
# Revoke token to prevent reuse
AccountService.revoke_email_register_token(req_data.token)
AccountService.revoke_email_register_token(args.token)
email = register_data.get("email", "")
normalized_email = email.lower()
@@ -183,9 +183,9 @@ class EmailRegisterResetApi(Resource):
account = self._create_new_account(
email=normalized_email,
password=req_data.password_confirm,
timezone=req_data.timezone,
language=req_data.language,
password=args.password_confirm,
timezone=args.timezone,
language=args.language,
)
token_pair = AccountService.login(account=account, session=db.session(), ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(normalized_email)

View File

@@ -15,7 +15,7 @@ from controllers.console.auth.error import (
PasswordMismatchError,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.wraps import email_password_login_enabled, model_validate, setup_required
from controllers.console.wraps import email_password_login_enabled, setup_required
from extensions.ext_database import db
from libs.helper import EmailStr, extract_remote_ip
from libs.password import hash_password
@@ -68,20 +68,20 @@ class ForgotPasswordSendEmailApi(Resource):
@console_ns.response(400, "Invalid email or rate limit exceeded")
@setup_required
@email_password_login_enabled
@model_validate(ForgotPasswordSendPayload)
def post(self, req_data: ForgotPasswordSendPayload):
normalized_email = req_data.email.lower()
def post(self):
args = ForgotPasswordSendPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
if req_data.language is not None and req_data.language == "zh-Hans":
if args.language is not None and args.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=db.session())
token = AccountService.send_reset_password_email(
account=account,
@@ -106,16 +106,16 @@ class ForgotPasswordCheckApi(Resource):
@console_ns.response(400, "Invalid code or token")
@setup_required
@email_password_login_enabled
@model_validate(ForgotPasswordCheckPayload)
def post(self, req_data: ForgotPasswordCheckPayload):
def post(self):
args = ForgotPasswordCheckPayload.model_validate(console_ns.payload)
user_email = req_data.email.lower()
user_email = args.email.lower()
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(user_email)
if is_forgot_password_error_rate_limit:
raise EmailPasswordResetLimitError()
token_data = AccountService.get_reset_password_data(req_data.token)
token_data = AccountService.get_reset_password_data(args.token)
if token_data is None:
raise InvalidTokenError()
@@ -127,16 +127,16 @@ class ForgotPasswordCheckApi(Resource):
if user_email != normalized_token_email:
raise InvalidEmailError()
if req_data.code != token_data.get("code"):
if args.code != token_data.get("code"):
AccountService.add_forgot_password_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_reset_password_token(req_data.token)
AccountService.revoke_reset_password_token(args.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_reset_password_token(
token_email, code=req_data.code, additional_data={"phase": "reset"}
token_email, code=args.code, additional_data={"phase": "reset"}
)
AccountService.reset_forgot_password_error_rate_limit(user_email)
@@ -156,15 +156,15 @@ class ForgotPasswordResetApi(Resource):
@console_ns.response(400, "Invalid token or password mismatch")
@setup_required
@email_password_login_enabled
@model_validate(ForgotPasswordResetPayload)
def post(self, req_data: ForgotPasswordResetPayload):
def post(self):
args = ForgotPasswordResetPayload.model_validate(console_ns.payload)
# Validate passwords match
if req_data.new_password != req_data.password_confirm:
if args.new_password != args.password_confirm:
raise PasswordMismatchError()
# Validate token and get reset data
reset_data = AccountService.get_reset_password_data(req_data.token)
reset_data = AccountService.get_reset_password_data(args.token)
if not reset_data:
raise InvalidTokenError()
# Must use token in reset phase
@@ -172,11 +172,11 @@ class ForgotPasswordResetApi(Resource):
raise InvalidTokenError()
# Revoke token to prevent reuse
AccountService.revoke_reset_password_token(req_data.token)
AccountService.revoke_reset_password_token(args.token)
# Generate secure salt and hash password
salt = secrets.token_bytes(16)
password_hashed = hash_password(req_data.new_password, salt)
password_hashed = hash_password(args.new_password, salt)
email = reset_data.get("email", "")
account = AccountService.get_account_by_email_with_case_fallback(email, session=db.session())

View File

@@ -39,7 +39,6 @@ from controllers.console.wraps import (
decrypt_code_field,
decrypt_password_field,
email_password_login_enabled,
model_validate,
setup_required,
with_current_user,
)
@@ -115,10 +114,10 @@ class LoginApi(Resource):
@console_ns.expect(console_ns.models[LoginPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultOptionalDataResponse.__name__])
@decrypt_password_field
@model_validate(LoginPayload)
def post(self, req_data: LoginPayload):
def post(self):
"""Authenticate user and login."""
request_email = req_data.email
args = LoginPayload.model_validate(console_ns.payload)
request_email = args.email
normalized_email = request_email.lower()
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
@@ -130,7 +129,7 @@ class LoginApi(Resource):
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.LOGIN_RATE_LIMITED)
raise EmailPasswordLoginLimitError()
invite_token = req_data.invite_token
invite_token = args.invite_token
invitation_data: InvitationDetailDict | None = None
if invite_token:
invitation_data = RegisterService.get_invitation_with_case_fallback(
@@ -151,7 +150,7 @@ class LoginApi(Resource):
)
raise InvalidEmailError()
account = _authenticate_account_with_case_fallback(
request_email, normalized_email, req_data.password, invite_token
request_email, normalized_email, args.password, invite_token
)
except services.errors.account.AccountLoginError:
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_BANNED)
@@ -216,16 +215,16 @@ class ResetPasswordSendEmailApi(Resource):
@email_password_login_enabled
@console_ns.expect(console_ns.models[EmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@model_validate(EmailPayload)
def post(self, req_data: EmailPayload):
normalized_email = req_data.email.lower()
def post(self):
args = EmailPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
if req_data.language is not None and req_data.language == "zh-Hans":
if args.language is not None and args.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
try:
account = _get_account_with_case_fallback(req_data.email)
account = _get_account_with_case_fallback(args.email)
except AccountRegisterError:
raise AccountInFreezeError()
@@ -244,20 +243,20 @@ class EmailCodeLoginSendEmailApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[EmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@model_validate(EmailPayload)
def post(self, req_data: EmailPayload):
normalized_email = req_data.email.lower()
def post(self):
args = EmailPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
if req_data.language is not None and req_data.language == "zh-Hans":
if args.language is not None and args.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
try:
account = _get_account_with_case_fallback(req_data.email)
account = _get_account_with_case_fallback(args.email)
except AccountRegisterError:
raise AccountInFreezeError()
@@ -278,14 +277,14 @@ class EmailCodeLoginApi(Resource):
@console_ns.expect(console_ns.models[EmailCodeLoginPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@decrypt_code_field
@model_validate(EmailCodeLoginPayload)
def post(self, req_data: EmailCodeLoginPayload):
def post(self):
args = EmailCodeLoginPayload.model_validate(console_ns.payload)
original_email = req_data.email
original_email = args.email
user_email = original_email.lower()
language = req_data.language
language = args.language
token_data = AccountService.get_email_code_login_data(req_data.token)
token_data = AccountService.get_email_code_login_data(args.token)
if token_data is None:
_log_console_login_failure(email=user_email, reason=LoginFailureReason.INVALID_EMAIL_CODE_TOKEN)
raise InvalidTokenError()
@@ -296,11 +295,11 @@ class EmailCodeLoginApi(Resource):
_log_console_login_failure(email=user_email, reason=LoginFailureReason.EMAIL_CODE_EMAIL_MISMATCH)
raise InvalidEmailError()
if token_data["code"] != req_data.code:
if token_data["code"] != args.code:
_log_console_login_failure(email=user_email, reason=LoginFailureReason.INVALID_EMAIL_CODE)
raise EmailCodeError()
AccountService.revoke_email_code_login_token(req_data.token)
AccountService.revoke_email_code_login_token(args.token)
try:
account = _get_account_with_case_fallback(original_email)
except Unauthorized as exc:
@@ -326,7 +325,7 @@ class EmailCodeLoginApi(Resource):
email=user_email,
name=user_email,
interface_language=get_valid_language(language),
timezone=req_data.timezone,
timezone=args.timezone,
session=db.session(),
)
except WorkSpaceNotAllowedCreateError:

View File

@@ -9,7 +9,7 @@ from pydantic import BaseModel
from werkzeug.exceptions import BadRequest, NotFound
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from core.db.session_factory import session_factory
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.login import login_required
@@ -154,8 +154,8 @@ class OAuthServerAppApi(Resource):
@console_ns.expect(console_ns.models[OAuthProviderRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderAppResponse.__name__])
@oauth_server_client_id_required
@model_validate(OAuthProviderRequest)
def post(self, payload: OAuthProviderRequest, oauth_provider_app: OAuthProviderApp):
def post(self, oauth_provider_app: OAuthProviderApp):
payload = OAuthProviderRequest.model_validate(request.get_json())
redirect_uri = payload.redirect_uri
# check if redirect_uri is valid
@@ -196,8 +196,9 @@ class OAuthServerUserTokenApi(Resource):
@console_ns.expect(console_ns.models[OAuthTokenRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderTokenResponse.__name__])
@oauth_server_client_id_required
@model_validate(OAuthTokenRequest)
def post(self, payload: OAuthTokenRequest, oauth_provider_app: OAuthProviderApp):
def post(self, oauth_provider_app: OAuthProviderApp):
payload = OAuthTokenRequest.model_validate(request.get_json())
try:
grant_type = OAuthGrantType(payload.grant_type)
except ValueError:

View File

@@ -1,6 +1,7 @@
import base64
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel
from werkzeug.exceptions import BadRequest
@@ -9,7 +10,6 @@ from controllers.common.schema import query_params_from_model, register_response
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_tenant_id,
@@ -54,10 +54,10 @@ class Subscription(Resource):
@only_edition_cloud
@with_current_user
@with_current_tenant_id
@model_validate(SubscriptionQuery)
def get(self, req_data: SubscriptionQuery, current_tenant_id: str, current_user: Account):
def get(self, current_tenant_id: str, current_user: Account):
args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True))
BillingService.is_tenant_owner_or_admin(current_user, session=db.session())
return BillingService.get_subscription(req_data.plan, req_data.interval, current_user.email, current_tenant_id)
return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id)
@console_ns.route("/billing/invoices")
@@ -87,10 +87,10 @@ class PartnerTenants(Resource):
@account_initialization_required
@only_edition_cloud
@with_current_user
@model_validate(PartnerTenantsPayload)
def put(self, req_data: PartnerTenantsPayload, current_user: Account, partner_key: str):
def put(self, current_user: Account, partner_key: str):
try:
click_id = req_data.click_id
args = PartnerTenantsPayload.model_validate(console_ns.payload or {})
click_id = args.click_id
decoded_partner_key = base64.b64decode(partner_key).decode("utf-8")
except Exception:
raise BadRequest("Invalid partner_key")

View File

@@ -14,7 +14,6 @@ from ...common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0
from .. import console_ns
from ..wraps import (
account_initialization_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_tenant_id,
@@ -49,13 +48,13 @@ class ComplianceApi(Resource):
@only_edition_cloud
@with_current_user
@with_current_tenant_id
@model_validate(ComplianceDownloadQuery)
def get(self, req_data: ComplianceDownloadQuery, current_tenant_id: str, current_user: Account):
def get(self, current_tenant_id: str, current_user: Account):
args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True))
ip_address = extract_remote_ip(request)
device_info = request.headers.get("User-Agent", "Unknown device")
return BillingService.get_compliance_download_link(
doc_name=req_data.doc_name,
doc_name=args.doc_name,
account_id=current_user.id,
tenant_id=current_tenant_id,
ip=ip_address,

View File

@@ -36,7 +36,6 @@ from ..wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -230,18 +229,12 @@ class DataSourceNotionListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
@model_validate(DataSourceNotionListQuery)
def get(
self,
req_data: DataSourceNotionListQuery,
session: Session,
current_tenant_id: str,
current_user: Account,
) -> tuple[dict[str, Any], int]:
def get(self, session: Session, current_tenant_id: str, current_user: Account) -> tuple[dict[str, Any], int]:
query = DataSourceNotionListQuery.model_validate(request.args.to_dict(flat=True))
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=req_data.credential_id,
credential_id=query.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -249,8 +242,8 @@ class DataSourceNotionListApi(Resource):
raise NotFound("Credential not found.")
exist_page_ids = []
# import notion in the exist dataset
if req_data.dataset_id:
dataset = DatasetService.get_dataset(req_data.dataset_id, session)
if query.dataset_id:
dataset = DatasetService.get_dataset(query.dataset_id, session)
if not dataset:
raise NotFound("Dataset not found.")
if dataset.data_source_type != "notion_import":
@@ -258,7 +251,7 @@ class DataSourceNotionListApi(Resource):
documents = session.scalars(
select(Document).where(
Document.dataset_id == req_data.dataset_id,
Document.dataset_id == query.dataset_id,
Document.tenant_id == current_tenant_id,
Document.data_source_type == "notion_import",
Document.enabled.is_(True),
@@ -325,19 +318,13 @@ class DataSourceNotionPreviewApi(Resource):
@console_ns.doc(params=query_params_from_model(DataSourceNotionPreviewQuery))
@console_ns.response(200, "Success", console_ns.models[TextContentResponse.__name__])
@with_current_tenant_id
@model_validate(DataSourceNotionPreviewQuery)
def get(
self,
req_data: DataSourceNotionPreviewQuery,
current_tenant_id: str,
page_id: UUID,
page_type: str,
) -> tuple[dict[str, str], int]:
def get(self, current_tenant_id: str, page_id: UUID, page_type: str) -> tuple[dict[str, str], int]:
query = DataSourceNotionPreviewQuery.model_validate(request.args.to_dict(flat=True))
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=req_data.credential_id,
credential_id=query.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -367,17 +354,12 @@ class DataSourceNotionIndexingEstimateApi(Resource):
@console_ns.response(200, "Success", console_ns.models[IndexingEstimate.__name__])
@with_current_tenant_id
@with_session
@model_validate(NotionEstimatePayload)
def post(
self,
req_data: NotionEstimatePayload,
session: Session,
current_tenant_id: str,
) -> tuple[dict[str, Any], int]:
args = req_data.model_dump()
def post(self, session: Session, current_tenant_id: str) -> tuple[dict[str, Any], int]:
payload = NotionEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
notion_info_list = req_data.notion_info_list
notion_info_list = payload.notion_info_list
extract_settings = []
for notion_info in notion_info_list:
workspace_id = notion_info["workspace_id"]

View File

@@ -26,7 +26,6 @@ from controllers.console.wraps import (
cloud_edition_billing_rate_limit_check,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -361,6 +360,7 @@ def _get_retrieval_methods_by_vector_type(vector_type: str | None, is_mock: bool
# Define vector database types that only support semantic search
semantic_only_types = {
VectorType.RELYT,
VectorType.TIDB_VECTOR,
VectorType.CHROMA,
VectorType.PGVECTO_RS,
VectorType.VIKINGDB,
@@ -408,9 +408,6 @@ def _get_retrieval_methods_by_vector_type(vector_type: str | None, is_mock: bool
if vector_type == VectorType.MILVUS:
return semantic_methods if is_mock else full_methods
if vector_type == VectorType.TIDB_VECTOR:
return full_methods if dify_config.TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH else semantic_methods
if vector_type in semantic_only_types:
return semantic_methods
elif vector_type in full_search_types:
@@ -573,8 +570,9 @@ class DatasetListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(DatasetCreatePayload)
def post(self, req_data: DatasetCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = DatasetCreatePayload.model_validate(console_ns.payload or {})
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
@@ -582,20 +580,20 @@ class DatasetListApi(Resource):
if dify_config.RBAC_ENABLED:
permission = DatasetPermissionEnum.ALL_TEAM
else:
permission = req_data.permission or DatasetPermissionEnum.ONLY_ME
permission = payload.permission or DatasetPermissionEnum.ONLY_ME
try:
dataset = DatasetService.create_empty_dataset(
session=session,
tenant_id=current_tenant_id,
name=req_data.name,
description=req_data.description,
indexing_technique=req_data.indexing_technique,
name=payload.name,
description=payload.description,
indexing_technique=payload.indexing_technique,
account=current_user,
permission=permission,
provider=req_data.provider,
external_knowledge_api_id=req_data.external_knowledge_api_id,
external_knowledge_id=req_data.external_knowledge_id,
provider=payload.provider,
external_knowledge_api_id=payload.external_knowledge_api_id,
external_knowledge_id=payload.external_knowledge_id,
)
except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError()
@@ -715,35 +713,28 @@ class DatasetApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(DatasetUpdatePayload)
def patch(
self,
req_data: DatasetUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
def patch(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
payload = DatasetUpdatePayload.model_validate(console_ns.payload or {})
# check embedding model setting
if (
req_data.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and req_data.embedding_model_provider is not None
and req_data.embedding_model is not None
payload.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and payload.embedding_model_provider is not None
and payload.embedding_model is not None
):
is_multimodal = DatasetService.check_is_multimodal_model(
dataset.tenant_id, req_data.embedding_model_provider, req_data.embedding_model
dataset.tenant_id, payload.embedding_model_provider, payload.embedding_model
)
req_data.is_multimodal = is_multimodal
payload_data = req_data.model_dump(exclude_unset=True)
payload.is_multimodal = is_multimodal
payload_data = payload.model_dump(exclude_unset=True)
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
if not dify_config.RBAC_ENABLED:
DatasetPermissionService.check_permission(
current_user, dataset, req_data.permission, req_data.partial_member_list, session=session
current_user, dataset, payload.permission, payload.partial_member_list, session=session
)
dataset = DatasetService.update_dataset(dataset_id_str, payload_data, current_user, session=session)
@@ -761,12 +752,12 @@ class DatasetApi(Resource):
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
tenant_id = current_tenant_id
if req_data.partial_member_list is not None and req_data.permission == DatasetPermissionEnum.PARTIAL_TEAM:
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
DatasetPermissionService.update_partial_member_list(
tenant_id, dataset_id_str, req_data.partial_member_list, session
tenant_id, dataset_id_str, payload.partial_member_list, session
)
# clear partial member list when permission is only_me or all_team_members
elif req_data.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
elif payload.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
DatasetPermissionService.clear_partial_member_list(dataset_id_str, session)
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, session)
@@ -879,9 +870,9 @@ class DatasetIndexingEstimateApi(Resource):
@console_ns.expect(console_ns.models[IndexingEstimatePayload.__name__])
@with_current_tenant_id
@with_session
@model_validate(IndexingEstimatePayload)
def post(self, req_data: IndexingEstimatePayload, session: Session, current_tenant_id: str):
args = req_data.model_dump()
def post(self, session: Session, current_tenant_id: str):
payload = IndexingEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
extract_settings = []

View File

@@ -21,7 +21,7 @@ from controllers.common.fields import SimpleResultMessageResponse, SimpleResultR
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
from core.entities.knowledge_entities import IndexingEstimate
from core.errors.error import (
LLMBadRequestError,
@@ -60,7 +60,6 @@ from services.dataset_ref_service import DatasetRefService
from services.dataset_service import DatasetService, DocumentService
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig, ProcessRule, RetrievalModel
from services.file_service import FileService
from services.vector_space_admission_service import get_vector_space_admission_error_fields
from tasks.generate_summary_index_task import generate_summary_index_task
from ..app.error import (
@@ -936,7 +935,6 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
"completed_at": document.completed_at,
"paused_at": document.paused_at,
"error": document.error,
**get_vector_space_admission_error_fields(document.error),
"stopped_at": document.stopped_at,
"completed_segments": completed_segments,
"total_segments": total_segments,
@@ -997,7 +995,6 @@ class DocumentIndexingStatusApi(DocumentResource):
"completed_at": document.completed_at,
"paused_at": document.paused_at,
"error": document.error,
**get_vector_space_admission_error_fields(document.error),
"stopped_at": document.stopped_at,
"completed_segments": completed_segments,
"total_segments": total_segments,
@@ -1267,20 +1264,13 @@ class DocumentMetadataApi(DocumentResource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(DocumentMetadataUpdatePayload)
def put(
self,
req_data: DocumentMetadataUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
document_id: UUID,
):
def put(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)
document = self.get_document(session, dataset_id_str, document_id_str, current_user, current_tenant_id)
req_data = DocumentMetadataUpdatePayload.model_validate(request.get_json() or {})
doc_type = req_data.doc_type
doc_metadata = req_data.doc_metadata
@@ -1451,11 +1441,9 @@ class DocumentRetryApi(DocumentResource):
retry_documents = []
if not dataset:
raise NotFound("Dataset not found.")
documents = DocumentService.get_documents_by_ids(dataset.id, payload.document_ids, session)
documents_by_id = {document.id: document for document in documents}
for document_id in payload.document_ids:
try:
document = documents_by_id.get(document_id)
document = DocumentService.get_document(dataset.id, document_id, session=session)
# 404 if document not found
if document is None:

View File

@@ -36,7 +36,6 @@ from controllers.console.wraps import (
cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -415,10 +414,8 @@ class DatasetDocumentSegmentAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentCreatePayload)
def post(
self,
req_data: SegmentCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -458,7 +455,8 @@ class DatasetDocumentSegmentAddApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
payload_dict = req_data.model_dump(exclude_none=True)
payload = SegmentCreatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
segment = type_cast(DocumentSegment, SegmentService.create_segment(payload_dict, document, dataset, session))
summary = SummaryIndexService.get_segment_summary(
@@ -487,10 +485,8 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentUpdatePayload)
def patch(
self,
req_data: SegmentUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -536,12 +532,13 @@ class DatasetDocumentSegmentUpdateApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload_dict = req_data.model_dump(exclude_none=True)
payload = SegmentUpdatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
# Update segment (summary update with change detection is handled in SegmentService.update_segment)
segment = SegmentService.update_segment(
SegmentUpdateArgs.model_validate(req_data.model_dump(exclude_none=True)),
SegmentUpdateArgs.model_validate(payload.model_dump(exclude_none=True)),
segment,
document,
dataset,
@@ -619,10 +616,8 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(BatchImportPayload)
def post(
self,
req_data: BatchImportPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -640,7 +635,8 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
if not document:
raise NotFound("Document not found.")
upload_file_id = req_data.upload_file_id
payload = BatchImportPayload.model_validate(console_ns.payload or {})
upload_file_id = payload.upload_file_id
upload_file = session.scalar(select(UploadFile).where(UploadFile.id == upload_file_id).limit(1))
if not upload_file:
@@ -701,10 +697,8 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkCreatePayload)
def post(
self,
req_data: ChildChunkCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -748,7 +742,8 @@ class ChildChunkAddApi(Resource):
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
try:
child_chunk = SegmentService.create_child_chunk(req_data.content, segment, document, dataset, session)
payload = ChildChunkCreatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200
@@ -817,10 +812,8 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkBatchUpdatePayload)
def patch(
self,
req_data: ChildChunkBatchUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -850,8 +843,9 @@ class ChildChunkAddApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload = ChildChunkBatchUpdatePayload.model_validate(console_ns.payload or {})
try:
child_chunks = SegmentService.update_child_chunks(req_data.chunks, segment, document, dataset, session)
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkBatchUpdateResponse, {"data": child_chunks}), 200
@@ -924,10 +918,8 @@ class ChildChunkUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkUpdatePayload)
def patch(
self,
req_data: ChildChunkUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -963,8 +955,9 @@ class ChildChunkUpdateApi(Resource):
raise NotFound("Child chunk not found.")
# validate args
try:
payload = ChildChunkUpdatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.update_child_chunk(
req_data.content, child_chunk, segment, document, dataset, session
payload.content, child_chunk, segment, document, dataset, session
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, field_validator
from sqlalchemy.orm import Session
@@ -25,7 +24,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -39,7 +37,6 @@ from models import Account
from models.dataset import ExternalKnowledgeApis
from services.dataset_service import DatasetService
from services.enterprise import rbac_service as enterprise_rbac_service
from services.entities.external_knowledge_entities.external_knowledge_entities import ExternalDatasetCreatePayload
from services.external_knowledge_service import ExternalDatasetService
from services.hit_testing_service import HitTestingService
from services.knowledge_service import BedrockRetrievalSetting, ExternalDatasetTestService
@@ -50,6 +47,14 @@ class ExternalKnowledgeApiPayload(BaseModel):
settings: dict[str, Any]
class ExternalDatasetCreatePayload(BaseModel):
external_knowledge_api_id: str
external_knowledge_id: str
name: str = Field(..., min_length=1, max_length=100)
description: str | None = Field(None, max_length=400)
external_retrieval_model: dict[str, Any] | None = None
class ExternalHitTestingPayload(BaseModel):
query: str
external_retrieval_model: dict[str, Any] | None = None
@@ -57,7 +62,7 @@ class ExternalHitTestingPayload(BaseModel):
class BedrockRetrievalPayload(BaseModel):
retrieval_setting: BedrockRetrievalSetting
retrieval_setting: "BedrockRetrievalSetting"
query: str
knowledge_id: str
@@ -186,18 +191,18 @@ class ExternalApiTemplateListApi(Resource):
@with_current_tenant_id
@account_initialization_required
@with_session(write=False)
@model_validate(ExternalApiTemplateListQuery)
def get(self, req_data: ExternalApiTemplateListQuery, session: Session, current_tenant_id: str):
def get(self, session: Session, current_tenant_id: str):
query = ExternalApiTemplateListQuery.model_validate(request.args.to_dict())
external_knowledge_apis, total = ExternalDatasetService.get_external_knowledge_apis(
req_data.page, req_data.limit, current_tenant_id, req_data.keyword, session=session
query.page, query.limit, current_tenant_id, query.keyword, session=session
)
return ExternalKnowledgeApiListResponse(
data=[external_knowledge_api_response(item, session=session) for item in external_knowledge_apis],
has_more=len(external_knowledge_apis) == req_data.limit,
limit=req_data.limit,
has_more=len(external_knowledge_apis) == query.limit,
limit=query.limit,
total=total,
page=req_data.page,
page=query.page,
).model_dump(mode="json"), 200
@console_ns.doc("create_external_api_template")
@@ -215,16 +220,10 @@ class ExternalApiTemplateListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(ExternalKnowledgeApiPayload)
def post(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
ExternalDatasetService.validate_api_list(req_data.settings)
ExternalDatasetService.validate_api_list(payload.settings)
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -234,7 +233,7 @@ class ExternalApiTemplateListApi(Resource):
external_knowledge_api = ExternalDatasetService.create_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=req_data.model_dump(),
args=payload.model_dump(),
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -285,24 +284,17 @@ class ExternalApiTemplateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(ExternalKnowledgeApiPayload)
def patch(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
external_knowledge_api_id: UUID,
):
def patch(self, session: Session, current_tenant_id: str, current_user: Account, external_knowledge_api_id: UUID):
external_knowledge_api_id_str = str(external_knowledge_api_id)
ExternalDatasetService.validate_api_list(req_data.settings)
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
ExternalDatasetService.validate_api_list(payload.settings)
external_knowledge_api = ExternalDatasetService.update_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
external_knowledge_api_id=external_knowledge_api_id_str,
args=req_data.model_dump(),
args=payload.model_dump(),
session=session,
)
@@ -367,15 +359,10 @@ class ExternalDatasetCreateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
@model_validate(ExternalDatasetCreatePayload)
def post(
self,
req_data: ExternalDatasetCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
def post(self, session: Session, current_tenant_id: str, current_user: Account):
# The role of the current user in the ta table must be admin, owner, or editor
payload = ExternalDatasetCreatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -385,7 +372,7 @@ class ExternalDatasetCreateApi(Resource):
dataset = ExternalDatasetService.create_external_dataset(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=req_data,
args=args,
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -424,8 +411,7 @@ class ExternalKnowledgeHitTestingApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
@with_session
@model_validate(ExternalHitTestingPayload)
def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID):
def post(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -436,16 +422,17 @@ class ExternalKnowledgeHitTestingApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
HitTestingService.hit_testing_args_check(req_data.model_dump())
payload = ExternalHitTestingPayload.model_validate(console_ns.payload or {})
HitTestingService.hit_testing_args_check(payload.model_dump())
try:
response = HitTestingService.external_retrieve(
session=session,
dataset=dataset,
query=req_data.query,
query=payload.query,
account=current_user,
external_retrieval_model=req_data.external_retrieval_model,
metadata_filtering_conditions=req_data.metadata_filtering_conditions,
external_retrieval_model=payload.external_retrieval_model,
metadata_filtering_conditions=payload.metadata_filtering_conditions,
)
return dump_response(ExternalHitTestingResponse, response)
@@ -460,11 +447,11 @@ class BedrockRetrievalApi(Resource):
@console_ns.doc(description="Bedrock retrieval test (internal use only)")
@console_ns.expect(console_ns.models[BedrockRetrievalPayload.__name__])
@console_ns.response(200, "Bedrock retrieval test completed", console_ns.models[BedrockRetrievalResponse.__name__])
@model_validate(BedrockRetrievalPayload)
def post(self, req_data: BedrockRetrievalPayload):
def post(self):
payload = BedrockRetrievalPayload.model_validate(console_ns.payload or {})
# Call the knowledge retrieval service
result = ExternalDatasetTestService.knowledge_retrieval(
req_data.retrieval_setting, req_data.query, req_data.knowledge_id
payload.retrieval_setting, payload.query, payload.knowledge_id
)
return dump_response(BedrockRetrievalResponse, result), 200

View File

@@ -14,7 +14,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
enterprise_license_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -60,15 +59,9 @@ class DatasetMetadataCreateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(MetadataArgs)
def post(
self,
req_data: MetadataArgs,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -76,7 +69,7 @@ class DatasetMetadataCreateApi(Resource):
DatasetService.check_dataset_permission(dataset, current_user, session)
metadata = MetadataService.create_metadata(
dataset_id_str, req_data, current_user, current_tenant_id, session=session
dataset_id_str, metadata_args, current_user, current_tenant_id, session=session
)
return dump_response(DatasetMetadataResponse, metadata), 201
@@ -110,17 +103,16 @@ class DatasetMetadataApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(MetadataUpdatePayload)
def patch(
self,
req_data: MetadataUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
metadata_id: UUID,
):
name = req_data.name
payload = MetadataUpdatePayload.model_validate(console_ns.payload or {})
name = payload.name
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
@@ -211,15 +203,16 @@ class DocumentMetadataEditApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(MetadataOperationData)
def post(self, req_data: MetadataOperationData, session: Session, current_user: Account, dataset_id: UUID):
def post(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, session)
MetadataService.update_documents_metadata(dataset, req_data, current_user, session=session)
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
MetadataService.update_documents_metadata(dataset, metadata_args, current_user, session=session)
# Frontend callers only await success and invalidate caches; no response body is consumed.
return "", 204

View File

@@ -14,7 +14,6 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -276,8 +275,8 @@ class DatasourceAuth(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceCredentialPayload)
def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCredentialPayload.model_validate(console_ns.payload or {})
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
@@ -285,8 +284,8 @@ class DatasourceAuth(Resource):
datasource_provider_service.add_datasource_api_key_provider(
tenant_id=current_tenant_id,
provider_id=datasource_provider_id,
credentials=req_data.credentials,
name=req_data.name,
credentials=payload.credentials,
name=payload.name,
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
@@ -326,16 +325,16 @@ class DatasourceAuthDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceCredentialDeletePayload)
def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
plugin_id = datasource_provider_id.plugin_id
provider_name = datasource_provider_id.provider_name
payload = DatasourceCredentialDeletePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.remove_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=req_data.credential_id,
auth_id=payload.credential_id,
provider=provider_name,
plugin_id=plugin_id,
session=db.session(),
@@ -355,18 +354,18 @@ class DatasourceAuthUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceCredentialUpdatePayload)
def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
payload = DatasourceCredentialUpdatePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=req_data.credential_id,
auth_id=payload.credential_id,
provider=datasource_provider_id.provider_name,
plugin_id=datasource_provider_id.plugin_id,
credentials=req_data.credentials or {},
name=req_data.name,
credentials=payload.credentials or {},
name=payload.name,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
@@ -421,15 +420,15 @@ class DatasourceAuthOauthCustomClient(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceCustomClientPayload)
def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCustomClientPayload.model_validate(console_ns.payload or {})
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.setup_oauth_custom_client_params(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
client_params=req_data.client_params or {},
enabled=req_data.enable_oauth_custom_client or False,
client_params=payload.client_params or {},
enabled=payload.enable_oauth_custom_client or False,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -458,14 +457,14 @@ class DatasourceAuthDefaultApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceDefaultPayload)
def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceDefaultPayload.model_validate(console_ns.payload or {})
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.set_default_datasource_provider(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
credential_id=req_data.id,
credential_id=payload.id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -480,14 +479,14 @@ class DatasourceUpdateProviderNameApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
@model_validate(DatasourceUpdateNamePayload)
def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str):
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceUpdateNamePayload.model_validate(console_ns.payload or {})
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_provider_name(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
name=req_data.name,
credential_id=req_data.credential_id,
name=payload.name,
credential_id=payload.credential_id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200

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