mirror of
https://github.com/langgenius/dify.git
synced 2026-04-19 10:17:26 +08:00
Compare commits
8 Commits
feat/workf
...
feat/suppo
| Author | SHA1 | Date | |
|---|---|---|---|
| 73eb9647a2 | |||
| a033a53a32 | |||
| cae7f7523b | |||
| 766d88b29e | |||
| 2434b97f84 | |||
| bc7cc06572 | |||
| 67b1190535 | |||
| 79d284d686 |
@ -1,5 +1,5 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/python:3.12
|
FROM mcr.microsoft.com/devcontainers/python:3.10
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
# [Optional] Uncomment this section to install additional OS packages.
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/anaconda
|
// README at: https://github.com/devcontainers/templates/tree/main/src/anaconda
|
||||||
{
|
{
|
||||||
"name": "Python 3.12",
|
"name": "Python 3.10",
|
||||||
"build": {
|
"build": {
|
||||||
"context": "..",
|
"context": "..",
|
||||||
"dockerfile": "Dockerfile"
|
"dockerfile": "Dockerfile"
|
||||||
|
|||||||
@ -7,6 +7,5 @@ echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run
|
|||||||
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
|
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
|
||||||
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
|
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
|
||||||
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
|
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
|
||||||
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify down"' >> ~/.bashrc
|
|
||||||
|
|
||||||
source /home/vscode/.bashrc
|
source /home/vscode/.bashrc
|
||||||
36
.github/actions/setup-poetry/action.yml
vendored
36
.github/actions/setup-poetry/action.yml
vendored
@ -1,36 +0,0 @@
|
|||||||
name: Setup Poetry and Python
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
python-version:
|
|
||||||
description: Python version to use and the Poetry installed with
|
|
||||||
required: true
|
|
||||||
default: '3.11'
|
|
||||||
poetry-version:
|
|
||||||
description: Poetry version to set up
|
|
||||||
required: true
|
|
||||||
default: '1.8.4'
|
|
||||||
poetry-lockfile:
|
|
||||||
description: Path to the Poetry lockfile to restore cache from
|
|
||||||
required: true
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
- name: Install Poetry
|
|
||||||
shell: bash
|
|
||||||
run: pip install poetry==${{ inputs.poetry-version }}
|
|
||||||
|
|
||||||
- name: Restore Poetry cache
|
|
||||||
if: ${{ inputs.poetry-lockfile != '' }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
cache: poetry
|
|
||||||
cache-dependency-path: ${{ inputs.poetry-lockfile }}
|
|
||||||
49
.github/pull_request_template.md
vendored
49
.github/pull_request_template.md
vendored
@ -1,25 +1,34 @@
|
|||||||
# Summary
|
# Checklist:
|
||||||
|
|
||||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
|
||||||
|
|
||||||
> [!Tip]
|
|
||||||
> Close issue syntax: `Fixes #<issue number>` or `Resolves #<issue number>`, see [documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more details.
|
|
||||||
|
|
||||||
|
|
||||||
# Screenshots
|
|
||||||
|
|
||||||
| Before | After |
|
|
||||||
|--------|-------|
|
|
||||||
| ... | ... |
|
|
||||||
|
|
||||||
# Checklist
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Please review the checklist below before submitting your pull request.
|
> Please review the checklist below before submitting your pull request.
|
||||||
|
|
||||||
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
- [ ] Please open an issue before creating a PR or link to an existing issue
|
||||||
- [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
- [ ] I have performed a self-review of my own code
|
||||||
- [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
- [x] I've updated the documentation accordingly.
|
- [ ] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
|
||||||
- [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
|
|
||||||
|
# Description
|
||||||
|
|
||||||
|
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. Close issue syntax: `Fixes #<issue number>`, see [documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more details.
|
||||||
|
|
||||||
|
Fixes
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
||||||
|
- [ ] Improvement, including but not limited to code refactoring, performance optimization, and UI/UX improvement
|
||||||
|
- [ ] Dependency upgrade
|
||||||
|
|
||||||
|
# Testing Instructions
|
||||||
|
|
||||||
|
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||||
|
|
||||||
|
- [ ] Test A
|
||||||
|
- [ ] Test B
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
43
.github/workflows/api-tests.yml
vendored
43
.github/workflows/api-tests.yml
vendored
@ -7,7 +7,6 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- api/**
|
- api/**
|
||||||
- docker/**
|
- docker/**
|
||||||
- .github/workflows/api-tests.yml
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: api-tests-${{ github.head_ref || github.run_id }}
|
group: api-tests-${{ github.head_ref || github.run_id }}
|
||||||
@ -20,6 +19,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
@ -27,11 +27,16 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: ./.github/actions/setup-poetry
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
poetry-lockfile: api/poetry.lock
|
cache-dependency-path: |
|
||||||
|
api/pyproject.toml
|
||||||
|
api/poetry.lock
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
uses: abatilo/actions-poetry@v3
|
||||||
|
|
||||||
- name: Check Poetry lockfile
|
- name: Check Poetry lockfile
|
||||||
run: |
|
run: |
|
||||||
@ -50,18 +55,9 @@ jobs:
|
|||||||
- name: Run ModelRuntime
|
- name: Run ModelRuntime
|
||||||
run: poetry run -C api bash dev/pytest/pytest_model_runtime.sh
|
run: poetry run -C api bash dev/pytest/pytest_model_runtime.sh
|
||||||
|
|
||||||
- name: Run dify config tests
|
|
||||||
run: poetry run -C api python dev/pytest/pytest_config_tests.py
|
|
||||||
|
|
||||||
- name: Run Tool
|
- name: Run Tool
|
||||||
run: poetry run -C api bash dev/pytest/pytest_tools.sh
|
run: poetry run -C api bash dev/pytest/pytest_tools.sh
|
||||||
|
|
||||||
- name: Run mypy
|
|
||||||
run: |
|
|
||||||
pushd api
|
|
||||||
poetry run python -m mypy --install-types --non-interactive .
|
|
||||||
popd
|
|
||||||
|
|
||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
@ -71,7 +67,7 @@ jobs:
|
|||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|
||||||
- name: Set up Sandbox
|
- name: Set up Sandbox
|
||||||
uses: hoverkraft-tech/compose-action@v2.0.2
|
uses: hoverkraft-tech/compose-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -81,3 +77,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Workflow
|
- name: Run Workflow
|
||||||
run: poetry run -C api bash dev/pytest/pytest_workflow.sh
|
run: poetry run -C api bash dev/pytest/pytest_workflow.sh
|
||||||
|
|
||||||
|
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase)
|
||||||
|
uses: hoverkraft-tech/compose-action@v2.0.0
|
||||||
|
with:
|
||||||
|
compose-file: |
|
||||||
|
docker/docker-compose.yaml
|
||||||
|
services: |
|
||||||
|
weaviate
|
||||||
|
qdrant
|
||||||
|
couchbase-server
|
||||||
|
etcd
|
||||||
|
minio
|
||||||
|
milvus-standalone
|
||||||
|
pgvecto-rs
|
||||||
|
pgvector
|
||||||
|
chroma
|
||||||
|
elasticsearch
|
||||||
|
- name: Test Vector Stores
|
||||||
|
run: poetry run -C api bash dev/pytest/pytest_vdb.sh
|
||||||
|
|||||||
21
.github/workflows/db-migration-test.yml
vendored
21
.github/workflows/db-migration-test.yml
vendored
@ -6,7 +6,6 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- api/migrations/**
|
- api/migrations/**
|
||||||
- .github/workflows/db-migration-test.yml
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: db-migration-test-${{ github.ref }}
|
group: db-migration-test-${{ github.ref }}
|
||||||
@ -15,15 +14,25 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
db-migration-test:
|
db-migration-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.10"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Poetry and Python
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: ./.github/actions/setup-poetry
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
poetry-lockfile: api/poetry.lock
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-path: |
|
||||||
|
api/pyproject.toml
|
||||||
|
api/poetry.lock
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
uses: abatilo/actions-poetry@v3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: poetry install -C api
|
run: poetry install -C api
|
||||||
@ -34,7 +43,7 @@ jobs:
|
|||||||
cp middleware.env.example middleware.env
|
cp middleware.env.example middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@v2.0.2
|
uses: hoverkraft-tech/compose-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -48,8 +57,6 @@ jobs:
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
- name: Run DB Migration
|
- name: Run DB Migration
|
||||||
env:
|
|
||||||
DEBUG: true
|
|
||||||
run: |
|
run: |
|
||||||
cd api
|
cd api
|
||||||
poetry run python -m flask upgrade-db
|
poetry run python -m flask upgrade-db
|
||||||
|
|||||||
3
.github/workflows/expose_service_ports.sh
vendored
3
.github/workflows/expose_service_ports.sh
vendored
@ -9,6 +9,5 @@ yq eval '.services["pgvecto-rs"].ports += ["5431:5432"]' -i docker/docker-compos
|
|||||||
yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-compose.yaml
|
yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-compose.yaml
|
||||||
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
|
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
|
||||||
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
|
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
|
||||||
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/docker-compose.yaml
|
|
||||||
|
|
||||||
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase"
|
echo "Ports exposed for sandbox, weaviate, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase"
|
||||||
|
|||||||
52
.github/workflows/style.yml
vendored
52
.github/workflows/style.yml
vendored
@ -22,29 +22,34 @@ jobs:
|
|||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v45
|
uses: tj-actions/changed-files@v45
|
||||||
with:
|
with:
|
||||||
files: |
|
files: api/**
|
||||||
api/**
|
|
||||||
.github/workflows/style.yml
|
|
||||||
|
|
||||||
- name: Setup Poetry and Python
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: ./.github/actions/setup-poetry
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install Poetry
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
uses: abatilo/actions-poetry@v3
|
||||||
|
|
||||||
|
- name: Python dependencies
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: poetry install -C api --only lint
|
run: poetry install -C api --only lint
|
||||||
|
|
||||||
- name: Ruff check
|
- name: Ruff check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: poetry run -C api ruff check ./api
|
||||||
poetry run -C api ruff --version
|
|
||||||
poetry run -C api ruff check ./api
|
|
||||||
poetry run -C api ruff format --check ./api
|
|
||||||
|
|
||||||
- name: Dotenv check
|
- name: Dotenv check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: poetry run -C api dotenv-linter ./api/.env.example ./web/.env.example
|
run: poetry run -C api dotenv-linter ./api/.env.example ./web/.env.example
|
||||||
|
|
||||||
|
- name: Ruff formatter check
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: poetry run -C api ruff format --check ./api
|
||||||
|
|
||||||
- name: Lint hints
|
- name: Lint hints
|
||||||
if: failure()
|
if: failure()
|
||||||
run: echo "Please run 'dev/reformat' to fix the fixable linting errors."
|
run: echo "Please run 'dev/reformat' to fix the fixable linting errors."
|
||||||
@ -82,33 +87,6 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn run lint
|
run: yarn run lint
|
||||||
|
|
||||||
docker-compose-template:
|
|
||||||
name: Docker Compose Template
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@v45
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
docker/generate_docker_compose
|
|
||||||
docker/.env.example
|
|
||||||
docker/docker-compose-template.yaml
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
|
|
||||||
- name: Generate Docker Compose
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: |
|
|
||||||
cd docker
|
|
||||||
./generate_docker_compose
|
|
||||||
|
|
||||||
- name: Check for changes
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
run: git diff --exit-code
|
|
||||||
|
|
||||||
superlinter:
|
superlinter:
|
||||||
name: SuperLinter
|
name: SuperLinter
|
||||||
|
|||||||
73
.github/workflows/vdb-tests.yml
vendored
73
.github/workflows/vdb-tests.yml
vendored
@ -1,73 +0,0 @@
|
|||||||
name: Run VDB Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- api/core/rag/datasource/**
|
|
||||||
- docker/**
|
|
||||||
- .github/workflows/vdb-tests.yml
|
|
||||||
- api/poetry.lock
|
|
||||||
- api/pyproject.toml
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: VDB Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version:
|
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
|
||||||
uses: ./.github/actions/setup-poetry
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
poetry-lockfile: api/poetry.lock
|
|
||||||
|
|
||||||
- name: Check Poetry lockfile
|
|
||||||
run: |
|
|
||||||
poetry check -C api --lock
|
|
||||||
poetry show -C api
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: poetry install -C api --with dev
|
|
||||||
|
|
||||||
- name: Set up dotenvs
|
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
- name: Set up Vector Stores (TiDB, Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase)
|
|
||||||
uses: hoverkraft-tech/compose-action@v2.0.2
|
|
||||||
with:
|
|
||||||
compose-file: |
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
services: |
|
|
||||||
weaviate
|
|
||||||
qdrant
|
|
||||||
couchbase-server
|
|
||||||
etcd
|
|
||||||
minio
|
|
||||||
milvus-standalone
|
|
||||||
pgvecto-rs
|
|
||||||
pgvector
|
|
||||||
chroma
|
|
||||||
elasticsearch
|
|
||||||
tidb
|
|
||||||
|
|
||||||
- name: Test Vector Stores
|
|
||||||
run: poetry run -C api bash dev/pytest/pytest_vdb.sh
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -175,7 +175,6 @@ docker/volumes/pgvector/data/*
|
|||||||
docker/volumes/pgvecto_rs/data/*
|
docker/volumes/pgvecto_rs/data/*
|
||||||
docker/volumes/couchbase/*
|
docker/volumes/couchbase/*
|
||||||
docker/volumes/oceanbase/*
|
docker/volumes/oceanbase/*
|
||||||
!docker/volumes/oceanbase/init.d
|
|
||||||
|
|
||||||
docker/nginx/conf.d/default.conf
|
docker/nginx/conf.d/default.conf
|
||||||
docker/nginx/ssl/*
|
docker/nginx/ssl/*
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
# CONTRIBUTING
|
|
||||||
|
|
||||||
So you're looking to contribute to Dify - that's awesome, we can't wait to see what you do. As a startup with limited headcount and funding, we have grand ambitions to design the most intuitive workflow for building and managing LLM applications. Any help from the community counts, truly.
|
So you're looking to contribute to Dify - that's awesome, we can't wait to see what you do. As a startup with limited headcount and funding, we have grand ambitions to design the most intuitive workflow for building and managing LLM applications. Any help from the community counts, truly.
|
||||||
|
|
||||||
We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.
|
We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.
|
||||||
|
|
||||||
This guide, like Dify itself, is a constant work in progress. We highly appreciate your understanding if at times it lags behind the actual project, and welcome any feedback for us to improve.
|
This guide, like Dify itself, is a constant work in progress. We highly appreciate your understanding if at times it lags behind the actual project, and welcome any feedback for us to improve.
|
||||||
|
|
||||||
@ -12,12 +10,14 @@ In terms of licensing, please take a minute to read our short [License and Contr
|
|||||||
|
|
||||||
[Find](https://github.com/langgenius/dify/issues?q=is:issue+is:open) an existing issue, or [open](https://github.com/langgenius/dify/issues/new/choose) a new one. We categorize issues into 2 types:
|
[Find](https://github.com/langgenius/dify/issues?q=is:issue+is:open) an existing issue, or [open](https://github.com/langgenius/dify/issues/new/choose) a new one. We categorize issues into 2 types:
|
||||||
|
|
||||||
### Feature requests
|
### Feature requests:
|
||||||
|
|
||||||
* If you're opening a new feature request, we'd like you to explain what the proposed feature achieves, and include as much context as possible. [@perzeusss](https://github.com/perzeuss) has made a solid [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) that helps you draft out your needs. Feel free to give it a try.
|
* If you're opening a new feature request, we'd like you to explain what the proposed feature achieves, and include as much context as possible. [@perzeusss](https://github.com/perzeuss) has made a solid [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) that helps you draft out your needs. Feel free to give it a try.
|
||||||
|
|
||||||
* If you want to pick one up from the existing issues, simply drop a comment below it saying so.
|
* If you want to pick one up from the existing issues, simply drop a comment below it saying so.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A team member working in the related direction will be looped in. If all looks good, they will give the go-ahead for you to start coding. We ask that you hold off working on the feature until then, so none of your work goes to waste should we propose changes.
|
A team member working in the related direction will be looped in. If all looks good, they will give the go-ahead for you to start coding. We ask that you hold off working on the feature until then, so none of your work goes to waste should we propose changes.
|
||||||
|
|
||||||
Depending on whichever area the proposed feature falls under, you might talk to different team members. Here's rundown of the areas each our team members are working on at the moment:
|
Depending on whichever area the proposed feature falls under, you might talk to different team members. Here's rundown of the areas each our team members are working on at the moment:
|
||||||
@ -40,7 +40,7 @@ In terms of licensing, please take a minute to read our short [License and Contr
|
|||||||
| Non-core features and minor enhancements | Low Priority |
|
| Non-core features and minor enhancements | Low Priority |
|
||||||
| Valuable but not immediate | Future-Feature |
|
| Valuable but not immediate | Future-Feature |
|
||||||
|
|
||||||
### Anything else (e.g. bug report, performance optimization, typo correction)
|
### Anything else (e.g. bug report, performance optimization, typo correction):
|
||||||
|
|
||||||
* Start coding right away.
|
* Start coding right away.
|
||||||
|
|
||||||
@ -52,6 +52,7 @@ In terms of licensing, please take a minute to read our short [License and Contr
|
|||||||
| Non-critical bugs, performance boosts | Medium Priority |
|
| Non-critical bugs, performance boosts | Medium Priority |
|
||||||
| Minor fixes (typos, confusing but working UI) | Low Priority |
|
| Minor fixes (typos, confusing but working UI) | Low Priority |
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
Here are the steps to set up Dify for development:
|
Here are the steps to set up Dify for development:
|
||||||
@ -62,7 +63,7 @@ Here are the steps to set up Dify for development:
|
|||||||
|
|
||||||
Clone the forked repository from your terminal:
|
Clone the forked repository from your terminal:
|
||||||
|
|
||||||
```shell
|
```
|
||||||
git clone git@github.com:<github_username>/dify.git
|
git clone git@github.com:<github_username>/dify.git
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -70,11 +71,11 @@ git clone git@github.com:<github_username>/dify.git
|
|||||||
|
|
||||||
Dify requires the following dependencies to build, make sure they're installed on your system:
|
Dify requires the following dependencies to build, make sure they're installed on your system:
|
||||||
|
|
||||||
* [Docker](https://www.docker.com/)
|
- [Docker](https://www.docker.com/)
|
||||||
* [Docker Compose](https://docs.docker.com/compose/install/)
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
* [Node.js v18.x (LTS)](http://nodejs.org)
|
- [Node.js v18.x (LTS)](http://nodejs.org)
|
||||||
* [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
||||||
* [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
- [Python](https://www.python.org/) version 3.10.x
|
||||||
|
|
||||||
### 4. Installations
|
### 4. Installations
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ Check the [installation FAQ](https://docs.dify.ai/learn-more/faq/install-faq) fo
|
|||||||
|
|
||||||
### 5. Visit dify in your browser
|
### 5. Visit dify in your browser
|
||||||
|
|
||||||
To validate your set up, head over to [http://localhost:3000](http://localhost:3000) (the default, or your self-configured URL and port) in your browser. You should now see Dify up and running.
|
To validate your set up, head over to [http://localhost:3000](http://localhost:3000) (the default, or your self-configured URL and port) in your browser. You should now see Dify up and running.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
@ -96,9 +97,9 @@ To help you quickly navigate where your contribution fits, a brief, annotated ou
|
|||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
Dify’s backend is written in Python using [Flask](https://flask.palletsprojects.com/en/3.0.x/). It uses [SQLAlchemy](https://www.sqlalchemy.org/) for ORM and [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) for task queueing. Authorization logic goes via Flask-login.
|
Dify’s backend is written in Python using [Flask](https://flask.palletsprojects.com/en/3.0.x/). It uses [SQLAlchemy](https://www.sqlalchemy.org/) for ORM and [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) for task queueing. Authorization logic goes via Flask-login.
|
||||||
|
|
||||||
```text
|
```
|
||||||
[api/]
|
[api/]
|
||||||
├── constants // Constant settings used throughout code base.
|
├── constants // Constant settings used throughout code base.
|
||||||
├── controllers // API route definitions and request handling logic.
|
├── controllers // API route definitions and request handling logic.
|
||||||
@ -120,7 +121,7 @@ Dify’s backend is written in Python using [Flask](https://flask.palletsproject
|
|||||||
|
|
||||||
The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typescript and uses [Tailwind CSS](https://tailwindcss.com/) for styling. [React-i18next](https://react.i18next.com/) is used for internationalization.
|
The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typescript and uses [Tailwind CSS](https://tailwindcss.com/) for styling. [React-i18next](https://react.i18next.com/) is used for internationalization.
|
||||||
|
|
||||||
```text
|
```
|
||||||
[web/]
|
[web/]
|
||||||
├── app // layouts, pages, and components
|
├── app // layouts, pages, and components
|
||||||
│ ├── (commonLayout) // common layout used throughout the app
|
│ ├── (commonLayout) // common layout used throughout the app
|
||||||
@ -148,10 +149,10 @@ The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typ
|
|||||||
|
|
||||||
## Submitting your PR
|
## Submitting your PR
|
||||||
|
|
||||||
At last, time to open a pull request (PR) to our repo. For major features, we first merge them into the `deploy/dev` branch for testing, before they go into the `main` branch. If you run into issues like merge conflicts or don't know how to open a pull request, check out [GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests).
|
At last, time to open a pull request (PR) to our repo. For major features, we first merge them into the `deploy/dev` branch for testing, before they go into the `main` branch. If you run into issues like merge conflicts or don't know how to open a pull request, check out [GitHub's pull request tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests).
|
||||||
|
|
||||||
And that's it! Once your PR is merged, you will be featured as a contributor in our [README](https://github.com/langgenius/dify/blob/main/README.md).
|
And that's it! Once your PR is merged, you will be featured as a contributor in our [README](https://github.com/langgenius/dify/blob/main/README.md).
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you ever get stuck or got a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
If you ever get stuck or got a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
||||||
|
|||||||
@ -71,7 +71,7 @@ Dify 依赖以下工具和库:
|
|||||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
- [Node.js v18.x (LTS)](http://nodejs.org)
|
- [Node.js v18.x (LTS)](http://nodejs.org)
|
||||||
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
||||||
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
- [Python](https://www.python.org/) version 3.10.x
|
||||||
|
|
||||||
### 4. 安装
|
### 4. 安装
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,7 @@ Dify を構築するには次の依存関係が必要です。それらがシス
|
|||||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
- [Node.js v18.x (LTS)](http://nodejs.org)
|
- [Node.js v18.x (LTS)](http://nodejs.org)
|
||||||
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
||||||
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
- [Python](https://www.python.org/) version 3.10.x
|
||||||
|
|
||||||
### 4. インストール
|
### 4. インストール
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ Dify yêu cầu các phụ thuộc sau để build, hãy đảm bảo chúng đ
|
|||||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
- [Node.js v18.x (LTS)](http://nodejs.org)
|
- [Node.js v18.x (LTS)](http://nodejs.org)
|
||||||
- [npm](https://www.npmjs.com/) phiên bản 8.x.x hoặc [Yarn](https://yarnpkg.com/)
|
- [npm](https://www.npmjs.com/) phiên bản 8.x.x hoặc [Yarn](https://yarnpkg.com/)
|
||||||
- [Python](https://www.python.org/) phiên bản 3.11.x hoặc 3.12.x
|
- [Python](https://www.python.org/) phiên bản 3.10.x
|
||||||
|
|
||||||
### 4. Cài đặt
|
### 4. Cài đặt
|
||||||
|
|
||||||
@ -153,4 +153,4 @@ Và thế là xong! Khi PR của bạn được merge, bạn sẽ được giớ
|
|||||||
|
|
||||||
## Nhận trợ giúp
|
## Nhận trợ giúp
|
||||||
|
|
||||||
Nếu bạn gặp khó khăn hoặc có câu hỏi cấp bách trong quá trình đóng góp, hãy đặt câu hỏi của bạn trong vấn đề GitHub liên quan, hoặc tham gia [Discord](https://discord.gg/8Tpq4AcN9c) của chúng tôi để trò chuyện nhanh chóng.
|
Nếu bạn gặp khó khăn hoặc có câu hỏi cấp bách trong quá trình đóng góp, hãy đặt câu hỏi của bạn trong vấn đề GitHub liên quan, hoặc tham gia [Discord](https://discord.gg/8Tpq4AcN9c) của chúng tôi để trò chuyện nhanh chóng.
|
||||||
84
README.md
84
README.md
@ -19,9 +19,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="join Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -49,18 +46,45 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production.
|
## Table of Content
|
||||||
|
0. [Quick-Start🚀](https://github.com/langgenius/dify?tab=readme-ov-file#quick-start)
|
||||||
|
|
||||||
|
1. [Intro📖](https://github.com/langgenius/dify?tab=readme-ov-file#intro)
|
||||||
|
|
||||||
|
2. [How to use🔧](https://github.com/langgenius/dify?tab=readme-ov-file#using-dify)
|
||||||
|
|
||||||
|
3. [Stay Ahead🏃](https://github.com/langgenius/dify?tab=readme-ov-file#staying-ahead)
|
||||||
|
|
||||||
|
4. [Next Steps🏹](https://github.com/langgenius/dify?tab=readme-ov-file#next-steps)
|
||||||
|
|
||||||
|
5. [Contributing💪](https://github.com/langgenius/dify?tab=readme-ov-file#contributing)
|
||||||
|
|
||||||
|
6. [Community and Contact🏠](https://github.com/langgenius/dify?tab=readme-ov-file#community--contact)
|
||||||
|
|
||||||
|
7. [Star-History📈](https://github.com/langgenius/dify?tab=readme-ov-file#star-history)
|
||||||
|
|
||||||
|
8. [Security🔒](https://github.com/langgenius/dify?tab=readme-ov-file#security-disclosure)
|
||||||
|
|
||||||
|
9. [License🤝](https://github.com/langgenius/dify?tab=readme-ov-file#license)
|
||||||
|
|
||||||
|
> Make sure you read through this README before you start utilizing Dify😊
|
||||||
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
The quickest way to deploy Dify locally is to run our [docker-compose.yml](https://github.com/langgenius/dify/blob/main/docker/docker-compose.yaml). Follow the instructions to start in 5 minutes.
|
||||||
|
|
||||||
> Before installing Dify, make sure your machine meets the following minimum system requirements:
|
> Before installing Dify, make sure your machine meets the following minimum system requirements:
|
||||||
>
|
>
|
||||||
>- CPU >= 2 Core
|
>- CPU >= 2 Core
|
||||||
>- RAM >= 4 GiB
|
>- RAM >= 4 GiB
|
||||||
|
>- Docker and Docker Compose Installed
|
||||||
</br>
|
</br>
|
||||||
|
|
||||||
The easiest way to start the Dify server is through [docker compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine:
|
Run the following command in your terminal to clone the whole repo.
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langgenius/dify.git
|
||||||
|
```
|
||||||
|
After cloning,run the following command one by one.
|
||||||
```bash
|
```bash
|
||||||
cd dify
|
cd dify
|
||||||
cd docker
|
cd docker
|
||||||
@ -68,14 +92,13 @@ cp .env.example .env
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process.
|
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process. You will be asked to setup an admin account.
|
||||||
|
For more info of quick setup, check [here](https://docs.dify.ai/getting-started/install-self-hosted/docker-compose)
|
||||||
|
|
||||||
#### Seeking help
|
## Intro
|
||||||
Please refer to our [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) if you encounter problems setting up Dify. Reach out to [the community and us](#community--contact) if you are still having issues.
|
Dify is an open-source LLM app development platform. Its intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production. Here's a list of the core features:
|
||||||
|
</br> </br>
|
||||||
|
|
||||||
> If you'd like to contribute to Dify or do additional development, refer to our [guide to deploying from source code](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code)
|
|
||||||
|
|
||||||
## Key features
|
|
||||||
**1. Workflow**:
|
**1. Workflow**:
|
||||||
Build and test powerful AI workflows on a visual canvas, leveraging all the following features and beyond.
|
Build and test powerful AI workflows on a visual canvas, leveraging all the following features and beyond.
|
||||||
|
|
||||||
@ -126,8 +149,20 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
## Advanced Setup
|
Go to [quick-start](https://github.com/langgenius/dify?tab=readme-ov-file#quick-start) to setup your Dify or setup by source code.
|
||||||
|
|
||||||
|
#### If you......
|
||||||
|
If you forget your admin account, you can refer to this [guide](https://docs.dify.ai/getting-started/install-self-hosted/faqs#id-4.-how-to-reset-the-password-of-the-admin-account) to reset the password.
|
||||||
|
|
||||||
|
> Use docker compose up without "-d" to enable logs printing out in your terminal. This might be useful if you have encountered unknow problems when using Dify.
|
||||||
|
|
||||||
|
If you encountered system error and would like to acquire help in Github issues, make sure you always paste logs of the error in the request to accerate the conversation. Go to [Community & contact](https://github.com/langgenius/dify?tab=readme-ov-file#community--contact) for more information.
|
||||||
|
|
||||||
|
> Please read the [Dify Documentation](https://docs.dify.ai/) for detailed how-to-use guidance. Most of the potential problems are explained in the doc.
|
||||||
|
|
||||||
|
> If you'd like to contribute to Dify or make additional development, refer to our [guide to deploying from source code](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code)
|
||||||
|
|
||||||
If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||||
|
|
||||||
@ -147,13 +182,6 @@ Deploy Dify to Cloud Platform with a single click using [terraform](https://www.
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### Using AWS CDK for Deployment
|
|
||||||
|
|
||||||
Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
@ -162,18 +190,19 @@ At the same time, please consider supporting Dify by sharing it on social media
|
|||||||
|
|
||||||
> We are looking for contributors to help with translating Dify to languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c).
|
> We are looking for contributors to help with translating Dify to languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c).
|
||||||
|
|
||||||
|
**Contributors**
|
||||||
|
|
||||||
|
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Community & contact
|
## Community & contact
|
||||||
|
|
||||||
* [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
|
* [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
|
||||||
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
|
* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
|
||||||
* [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.
|
* [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.
|
||||||
|
* Make sure a log, if possible, is attached to an error reported to maximize solution efficiency.
|
||||||
**Contributors**
|
|
||||||
|
|
||||||
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Star history
|
## Star history
|
||||||
|
|
||||||
@ -187,4 +216,3 @@ To protect your privacy, please avoid posting security issues on GitHub. Instead
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions.
|
This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions.
|
||||||
|
|
||||||
|
|||||||
17
README_AR.md
17
README_AR.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="join Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -190,13 +187,6 @@ docker compose up -d
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform بواسطة @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform بواسطة @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### استخدام AWS CDK للنشر
|
|
||||||
|
|
||||||
انشر Dify على AWS باستخدام [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK بواسطة @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## المساهمة
|
## المساهمة
|
||||||
|
|
||||||
لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا.
|
لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا.
|
||||||
@ -229,10 +219,3 @@ docker compose up -d
|
|||||||
## الرخصة
|
## الرخصة
|
||||||
|
|
||||||
هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية.
|
هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية.
|
||||||
## الكشف عن الأمان
|
|
||||||
|
|
||||||
لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى security@dify.ai وسنقدم لك إجابة أكثر تفصيلاً.
|
|
||||||
|
|
||||||
## الرخصة
|
|
||||||
|
|
||||||
هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية.
|
|
||||||
|
|||||||
10
README_CN.md
10
README_CN.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="join Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -213,13 +210,6 @@ docker compose up -d
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### 使用 AWS CDK 部署
|
|
||||||
|
|
||||||
使用 [CDK](https://aws.amazon.com/cdk/) 将 Dify 部署到 AWS
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#langgenius/dify&Date)
|
[](https://star-history.com/#langgenius/dify&Date)
|
||||||
|
|||||||
17
README_ES.md
17
README_ES.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat en Discord"></a>
|
alt="chat en Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="join Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="seguir en X(Twitter)"></a>
|
alt="seguir en X(Twitter)"></a>
|
||||||
@ -215,13 +212,6 @@ Despliega Dify en una plataforma en la nube con un solo clic utilizando [terrafo
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### Usando AWS CDK para el Despliegue
|
|
||||||
|
|
||||||
Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Contribuir
|
## Contribuir
|
||||||
|
|
||||||
Para aquellos que deseen contribuir con código, consulten nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
Para aquellos que deseen contribuir con código, consulten nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
@ -255,10 +245,3 @@ Para proteger tu privacidad, evita publicar problemas de seguridad en GitHub. En
|
|||||||
## Licencia
|
## Licencia
|
||||||
|
|
||||||
Este repositorio está disponible bajo la [Licencia de Código Abierto de Dify](LICENSE), que es esencialmente Apache 2.0 con algunas restricciones adicionales.
|
Este repositorio está disponible bajo la [Licencia de Código Abierto de Dify](LICENSE), que es esencialmente Apache 2.0 con algunas restricciones adicionales.
|
||||||
## Divulgación de Seguridad
|
|
||||||
|
|
||||||
Para proteger tu privacidad, evita publicar problemas de seguridad en GitHub. En su lugar, envía tus preguntas a security@dify.ai y te proporcionaremos una respuesta más detallada.
|
|
||||||
|
|
||||||
## Licencia
|
|
||||||
|
|
||||||
Este repositorio está disponible bajo la [Licencia de Código Abierto de Dify](LICENSE), que es esencialmente Apache 2.0 con algunas restricciones adicionales.
|
|
||||||
|
|||||||
17
README_FR.md
17
README_FR.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat sur Discord"></a>
|
alt="chat sur Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="join Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="suivre sur X(Twitter)"></a>
|
alt="suivre sur X(Twitter)"></a>
|
||||||
@ -213,13 +210,6 @@ Déployez Dify sur une plateforme cloud en un clic en utilisant [terraform](http
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform par @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform par @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### Utilisation d'AWS CDK pour le déploiement
|
|
||||||
|
|
||||||
Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK par @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Contribuer
|
## Contribuer
|
||||||
|
|
||||||
Pour ceux qui souhaitent contribuer du code, consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
Pour ceux qui souhaitent contribuer du code, consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
@ -253,10 +243,3 @@ Pour protéger votre vie privée, veuillez éviter de publier des problèmes de
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Ce référentiel est disponible sous la [Licence open source Dify](LICENSE), qui est essentiellement l'Apache 2.0 avec quelques restrictions supplémentaires.
|
Ce référentiel est disponible sous la [Licence open source Dify](LICENSE), qui est essentiellement l'Apache 2.0 avec quelques restrictions supplémentaires.
|
||||||
## Divulgation de sécurité
|
|
||||||
|
|
||||||
Pour protéger votre vie privée, veuillez éviter de publier des problèmes de sécurité sur GitHub. Au lieu de cela, envoyez vos questions à security@dify.ai et nous vous fournirons une réponse plus détaillée.
|
|
||||||
|
|
||||||
## Licence
|
|
||||||
|
|
||||||
Ce référentiel est disponible sous la [Licence open source Dify](LICENSE), qui est essentiellement l'Apache 2.0 avec quelques restrictions supplémentaires.
|
|
||||||
|
|||||||
10
README_JA.md
10
README_JA.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="Discordでチャット"></a>
|
alt="Discordでチャット"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="X(Twitter)でフォロー"></a>
|
alt="X(Twitter)でフォロー"></a>
|
||||||
@ -212,13 +209,6 @@ docker compose up -d
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [@sotazumによるGoogle Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [@sotazumによるGoogle Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### AWS CDK を使用したデプロイ
|
|
||||||
|
|
||||||
[CDK](https://aws.amazon.com/cdk/) を使用して、DifyをAWSにデプロイします
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [@KevinZhaoによるAWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## 貢献
|
## 貢献
|
||||||
|
|
||||||
コードに貢献したい方は、[Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)を参照してください。
|
コードに貢献したい方は、[Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)を参照してください。
|
||||||
|
|||||||
10
README_KL.md
10
README_KL.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Follow Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -213,13 +210,6 @@ wa'logh nIqHom neH ghun deployment toy'wI' [terraform](https://www.terraform.io/
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform qachlot @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform qachlot @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### AWS CDK atorlugh pilersitsineq
|
|
||||||
|
|
||||||
wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo'laH.
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK qachlot @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
|
|||||||
10
README_KR.md
10
README_KR.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Follow Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -205,13 +202,6 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [sotazum의 Google Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [sotazum의 Google Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### AWS CDK를 사용한 배포
|
|
||||||
|
|
||||||
[CDK](https://aws.amazon.com/cdk/)를 사용하여 AWS에 Dify 배포
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [KevinZhao의 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## 기여
|
## 기여
|
||||||
|
|
||||||
코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
|
코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
|
||||||
|
|||||||
12
README_PT.md
12
README_PT.md
@ -19,9 +19,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat on Discord"></a>
|
alt="chat on Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Follow Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="follow on X(Twitter)"></a>
|
alt="follow on X(Twitter)"></a>
|
||||||
@ -211,13 +208,6 @@ Implante o Dify na Plataforma Cloud com um único clique usando [terraform](http
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### Usando AWS CDK para Implantação
|
|
||||||
|
|
||||||
Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Contribuindo
|
## Contribuindo
|
||||||
|
|
||||||
Para aqueles que desejam contribuir com código, veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
Para aqueles que desejam contribuir com código, veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
@ -248,4 +238,4 @@ Para proteger sua privacidade, evite postar problemas de segurança no GitHub. E
|
|||||||
|
|
||||||
## Licença
|
## Licença
|
||||||
|
|
||||||
Este repositório está disponível sob a [Licença de Código Aberto Dify](LICENSE), que é essencialmente Apache 2.0 com algumas restrições adicionais.
|
Este repositório está disponível sob a [Licença de Código Aberto Dify](LICENSE), que é essencialmente Apache 2.0 com algumas restrições adicionais.
|
||||||
187
README_SI.md
187
README_SI.md
@ -1,187 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Predstavljamo nalaganje datotek Dify Workflow: znova ustvarite Google NotebookLM Podcast</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
|
||||||
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Samostojno gostovanje</a> ·
|
|
||||||
<a href="https://docs.dify.ai">Dokumentacija</a> ·
|
|
||||||
<a href="https://udify.app/chat/22L1zSxg6yW1cWQg">Povpraševanje za podjetja</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://dify.ai" target="_blank">
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/Product-F04438"></a>
|
|
||||||
<a href="https://dify.ai/pricing" target="_blank">
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/free-pricing?logo=free&color=%20%23155EEF&label=pricing&labelColor=%20%23528bff"></a>
|
|
||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
|
||||||
alt="chat on Discord"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
|
||||||
alt="follow on X(Twitter)"></a>
|
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
|
||||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
|
||||||
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
|
||||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langgenius/dify?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
|
||||||
<a href="https://github.com/langgenius/dify/" target="_blank">
|
|
||||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alanggenius%2Fdify%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
|
||||||
<a href="https://github.com/langgenius/dify/discussions/" target="_blank">
|
|
||||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langgenius/dify?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
|
||||||
<a href="./README_CN.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
|
||||||
<a href="./README_JA.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
|
||||||
<a href="./README_ES.md"><img alt="README en Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
|
||||||
<a href="./README_FR.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-d9d9d9"></a>
|
|
||||||
<a href="./README_KL.md"><img alt="README tlhIngan Hol" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
|
|
||||||
<a href="./README_KR.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
|
||||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
|
||||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
|
||||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
|
||||||
<a href="./README_SI.md"><img alt="README Slovenščina" src="https://img.shields.io/badge/Sloven%C5%A1%C4%8Dina-d9d9d9"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
Dify je odprtokodna platforma za razvoj aplikacij LLM. Njegov intuitivni vmesnik združuje agentski potek dela z umetno inteligenco, cevovod RAG, zmogljivosti agentov, upravljanje modelov, funkcije opazovanja in več, kar vam omogoča hiter prehod od prototipa do proizvodnje.
|
|
||||||
|
|
||||||
## Hitri začetek
|
|
||||||
> Preden namestite Dify, se prepričajte, da vaša naprava izpolnjuje naslednje minimalne sistemske zahteve:
|
|
||||||
>
|
|
||||||
>- CPU >= 2 Core
|
|
||||||
>- RAM >= 4 GiB
|
|
||||||
|
|
||||||
</br>
|
|
||||||
|
|
||||||
Najlažji način za zagon strežnika Dify je prek docker compose . Preden zaženete Dify z naslednjimi ukazi, se prepričajte, da sta Docker in Docker Compose nameščena na vašem računalniku:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd dify
|
|
||||||
cd docker
|
|
||||||
cp .env.example .env
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Po zagonu lahko dostopate do nadzorne plošče Dify v brskalniku na [http://localhost/install](http://localhost/install) in začnete postopek inicializacije.
|
|
||||||
|
|
||||||
#### Iskanje pomoči
|
|
||||||
Prosimo, glejte naša pogosta vprašanja [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) če naletite na težave pri nastavitvi Dify. Če imate še vedno težave, se obrnite na [skupnost ali nas](#community--contact).
|
|
||||||
|
|
||||||
> Če želite prispevati k Difyju ali narediti dodaten razvoj, glejte naš vodnik za [uvajanje iz izvorne kode](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code)
|
|
||||||
|
|
||||||
## Ključne značilnosti
|
|
||||||
**1. Potek dela**:
|
|
||||||
Zgradite in preizkusite zmogljive poteke dela AI na vizualnem platnu, pri čemer izkoristite vse naslednje funkcije in več.
|
|
||||||
|
|
||||||
|
|
||||||
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**2. Celovita podpora za modele**:
|
|
||||||
Brezhibna integracija s stotinami lastniških/odprtokodnih LLM-jev ducatov ponudnikov sklepanja in samostojnih rešitev, ki pokrivajo GPT, Mistral, Llama3 in vse modele, združljive z API-jem OpenAI. Celoten seznam podprtih ponudnikov modelov najdete [tukaj](https://docs.dify.ai/getting-started/readme/model-providers).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
**3. Prompt IDE**:
|
|
||||||
intuitivni vmesnik za ustvarjanje pozivov, primerjavo zmogljivosti modela in dodajanje dodatnih funkcij, kot je pretvorba besedila v govor, aplikaciji, ki temelji na klepetu.
|
|
||||||
|
|
||||||
**4. RAG Pipeline**:
|
|
||||||
E Obsežne zmogljivosti RAG, ki pokrivajo vse od vnosa dokumenta do priklica, s podporo za ekstrakcijo besedila iz datotek PDF, PPT in drugih običajnih formatov dokumentov.
|
|
||||||
|
|
||||||
**5. Agent capabilities**:
|
|
||||||
definirate lahko agente, ki temeljijo na klicanju funkcij LLM ali ReAct, in dodate vnaprej izdelana orodja ali orodja po meri za agenta. Dify ponuja več kot 50 vgrajenih orodij za agente AI, kot so Google Search, DALL·E, Stable Diffusion in WolframAlpha.
|
|
||||||
|
|
||||||
**6. LLMOps**:
|
|
||||||
Spremljajte in analizirajte dnevnike aplikacij in učinkovitost skozi čas. Pozive, nabore podatkov in modele lahko nenehno izboljšujete na podlagi proizvodnih podatkov in opomb.
|
|
||||||
|
|
||||||
**7. Backend-as-a-Service**:
|
|
||||||
AVse ponudbe Difyja so opremljene z ustreznimi API-ji, tako da lahko Dify brez težav integrirate v svojo poslovno logiko.
|
|
||||||
|
|
||||||
|
|
||||||
## Uporaba Dify
|
|
||||||
|
|
||||||
- **Cloud </br>**
|
|
||||||
Gostimo storitev Dify Cloud za vsakogar, ki jo lahko preizkusite brez nastavitev. Zagotavlja vse zmožnosti različice za samostojno namestitev in vključuje 200 brezplačnih klicev GPT-4 v načrtu peskovnika.
|
|
||||||
|
|
||||||
- **Self-hosting Dify Community Edition</br>**
|
|
||||||
Hitro zaženite Dify v svojem okolju s tem [začetnim vodnikom](#quick-start) . Za dodatne reference in podrobnejša navodila uporabite našo [dokumentacijo](https://docs.dify.ai) .
|
|
||||||
|
|
||||||
|
|
||||||
- **Dify za podjetja/organizacije</br>**
|
|
||||||
Ponujamo dodatne funkcije, osredotočene na podjetja. Zabeležite svoja vprašanja prek tega klepetalnega robota ali nam pošljite e-pošto, da se pogovorimo o potrebah podjetja. </br>
|
|
||||||
> Za novoustanovljena podjetja in mala podjetja, ki uporabljajo AWS, si oglejte Dify Premium na AWS Marketplace in ga z enim klikom uvedite v svoj AWS VPC. To je cenovno ugodna ponudba AMI z možnostjo ustvarjanja aplikacij z logotipom in blagovno znamko po meri.
|
|
||||||
|
|
||||||
|
|
||||||
## Staying ahead
|
|
||||||
|
|
||||||
Star Dify on GitHub and be instantly notified of new releases.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Napredne nastavitve
|
|
||||||
|
|
||||||
Če morate prilagoditi konfiguracijo, si oglejte komentarje v naši datoteki .env.example in posodobite ustrezne vrednosti v svoji .env datoteki. Poleg tega boste morda morali prilagoditi docker-compose.yamlsamo datoteko, na primer spremeniti različice slike, preslikave vrat ali namestitve nosilca, glede na vaše specifično okolje in zahteve za uvajanje. Po kakršnih koli spremembah ponovno zaženite docker-compose up -d. Celoten seznam razpoložljivih spremenljivk okolja najdete tukaj .
|
|
||||||
|
|
||||||
Če želite konfigurirati visoko razpoložljivo nastavitev, so na voljo Helm Charts in datoteke YAML, ki jih prispeva skupnost, ki omogočajo uvedbo Difyja v Kubernetes.
|
|
||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
|
||||||
|
|
||||||
#### Uporaba Terraform za uvajanje
|
|
||||||
|
|
||||||
namestite Dify v Cloud Platform z enim klikom z uporabo [terraform](https://www.terraform.io/)
|
|
||||||
|
|
||||||
##### Azure Global
|
|
||||||
- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform)
|
|
||||||
|
|
||||||
##### Google Cloud
|
|
||||||
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
|
||||||
|
|
||||||
#### Uporaba AWS CDK za uvajanje
|
|
||||||
|
|
||||||
Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Prispevam
|
|
||||||
|
|
||||||
Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkrati vas prosimo, da podprete Dify tako, da ga delite na družbenih medijih ter na dogodkih in konferencah.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> Iščemo sodelavce za pomoč pri prevajanju Difyja v jezike, ki niso mandarinščina ali angleščina. Če želite pomagati, si oglejte i18n README za več informacij in nam pustite komentar v global-userskanalu našega strežnika skupnosti Discord .
|
|
||||||
|
|
||||||
## Skupnost in stik
|
|
||||||
|
|
||||||
* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj.
|
|
||||||
* [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
|
||||||
* [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.
|
|
||||||
* [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.
|
|
||||||
|
|
||||||
**Contributors**
|
|
||||||
|
|
||||||
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Star history
|
|
||||||
|
|
||||||
[](https://star-history.com/#langgenius/dify&Date)
|
|
||||||
|
|
||||||
|
|
||||||
## Varnostno razkritje
|
|
||||||
|
|
||||||
Zaradi zaščite vaše zasebnosti se izogibajte objavljanju varnostnih vprašanj na GitHub. Namesto tega pošljite vprašanja na security@dify.ai in zagotovili vam bomo podrobnejši odgovor.
|
|
||||||
|
|
||||||
## Licenca
|
|
||||||
|
|
||||||
To skladišče je na voljo pod [odprtokodno licenco Dify](LICENSE) , ki je v bistvu Apache 2.0 z nekaj dodatnimi omejitvami.
|
|
||||||
10
README_TR.md
10
README_TR.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="Discord'da sohbet et"></a>
|
alt="Discord'da sohbet et"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Follow Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="X(Twitter)'da takip et"></a>
|
alt="X(Twitter)'da takip et"></a>
|
||||||
@ -211,13 +208,6 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform tarafından @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform tarafından @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### AWS CDK ile Dağıtım
|
|
||||||
|
|
||||||
[CDK](https://aws.amazon.com/cdk/) kullanarak Dify'ı AWS'ye dağıtın
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK tarafından @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Katkıda Bulunma
|
## Katkıda Bulunma
|
||||||
|
|
||||||
Kod katkısında bulunmak isteyenler için [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakabilirsiniz.
|
Kod katkısında bulunmak isteyenler için [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakabilirsiniz.
|
||||||
|
|||||||
12
README_VI.md
12
README_VI.md
@ -15,9 +15,6 @@
|
|||||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
alt="chat trên Discord"></a>
|
alt="chat trên Discord"></a>
|
||||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
|
||||||
alt="Follow Reddit"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
alt="theo dõi trên X(Twitter)"></a>
|
alt="theo dõi trên X(Twitter)"></a>
|
||||||
@ -207,13 +204,6 @@ Triển khai Dify lên nền tảng đám mây với một cú nhấp chuột b
|
|||||||
##### Google Cloud
|
##### Google Cloud
|
||||||
- [Google Cloud Terraform bởi @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
- [Google Cloud Terraform bởi @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
#### Sử dụng AWS CDK để Triển khai
|
|
||||||
|
|
||||||
Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/)
|
|
||||||
|
|
||||||
##### AWS
|
|
||||||
- [AWS CDK bởi @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
|
||||||
|
|
||||||
## Đóng góp
|
## Đóng góp
|
||||||
|
|
||||||
Đối với những người muốn đóng góp mã, xem [Hướng dẫn Đóng góp](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) của chúng tôi.
|
Đối với những người muốn đóng góp mã, xem [Hướng dẫn Đóng góp](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) của chúng tôi.
|
||||||
@ -245,4 +235,4 @@ Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/)
|
|||||||
|
|
||||||
## Giấy phép
|
## Giấy phép
|
||||||
|
|
||||||
Kho lưu trữ này có sẵn theo [Giấy phép Mã nguồn Mở Dify](LICENSE), về cơ bản là Apache 2.0 với một vài hạn chế bổ sung.
|
Kho lưu trữ này có sẵn theo [Giấy phép Mã nguồn Mở Dify](LICENSE), về cơ bản là Apache 2.0 với một vài hạn chế bổ sung.
|
||||||
@ -23,9 +23,6 @@ FILES_ACCESS_TIMEOUT=300
|
|||||||
# Access token expiration time in minutes
|
# Access token expiration time in minutes
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
|
||||||
# Refresh token expiration time in days
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS=30
|
|
||||||
|
|
||||||
# celery configuration
|
# celery configuration
|
||||||
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
|
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
|
||||||
|
|
||||||
@ -45,11 +42,6 @@ REDIS_SENTINEL_USERNAME=
|
|||||||
REDIS_SENTINEL_PASSWORD=
|
REDIS_SENTINEL_PASSWORD=
|
||||||
REDIS_SENTINEL_SOCKET_TIMEOUT=0.1
|
REDIS_SENTINEL_SOCKET_TIMEOUT=0.1
|
||||||
|
|
||||||
# redis Cluster configuration.
|
|
||||||
REDIS_USE_CLUSTERS=false
|
|
||||||
REDIS_CLUSTERS=
|
|
||||||
REDIS_CLUSTERS_PASSWORD=
|
|
||||||
|
|
||||||
# PostgreSQL database configuration
|
# PostgreSQL database configuration
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=difyai123456
|
DB_PASSWORD=difyai123456
|
||||||
@ -59,27 +51,20 @@ DB_DATABASE=dify
|
|||||||
|
|
||||||
# Storage configuration
|
# Storage configuration
|
||||||
# use for store upload files, private keys...
|
# use for store upload files, private keys...
|
||||||
# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
|
# storage type: local, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
|
||||||
STORAGE_TYPE=opendal
|
STORAGE_TYPE=local
|
||||||
|
STORAGE_LOCAL_PATH=storage
|
||||||
# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal
|
|
||||||
OPENDAL_SCHEME=fs
|
|
||||||
OPENDAL_FS_ROOT=storage
|
|
||||||
|
|
||||||
# S3 Storage configuration
|
|
||||||
S3_USE_AWS_MANAGED_IAM=false
|
S3_USE_AWS_MANAGED_IAM=false
|
||||||
S3_ENDPOINT=https://your-bucket-name.storage.s3.cloudflare.com
|
S3_ENDPOINT=https://your-bucket-name.storage.s3.clooudflare.com
|
||||||
S3_BUCKET_NAME=your-bucket-name
|
S3_BUCKET_NAME=your-bucket-name
|
||||||
S3_ACCESS_KEY=your-access-key
|
S3_ACCESS_KEY=your-access-key
|
||||||
S3_SECRET_KEY=your-secret-key
|
S3_SECRET_KEY=your-secret-key
|
||||||
S3_REGION=your-region
|
S3_REGION=your-region
|
||||||
|
|
||||||
# Azure Blob Storage configuration
|
# Azure Blob Storage configuration
|
||||||
AZURE_BLOB_ACCOUNT_NAME=your-account-name
|
AZURE_BLOB_ACCOUNT_NAME=your-account-name
|
||||||
AZURE_BLOB_ACCOUNT_KEY=your-account-key
|
AZURE_BLOB_ACCOUNT_KEY=your-account-key
|
||||||
AZURE_BLOB_CONTAINER_NAME=your-container-name
|
AZURE_BLOB_CONTAINER_NAME=yout-container-name
|
||||||
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
||||||
|
|
||||||
# Aliyun oss Storage configuration
|
# Aliyun oss Storage configuration
|
||||||
ALIYUN_OSS_BUCKET_NAME=your-bucket-name
|
ALIYUN_OSS_BUCKET_NAME=your-bucket-name
|
||||||
ALIYUN_OSS_ACCESS_KEY=your-access-key
|
ALIYUN_OSS_ACCESS_KEY=your-access-key
|
||||||
@ -89,9 +74,8 @@ ALIYUN_OSS_AUTH_VERSION=v1
|
|||||||
ALIYUN_OSS_REGION=your-region
|
ALIYUN_OSS_REGION=your-region
|
||||||
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
||||||
ALIYUN_OSS_PATH=your-path
|
ALIYUN_OSS_PATH=your-path
|
||||||
|
|
||||||
# Google Storage configuration
|
# Google Storage configuration
|
||||||
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
GOOGLE_STORAGE_BUCKET_NAME=yout-bucket-name
|
||||||
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string
|
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string
|
||||||
|
|
||||||
# Tencent COS Storage configuration
|
# Tencent COS Storage configuration
|
||||||
@ -136,8 +120,8 @@ SUPABASE_URL=your-server-url
|
|||||||
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
||||||
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
||||||
|
|
||||||
# Vector database configuration
|
|
||||||
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase
|
# Vector database configuration, support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm
|
||||||
VECTOR_STORE=weaviate
|
VECTOR_STORE=weaviate
|
||||||
|
|
||||||
# Weaviate configuration
|
# Weaviate configuration
|
||||||
@ -250,10 +234,6 @@ ANALYTICDB_ACCOUNT=testaccount
|
|||||||
ANALYTICDB_PASSWORD=testpassword
|
ANALYTICDB_PASSWORD=testpassword
|
||||||
ANALYTICDB_NAMESPACE=dify
|
ANALYTICDB_NAMESPACE=dify
|
||||||
ANALYTICDB_NAMESPACE_PASSWORD=difypassword
|
ANALYTICDB_NAMESPACE_PASSWORD=difypassword
|
||||||
ANALYTICDB_HOST=gp-test.aliyuncs.com
|
|
||||||
ANALYTICDB_PORT=5432
|
|
||||||
ANALYTICDB_MIN_CONNECTION=1
|
|
||||||
ANALYTICDB_MAX_CONNECTION=5
|
|
||||||
|
|
||||||
# OpenSearch configuration
|
# OpenSearch configuration
|
||||||
OPENSEARCH_HOST=127.0.0.1
|
OPENSEARCH_HOST=127.0.0.1
|
||||||
@ -288,13 +268,12 @@ VIKINGDB_SOCKET_TIMEOUT=30
|
|||||||
LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070
|
LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070
|
||||||
LINDORM_USERNAME=admin
|
LINDORM_USERNAME=admin
|
||||||
LINDORM_PASSWORD=admin
|
LINDORM_PASSWORD=admin
|
||||||
USING_UGC_INDEX=False
|
|
||||||
|
|
||||||
# OceanBase Vector configuration
|
# OceanBase Vector configuration
|
||||||
OCEANBASE_VECTOR_HOST=127.0.0.1
|
OCEANBASE_VECTOR_HOST=127.0.0.1
|
||||||
OCEANBASE_VECTOR_PORT=2881
|
OCEANBASE_VECTOR_PORT=2881
|
||||||
OCEANBASE_VECTOR_USER=root@test
|
OCEANBASE_VECTOR_USER=root@test
|
||||||
OCEANBASE_VECTOR_PASSWORD=difyai123456
|
OCEANBASE_VECTOR_PASSWORD=
|
||||||
OCEANBASE_VECTOR_DATABASE=test
|
OCEANBASE_VECTOR_DATABASE=test
|
||||||
OCEANBASE_MEMORY_LIMIT=6G
|
OCEANBASE_MEMORY_LIMIT=6G
|
||||||
|
|
||||||
@ -306,8 +285,8 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
|
|||||||
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
||||||
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
||||||
|
|
||||||
# Model configuration
|
# Model Configuration
|
||||||
MULTIMODAL_SEND_FORMAT=base64
|
MULTIMODAL_SEND_IMAGE_FORMAT=base64
|
||||||
PROMPT_GENERATION_MAX_TOKENS=512
|
PROMPT_GENERATION_MAX_TOKENS=512
|
||||||
CODE_GENERATION_MAX_TOKENS=1024
|
CODE_GENERATION_MAX_TOKENS=1024
|
||||||
|
|
||||||
@ -340,16 +319,10 @@ NOTION_INTERNAL_SECRET=you-internal-secret
|
|||||||
ETL_TYPE=dify
|
ETL_TYPE=dify
|
||||||
UNSTRUCTURED_API_URL=
|
UNSTRUCTURED_API_URL=
|
||||||
UNSTRUCTURED_API_KEY=
|
UNSTRUCTURED_API_KEY=
|
||||||
SCARF_NO_ANALYTICS=true
|
|
||||||
|
|
||||||
#ssrf
|
|
||||||
SSRF_PROXY_HTTP_URL=
|
SSRF_PROXY_HTTP_URL=
|
||||||
SSRF_PROXY_HTTPS_URL=
|
SSRF_PROXY_HTTPS_URL=
|
||||||
SSRF_DEFAULT_MAX_RETRIES=3
|
SSRF_DEFAULT_MAX_RETRIES=3
|
||||||
SSRF_DEFAULT_TIME_OUT=5
|
|
||||||
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
|
||||||
SSRF_DEFAULT_READ_TIME_OUT=5
|
|
||||||
SSRF_DEFAULT_WRITE_TIME_OUT=5
|
|
||||||
|
|
||||||
BATCH_UPLOAD_LIMIT=10
|
BATCH_UPLOAD_LIMIT=10
|
||||||
KEYWORD_DATA_SOURCE_TYPE=database
|
KEYWORD_DATA_SOURCE_TYPE=database
|
||||||
@ -388,21 +361,14 @@ LOG_FILE=
|
|||||||
LOG_FILE_MAX_SIZE=20
|
LOG_FILE_MAX_SIZE=20
|
||||||
# Log file max backup count
|
# Log file max backup count
|
||||||
LOG_FILE_BACKUP_COUNT=5
|
LOG_FILE_BACKUP_COUNT=5
|
||||||
# Log dateformat
|
|
||||||
LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S
|
|
||||||
# Log Timezone
|
|
||||||
LOG_TZ=UTC
|
|
||||||
# Log format
|
|
||||||
LOG_FORMAT=%(asctime)s,%(msecs)d %(levelname)-2s [%(filename)s:%(lineno)d] %(req_id)s %(message)s
|
|
||||||
|
|
||||||
# Indexing configuration
|
# Indexing configuration
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=1000
|
||||||
|
|
||||||
# Workflow runtime configuration
|
# Workflow runtime configuration
|
||||||
WORKFLOW_MAX_EXECUTION_STEPS=500
|
WORKFLOW_MAX_EXECUTION_STEPS=500
|
||||||
WORKFLOW_MAX_EXECUTION_TIME=1200
|
WORKFLOW_MAX_EXECUTION_TIME=1200
|
||||||
WORKFLOW_CALL_MAX_DEPTH=5
|
WORKFLOW_CALL_MAX_DEPTH=5
|
||||||
WORKFLOW_PARALLEL_DEPTH_LIMIT=3
|
|
||||||
MAX_VARIABLE_SIZE=204800
|
MAX_VARIABLE_SIZE=204800
|
||||||
|
|
||||||
# App configuration
|
# App configuration
|
||||||
@ -424,10 +390,3 @@ POSITION_PROVIDER_EXCLUDES=
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
|
||||||
|
|
||||||
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
|
|
||||||
MAX_SUBMIT_COUNT=100
|
|
||||||
# Lockout duration in seconds
|
|
||||||
LOGIN_LOCKOUT_DURATION=86400
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
exclude = [
|
|
||||||
"migrations/*",
|
|
||||||
]
|
|
||||||
line-length = 120
|
|
||||||
|
|
||||||
[format]
|
|
||||||
quote-style = "double"
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
preview = true
|
|
||||||
select = [
|
|
||||||
"B", # flake8-bugbear rules
|
|
||||||
"C4", # flake8-comprehensions
|
|
||||||
"E", # pycodestyle E rules
|
|
||||||
"F", # pyflakes rules
|
|
||||||
"FURB", # refurb rules
|
|
||||||
"I", # isort rules
|
|
||||||
"N", # pep8-naming
|
|
||||||
"PT", # flake8-pytest-style rules
|
|
||||||
"PLC0208", # iteration-over-set
|
|
||||||
"PLC2801", # unnecessary-dunder-call
|
|
||||||
"PLC0414", # useless-import-alias
|
|
||||||
"PLE0604", # invalid-all-object
|
|
||||||
"PLE0605", # invalid-all-format
|
|
||||||
"PLR0402", # manual-from-import
|
|
||||||
"PLR1711", # useless-return
|
|
||||||
"PLR1714", # repeated-equality-comparison
|
|
||||||
"RUF013", # implicit-optional
|
|
||||||
"RUF019", # unnecessary-key-check
|
|
||||||
"RUF100", # unused-noqa
|
|
||||||
"RUF101", # redirected-noqa
|
|
||||||
"RUF200", # invalid-pyproject-toml
|
|
||||||
"RUF022", # unsorted-dunder-all
|
|
||||||
"S506", # unsafe-yaml-load
|
|
||||||
"SIM", # flake8-simplify rules
|
|
||||||
"TRY400", # error-instead-of-exception
|
|
||||||
"TRY401", # verbose-log-message
|
|
||||||
"UP", # pyupgrade rules
|
|
||||||
"W191", # tab-indentation
|
|
||||||
"W605", # invalid-escape-sequence
|
|
||||||
]
|
|
||||||
|
|
||||||
ignore = [
|
|
||||||
"E402", # module-import-not-at-top-of-file
|
|
||||||
"E711", # none-comparison
|
|
||||||
"E712", # true-false-comparison
|
|
||||||
"E721", # type-comparison
|
|
||||||
"E722", # bare-except
|
|
||||||
"E731", # lambda-assignment
|
|
||||||
"F821", # undefined-name
|
|
||||||
"F841", # unused-variable
|
|
||||||
"FURB113", # repeated-append
|
|
||||||
"FURB152", # math-constant
|
|
||||||
"UP007", # non-pep604-annotation
|
|
||||||
"UP032", # f-string
|
|
||||||
"B005", # strip-with-multi-characters
|
|
||||||
"B006", # mutable-argument-default
|
|
||||||
"B007", # unused-loop-control-variable
|
|
||||||
"B026", # star-arg-unpacking-after-keyword-arg
|
|
||||||
"B904", # raise-without-from-inside-except
|
|
||||||
"B905", # zip-without-explicit-strict
|
|
||||||
"N806", # non-lowercase-variable-in-function
|
|
||||||
"N815", # mixed-case-variable-in-class-scope
|
|
||||||
"PT011", # pytest-raises-too-broad
|
|
||||||
"SIM102", # collapsible-if
|
|
||||||
"SIM103", # needless-bool
|
|
||||||
"SIM105", # suppressible-exception
|
|
||||||
"SIM107", # return-in-try-except-finally
|
|
||||||
"SIM108", # if-else-block-instead-of-if-exp
|
|
||||||
"SIM113", # enumerate-for-loop
|
|
||||||
"SIM117", # multiple-with-statements
|
|
||||||
"SIM210", # if-expr-with-true-false
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
|
||||||
"__init__.py" = [
|
|
||||||
"F401", # unused-import
|
|
||||||
"F811", # redefined-while-unused
|
|
||||||
]
|
|
||||||
"configs/*" = [
|
|
||||||
"N802", # invalid-function-name
|
|
||||||
]
|
|
||||||
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
|
||||||
"N803", # invalid-argument-name
|
|
||||||
]
|
|
||||||
"tests/*" = [
|
|
||||||
"F811", # redefined-while-unused
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.pyflakes]
|
|
||||||
allowed-unused-imports = [
|
|
||||||
"_pytest.monkeypatch",
|
|
||||||
"tests.integration_tests",
|
|
||||||
"tests.unit_tests",
|
|
||||||
]
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
# base image
|
# base image
|
||||||
FROM python:3.12-slim-bookworm AS base
|
FROM python:3.10-slim-bookworm AS base
|
||||||
|
|
||||||
WORKDIR /app/api
|
WORKDIR /app/api
|
||||||
|
|
||||||
# Install Poetry
|
# Install Poetry
|
||||||
ENV POETRY_VERSION=1.8.4
|
ENV POETRY_VERSION=1.8.3
|
||||||
|
|
||||||
# if you located in China, you can use aliyun mirror to speed up
|
# if you located in China, you can use aliyun mirror to speed up
|
||||||
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
|
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
|
||||||
@ -55,7 +55,7 @@ RUN apt-get update \
|
|||||||
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
|
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
# For Security
|
# For Security
|
||||||
&& apt-get install -y --no-install-recommends expat=2.6.4-1 libldap-2.5-0=2.5.19+dfsg-1 perl=5.40.0-8 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \
|
&& apt-get install -y --no-install-recommends expat=2.6.3-2 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \
|
||||||
# install a chinese font to support the use of tools like matplotlib
|
# install a chinese font to support the use of tools like matplotlib
|
||||||
&& apt-get install -y fonts-noto-cjk \
|
&& apt-get install -y fonts-noto-cjk \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
|
|||||||
@ -18,17 +18,12 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Copy `.env.example` to `.env`
|
2. Copy `.env.example` to `.env`
|
||||||
|
|
||||||
```cli
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
3. Generate a `SECRET_KEY` in the `.env` file.
|
3. Generate a `SECRET_KEY` in the `.env` file.
|
||||||
|
|
||||||
bash for Linux
|
|
||||||
```bash for Linux
|
```bash for Linux
|
||||||
sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env
|
sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env
|
||||||
```
|
```
|
||||||
bash for Mac
|
|
||||||
```bash for Mac
|
```bash for Mac
|
||||||
secret_key=$(openssl rand -base64 42)
|
secret_key=$(openssl rand -base64 42)
|
||||||
sed -i '' "/^SECRET_KEY=/c\\
|
sed -i '' "/^SECRET_KEY=/c\\
|
||||||
@ -42,10 +37,18 @@
|
|||||||
5. Install dependencies
|
5. Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry env use 3.12
|
poetry env use 3.10
|
||||||
poetry install
|
poetry install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In case of contributors missing to update dependencies for `pyproject.toml`, you can perform the following shell instead.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry shell # activate current environment
|
||||||
|
poetry add $(cat requirements.txt) # install dependencies of production and update pyproject.toml
|
||||||
|
poetry add $(cat requirements-dev.txt) --group dev # install dependencies of development and update pyproject.toml
|
||||||
|
```
|
||||||
|
|
||||||
6. Run migrate
|
6. Run migrate
|
||||||
|
|
||||||
Before the first launch, migrate the database to the latest version.
|
Before the first launch, migrate the database to the latest version.
|
||||||
@ -73,11 +76,13 @@
|
|||||||
1. Install dependencies for both the backend and the test environment
|
1. Install dependencies for both the backend and the test environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry install -C api --with dev
|
poetry install --with dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
|
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd ../
|
||||||
poetry run -C api bash dev/pytest/pytest_all_tests.sh
|
poetry run -C api bash dev/pytest/pytest_all_tests.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
115
api/app.py
115
api/app.py
@ -1,41 +1,108 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
from configs import dify_config
|
||||||
|
|
||||||
|
if os.environ.get("DEBUG", "false").lower() != "true":
|
||||||
|
from gevent import monkey
|
||||||
|
|
||||||
|
monkey.patch_all()
|
||||||
|
|
||||||
|
import grpc.experimental.gevent
|
||||||
|
|
||||||
|
grpc.experimental.gevent.init_gevent()
|
||||||
|
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
from app_factory import create_app
|
||||||
|
|
||||||
|
# DO NOT REMOVE BELOW
|
||||||
|
from events import event_handlers # noqa: F401
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
# TODO: Find a way to avoid importing models here
|
||||||
|
from models import account, dataset, model, source, task, tool, tools, web # noqa: F401
|
||||||
|
|
||||||
|
# DO NOT REMOVE ABOVE
|
||||||
|
|
||||||
|
|
||||||
def is_db_command():
|
warnings.simplefilter("ignore", ResourceWarning)
|
||||||
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
|
|
||||||
return True
|
os.environ["TZ"] = "UTC"
|
||||||
return False
|
# windows platform not support tzset
|
||||||
|
if hasattr(time, "tzset"):
|
||||||
|
time.tzset()
|
||||||
|
|
||||||
|
|
||||||
# create app
|
# create app
|
||||||
if is_db_command():
|
app = create_app()
|
||||||
from app_factory import create_migrations_app
|
celery = app.extensions["celery"]
|
||||||
|
|
||||||
app = create_migrations_app()
|
if dify_config.TESTING:
|
||||||
else:
|
print("App is running in TESTING mode")
|
||||||
# It seems that JetBrains Python debugger does not work well with gevent,
|
|
||||||
# so we need to disable gevent in debug mode.
|
|
||||||
# If you are using debugpy and set GEVENT_SUPPORT=True, you can debug with gevent.
|
|
||||||
if (flask_debug := os.environ.get("FLASK_DEBUG", "0")) and flask_debug.lower() in {"false", "0", "no"}:
|
|
||||||
from gevent import monkey # type: ignore
|
|
||||||
|
|
||||||
# gevent
|
|
||||||
monkey.patch_all()
|
|
||||||
|
|
||||||
from grpc.experimental import gevent as grpc_gevent # type: ignore
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
"""Add Version headers to the response."""
|
||||||
|
response.set_cookie("remember_token", "", expires=0)
|
||||||
|
response.headers.add("X-Version", dify_config.CURRENT_VERSION)
|
||||||
|
response.headers.add("X-Env", dify_config.DEPLOY_ENV)
|
||||||
|
return response
|
||||||
|
|
||||||
# grpc gevent
|
|
||||||
grpc_gevent.init_gevent()
|
|
||||||
|
|
||||||
import psycogreen.gevent # type: ignore
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return Response(
|
||||||
|
json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}),
|
||||||
|
status=200,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
psycogreen.gevent.patch_psycopg()
|
|
||||||
|
|
||||||
from app_factory import create_app
|
@app.route("/threads")
|
||||||
|
def threads():
|
||||||
|
num_threads = threading.active_count()
|
||||||
|
threads = threading.enumerate()
|
||||||
|
|
||||||
|
thread_list = []
|
||||||
|
for thread in threads:
|
||||||
|
thread_name = thread.name
|
||||||
|
thread_id = thread.ident
|
||||||
|
is_alive = thread.is_alive()
|
||||||
|
|
||||||
|
thread_list.append(
|
||||||
|
{
|
||||||
|
"name": thread_name,
|
||||||
|
"id": thread_id,
|
||||||
|
"is_alive": is_alive,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pid": os.getpid(),
|
||||||
|
"thread_num": num_threads,
|
||||||
|
"threads": thread_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/db-pool-stat")
|
||||||
|
def pool_stat():
|
||||||
|
engine = db.engine
|
||||||
|
return {
|
||||||
|
"pid": os.getpid(),
|
||||||
|
"pool_size": engine.pool.size(),
|
||||||
|
"checked_in_connections": engine.pool.checkedin(),
|
||||||
|
"checked_out_connections": engine.pool.checkedout(),
|
||||||
|
"overflow_connections": engine.pool.overflow(),
|
||||||
|
"connection_timeout": engine.pool.timeout(),
|
||||||
|
"recycle_time": db.engine.pool._recycle,
|
||||||
|
}
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
celery = app.extensions["celery"]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5001)
|
app.run(host="0.0.0.0", port=5001)
|
||||||
|
|||||||
@ -1,14 +1,52 @@
|
|||||||
import logging
|
import os
|
||||||
import time
|
|
||||||
|
|
||||||
|
if os.environ.get("DEBUG", "false").lower() != "true":
|
||||||
|
from gevent import monkey
|
||||||
|
|
||||||
|
monkey.patch_all()
|
||||||
|
|
||||||
|
import grpc.experimental.gevent
|
||||||
|
|
||||||
|
grpc.experimental.gevent.init_gevent()
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import Flask, Response, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
|
import contexts
|
||||||
|
from commands import register_commands
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from dify_app import DifyApp
|
from extensions import (
|
||||||
|
ext_celery,
|
||||||
|
ext_code_based_extension,
|
||||||
|
ext_compress,
|
||||||
|
ext_database,
|
||||||
|
ext_hosting_provider,
|
||||||
|
ext_logging,
|
||||||
|
ext_login,
|
||||||
|
ext_mail,
|
||||||
|
ext_migrate,
|
||||||
|
ext_proxy_fix,
|
||||||
|
ext_redis,
|
||||||
|
ext_sentry,
|
||||||
|
ext_storage,
|
||||||
|
)
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_login import login_manager
|
||||||
|
from libs.passport import PassportService
|
||||||
|
from services.account_service import AccountService
|
||||||
|
|
||||||
|
|
||||||
|
class DifyApp(Flask):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Application Factory Function
|
# Application Factory Function
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
def create_flask_app_with_configs() -> DifyApp:
|
def create_flask_app_with_configs() -> Flask:
|
||||||
"""
|
"""
|
||||||
create a raw flask app
|
create a raw flask app
|
||||||
with configs loaded from .env file
|
with configs loaded from .env file
|
||||||
@ -16,86 +54,123 @@ def create_flask_app_with_configs() -> DifyApp:
|
|||||||
dify_app = DifyApp(__name__)
|
dify_app = DifyApp(__name__)
|
||||||
dify_app.config.from_mapping(dify_config.model_dump())
|
dify_app.config.from_mapping(dify_config.model_dump())
|
||||||
|
|
||||||
|
# populate configs into system environment variables
|
||||||
|
for key, value in dify_app.config.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
os.environ[key] = value
|
||||||
|
elif isinstance(value, int | float | bool):
|
||||||
|
os.environ[key] = str(value)
|
||||||
|
elif value is None:
|
||||||
|
os.environ[key] = ""
|
||||||
|
|
||||||
return dify_app
|
return dify_app
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> DifyApp:
|
def create_app() -> Flask:
|
||||||
start_time = time.perf_counter()
|
|
||||||
app = create_flask_app_with_configs()
|
app = create_flask_app_with_configs()
|
||||||
|
app.secret_key = dify_config.SECRET_KEY
|
||||||
initialize_extensions(app)
|
initialize_extensions(app)
|
||||||
end_time = time.perf_counter()
|
register_blueprints(app)
|
||||||
if dify_config.DEBUG:
|
register_commands(app)
|
||||||
logging.info(f"Finished create_app ({round((end_time - start_time) * 1000, 2)} ms)")
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def initialize_extensions(app: DifyApp):
|
def initialize_extensions(app):
|
||||||
from extensions import (
|
# Since the application instance is now created, pass it to each Flask
|
||||||
ext_app_metrics,
|
# extension instance to bind it to the Flask application instance (app)
|
||||||
ext_blueprints,
|
ext_logging.init_app(app)
|
||||||
ext_celery,
|
ext_compress.init_app(app)
|
||||||
ext_code_based_extension,
|
ext_code_based_extension.init()
|
||||||
ext_commands,
|
ext_database.init_app(app)
|
||||||
ext_compress,
|
ext_migrate.init(app, db)
|
||||||
ext_database,
|
ext_redis.init_app(app)
|
||||||
ext_hosting_provider,
|
ext_storage.init_app(app)
|
||||||
ext_import_modules,
|
ext_celery.init_app(app)
|
||||||
ext_logging,
|
ext_login.init_app(app)
|
||||||
ext_login,
|
ext_mail.init_app(app)
|
||||||
ext_mail,
|
ext_hosting_provider.init_app(app)
|
||||||
ext_migrate,
|
ext_sentry.init_app(app)
|
||||||
ext_proxy_fix,
|
ext_proxy_fix.init_app(app)
|
||||||
ext_redis,
|
|
||||||
ext_sentry,
|
|
||||||
ext_set_secretkey,
|
# Flask-Login configuration
|
||||||
ext_storage,
|
@login_manager.request_loader
|
||||||
ext_timezone,
|
def load_user_from_request(request_from_flask_login):
|
||||||
ext_warnings,
|
"""Load user based on the request."""
|
||||||
|
if request.blueprint not in {"console", "inner_api"}:
|
||||||
|
return None
|
||||||
|
# Check if the user_id contains a dot, indicating the old format
|
||||||
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
if not auth_header:
|
||||||
|
auth_token = request.args.get("_token")
|
||||||
|
if not auth_token:
|
||||||
|
raise Unauthorized("Invalid Authorization token.")
|
||||||
|
else:
|
||||||
|
if " " not in auth_header:
|
||||||
|
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
|
||||||
|
auth_scheme, auth_token = auth_header.split(None, 1)
|
||||||
|
auth_scheme = auth_scheme.lower()
|
||||||
|
if auth_scheme != "bearer":
|
||||||
|
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
|
||||||
|
|
||||||
|
decoded = PassportService().verify(auth_token)
|
||||||
|
user_id = decoded.get("user_id")
|
||||||
|
|
||||||
|
logged_in_account = AccountService.load_logged_in_account(account_id=user_id)
|
||||||
|
if logged_in_account:
|
||||||
|
contexts.tenant_id.set(logged_in_account.current_tenant_id)
|
||||||
|
return logged_in_account
|
||||||
|
|
||||||
|
|
||||||
|
@login_manager.unauthorized_handler
|
||||||
|
def unauthorized_handler():
|
||||||
|
"""Handle unauthorized requests."""
|
||||||
|
return Response(
|
||||||
|
json.dumps({"code": "unauthorized", "message": "Unauthorized."}),
|
||||||
|
status=401,
|
||||||
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
extensions = [
|
|
||||||
ext_timezone,
|
|
||||||
ext_logging,
|
|
||||||
ext_warnings,
|
|
||||||
ext_import_modules,
|
|
||||||
ext_set_secretkey,
|
|
||||||
ext_compress,
|
|
||||||
ext_code_based_extension,
|
|
||||||
ext_database,
|
|
||||||
ext_app_metrics,
|
|
||||||
ext_migrate,
|
|
||||||
ext_redis,
|
|
||||||
ext_storage,
|
|
||||||
ext_celery,
|
|
||||||
ext_login,
|
|
||||||
ext_mail,
|
|
||||||
ext_hosting_provider,
|
|
||||||
ext_sentry,
|
|
||||||
ext_proxy_fix,
|
|
||||||
ext_blueprints,
|
|
||||||
ext_commands,
|
|
||||||
]
|
|
||||||
for ext in extensions:
|
|
||||||
short_name = ext.__name__.split(".")[-1]
|
|
||||||
is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
|
|
||||||
if not is_enabled:
|
|
||||||
if dify_config.DEBUG:
|
|
||||||
logging.info(f"Skipped {short_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
# register blueprint routers
|
||||||
ext.init_app(app)
|
def register_blueprints(app):
|
||||||
end_time = time.perf_counter()
|
from controllers.console import bp as console_app_bp
|
||||||
if dify_config.DEBUG:
|
from controllers.files import bp as files_bp
|
||||||
logging.info(f"Loaded {short_name} ({round((end_time - start_time) * 1000, 2)} ms)")
|
from controllers.inner_api import bp as inner_api_bp
|
||||||
|
from controllers.service_api import bp as service_api_bp
|
||||||
|
from controllers.web import bp as web_bp
|
||||||
|
|
||||||
|
CORS(
|
||||||
|
service_api_bp,
|
||||||
|
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
)
|
||||||
|
app.register_blueprint(service_api_bp)
|
||||||
|
|
||||||
def create_migrations_app():
|
CORS(
|
||||||
app = create_flask_app_with_configs()
|
web_bp,
|
||||||
from extensions import ext_database, ext_migrate
|
resources={r"/*": {"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS}},
|
||||||
|
supports_credentials=True,
|
||||||
|
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
expose_headers=["X-Version", "X-Env"],
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize only required extensions
|
app.register_blueprint(web_bp)
|
||||||
ext_database.init_app(app)
|
|
||||||
ext_migrate.init_app(app)
|
|
||||||
|
|
||||||
return app
|
CORS(
|
||||||
|
console_app_bp,
|
||||||
|
resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}},
|
||||||
|
supports_credentials=True,
|
||||||
|
allow_headers=["Content-Type", "Authorization"],
|
||||||
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
expose_headers=["X-Version", "X-Env"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(console_app_bp)
|
||||||
|
|
||||||
|
CORS(files_bp, allow_headers=["Content-Type"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"])
|
||||||
|
app.register_blueprint(files_bp)
|
||||||
|
|
||||||
|
app.register_blueprint(inner_api_bp)
|
||||||
|
|||||||
@ -159,7 +159,8 @@ def migrate_annotation_vector_database():
|
|||||||
try:
|
try:
|
||||||
# get apps info
|
# get apps info
|
||||||
apps = (
|
apps = (
|
||||||
App.query.filter(App.status == "normal")
|
db.session.query(App)
|
||||||
|
.filter(App.status == "normal")
|
||||||
.order_by(App.created_at.desc())
|
.order_by(App.created_at.desc())
|
||||||
.paginate(page=page, per_page=50)
|
.paginate(page=page, per_page=50)
|
||||||
)
|
)
|
||||||
@ -258,7 +259,7 @@ def migrate_knowledge_vector_database():
|
|||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
total_count = 0
|
total_count = 0
|
||||||
vector_type = dify_config.VECTOR_STORE
|
vector_type = dify_config.VECTOR_STORE
|
||||||
upper_collection_vector_types = {
|
upper_colletion_vector_types = {
|
||||||
VectorType.MILVUS,
|
VectorType.MILVUS,
|
||||||
VectorType.PGVECTOR,
|
VectorType.PGVECTOR,
|
||||||
VectorType.RELYT,
|
VectorType.RELYT,
|
||||||
@ -266,7 +267,7 @@ def migrate_knowledge_vector_database():
|
|||||||
VectorType.ORACLE,
|
VectorType.ORACLE,
|
||||||
VectorType.ELASTICSEARCH,
|
VectorType.ELASTICSEARCH,
|
||||||
}
|
}
|
||||||
lower_collection_vector_types = {
|
lower_colletion_vector_types = {
|
||||||
VectorType.ANALYTICDB,
|
VectorType.ANALYTICDB,
|
||||||
VectorType.CHROMA,
|
VectorType.CHROMA,
|
||||||
VectorType.MYSCALE,
|
VectorType.MYSCALE,
|
||||||
@ -284,7 +285,8 @@ def migrate_knowledge_vector_database():
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
datasets = (
|
datasets = (
|
||||||
Dataset.query.filter(Dataset.indexing_technique == "high_quality")
|
db.session.query(Dataset)
|
||||||
|
.filter(Dataset.indexing_technique == "high_quality")
|
||||||
.order_by(Dataset.created_at.desc())
|
.order_by(Dataset.created_at.desc())
|
||||||
.paginate(page=page, per_page=50)
|
.paginate(page=page, per_page=50)
|
||||||
)
|
)
|
||||||
@ -305,7 +307,7 @@ def migrate_knowledge_vector_database():
|
|||||||
continue
|
continue
|
||||||
collection_name = ""
|
collection_name = ""
|
||||||
dataset_id = dataset.id
|
dataset_id = dataset.id
|
||||||
if vector_type in upper_collection_vector_types:
|
if vector_type in upper_colletion_vector_types:
|
||||||
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
||||||
elif vector_type == VectorType.QDRANT:
|
elif vector_type == VectorType.QDRANT:
|
||||||
if dataset.collection_binding_id:
|
if dataset.collection_binding_id:
|
||||||
@ -321,7 +323,7 @@ def migrate_knowledge_vector_database():
|
|||||||
else:
|
else:
|
||||||
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
||||||
|
|
||||||
elif vector_type in lower_collection_vector_types:
|
elif vector_type in lower_colletion_vector_types:
|
||||||
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
|
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Vector store {vector_type} is not supported.")
|
raise ValueError(f"Vector store {vector_type} is not supported.")
|
||||||
@ -448,8 +450,7 @@ def convert_to_agent_apps():
|
|||||||
if app_id not in proceeded_app_ids:
|
if app_id not in proceeded_app_ids:
|
||||||
proceeded_app_ids.append(app_id)
|
proceeded_app_ids.append(app_id)
|
||||||
app = db.session.query(App).filter(App.id == app_id).first()
|
app = db.session.query(App).filter(App.id == app_id).first()
|
||||||
if app is not None:
|
apps.append(app)
|
||||||
apps.append(app)
|
|
||||||
|
|
||||||
if len(apps) == 0:
|
if len(apps) == 0:
|
||||||
break
|
break
|
||||||
@ -554,20 +555,14 @@ def create_tenant(email: str, language: Optional[str] = None, name: Optional[str
|
|||||||
if language not in languages:
|
if language not in languages:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
|
|
||||||
# Validates name encoding for non-Latin characters.
|
name = name.strip()
|
||||||
name = name.strip().encode("utf-8").decode("utf-8") if name else None
|
|
||||||
|
|
||||||
# generate random password
|
# generate random password
|
||||||
new_password = secrets.token_urlsafe(16)
|
new_password = secrets.token_urlsafe(16)
|
||||||
|
|
||||||
# register account
|
# register account
|
||||||
account = RegisterService.register(
|
account = RegisterService.register(email=email, name=account_name, password=new_password, language=language)
|
||||||
email=email,
|
|
||||||
name=account_name,
|
|
||||||
password=new_password,
|
|
||||||
language=language,
|
|
||||||
create_workspace_required=False,
|
|
||||||
)
|
|
||||||
TenantService.create_owner_tenant_if_not_exist(account, name)
|
TenantService.create_owner_tenant_if_not_exist(account, name)
|
||||||
|
|
||||||
click.echo(
|
click.echo(
|
||||||
@ -587,14 +582,14 @@ def upgrade_db():
|
|||||||
click.echo(click.style("Starting database migration.", fg="green"))
|
click.echo(click.style("Starting database migration.", fg="green"))
|
||||||
|
|
||||||
# run db migration
|
# run db migration
|
||||||
import flask_migrate # type: ignore
|
import flask_migrate
|
||||||
|
|
||||||
flask_migrate.upgrade()
|
flask_migrate.upgrade()
|
||||||
|
|
||||||
click.echo(click.style("Database migration successful!", fg="green"))
|
click.echo(click.style("Database migration successful!", fg="green"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Failed to execute database migration")
|
logging.exception(f"Database migration failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
lock.release()
|
lock.release()
|
||||||
else:
|
else:
|
||||||
@ -625,10 +620,6 @@ where sites.id is null limit 1000"""
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
app = db.session.query(App).filter(App.id == app_id).first()
|
app = db.session.query(App).filter(App.id == app_id).first()
|
||||||
if not app:
|
|
||||||
print(f"App {app_id} not found")
|
|
||||||
continue
|
|
||||||
|
|
||||||
tenant = app.tenant
|
tenant = app.tenant
|
||||||
if tenant:
|
if tenant:
|
||||||
accounts = tenant.get_accounts()
|
accounts = tenant.get_accounts()
|
||||||
@ -642,10 +633,22 @@ where sites.id is null limit 1000"""
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed_app_ids.append(app_id)
|
failed_app_ids.append(app_id)
|
||||||
click.echo(click.style("Failed to fix missing site for app {}".format(app_id), fg="red"))
|
click.echo(click.style("Failed to fix missing site for app {}".format(app_id), fg="red"))
|
||||||
logging.exception(f"Failed to fix app related site missing issue, app_id: {app_id}")
|
logging.exception(f"Fix app related site missing issue failed, error: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not processed_count:
|
if not processed_count:
|
||||||
break
|
break
|
||||||
|
|
||||||
click.echo(click.style("Fix for missing app-related sites completed successfully!", fg="green"))
|
click.echo(click.style("Fix for missing app-related sites completed successfully!", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
def register_commands(app):
|
||||||
|
app.cli.add_command(reset_password)
|
||||||
|
app.cli.add_command(reset_email)
|
||||||
|
app.cli.add_command(reset_encrypt_key_pair)
|
||||||
|
app.cli.add_command(vdb_migrate)
|
||||||
|
app.cli.add_command(convert_to_agent_apps)
|
||||||
|
app.cli.add_command(add_qdrant_doc_id_index)
|
||||||
|
app.cli.add_command(create_tenant)
|
||||||
|
app.cli.add_command(upgrade_db)
|
||||||
|
app.cli.add_command(fix_app_site_missing)
|
||||||
|
|||||||
@ -1,51 +1,11 @@
|
|||||||
import logging
|
from pydantic_settings import SettingsConfigDict
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pydantic.fields import FieldInfo
|
from configs.deploy import DeploymentConfig
|
||||||
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
|
from configs.enterprise import EnterpriseFeatureConfig
|
||||||
|
from configs.extra import ExtraServiceConfig
|
||||||
from .deploy import DeploymentConfig
|
from configs.feature import FeatureConfig
|
||||||
from .enterprise import EnterpriseFeatureConfig
|
from configs.middleware import MiddlewareConfig
|
||||||
from .extra import ExtraServiceConfig
|
from configs.packaging import PackagingInfo
|
||||||
from .feature import FeatureConfig
|
|
||||||
from .middleware import MiddlewareConfig
|
|
||||||
from .packaging import PackagingInfo
|
|
||||||
from .remote_settings_sources import RemoteSettingsSource, RemoteSettingsSourceConfig, RemoteSettingsSourceName
|
|
||||||
from .remote_settings_sources.apollo import ApolloSettingsSource
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteSettingsSourceFactory(PydanticBaseSettingsSource):
|
|
||||||
def __init__(self, settings_cls: type[BaseSettings]):
|
|
||||||
super().__init__(settings_cls)
|
|
||||||
|
|
||||||
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __call__(self) -> dict[str, Any]:
|
|
||||||
current_state = self.current_state
|
|
||||||
remote_source_name = current_state.get("REMOTE_SETTINGS_SOURCE_NAME")
|
|
||||||
if not remote_source_name:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
remote_source: RemoteSettingsSource | None = None
|
|
||||||
match remote_source_name:
|
|
||||||
case RemoteSettingsSourceName.APOLLO:
|
|
||||||
remote_source = ApolloSettingsSource(current_state)
|
|
||||||
case _:
|
|
||||||
logger.warning(f"Unsupported remote source: {remote_source_name}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
d: dict[str, Any] = {}
|
|
||||||
|
|
||||||
for field_name, field in self.settings_cls.model_fields.items():
|
|
||||||
field_value, field_key, value_is_complex = remote_source.get_field_value(field, field_name)
|
|
||||||
field_value = remote_source.prepare_field_value(field_name, field, field_value, value_is_complex)
|
|
||||||
if field_value is not None:
|
|
||||||
d[field_key] = field_value
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
class DifyConfig(
|
class DifyConfig(
|
||||||
@ -59,8 +19,6 @@ class DifyConfig(
|
|||||||
MiddlewareConfig,
|
MiddlewareConfig,
|
||||||
# Extra service configs
|
# Extra service configs
|
||||||
ExtraServiceConfig,
|
ExtraServiceConfig,
|
||||||
# Remote source configs
|
|
||||||
RemoteSettingsSourceConfig,
|
|
||||||
# Enterprise feature configs
|
# Enterprise feature configs
|
||||||
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
|
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
|
||||||
EnterpriseFeatureConfig,
|
EnterpriseFeatureConfig,
|
||||||
@ -69,6 +27,7 @@ class DifyConfig(
|
|||||||
# read from dotenv format config file
|
# read from dotenv format config file
|
||||||
env_file=".env",
|
env_file=".env",
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
|
frozen=True,
|
||||||
# ignore extra attributes
|
# ignore extra attributes
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
)
|
)
|
||||||
@ -77,20 +36,3 @@ class DifyConfig(
|
|||||||
# please consider to arrange it in the proper config group of existed or added
|
# please consider to arrange it in the proper config group of existed or added
|
||||||
# for better readability and maintainability.
|
# for better readability and maintainability.
|
||||||
# Thanks for your concentration and consideration.
|
# Thanks for your concentration and consideration.
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def settings_customise_sources(
|
|
||||||
cls,
|
|
||||||
settings_cls: type[BaseSettings],
|
|
||||||
init_settings: PydanticBaseSettingsSource,
|
|
||||||
env_settings: PydanticBaseSettingsSource,
|
|
||||||
dotenv_settings: PydanticBaseSettingsSource,
|
|
||||||
file_secret_settings: PydanticBaseSettingsSource,
|
|
||||||
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
||||||
return (
|
|
||||||
init_settings,
|
|
||||||
env_settings,
|
|
||||||
RemoteSettingsSourceFactory(settings_cls),
|
|
||||||
dotenv_settings,
|
|
||||||
file_secret_settings,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -17,6 +17,11 @@ class DeploymentConfig(BaseSettings):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TESTING: bool = Field(
|
||||||
|
description="Enable testing mode for running automated tests",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
EDITION: str = Field(
|
EDITION: str = Field(
|
||||||
description="Deployment edition of the application (e.g., 'SELF_HOSTED', 'CLOUD')",
|
description="Deployment edition of the application (e.g., 'SELF_HOSTED', 'CLOUD')",
|
||||||
default="SELF_HOSTED",
|
default="SELF_HOSTED",
|
||||||
|
|||||||
@ -109,7 +109,7 @@ class CodeExecutionSandboxConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
CODE_MAX_PRECISION: PositiveInt = Field(
|
CODE_MAX_PRECISION: PositiveInt = Field(
|
||||||
description="Maximum number of decimal places for floating-point numbers in code execution",
|
description="mMaximum number of decimal places for floating-point numbers in code execution",
|
||||||
default=20,
|
default=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -239,6 +239,7 @@ class HttpConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]:
|
def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]:
|
||||||
return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(",")
|
return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(",")
|
||||||
|
|
||||||
@ -249,6 +250,7 @@ class HttpConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
|
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
|
||||||
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
|
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
|
||||||
|
|
||||||
@ -274,16 +276,6 @@ class HttpConfig(BaseSettings):
|
|||||||
default=1 * 1024 * 1024,
|
default=1 * 1024 * 1024,
|
||||||
)
|
)
|
||||||
|
|
||||||
SSRF_DEFAULT_MAX_RETRIES: PositiveInt = Field(
|
|
||||||
description="Maximum number of retries for network requests (SSRF)",
|
|
||||||
default=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
SSRF_PROXY_ALL_URL: Optional[str] = Field(
|
|
||||||
description="Proxy URL for HTTP or HTTPS requests to prevent Server-Side Request Forgery (SSRF)",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
SSRF_PROXY_HTTP_URL: Optional[str] = Field(
|
SSRF_PROXY_HTTP_URL: Optional[str] = Field(
|
||||||
description="Proxy URL for HTTP requests to prevent Server-Side Request Forgery (SSRF)",
|
description="Proxy URL for HTTP requests to prevent Server-Side Request Forgery (SSRF)",
|
||||||
default=None,
|
default=None,
|
||||||
@ -294,26 +286,6 @@ class HttpConfig(BaseSettings):
|
|||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
SSRF_DEFAULT_TIME_OUT: PositiveFloat = Field(
|
|
||||||
description="The default timeout period used for network requests (SSRF)",
|
|
||||||
default=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
SSRF_DEFAULT_CONNECT_TIME_OUT: PositiveFloat = Field(
|
|
||||||
description="The default connect timeout period used for network requests (SSRF)",
|
|
||||||
default=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
SSRF_DEFAULT_READ_TIME_OUT: PositiveFloat = Field(
|
|
||||||
description="The default read timeout period used for network requests (SSRF)",
|
|
||||||
default=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
SSRF_DEFAULT_WRITE_TIME_OUT: PositiveFloat = Field(
|
|
||||||
description="The default write timeout period used for network requests (SSRF)",
|
|
||||||
default=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
RESPECT_XFORWARD_HEADERS_ENABLED: bool = Field(
|
RESPECT_XFORWARD_HEADERS_ENABLED: bool = Field(
|
||||||
description="Enable or disable the X-Forwarded-For Proxy Fix middleware from Werkzeug"
|
description="Enable or disable the X-Forwarded-For Proxy Fix middleware from Werkzeug"
|
||||||
" to respect X-* headers to redirect clients",
|
" to respect X-* headers to redirect clients",
|
||||||
@ -374,7 +346,7 @@ class LoggingConfig(BaseSettings):
|
|||||||
|
|
||||||
LOG_TZ: Optional[str] = Field(
|
LOG_TZ: Optional[str] = Field(
|
||||||
description="Timezone for log timestamps (e.g., 'America/New_York')",
|
description="Timezone for log timestamps (e.g., 'America/New_York')",
|
||||||
default="UTC",
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -431,28 +403,12 @@ class WorkflowConfig(BaseSettings):
|
|||||||
default=5,
|
default=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
WORKFLOW_PARALLEL_DEPTH_LIMIT: PositiveInt = Field(
|
|
||||||
description="Maximum allowed depth for nested parallel executions",
|
|
||||||
default=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
MAX_VARIABLE_SIZE: PositiveInt = Field(
|
MAX_VARIABLE_SIZE: PositiveInt = Field(
|
||||||
description="Maximum size in bytes for a single variable in workflows. Default to 200 KB.",
|
description="Maximum size in bytes for a single variable in workflows. Default to 200 KB.",
|
||||||
default=200 * 1024,
|
default=200 * 1024,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowNodeExecutionConfig(BaseSettings):
|
|
||||||
"""
|
|
||||||
Configuration for workflow node execution
|
|
||||||
"""
|
|
||||||
|
|
||||||
MAX_SUBMIT_COUNT: PositiveInt = Field(
|
|
||||||
description="Maximum number of submitted thread count in a ThreadPool for parallel node execution",
|
|
||||||
default=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthConfig(BaseSettings):
|
class AuthConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
Configuration for authentication and OAuth
|
Configuration for authentication and OAuth
|
||||||
@ -488,16 +444,6 @@ class AuthConfig(BaseSettings):
|
|||||||
default=60,
|
default=60,
|
||||||
)
|
)
|
||||||
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS: PositiveFloat = Field(
|
|
||||||
description="Expiration time for refresh tokens in days",
|
|
||||||
default=30,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOGIN_LOCKOUT_DURATION: PositiveInt = Field(
|
|
||||||
description="Time (in seconds) a user must wait before retrying login after exceeding the rate limit.",
|
|
||||||
default=86400,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModerationConfig(BaseSettings):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
@ -606,12 +552,7 @@ class RagEtlConfig(BaseSettings):
|
|||||||
|
|
||||||
UNSTRUCTURED_API_KEY: Optional[str] = Field(
|
UNSTRUCTURED_API_KEY: Optional[str] = Field(
|
||||||
description="API key for Unstructured.io service",
|
description="API key for Unstructured.io service",
|
||||||
default="",
|
default=None,
|
||||||
)
|
|
||||||
|
|
||||||
SCARF_NO_ANALYTICS: Optional[str] = Field(
|
|
||||||
description="This is about whether to disable Scarf analytics in Unstructured library.",
|
|
||||||
default="false",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -640,16 +581,6 @@ class DataSetConfig(BaseSettings):
|
|||||||
default=500,
|
default=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
CREATE_TIDB_SERVICE_JOB_ENABLED: bool = Field(
|
|
||||||
description="Enable or disable create tidb service job",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLAN_SANDBOX_CLEAN_MESSAGE_DAY_SETTING: PositiveInt = Field(
|
|
||||||
description="Interval in days for message cleanup operations - plan: sandbox",
|
|
||||||
default=30,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceConfig(BaseSettings):
|
class WorkspaceConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
@ -669,18 +600,13 @@ class IndexingConfig(BaseSettings):
|
|||||||
|
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: PositiveInt = Field(
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: PositiveInt = Field(
|
||||||
description="Maximum token length for text segmentation during indexing",
|
description="Maximum token length for text segmentation during indexing",
|
||||||
default=4000,
|
default=1000,
|
||||||
)
|
|
||||||
|
|
||||||
CHILD_CHUNKS_PREVIEW_NUMBER: PositiveInt = Field(
|
|
||||||
description="Maximum number of child chunks to preview",
|
|
||||||
default=50,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MultiModalTransferConfig(BaseSettings):
|
class ImageFormatConfig(BaseSettings):
|
||||||
MULTIMODAL_SEND_FORMAT: Literal["base64", "url"] = Field(
|
MULTIMODAL_SEND_IMAGE_FORMAT: Literal["base64", "url"] = Field(
|
||||||
description="Format for sending files in multimodal contexts ('base64' or 'url'), default is base64",
|
description="Format for sending images in multimodal contexts ('base64' or 'url'), default is base64",
|
||||||
default="base64",
|
default="base64",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -723,27 +649,27 @@ class PositionConfig(BaseSettings):
|
|||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_PROVIDER_PINS_LIST(self) -> list[str]:
|
def POSITION_PROVIDER_PINS_LIST(self) -> list[str]:
|
||||||
return [item.strip() for item in self.POSITION_PROVIDER_PINS.split(",") if item.strip() != ""]
|
return [item.strip() for item in self.POSITION_PROVIDER_PINS.split(",") if item.strip() != ""]
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_PROVIDER_INCLUDES_SET(self) -> set[str]:
|
def POSITION_PROVIDER_INCLUDES_SET(self) -> set[str]:
|
||||||
return {item.strip() for item in self.POSITION_PROVIDER_INCLUDES.split(",") if item.strip() != ""}
|
return {item.strip() for item in self.POSITION_PROVIDER_INCLUDES.split(",") if item.strip() != ""}
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_PROVIDER_EXCLUDES_SET(self) -> set[str]:
|
def POSITION_PROVIDER_EXCLUDES_SET(self) -> set[str]:
|
||||||
return {item.strip() for item in self.POSITION_PROVIDER_EXCLUDES.split(",") if item.strip() != ""}
|
return {item.strip() for item in self.POSITION_PROVIDER_EXCLUDES.split(",") if item.strip() != ""}
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_TOOL_PINS_LIST(self) -> list[str]:
|
def POSITION_TOOL_PINS_LIST(self) -> list[str]:
|
||||||
return [item.strip() for item in self.POSITION_TOOL_PINS.split(",") if item.strip() != ""]
|
return [item.strip() for item in self.POSITION_TOOL_PINS.split(",") if item.strip() != ""]
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_TOOL_INCLUDES_SET(self) -> set[str]:
|
def POSITION_TOOL_INCLUDES_SET(self) -> set[str]:
|
||||||
return {item.strip() for item in self.POSITION_TOOL_INCLUDES.split(",") if item.strip() != ""}
|
return {item.strip() for item in self.POSITION_TOOL_INCLUDES.split(",") if item.strip() != ""}
|
||||||
|
|
||||||
@property
|
@computed_field
|
||||||
def POSITION_TOOL_EXCLUDES_SET(self) -> set[str]:
|
def POSITION_TOOL_EXCLUDES_SET(self) -> set[str]:
|
||||||
return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""}
|
return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""}
|
||||||
|
|
||||||
@ -775,13 +701,6 @@ class LoginConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountConfig(BaseSettings):
|
|
||||||
ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
|
||||||
description="Duration in minutes for which a account deletion token remains valid",
|
|
||||||
default=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureConfig(
|
class FeatureConfig(
|
||||||
# place the configs in alphabet order
|
# place the configs in alphabet order
|
||||||
AppExecutionConfig,
|
AppExecutionConfig,
|
||||||
@ -793,23 +712,21 @@ class FeatureConfig(
|
|||||||
FileAccessConfig,
|
FileAccessConfig,
|
||||||
FileUploadConfig,
|
FileUploadConfig,
|
||||||
HttpConfig,
|
HttpConfig,
|
||||||
|
ImageFormatConfig,
|
||||||
InnerAPIConfig,
|
InnerAPIConfig,
|
||||||
IndexingConfig,
|
IndexingConfig,
|
||||||
LoggingConfig,
|
LoggingConfig,
|
||||||
MailConfig,
|
MailConfig,
|
||||||
ModelLoadBalanceConfig,
|
ModelLoadBalanceConfig,
|
||||||
ModerationConfig,
|
ModerationConfig,
|
||||||
MultiModalTransferConfig,
|
|
||||||
PositionConfig,
|
PositionConfig,
|
||||||
RagEtlConfig,
|
RagEtlConfig,
|
||||||
SecurityConfig,
|
SecurityConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
WorkflowConfig,
|
WorkflowConfig,
|
||||||
WorkflowNodeExecutionConfig,
|
|
||||||
WorkspaceConfig,
|
WorkspaceConfig,
|
||||||
LoginConfig,
|
LoginConfig,
|
||||||
AccountConfig,
|
|
||||||
# hosted services config
|
# hosted services config
|
||||||
HostedServiceConfig,
|
HostedServiceConfig,
|
||||||
CeleryBeatConfig,
|
CeleryBeatConfig,
|
||||||
|
|||||||
@ -1,69 +1,54 @@
|
|||||||
from typing import Any, Literal, Optional
|
from typing import Any, Optional
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt, computed_field
|
from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt, computed_field
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
from .cache.redis_config import RedisConfig
|
from configs.middleware.cache.redis_config import RedisConfig
|
||||||
from .storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
|
from configs.middleware.storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
|
||||||
from .storage.amazon_s3_storage_config import S3StorageConfig
|
from configs.middleware.storage.amazon_s3_storage_config import S3StorageConfig
|
||||||
from .storage.azure_blob_storage_config import AzureBlobStorageConfig
|
from configs.middleware.storage.azure_blob_storage_config import AzureBlobStorageConfig
|
||||||
from .storage.baidu_obs_storage_config import BaiduOBSStorageConfig
|
from configs.middleware.storage.baidu_obs_storage_config import BaiduOBSStorageConfig
|
||||||
from .storage.google_cloud_storage_config import GoogleCloudStorageConfig
|
from configs.middleware.storage.google_cloud_storage_config import GoogleCloudStorageConfig
|
||||||
from .storage.huawei_obs_storage_config import HuaweiCloudOBSStorageConfig
|
from configs.middleware.storage.huawei_obs_storage_config import HuaweiCloudOBSStorageConfig
|
||||||
from .storage.oci_storage_config import OCIStorageConfig
|
from configs.middleware.storage.oci_storage_config import OCIStorageConfig
|
||||||
from .storage.opendal_storage_config import OpenDALStorageConfig
|
from configs.middleware.storage.supabase_storage_config import SupabaseStorageConfig
|
||||||
from .storage.supabase_storage_config import SupabaseStorageConfig
|
from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig
|
||||||
from .storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig
|
from configs.middleware.storage.volcengine_tos_storage_config import VolcengineTOSStorageConfig
|
||||||
from .storage.volcengine_tos_storage_config import VolcengineTOSStorageConfig
|
from configs.middleware.vdb.analyticdb_config import AnalyticdbConfig
|
||||||
from .vdb.analyticdb_config import AnalyticdbConfig
|
from configs.middleware.vdb.baidu_vector_config import BaiduVectorDBConfig
|
||||||
from .vdb.baidu_vector_config import BaiduVectorDBConfig
|
from configs.middleware.vdb.chroma_config import ChromaConfig
|
||||||
from .vdb.chroma_config import ChromaConfig
|
from configs.middleware.vdb.couchbase_config import CouchbaseConfig
|
||||||
from .vdb.couchbase_config import CouchbaseConfig
|
from configs.middleware.vdb.elasticsearch_config import ElasticsearchConfig
|
||||||
from .vdb.elasticsearch_config import ElasticsearchConfig
|
from configs.middleware.vdb.lindorm_config import LindormConfig
|
||||||
from .vdb.lindorm_config import LindormConfig
|
from configs.middleware.vdb.milvus_config import MilvusConfig
|
||||||
from .vdb.milvus_config import MilvusConfig
|
from configs.middleware.vdb.myscale_config import MyScaleConfig
|
||||||
from .vdb.myscale_config import MyScaleConfig
|
from configs.middleware.vdb.oceanbase_config import OceanBaseVectorConfig
|
||||||
from .vdb.oceanbase_config import OceanBaseVectorConfig
|
from configs.middleware.vdb.opensearch_config import OpenSearchConfig
|
||||||
from .vdb.opensearch_config import OpenSearchConfig
|
from configs.middleware.vdb.oracle_config import OracleConfig
|
||||||
from .vdb.oracle_config import OracleConfig
|
from configs.middleware.vdb.pgvector_config import PGVectorConfig
|
||||||
from .vdb.pgvector_config import PGVectorConfig
|
from configs.middleware.vdb.pgvectors_config import PGVectoRSConfig
|
||||||
from .vdb.pgvectors_config import PGVectoRSConfig
|
from configs.middleware.vdb.qdrant_config import QdrantConfig
|
||||||
from .vdb.qdrant_config import QdrantConfig
|
from configs.middleware.vdb.relyt_config import RelytConfig
|
||||||
from .vdb.relyt_config import RelytConfig
|
from configs.middleware.vdb.tencent_vector_config import TencentVectorDBConfig
|
||||||
from .vdb.tencent_vector_config import TencentVectorDBConfig
|
from configs.middleware.vdb.tidb_on_qdrant_config import TidbOnQdrantConfig
|
||||||
from .vdb.tidb_on_qdrant_config import TidbOnQdrantConfig
|
from configs.middleware.vdb.tidb_vector_config import TiDBVectorConfig
|
||||||
from .vdb.tidb_vector_config import TiDBVectorConfig
|
from configs.middleware.vdb.upstash_config import UpstashConfig
|
||||||
from .vdb.upstash_config import UpstashConfig
|
from configs.middleware.vdb.vikingdb_config import VikingDBConfig
|
||||||
from .vdb.vikingdb_config import VikingDBConfig
|
from configs.middleware.vdb.weaviate_config import WeaviateConfig
|
||||||
from .vdb.weaviate_config import WeaviateConfig
|
|
||||||
|
|
||||||
|
|
||||||
class StorageConfig(BaseSettings):
|
class StorageConfig(BaseSettings):
|
||||||
STORAGE_TYPE: Literal[
|
STORAGE_TYPE: str = Field(
|
||||||
"opendal",
|
|
||||||
"s3",
|
|
||||||
"aliyun-oss",
|
|
||||||
"azure-blob",
|
|
||||||
"baidu-obs",
|
|
||||||
"google-storage",
|
|
||||||
"huawei-obs",
|
|
||||||
"oci-storage",
|
|
||||||
"tencent-cos",
|
|
||||||
"volcengine-tos",
|
|
||||||
"supabase",
|
|
||||||
"local",
|
|
||||||
] = Field(
|
|
||||||
description="Type of storage to use."
|
description="Type of storage to use."
|
||||||
" Options: 'opendal', '(deprecated) local', 's3', 'aliyun-oss', 'azure-blob', 'baidu-obs', 'google-storage', "
|
" Options: 'local', 's3', 'aliyun-oss', 'azure-blob', 'baidu-obs', 'google-storage', 'huawei-obs', "
|
||||||
"'huawei-obs', 'oci-storage', 'tencent-cos', 'volcengine-tos', 'supabase'. Default is 'opendal'.",
|
"'oci-storage', 'tencent-cos', 'volcengine-tos', 'supabase'. Default is 'local'.",
|
||||||
default="opendal",
|
default="local",
|
||||||
)
|
)
|
||||||
|
|
||||||
STORAGE_LOCAL_PATH: str = Field(
|
STORAGE_LOCAL_PATH: str = Field(
|
||||||
description="Path for local storage when STORAGE_TYPE is set to 'local'.",
|
description="Path for local storage when STORAGE_TYPE is set to 'local'.",
|
||||||
default="storage",
|
default="storage",
|
||||||
deprecated=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +73,7 @@ class KeywordStoreConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(BaseSettings):
|
class DatabaseConfig:
|
||||||
DB_HOST: str = Field(
|
DB_HOST: str = Field(
|
||||||
description="Hostname or IP address of the database server.",
|
description="Hostname or IP address of the database server.",
|
||||||
default="localhost",
|
default="localhost",
|
||||||
@ -130,6 +115,7 @@ class DatabaseConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def SQLALCHEMY_DATABASE_URI(self) -> str:
|
def SQLALCHEMY_DATABASE_URI(self) -> str:
|
||||||
db_extras = (
|
db_extras = (
|
||||||
f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS
|
f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS
|
||||||
@ -167,6 +153,7 @@ class DatabaseConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
|
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"pool_size": self.SQLALCHEMY_POOL_SIZE,
|
"pool_size": self.SQLALCHEMY_POOL_SIZE,
|
||||||
@ -204,6 +191,7 @@ class CeleryConfig(DatabaseConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
@property
|
||||||
def CELERY_RESULT_BACKEND(self) -> str | None:
|
def CELERY_RESULT_BACKEND(self) -> str | None:
|
||||||
return (
|
return (
|
||||||
"db+{}".format(self.SQLALCHEMY_DATABASE_URI)
|
"db+{}".format(self.SQLALCHEMY_DATABASE_URI)
|
||||||
@ -211,6 +199,7 @@ class CeleryConfig(DatabaseConfig):
|
|||||||
else self.CELERY_BROKER_URL
|
else self.CELERY_BROKER_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def BROKER_USE_SSL(self) -> bool:
|
def BROKER_USE_SSL(self) -> bool:
|
||||||
return self.CELERY_BROKER_URL.startswith("rediss://") if self.CELERY_BROKER_URL else False
|
return self.CELERY_BROKER_URL.startswith("rediss://") if self.CELERY_BROKER_URL else False
|
||||||
@ -246,7 +235,6 @@ class MiddlewareConfig(
|
|||||||
GoogleCloudStorageConfig,
|
GoogleCloudStorageConfig,
|
||||||
HuaweiCloudOBSStorageConfig,
|
HuaweiCloudOBSStorageConfig,
|
||||||
OCIStorageConfig,
|
OCIStorageConfig,
|
||||||
OpenDALStorageConfig,
|
|
||||||
S3StorageConfig,
|
S3StorageConfig,
|
||||||
SupabaseStorageConfig,
|
SupabaseStorageConfig,
|
||||||
TencentCloudCOSStorageConfig,
|
TencentCloudCOSStorageConfig,
|
||||||
|
|||||||
15
api/configs/middleware/cache/redis_config.py
vendored
15
api/configs/middleware/cache/redis_config.py
vendored
@ -68,18 +68,3 @@ class RedisConfig(BaseSettings):
|
|||||||
description="Socket timeout in seconds for Redis Sentinel connections",
|
description="Socket timeout in seconds for Redis Sentinel connections",
|
||||||
default=0.1,
|
default=0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
REDIS_USE_CLUSTERS: bool = Field(
|
|
||||||
description="Enable Redis Clusters mode for high availability",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
REDIS_CLUSTERS: Optional[str] = Field(
|
|
||||||
description="Comma-separated list of Redis Clusters nodes (host:port)",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
REDIS_CLUSTERS_PASSWORD: Optional[str] = Field(
|
|
||||||
description="Password for Redis Clusters authentication (if required)",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class BaiduOBSStorageConfig(BaseSettings):
|
class BaiduOBSStorageConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration settings for Baidu Object Storage Service (OBS)
|
Configuration settings for Baidu Object Storage Service (OBS)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class HuaweiCloudOBSStorageConfig(BaseSettings):
|
class HuaweiCloudOBSStorageConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration settings for Huawei Cloud Object Storage Service (OBS)
|
Configuration settings for Huawei Cloud Object Storage Service (OBS)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
from pydantic import Field
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class OpenDALStorageConfig(BaseSettings):
|
|
||||||
OPENDAL_SCHEME: str = Field(
|
|
||||||
default="fs",
|
|
||||||
description="OpenDAL scheme.",
|
|
||||||
)
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class SupabaseStorageConfig(BaseSettings):
|
class SupabaseStorageConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration settings for Supabase Object Storage Service
|
Configuration settings for Supabase Object Storage Service
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class VolcengineTOSStorageConfig(BaseSettings):
|
class VolcengineTOSStorageConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration settings for Volcengine Tinder Object Storage (TOS)
|
Configuration settings for Volcengine Tinder Object Storage (TOS)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field, PositiveInt
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class AnalyticdbConfig(BaseSettings):
|
class AnalyticdbConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration for connecting to Alibaba Cloud AnalyticDB for PostgreSQL.
|
Configuration for connecting to Alibaba Cloud AnalyticDB for PostgreSQL.
|
||||||
Refer to the following documentation for details on obtaining credentials:
|
Refer to the following documentation for details on obtaining credentials:
|
||||||
@ -41,11 +40,3 @@ class AnalyticdbConfig(BaseSettings):
|
|||||||
description="The password for accessing the specified namespace within the AnalyticDB instance"
|
description="The password for accessing the specified namespace within the AnalyticDB instance"
|
||||||
" (if namespace feature is enabled).",
|
" (if namespace feature is enabled).",
|
||||||
)
|
)
|
||||||
ANALYTICDB_HOST: Optional[str] = Field(
|
|
||||||
default=None, description="The host of the AnalyticDB instance you want to connect to."
|
|
||||||
)
|
|
||||||
ANALYTICDB_PORT: PositiveInt = Field(
|
|
||||||
default=5432, description="The port of the AnalyticDB instance you want to connect to."
|
|
||||||
)
|
|
||||||
ANALYTICDB_MIN_CONNECTION: PositiveInt = Field(default=1, description="Min connection of the AnalyticDB database.")
|
|
||||||
ANALYTICDB_MAX_CONNECTION: PositiveInt = Field(default=5, description="Max connection of the AnalyticDB database.")
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class CouchbaseConfig(BaseSettings):
|
class CouchbaseConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Couchbase configs
|
Couchbase configs
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -21,14 +21,3 @@ class LindormConfig(BaseSettings):
|
|||||||
description="Lindorm password",
|
description="Lindorm password",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
DEFAULT_INDEX_TYPE: Optional[str] = Field(
|
|
||||||
description="Lindorm Vector Index Type, hnsw or flat is available in dify",
|
|
||||||
default="hnsw",
|
|
||||||
)
|
|
||||||
DEFAULT_DISTANCE_TYPE: Optional[str] = Field(
|
|
||||||
description="Vector Distance Type, support l2, cosinesimil, innerproduct", default="l2"
|
|
||||||
)
|
|
||||||
USING_UGC_INDEX: Optional[bool] = Field(
|
|
||||||
description="Using UGC index will store the same type of Index in a single index but can retrieve separately.",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -33,9 +33,3 @@ class MilvusConfig(BaseSettings):
|
|||||||
description="Name of the Milvus database to connect to (default is 'default')",
|
description="Name of the Milvus database to connect to (default is 'default')",
|
||||||
default="default",
|
default="default",
|
||||||
)
|
)
|
||||||
|
|
||||||
MILVUS_ENABLE_HYBRID_SEARCH: bool = Field(
|
|
||||||
description="Enable hybrid search features (requires Milvus >= 2.5.0). Set to false for compatibility with "
|
|
||||||
"older versions",
|
|
||||||
default=True,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
from pydantic import Field, PositiveInt
|
from pydantic import BaseModel, Field, PositiveInt
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class MyScaleConfig(BaseSettings):
|
class MyScaleConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration settings for MyScale vector database
|
Configuration settings for MyScale vector database
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class VikingDBConfig(BaseSettings):
|
class VikingDBConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
Configuration for connecting to Volcengine VikingDB.
|
Configuration for connecting to Volcengine VikingDB.
|
||||||
Refer to the following documentation for details on obtaining credentials:
|
Refer to the following documentation for details on obtaining credentials:
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
|||||||
|
|
||||||
CURRENT_VERSION: str = Field(
|
CURRENT_VERSION: str = Field(
|
||||||
description="Dify version",
|
description="Dify version",
|
||||||
default="0.15.0",
|
default="0.10.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
COMMIT_SHA: str = Field(
|
COMMIT_SHA: str = Field(
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
from .apollo import ApolloSettingsSourceInfo
|
|
||||||
from .base import RemoteSettingsSource
|
|
||||||
from .enums import RemoteSettingsSourceName
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteSettingsSourceConfig(ApolloSettingsSourceInfo):
|
|
||||||
REMOTE_SETTINGS_SOURCE_NAME: RemoteSettingsSourceName | str = Field(
|
|
||||||
description="name of remote config source",
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["RemoteSettingsSource", "RemoteSettingsSourceConfig", "RemoteSettingsSourceName"]
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
from configs.remote_settings_sources.base import RemoteSettingsSource
|
|
||||||
|
|
||||||
from .client import ApolloClient
|
|
||||||
|
|
||||||
|
|
||||||
class ApolloSettingsSourceInfo(BaseSettings):
|
|
||||||
"""
|
|
||||||
Packaging build information
|
|
||||||
"""
|
|
||||||
|
|
||||||
APOLLO_APP_ID: Optional[str] = Field(
|
|
||||||
description="apollo app_id",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
APOLLO_CLUSTER: Optional[str] = Field(
|
|
||||||
description="apollo cluster",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
APOLLO_CONFIG_URL: Optional[str] = Field(
|
|
||||||
description="apollo config url",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
APOLLO_NAMESPACE: Optional[str] = Field(
|
|
||||||
description="apollo namespace",
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ApolloSettingsSource(RemoteSettingsSource):
|
|
||||||
def __init__(self, configs: Mapping[str, Any]):
|
|
||||||
self.client = ApolloClient(
|
|
||||||
app_id=configs["APOLLO_APP_ID"],
|
|
||||||
cluster=configs["APOLLO_CLUSTER"],
|
|
||||||
config_url=configs["APOLLO_CONFIG_URL"],
|
|
||||||
start_hot_update=False,
|
|
||||||
_notification_map={configs["APOLLO_NAMESPACE"]: -1},
|
|
||||||
)
|
|
||||||
self.namespace = configs["APOLLO_NAMESPACE"]
|
|
||||||
self.remote_configs = self.client.get_all_dicts(self.namespace)
|
|
||||||
|
|
||||||
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
|
|
||||||
if not isinstance(self.remote_configs, dict):
|
|
||||||
raise ValueError(f"remote configs is not dict, but {type(self.remote_configs)}")
|
|
||||||
field_value = self.remote_configs.get(field_name)
|
|
||||||
return field_value, field_name, False
|
|
||||||
@ -1,304 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .python_3x import http_request, makedirs_wrapper
|
|
||||||
from .utils import (
|
|
||||||
CONFIGURATIONS,
|
|
||||||
NAMESPACE_NAME,
|
|
||||||
NOTIFICATION_ID,
|
|
||||||
get_value_from_dict,
|
|
||||||
init_ip,
|
|
||||||
no_key_cache_key,
|
|
||||||
signature,
|
|
||||||
url_encode_wrapper,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ApolloClient:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config_url,
|
|
||||||
app_id,
|
|
||||||
cluster="default",
|
|
||||||
secret="",
|
|
||||||
start_hot_update=True,
|
|
||||||
change_listener=None,
|
|
||||||
_notification_map=None,
|
|
||||||
):
|
|
||||||
# Core routing parameters
|
|
||||||
self.config_url = config_url
|
|
||||||
self.cluster = cluster
|
|
||||||
self.app_id = app_id
|
|
||||||
|
|
||||||
# Non-core parameters
|
|
||||||
self.ip = init_ip()
|
|
||||||
self.secret = secret
|
|
||||||
|
|
||||||
# Check the parameter variables
|
|
||||||
|
|
||||||
# Private control variables
|
|
||||||
self._cycle_time = 5
|
|
||||||
self._stopping = False
|
|
||||||
self._cache = {}
|
|
||||||
self._no_key = {}
|
|
||||||
self._hash = {}
|
|
||||||
self._pull_timeout = 75
|
|
||||||
self._cache_file_path = os.path.expanduser("~") + "/.dify/config/remote-settings/apollo/cache/"
|
|
||||||
self._long_poll_thread = None
|
|
||||||
self._change_listener = change_listener # "add" "delete" "update"
|
|
||||||
if _notification_map is None:
|
|
||||||
_notification_map = {"application": -1}
|
|
||||||
self._notification_map = _notification_map
|
|
||||||
self.last_release_key = None
|
|
||||||
# Private startup method
|
|
||||||
self._path_checker()
|
|
||||||
if start_hot_update:
|
|
||||||
self._start_hot_update()
|
|
||||||
|
|
||||||
# start the heartbeat thread
|
|
||||||
heartbeat = threading.Thread(target=self._heart_beat)
|
|
||||||
heartbeat.daemon = True
|
|
||||||
heartbeat.start()
|
|
||||||
|
|
||||||
def get_json_from_net(self, namespace="application"):
|
|
||||||
url = "{}/configs/{}/{}/{}?releaseKey={}&ip={}".format(
|
|
||||||
self.config_url, self.app_id, self.cluster, namespace, "", self.ip
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
code, body = http_request(url, timeout=3, headers=self._sign_headers(url))
|
|
||||||
if code == 200:
|
|
||||||
if not body:
|
|
||||||
logger.error(f"get_json_from_net load configs failed, body is {body}")
|
|
||||||
return None
|
|
||||||
data = json.loads(body)
|
|
||||||
data = data["configurations"]
|
|
||||||
return_data = {CONFIGURATIONS: data}
|
|
||||||
return return_data
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
logger.exception("an error occurred in get_json_from_net")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_value(self, key, default_val=None, namespace="application"):
|
|
||||||
try:
|
|
||||||
# read memory configuration
|
|
||||||
namespace_cache = self._cache.get(namespace)
|
|
||||||
val = get_value_from_dict(namespace_cache, key)
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
|
|
||||||
no_key = no_key_cache_key(namespace, key)
|
|
||||||
if no_key in self._no_key:
|
|
||||||
return default_val
|
|
||||||
|
|
||||||
# read the network configuration
|
|
||||||
namespace_data = self.get_json_from_net(namespace)
|
|
||||||
val = get_value_from_dict(namespace_data, key)
|
|
||||||
if val is not None:
|
|
||||||
self._update_cache_and_file(namespace_data, namespace)
|
|
||||||
return val
|
|
||||||
|
|
||||||
# read the file configuration
|
|
||||||
namespace_cache = self._get_local_cache(namespace)
|
|
||||||
val = get_value_from_dict(namespace_cache, key)
|
|
||||||
if val is not None:
|
|
||||||
self._update_cache_and_file(namespace_cache, namespace)
|
|
||||||
return val
|
|
||||||
|
|
||||||
# If all of them are not obtained, the default value is returned
|
|
||||||
# and the local cache is set to None
|
|
||||||
self._set_local_cache_none(namespace, key)
|
|
||||||
return default_val
|
|
||||||
except Exception:
|
|
||||||
logger.exception("get_value has error, [key is %s], [namespace is %s]", key, namespace)
|
|
||||||
return default_val
|
|
||||||
|
|
||||||
# Set the key of a namespace to none, and do not set default val
|
|
||||||
# to ensure the real-time correctness of the function call.
|
|
||||||
# If the user does not have the same default val twice
|
|
||||||
# and the default val is used here, there may be a problem.
|
|
||||||
def _set_local_cache_none(self, namespace, key):
|
|
||||||
no_key = no_key_cache_key(namespace, key)
|
|
||||||
self._no_key[no_key] = key
|
|
||||||
|
|
||||||
def _start_hot_update(self):
|
|
||||||
self._long_poll_thread = threading.Thread(target=self._listener)
|
|
||||||
# When the asynchronous thread is started, the daemon thread will automatically exit
|
|
||||||
# when the main thread is launched.
|
|
||||||
self._long_poll_thread.daemon = True
|
|
||||||
self._long_poll_thread.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._stopping = True
|
|
||||||
logger.info("Stopping listener...")
|
|
||||||
|
|
||||||
# Call the set callback function, and if it is abnormal, try it out
|
|
||||||
def _call_listener(self, namespace, old_kv, new_kv):
|
|
||||||
if self._change_listener is None:
|
|
||||||
return
|
|
||||||
if old_kv is None:
|
|
||||||
old_kv = {}
|
|
||||||
if new_kv is None:
|
|
||||||
new_kv = {}
|
|
||||||
try:
|
|
||||||
for key in old_kv:
|
|
||||||
new_value = new_kv.get(key)
|
|
||||||
old_value = old_kv.get(key)
|
|
||||||
if new_value is None:
|
|
||||||
# If newValue is empty, it means key, and the value is deleted.
|
|
||||||
self._change_listener("delete", namespace, key, old_value)
|
|
||||||
continue
|
|
||||||
if new_value != old_value:
|
|
||||||
self._change_listener("update", namespace, key, new_value)
|
|
||||||
continue
|
|
||||||
for key in new_kv:
|
|
||||||
new_value = new_kv.get(key)
|
|
||||||
old_value = old_kv.get(key)
|
|
||||||
if old_value is None:
|
|
||||||
self._change_listener("add", namespace, key, new_value)
|
|
||||||
except BaseException as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
|
|
||||||
def _path_checker(self):
|
|
||||||
if not os.path.isdir(self._cache_file_path):
|
|
||||||
makedirs_wrapper(self._cache_file_path)
|
|
||||||
|
|
||||||
# update the local cache and file cache
|
|
||||||
def _update_cache_and_file(self, namespace_data, namespace="application"):
|
|
||||||
# update the local cache
|
|
||||||
self._cache[namespace] = namespace_data
|
|
||||||
# update the file cache
|
|
||||||
new_string = json.dumps(namespace_data)
|
|
||||||
new_hash = hashlib.md5(new_string.encode("utf-8")).hexdigest()
|
|
||||||
if self._hash.get(namespace) == new_hash:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
file_path = Path(self._cache_file_path) / f"{self.app_id}_configuration_{namespace}.txt"
|
|
||||||
file_path.write_text(new_string)
|
|
||||||
self._hash[namespace] = new_hash
|
|
||||||
|
|
||||||
# get the configuration from the local file
|
|
||||||
def _get_local_cache(self, namespace="application"):
|
|
||||||
cache_file_path = os.path.join(self._cache_file_path, f"{self.app_id}_configuration_{namespace}.txt")
|
|
||||||
if os.path.isfile(cache_file_path):
|
|
||||||
with open(cache_file_path) as f:
|
|
||||||
result = json.loads(f.readline())
|
|
||||||
return result
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _long_poll(self):
|
|
||||||
notifications = []
|
|
||||||
for key in self._cache:
|
|
||||||
namespace_data = self._cache[key]
|
|
||||||
notification_id = -1
|
|
||||||
if NOTIFICATION_ID in namespace_data:
|
|
||||||
notification_id = self._cache[key][NOTIFICATION_ID]
|
|
||||||
notifications.append({NAMESPACE_NAME: key, NOTIFICATION_ID: notification_id})
|
|
||||||
try:
|
|
||||||
# if the length is 0 it is returned directly
|
|
||||||
if len(notifications) == 0:
|
|
||||||
return
|
|
||||||
url = "{}/notifications/v2".format(self.config_url)
|
|
||||||
params = {
|
|
||||||
"appId": self.app_id,
|
|
||||||
"cluster": self.cluster,
|
|
||||||
"notifications": json.dumps(notifications, ensure_ascii=False),
|
|
||||||
}
|
|
||||||
param_str = url_encode_wrapper(params)
|
|
||||||
url = url + "?" + param_str
|
|
||||||
code, body = http_request(url, self._pull_timeout, headers=self._sign_headers(url))
|
|
||||||
http_code = code
|
|
||||||
if http_code == 304:
|
|
||||||
logger.debug("No change, loop...")
|
|
||||||
return
|
|
||||||
if http_code == 200:
|
|
||||||
if not body:
|
|
||||||
logger.error(f"_long_poll load configs failed,body is {body}")
|
|
||||||
return
|
|
||||||
data = json.loads(body)
|
|
||||||
for entry in data:
|
|
||||||
namespace = entry[NAMESPACE_NAME]
|
|
||||||
n_id = entry[NOTIFICATION_ID]
|
|
||||||
logger.info("%s has changes: notificationId=%d", namespace, n_id)
|
|
||||||
self._get_net_and_set_local(namespace, n_id, call_change=True)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
logger.warning("Sleep...")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
|
|
||||||
def _get_net_and_set_local(self, namespace, n_id, call_change=False):
|
|
||||||
namespace_data = self.get_json_from_net(namespace)
|
|
||||||
if not namespace_data:
|
|
||||||
return
|
|
||||||
namespace_data[NOTIFICATION_ID] = n_id
|
|
||||||
old_namespace = self._cache.get(namespace)
|
|
||||||
self._update_cache_and_file(namespace_data, namespace)
|
|
||||||
if self._change_listener is not None and call_change and old_namespace:
|
|
||||||
old_kv = old_namespace.get(CONFIGURATIONS)
|
|
||||||
new_kv = namespace_data.get(CONFIGURATIONS)
|
|
||||||
self._call_listener(namespace, old_kv, new_kv)
|
|
||||||
|
|
||||||
def _listener(self):
|
|
||||||
logger.info("start long_poll")
|
|
||||||
while not self._stopping:
|
|
||||||
self._long_poll()
|
|
||||||
time.sleep(self._cycle_time)
|
|
||||||
logger.info("stopped, long_poll")
|
|
||||||
|
|
||||||
# add the need for endorsement to the header
|
|
||||||
def _sign_headers(self, url: str) -> Mapping[str, str]:
|
|
||||||
headers: dict[str, str] = {}
|
|
||||||
if self.secret == "":
|
|
||||||
return headers
|
|
||||||
uri = url[len(self.config_url) : len(url)]
|
|
||||||
time_unix_now = str(int(round(time.time() * 1000)))
|
|
||||||
headers["Authorization"] = "Apollo " + self.app_id + ":" + signature(time_unix_now, uri, self.secret)
|
|
||||||
headers["Timestamp"] = time_unix_now
|
|
||||||
return headers
|
|
||||||
|
|
||||||
def _heart_beat(self):
|
|
||||||
while not self._stopping:
|
|
||||||
for namespace in self._notification_map:
|
|
||||||
self._do_heart_beat(namespace)
|
|
||||||
time.sleep(60 * 10) # 10分钟
|
|
||||||
|
|
||||||
def _do_heart_beat(self, namespace):
|
|
||||||
url = "{}/configs/{}/{}/{}?ip={}".format(self.config_url, self.app_id, self.cluster, namespace, self.ip)
|
|
||||||
try:
|
|
||||||
code, body = http_request(url, timeout=3, headers=self._sign_headers(url))
|
|
||||||
if code == 200:
|
|
||||||
if not body:
|
|
||||||
logger.error(f"_do_heart_beat load configs failed,body is {body}")
|
|
||||||
return None
|
|
||||||
data = json.loads(body)
|
|
||||||
if self.last_release_key == data["releaseKey"]:
|
|
||||||
return None
|
|
||||||
self.last_release_key = data["releaseKey"]
|
|
||||||
data = data["configurations"]
|
|
||||||
self._update_cache_and_file(data, namespace)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
logger.exception("an error occurred in _do_heart_beat")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_dicts(self, namespace):
|
|
||||||
namespace_data = self._cache.get(namespace)
|
|
||||||
if namespace_data is None:
|
|
||||||
net_namespace_data = self.get_json_from_net(namespace)
|
|
||||||
if not net_namespace_data:
|
|
||||||
return namespace_data
|
|
||||||
namespace_data = net_namespace_data.get(CONFIGURATIONS)
|
|
||||||
if namespace_data:
|
|
||||||
self._update_cache_and_file(namespace_data, namespace)
|
|
||||||
return namespace_data
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import ssl
|
|
||||||
import urllib.request
|
|
||||||
from urllib import parse
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
|
|
||||||
# Create an SSL context that allows for a lower level of security
|
|
||||||
ssl_context = ssl.create_default_context()
|
|
||||||
ssl_context.set_ciphers("HIGH:!DH:!aNULL")
|
|
||||||
ssl_context.check_hostname = False
|
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
# Create an opener object and pass in a custom SSL context
|
|
||||||
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context))
|
|
||||||
|
|
||||||
urllib.request.install_opener(opener)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def http_request(url, timeout, headers={}):
|
|
||||||
try:
|
|
||||||
request = urllib.request.Request(url, headers=headers)
|
|
||||||
res = urllib.request.urlopen(request, timeout=timeout)
|
|
||||||
body = res.read().decode("utf-8")
|
|
||||||
return res.code, body
|
|
||||||
except HTTPError as e:
|
|
||||||
if e.code == 304:
|
|
||||||
logger.warning("http_request error,code is 304, maybe you should check secret")
|
|
||||||
return 304, None
|
|
||||||
logger.warning("http_request error,code is %d, msg is %s", e.code, e.msg)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def url_encode(params):
|
|
||||||
return parse.urlencode(params)
|
|
||||||
|
|
||||||
|
|
||||||
def makedirs_wrapper(path):
|
|
||||||
os.makedirs(path, exist_ok=True)
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from .python_3x import url_encode
|
|
||||||
|
|
||||||
# define constants
|
|
||||||
CONFIGURATIONS = "configurations"
|
|
||||||
NOTIFICATION_ID = "notificationId"
|
|
||||||
NAMESPACE_NAME = "namespaceName"
|
|
||||||
|
|
||||||
|
|
||||||
# add timestamps uris and keys
|
|
||||||
def signature(timestamp, uri, secret):
|
|
||||||
import base64
|
|
||||||
import hmac
|
|
||||||
|
|
||||||
string_to_sign = "" + timestamp + "\n" + uri
|
|
||||||
hmac_code = hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha1).digest()
|
|
||||||
return base64.b64encode(hmac_code).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def url_encode_wrapper(params):
|
|
||||||
return url_encode(params)
|
|
||||||
|
|
||||||
|
|
||||||
def no_key_cache_key(namespace, key):
|
|
||||||
return "{}{}{}".format(namespace, len(namespace), key)
|
|
||||||
|
|
||||||
|
|
||||||
# Returns whether the obtained value is obtained, and None if it does not
|
|
||||||
def get_value_from_dict(namespace_cache, key):
|
|
||||||
if namespace_cache:
|
|
||||||
kv_data = namespace_cache.get(CONFIGURATIONS)
|
|
||||||
if kv_data is None:
|
|
||||||
return None
|
|
||||||
if key in kv_data:
|
|
||||||
return kv_data[key]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def init_ip():
|
|
||||||
ip = ""
|
|
||||||
s = None
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.connect(("8.8.8.8", 53))
|
|
||||||
ip = s.getsockname()[0]
|
|
||||||
finally:
|
|
||||||
if s:
|
|
||||||
s.close()
|
|
||||||
return ip
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteSettingsSource:
|
|
||||||
def __init__(self, configs: Mapping[str, Any]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
|
|
||||||
return value
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from enum import StrEnum
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteSettingsSourceName(StrEnum):
|
|
||||||
APOLLO = "apollo"
|
|
||||||
@ -14,11 +14,11 @@ AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
|
|||||||
|
|
||||||
|
|
||||||
if dify_config.ETL_TYPE == "Unstructured":
|
if dify_config.ETL_TYPE == "Unstructured":
|
||||||
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls"]
|
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "pdf", "html", "htm", "xlsx", "xls"]
|
||||||
DOCUMENT_EXTENSIONS.extend(("docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
DOCUMENT_EXTENSIONS.extend(("docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
||||||
if dify_config.UNSTRUCTURED_API_URL:
|
if dify_config.UNSTRUCTURED_API_URL:
|
||||||
DOCUMENT_EXTENSIONS.append("ppt")
|
DOCUMENT_EXTENSIONS.append("ppt")
|
||||||
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
||||||
else:
|
else:
|
||||||
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"]
|
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"]
|
||||||
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
||||||
|
|||||||
@ -17,8 +17,6 @@ language_timezone_mapping = {
|
|||||||
"hi-IN": "Asia/Kolkata",
|
"hi-IN": "Asia/Kolkata",
|
||||||
"tr-TR": "Europe/Istanbul",
|
"tr-TR": "Europe/Istanbul",
|
||||||
"fa-IR": "Asia/Tehran",
|
"fa-IR": "Asia/Tehran",
|
||||||
"sl-SI": "Europe/Ljubljana",
|
|
||||||
"th-TH": "Asia/Bangkok",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
languages = list(language_timezone_mapping.keys())
|
languages = list(language_timezone_mapping.keys())
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
|
||||||
|
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
|
|
||||||
default_app_templates: Mapping[AppMode, Mapping] = {
|
default_app_templates = {
|
||||||
# workflow default mode
|
# workflow default mode
|
||||||
AppMode.WORKFLOW: {
|
AppMode.WORKFLOW: {
|
||||||
"app": {
|
"app": {
|
||||||
|
|||||||
@ -4,8 +4,3 @@ from werkzeug.exceptions import HTTPException
|
|||||||
class FilenameNotExistsError(HTTPException):
|
class FilenameNotExistsError(HTTPException):
|
||||||
code = 400
|
code = 400
|
||||||
description = "The specified filename does not exist."
|
description = "The specified filename does not exist."
|
||||||
|
|
||||||
|
|
||||||
class RemoteFileUploadError(HTTPException):
|
|
||||||
code = 400
|
|
||||||
description = "Error uploading remote file."
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from flask_restful import fields # type: ignore
|
from flask_restful import fields
|
||||||
|
|
||||||
parameters__system_parameters = {
|
parameters__system_parameters = {
|
||||||
"image_file_size_limit": fields.Integer,
|
"image_file_size_limit": fields.Integer,
|
||||||
|
|||||||
@ -2,26 +2,6 @@ from flask import Blueprint
|
|||||||
|
|
||||||
from libs.external_api import ExternalApi
|
from libs.external_api import ExternalApi
|
||||||
|
|
||||||
from .app.app_import import AppImportApi, AppImportConfirmApi
|
|
||||||
from .explore.audio import ChatAudioApi, ChatTextApi
|
|
||||||
from .explore.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi
|
|
||||||
from .explore.conversation import (
|
|
||||||
ConversationApi,
|
|
||||||
ConversationListApi,
|
|
||||||
ConversationPinApi,
|
|
||||||
ConversationRenameApi,
|
|
||||||
ConversationUnPinApi,
|
|
||||||
)
|
|
||||||
from .explore.message import (
|
|
||||||
MessageFeedbackApi,
|
|
||||||
MessageListApi,
|
|
||||||
MessageMoreLikeThisApi,
|
|
||||||
MessageSuggestedQuestionApi,
|
|
||||||
)
|
|
||||||
from .explore.workflow import (
|
|
||||||
InstalledAppWorkflowRunApi,
|
|
||||||
InstalledAppWorkflowTaskStopApi,
|
|
||||||
)
|
|
||||||
from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
||||||
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
||||||
|
|
||||||
@ -37,10 +17,6 @@ api.add_resource(FileSupportTypeApi, "/files/support-type")
|
|||||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
||||||
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
||||||
|
|
||||||
# Import App
|
|
||||||
api.add_resource(AppImportApi, "/apps/imports")
|
|
||||||
api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm")
|
|
||||||
|
|
||||||
# Import other controllers
|
# Import other controllers
|
||||||
from . import admin, apikey, extension, feature, ping, setup, version
|
from . import admin, apikey, extension, feature, ping, setup, version
|
||||||
|
|
||||||
@ -79,87 +55,22 @@ from .datasets import (
|
|||||||
datasets_document,
|
datasets_document,
|
||||||
datasets_segments,
|
datasets_segments,
|
||||||
external,
|
external,
|
||||||
|
fta_test,
|
||||||
hit_testing,
|
hit_testing,
|
||||||
website,
|
website,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import explore controllers
|
# Import explore controllers
|
||||||
from .explore import (
|
from .explore import (
|
||||||
|
audio,
|
||||||
|
completion,
|
||||||
|
conversation,
|
||||||
installed_app,
|
installed_app,
|
||||||
|
message,
|
||||||
parameter,
|
parameter,
|
||||||
recommended_app,
|
recommended_app,
|
||||||
saved_message,
|
saved_message,
|
||||||
)
|
workflow,
|
||||||
|
|
||||||
# Explore Audio
|
|
||||||
api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio")
|
|
||||||
api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text")
|
|
||||||
|
|
||||||
# Explore Completion
|
|
||||||
api.add_resource(
|
|
||||||
CompletionApi, "/installed-apps/<uuid:installed_app_id>/completion-messages", endpoint="installed_app_completion"
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
CompletionStopApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/completion-messages/<string:task_id>/stop",
|
|
||||||
endpoint="installed_app_stop_completion",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ChatApi, "/installed-apps/<uuid:installed_app_id>/chat-messages", endpoint="installed_app_chat_completion"
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ChatStopApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/chat-messages/<string:task_id>/stop",
|
|
||||||
endpoint="installed_app_stop_chat_completion",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Explore Conversation
|
|
||||||
api.add_resource(
|
|
||||||
ConversationRenameApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/name",
|
|
||||||
endpoint="installed_app_conversation_rename",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ConversationListApi, "/installed-apps/<uuid:installed_app_id>/conversations", endpoint="installed_app_conversations"
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ConversationApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>",
|
|
||||||
endpoint="installed_app_conversation",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ConversationPinApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/pin",
|
|
||||||
endpoint="installed_app_conversation_pin",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ConversationUnPinApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/unpin",
|
|
||||||
endpoint="installed_app_conversation_unpin",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Explore Message
|
|
||||||
api.add_resource(MessageListApi, "/installed-apps/<uuid:installed_app_id>/messages", endpoint="installed_app_messages")
|
|
||||||
api.add_resource(
|
|
||||||
MessageFeedbackApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/feedbacks",
|
|
||||||
endpoint="installed_app_message_feedback",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
MessageMoreLikeThisApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/more-like-this",
|
|
||||||
endpoint="installed_app_more_like_this",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
MessageSuggestedQuestionApi,
|
|
||||||
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/suggested-questions",
|
|
||||||
endpoint="installed_app_suggested_question",
|
|
||||||
)
|
|
||||||
# Explore Workflow
|
|
||||||
api.add_resource(InstalledAppWorkflowRunApi, "/installed-apps/<uuid:installed_app_id>/workflows/run")
|
|
||||||
api.add_resource(
|
|
||||||
InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import tag controllers
|
# Import tag controllers
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -31,7 +31,7 @@ def admin_required(view):
|
|||||||
if auth_scheme != "bearer":
|
if auth_scheme != "bearer":
|
||||||
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
|
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
|
||||||
|
|
||||||
if auth_token != dify_config.ADMIN_API_KEY:
|
if dify_config.ADMIN_API_KEY != auth_token:
|
||||||
raise Unauthorized("API key is invalid.")
|
raise Unauthorized("API key is invalid.")
|
||||||
|
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
from typing import Any
|
import flask_restful
|
||||||
|
from flask_login import current_user
|
||||||
import flask_restful # type: ignore
|
|
||||||
from flask_login import current_user # type: ignore
|
|
||||||
from flask_restful import Resource, fields, marshal_with
|
from flask_restful import Resource, fields, marshal_with
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
@ -37,15 +35,14 @@ def _get_resource(resource_id, tenant_id, resource_model):
|
|||||||
class BaseApiKeyListResource(Resource):
|
class BaseApiKeyListResource(Resource):
|
||||||
method_decorators = [account_initialization_required, login_required, setup_required]
|
method_decorators = [account_initialization_required, login_required, setup_required]
|
||||||
|
|
||||||
resource_type: str | None = None
|
resource_type = None
|
||||||
resource_model: Any = None
|
resource_model = None
|
||||||
resource_id_field: str | None = None
|
resource_id_field = None
|
||||||
token_prefix: str | None = None
|
token_prefix = None
|
||||||
max_keys = 10
|
max_keys = 10
|
||||||
|
|
||||||
@marshal_with(api_key_list)
|
@marshal_with(api_key_list)
|
||||||
def get(self, resource_id):
|
def get(self, resource_id):
|
||||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
|
||||||
resource_id = str(resource_id)
|
resource_id = str(resource_id)
|
||||||
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
||||||
keys = (
|
keys = (
|
||||||
@ -57,7 +54,6 @@ class BaseApiKeyListResource(Resource):
|
|||||||
|
|
||||||
@marshal_with(api_key_fields)
|
@marshal_with(api_key_fields)
|
||||||
def post(self, resource_id):
|
def post(self, resource_id):
|
||||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
|
||||||
resource_id = str(resource_id)
|
resource_id = str(resource_id)
|
||||||
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
@ -90,12 +86,11 @@ class BaseApiKeyListResource(Resource):
|
|||||||
class BaseApiKeyResource(Resource):
|
class BaseApiKeyResource(Resource):
|
||||||
method_decorators = [account_initialization_required, login_required, setup_required]
|
method_decorators = [account_initialization_required, login_required, setup_required]
|
||||||
|
|
||||||
resource_type: str | None = None
|
resource_type = None
|
||||||
resource_model: Any = None
|
resource_model = None
|
||||||
resource_id_field: str | None = None
|
resource_id_field = None
|
||||||
|
|
||||||
def delete(self, resource_id, api_key_id):
|
def delete(self, resource_id, api_key_id):
|
||||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
|
||||||
resource_id = str(resource_id)
|
resource_id = str(resource_id)
|
||||||
api_key_id = str(api_key_id)
|
api_key_id = str(api_key_id)
|
||||||
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -110,7 +110,7 @@ class AnnotationListApi(Resource):
|
|||||||
|
|
||||||
page = request.args.get("page", default=1, type=int)
|
page = request.args.get("page", default=1, type=int)
|
||||||
limit = request.args.get("limit", default=20, type=int)
|
limit = request.args.get("limit", default=20, type=int)
|
||||||
keyword = request.args.get("keyword", default="", type=str)
|
keyword = request.args.get("keyword", default=None, type=str)
|
||||||
|
|
||||||
app_id = str(app_id)
|
app_id = str(app_id)
|
||||||
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)
|
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -12,19 +9,16 @@ from controllers.console.app.wraps import get_app_model
|
|||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_resource_check,
|
cloud_edition_billing_resource_check,
|
||||||
enterprise_license_required,
|
|
||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
from core.ops.ops_trace_manager import OpsTraceManager
|
from core.ops.ops_trace_manager import OpsTraceManager
|
||||||
from extensions.ext_database import db
|
|
||||||
from fields.app_fields import (
|
from fields.app_fields import (
|
||||||
app_detail_fields,
|
app_detail_fields,
|
||||||
app_detail_fields_with_site,
|
app_detail_fields_with_site,
|
||||||
app_pagination_fields,
|
app_pagination_fields,
|
||||||
)
|
)
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Account, App
|
from services.app_dsl_service import AppDslService
|
||||||
from services.app_dsl_service import AppDslService, ImportMode
|
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
|
|
||||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||||
@ -34,7 +28,6 @@ class AppListApi(Resource):
|
|||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@enterprise_license_required
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""Get app list"""
|
"""Get app list"""
|
||||||
|
|
||||||
@ -57,13 +50,12 @@ class AppListApi(Resource):
|
|||||||
)
|
)
|
||||||
parser.add_argument("name", type=str, location="args", required=False)
|
parser.add_argument("name", type=str, location="args", required=False)
|
||||||
parser.add_argument("tag_ids", type=uuid_list, location="args", required=False)
|
parser.add_argument("tag_ids", type=uuid_list, location="args", required=False)
|
||||||
parser.add_argument("is_created_by_me", type=inputs.boolean, location="args", required=False)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# get app list
|
# get app list
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_pagination = app_service.get_paginate_apps(current_user.id, current_user.current_tenant_id, args)
|
app_pagination = app_service.get_paginate_apps(current_user.current_tenant_id, args)
|
||||||
if not app_pagination:
|
if not app_pagination:
|
||||||
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
|
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
|
||||||
|
|
||||||
@ -98,11 +90,65 @@ class AppListApi(Resource):
|
|||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
|
class AppImportApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@marshal_with(app_detail_fields_with_site)
|
||||||
|
@cloud_edition_billing_resource_check("apps")
|
||||||
|
def post(self):
|
||||||
|
"""Import app"""
|
||||||
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("data", type=str, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("name", type=str, location="json")
|
||||||
|
parser.add_argument("description", type=str, location="json")
|
||||||
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
|
parser.add_argument("icon", type=str, location="json")
|
||||||
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app = AppDslService.import_and_create_new_app(
|
||||||
|
tenant_id=current_user.current_tenant_id, data=args["data"], args=args, account=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
|
class AppImportFromUrlApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@marshal_with(app_detail_fields_with_site)
|
||||||
|
@cloud_edition_billing_resource_check("apps")
|
||||||
|
def post(self):
|
||||||
|
"""Import app from url"""
|
||||||
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("url", type=str, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("name", type=str, location="json")
|
||||||
|
parser.add_argument("description", type=str, location="json")
|
||||||
|
parser.add_argument("icon", type=str, location="json")
|
||||||
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app = AppDslService.import_and_create_new_app_from_url(
|
||||||
|
tenant_id=current_user.current_tenant_id, url=args["url"], args=args, account=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
class AppApi(Resource):
|
class AppApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@enterprise_license_required
|
|
||||||
@get_app_model
|
@get_app_model
|
||||||
@marshal_with(app_detail_fields_with_site)
|
@marshal_with(app_detail_fields_with_site)
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
@ -175,24 +221,10 @@ class AppCopyApi(Resource):
|
|||||||
parser.add_argument("icon_background", type=str, location="json")
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
with Session(db.engine) as session:
|
data = AppDslService.export_dsl(app_model=app_model, include_secret=True)
|
||||||
import_service = AppDslService(session)
|
app = AppDslService.import_and_create_new_app(
|
||||||
yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
|
tenant_id=current_user.current_tenant_id, data=data, args=args, account=current_user
|
||||||
account = cast(Account, current_user)
|
)
|
||||||
result = import_service.import_app(
|
|
||||||
account=account,
|
|
||||||
import_mode=ImportMode.YAML_CONTENT.value,
|
|
||||||
yaml_content=yaml_content,
|
|
||||||
name=args.get("name"),
|
|
||||||
description=args.get("description"),
|
|
||||||
icon_type=args.get("icon_type"),
|
|
||||||
icon=args.get("icon"),
|
|
||||||
icon_background=args.get("icon_background"),
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
stmt = select(App).where(App.id == result.app_id)
|
|
||||||
app = session.scalar(stmt)
|
|
||||||
|
|
||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
@ -333,6 +365,8 @@ class AppTraceApi(Resource):
|
|||||||
|
|
||||||
|
|
||||||
api.add_resource(AppListApi, "/apps")
|
api.add_resource(AppListApi, "/apps")
|
||||||
|
api.add_resource(AppImportApi, "/apps/import")
|
||||||
|
api.add_resource(AppImportFromUrlApi, "/apps/import/url")
|
||||||
api.add_resource(AppApi, "/apps/<uuid:app_id>")
|
api.add_resource(AppApi, "/apps/<uuid:app_id>")
|
||||||
api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
|
api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
|
||||||
api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")
|
api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
from typing import cast
|
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from werkzeug.exceptions import Forbidden
|
|
||||||
|
|
||||||
from controllers.console.wraps import (
|
|
||||||
account_initialization_required,
|
|
||||||
setup_required,
|
|
||||||
)
|
|
||||||
from extensions.ext_database import db
|
|
||||||
from fields.app_fields import app_import_fields
|
|
||||||
from libs.login import login_required
|
|
||||||
from models import Account
|
|
||||||
from services.app_dsl_service import AppDslService, ImportStatus
|
|
||||||
|
|
||||||
|
|
||||||
class AppImportApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@marshal_with(app_import_fields)
|
|
||||||
def post(self):
|
|
||||||
# Check user role first
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("mode", type=str, required=True, location="json")
|
|
||||||
parser.add_argument("yaml_content", type=str, location="json")
|
|
||||||
parser.add_argument("yaml_url", type=str, location="json")
|
|
||||||
parser.add_argument("name", type=str, location="json")
|
|
||||||
parser.add_argument("description", type=str, location="json")
|
|
||||||
parser.add_argument("icon_type", type=str, location="json")
|
|
||||||
parser.add_argument("icon", type=str, location="json")
|
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
|
||||||
parser.add_argument("app_id", type=str, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Create service with session
|
|
||||||
with Session(db.engine) as session:
|
|
||||||
import_service = AppDslService(session)
|
|
||||||
# Import app
|
|
||||||
account = cast(Account, current_user)
|
|
||||||
result = import_service.import_app(
|
|
||||||
account=account,
|
|
||||||
import_mode=args["mode"],
|
|
||||||
yaml_content=args.get("yaml_content"),
|
|
||||||
yaml_url=args.get("yaml_url"),
|
|
||||||
name=args.get("name"),
|
|
||||||
description=args.get("description"),
|
|
||||||
icon_type=args.get("icon_type"),
|
|
||||||
icon=args.get("icon"),
|
|
||||||
icon_background=args.get("icon_background"),
|
|
||||||
app_id=args.get("app_id"),
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Return appropriate status code based on result
|
|
||||||
status = result.status
|
|
||||||
if status == ImportStatus.FAILED.value:
|
|
||||||
return result.model_dump(mode="json"), 400
|
|
||||||
elif status == ImportStatus.PENDING.value:
|
|
||||||
return result.model_dump(mode="json"), 202
|
|
||||||
return result.model_dump(mode="json"), 200
|
|
||||||
|
|
||||||
|
|
||||||
class AppImportConfirmApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@marshal_with(app_import_fields)
|
|
||||||
def post(self, import_id):
|
|
||||||
# Check user role first
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
# Create service with session
|
|
||||||
with Session(db.engine) as session:
|
|
||||||
import_service = AppDslService(session)
|
|
||||||
# Confirm import
|
|
||||||
account = cast(Account, current_user)
|
|
||||||
result = import_service.confirm_import(import_id=import_id, account=account)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Return appropriate status code based on result
|
|
||||||
if result.status == ImportStatus.FAILED.value:
|
|
||||||
return result.model_dump(mode="json"), 400
|
|
||||||
return result.model_dump(mode="json"), 200
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -70,7 +70,7 @@ class ChatMessageAudioApi(Resource):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Failed to handle post request to ChatMessageAudioApi")
|
logging.exception(f"internal server error, {str(e)}.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ class ChatMessageTextApi(Resource):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Failed to handle post request to ChatMessageTextApi")
|
logging.exception(f"internal server error, {str(e)}.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ class TextModesApi(Resource):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Failed to handle get request to TextModesApi")
|
logging.exception(f"internal server error, {str(e)}.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import flask_login # type: ignore
|
import flask_login
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -20,6 +20,7 @@ from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpErr
|
|||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.errors.error import (
|
from core.errors.error import (
|
||||||
|
AppInvokeQuotaExceededError,
|
||||||
ModelCurrentlyNotSupportError,
|
ModelCurrentlyNotSupportError,
|
||||||
ProviderTokenNotInitError,
|
ProviderTokenNotInitError,
|
||||||
QuotaExceededError,
|
QuotaExceededError,
|
||||||
@ -75,7 +76,7 @@ class CompletionMessageApi(Resource):
|
|||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except (ValueError, AppInvokeQuotaExceededError) as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
@ -140,7 +141,7 @@ class ChatMessageApi(Resource):
|
|||||||
raise InvokeRateLimitHttpError(ex.description)
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except (ValueError, AppInvokeQuotaExceededError) as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import pytz # pip install pytz
|
import pytz
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from flask_restful.inputs import int_range # type: ignore
|
from flask_restful.inputs import int_range
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
@ -77,9 +77,8 @@ class CompletionConversationApi(Resource):
|
|||||||
|
|
||||||
query = query.where(Conversation.created_at < end_datetime_utc)
|
query = query.where(Conversation.created_at < end_datetime_utc)
|
||||||
|
|
||||||
# FIXME, the type ignore in this file
|
|
||||||
if args["annotation_status"] == "annotated":
|
if args["annotation_status"] == "annotated":
|
||||||
query = query.options(joinedload(Conversation.message_annotations)).join( # type: ignore
|
query = query.options(joinedload(Conversation.message_annotations)).join(
|
||||||
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
|
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
|
||||||
)
|
)
|
||||||
elif args["annotation_status"] == "not_annotated":
|
elif args["annotation_status"] == "not_annotated":
|
||||||
@ -223,7 +222,7 @@ class ChatConversationApi(Resource):
|
|||||||
query = query.where(Conversation.created_at <= end_datetime_utc)
|
query = query.where(Conversation.created_at <= end_datetime_utc)
|
||||||
|
|
||||||
if args["annotation_status"] == "annotated":
|
if args["annotation_status"] == "annotated":
|
||||||
query = query.options(joinedload(Conversation.message_annotations)).join( # type: ignore
|
query = query.options(joinedload(Conversation.message_annotations)).join(
|
||||||
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
|
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
|
||||||
)
|
)
|
||||||
elif args["annotation_status"] == "not_annotated":
|
elif args["annotation_status"] == "not_annotated":
|
||||||
@ -235,7 +234,7 @@ class ChatConversationApi(Resource):
|
|||||||
|
|
||||||
if args["message_count_gte"] and args["message_count_gte"] >= 1:
|
if args["message_count_gte"] and args["message_count_gte"] >= 1:
|
||||||
query = (
|
query = (
|
||||||
query.options(joinedload(Conversation.messages)) # type: ignore
|
query.options(joinedload(Conversation.messages))
|
||||||
.join(Message, Message.conversation_id == Conversation.id)
|
.join(Message, Message.conversation_id == Conversation.id)
|
||||||
.group_by(Conversation.id)
|
.group_by(Conversation.id)
|
||||||
.having(func.count(Message.id) >= args["message_count_gte"])
|
.having(func.count(Message.id) >= args["message_count_gte"])
|
||||||
@ -315,7 +314,7 @@ def _get_conversation(app_model, conversation_id):
|
|||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
|
||||||
if not conversation.read_at:
|
if not conversation.read_at:
|
||||||
conversation.read_at = datetime.now(UTC).replace(tzinfo=None)
|
conversation.read_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
conversation.read_account_id = current_user.id
|
conversation.read_account_id = current_user.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import (
|
from controllers.console.app.error import (
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, fields, marshal_with, reqparse
|
||||||
from flask_restful.inputs import int_range # type: ignore
|
from flask_restful.inputs import int_range
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource # type: ignore
|
from flask_restful import Resource
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
@ -27,9 +26,7 @@ class ModelConfigResource(Resource):
|
|||||||
"""Modify app model config"""
|
"""Modify app model config"""
|
||||||
# validate config
|
# validate config
|
||||||
model_configuration = AppModelConfigService.validate_configuration(
|
model_configuration = AppModelConfigService.validate_configuration(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id, config=request.json, app_mode=AppMode.value_of(app_model.mode)
|
||||||
config=cast(dict, request.json),
|
|
||||||
app_mode=AppMode.value_of(app_model.mode),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
new_app_model_config = AppModelConfig(
|
new_app_model_config = AppModelConfig(
|
||||||
@ -41,11 +38,9 @@ class ModelConfigResource(Resource):
|
|||||||
|
|
||||||
if app_model.mode == AppMode.AGENT_CHAT.value or app_model.is_agent:
|
if app_model.mode == AppMode.AGENT_CHAT.value or app_model.is_agent:
|
||||||
# get original app model config
|
# get original app model config
|
||||||
original_app_model_config = (
|
original_app_model_config: AppModelConfig = (
|
||||||
db.session.query(AppModelConfig).filter(AppModelConfig.id == app_model.app_model_config_id).first()
|
db.session.query(AppModelConfig).filter(AppModelConfig.id == app_model.app_model_config_id).first()
|
||||||
)
|
)
|
||||||
if original_app_model_config is None:
|
|
||||||
raise ValueError("Original app model config not found")
|
|
||||||
agent_mode = original_app_model_config.agent_mode_dict
|
agent_mode = original_app_model_config.agent_mode_dict
|
||||||
# decrypt agent tool parameters if it's secret-input
|
# decrypt agent tool parameters if it's secret-input
|
||||||
parameter_map = {}
|
parameter_map = {}
|
||||||
@ -70,7 +65,7 @@ class ModelConfigResource(Resource):
|
|||||||
provider_type=agent_tool_entity.provider_type,
|
provider_type=agent_tool_entity.provider_type,
|
||||||
identity_id=f"AGENT.{app_model.id}",
|
identity_id=f"AGENT.{app_model.id}",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# get decrypted parameters
|
# get decrypted parameters
|
||||||
@ -102,7 +97,7 @@ class ModelConfigResource(Resource):
|
|||||||
app_id=app_model.id,
|
app_id=app_model.id,
|
||||||
agent_tool=agent_tool_entity,
|
agent_tool=agent_tool_entity,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
manager = ToolParameterConfigurationManager(
|
manager = ToolParameterConfigurationManager(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import BadRequest
|
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
||||||
@ -27,7 +26,7 @@ class TraceAppConfigApi(Resource):
|
|||||||
return {"has_not_configured": True}
|
return {"has_not_configured": True}
|
||||||
return trace_config
|
return trace_config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise e
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -49,7 +48,7 @@ class TraceAppConfigApi(Resource):
|
|||||||
raise TracingConfigCheckError()
|
raise TracingConfigCheckError()
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise e
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -69,7 +68,7 @@ class TraceAppConfigApi(Resource):
|
|||||||
raise TracingConfigNotExist()
|
raise TracingConfigNotExist()
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise e
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -86,7 +85,7 @@ class TraceAppConfigApi(Resource):
|
|||||||
raise TracingConfigNotExist()
|
raise TracingConfigNotExist()
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise e
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(TraceAppConfigApi, "/apps/<uuid:app_id>/trace-config")
|
api.add_resource(TraceAppConfigApi, "/apps/<uuid:app_id>/trace-config")
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
@ -50,7 +50,7 @@ class AppSite(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
site = Site.query.filter(Site.app_id == app_model.id).one_or_404()
|
site = db.session.query(Site).filter(Site.app_id == app_model.id).one_or_404()
|
||||||
|
|
||||||
for attr_name in [
|
for attr_name in [
|
||||||
"title",
|
"title",
|
||||||
@ -75,7 +75,7 @@ class AppSite(Resource):
|
|||||||
setattr(site, attr_name, value)
|
setattr(site, attr_name, value)
|
||||||
|
|
||||||
site.updated_by = current_user.id
|
site.updated_by = current_user.id
|
||||||
site.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return site
|
return site
|
||||||
@ -99,7 +99,7 @@ class AppSiteAccessTokenReset(Resource):
|
|||||||
|
|
||||||
site.code = Site.generate_code(16)
|
site.code = Site.generate_code(16)
|
||||||
site.updated_by = current_user.id
|
site.updated_by = current_user.id
|
||||||
site.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return site
|
return site
|
||||||
|
|||||||
@ -3,8 +3,8 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
@ -273,7 +273,8 @@ FROM
|
|||||||
messages m
|
messages m
|
||||||
ON c.id = m.conversation_id
|
ON c.id = m.conversation_id
|
||||||
WHERE
|
WHERE
|
||||||
c.app_id = :app_id"""
|
c.override_model_configs IS NULL
|
||||||
|
AND c.app_id = :app_id"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id}
|
||||||
|
|
||||||
timezone = pytz.timezone(account.timezone)
|
timezone = pytz.timezone(account.timezone)
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import abort, request
|
from flask import abort, request
|
||||||
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
@ -14,13 +13,14 @@ from controllers.console.wraps import account_initialization_required, setup_req
|
|||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from factories import variable_factory
|
from factories import variable_factory
|
||||||
from fields.workflow_fields import workflow_fields, workflow_pagination_fields
|
from fields.workflow_fields import workflow_fields
|
||||||
from fields.workflow_run_fields import workflow_run_node_execution_fields
|
from fields.workflow_run_fields import workflow_run_node_execution_fields
|
||||||
from libs import helper
|
from libs import helper
|
||||||
from libs.helper import TimestampField, uuid_value
|
from libs.helper import TimestampField, uuid_value
|
||||||
from libs.login import current_user, login_required
|
from libs.login import current_user, login_required
|
||||||
from models import App
|
from models import App
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
|
from services.app_dsl_service import AppDslService
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.app import WorkflowHashNotEqualError
|
from services.errors.app import WorkflowHashNotEqualError
|
||||||
from services.workflow_service import WorkflowService
|
from services.workflow_service import WorkflowService
|
||||||
@ -101,11 +101,11 @@ class DraftWorkflowApi(Resource):
|
|||||||
try:
|
try:
|
||||||
environment_variables_list = args.get("environment_variables") or []
|
environment_variables_list = args.get("environment_variables") or []
|
||||||
environment_variables = [
|
environment_variables = [
|
||||||
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
|
variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list
|
||||||
]
|
]
|
||||||
conversation_variables_list = args.get("conversation_variables") or []
|
conversation_variables_list = args.get("conversation_variables") or []
|
||||||
conversation_variables = [
|
conversation_variables = [
|
||||||
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
|
variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list
|
||||||
]
|
]
|
||||||
workflow = workflow_service.sync_draft_workflow(
|
workflow = workflow_service.sync_draft_workflow(
|
||||||
app_model=app_model,
|
app_model=app_model,
|
||||||
@ -126,6 +126,31 @@ class DraftWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DraftWorkflowImportApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||||
|
@marshal_with(workflow_fields)
|
||||||
|
def post(self, app_model: App):
|
||||||
|
"""
|
||||||
|
Import draft workflow
|
||||||
|
"""
|
||||||
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("data", type=str, required=True, nullable=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
workflow = AppDslService.import_and_overwrite_workflow(
|
||||||
|
app_model=app_model, data=args["data"], account=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
return workflow
|
||||||
|
|
||||||
|
|
||||||
class AdvancedChatDraftWorkflowRunApi(Resource):
|
class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -383,7 +408,7 @@ class DefaultBlockConfigApi(Resource):
|
|||||||
filters = None
|
filters = None
|
||||||
if args.get("q"):
|
if args.get("q"):
|
||||||
try:
|
try:
|
||||||
filters = json.loads(args.get("q", ""))
|
filters = json.loads(args.get("q"))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ValueError("Invalid filters")
|
raise ValueError("Invalid filters")
|
||||||
|
|
||||||
@ -427,46 +452,8 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WorkflowConfigApi(Resource):
|
|
||||||
"""Resource for workflow configuration."""
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
||||||
def get(self, app_model: App):
|
|
||||||
return {
|
|
||||||
"parallel_depth_limit": dify_config.WORKFLOW_PARALLEL_DEPTH_LIMIT,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PublishedAllWorkflowApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
||||||
@marshal_with(workflow_pagination_fields)
|
|
||||||
def get(self, app_model: App):
|
|
||||||
"""
|
|
||||||
Get published workflows
|
|
||||||
"""
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
|
|
||||||
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
|
|
||||||
args = parser.parse_args()
|
|
||||||
page = args.get("page")
|
|
||||||
limit = args.get("limit")
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
workflows, has_more = workflow_service.get_all_published_workflow(app_model=app_model, page=page, limit=limit)
|
|
||||||
|
|
||||||
return {"items": workflows, "page": page, "limit": limit, "has_more": has_more}
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
|
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
|
||||||
api.add_resource(WorkflowConfigApi, "/apps/<uuid:app_id>/workflows/draft/config")
|
api.add_resource(DraftWorkflowImportApi, "/apps/<uuid:app_id>/workflows/draft/import")
|
||||||
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
||||||
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
|
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
|
||||||
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
|
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
|
||||||
@ -479,7 +466,6 @@ api.add_resource(
|
|||||||
WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
|
WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
|
||||||
)
|
)
|
||||||
api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
|
api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
|
||||||
api.add_resource(PublishedAllWorkflowApi, "/apps/<uuid:app_id>/workflows")
|
|
||||||
api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
|
api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"
|
DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from flask_restful.inputs import int_range # type: ignore
|
from flask_restful.inputs import int_range
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from flask_restful.inputs import int_range # type: ignore
|
from flask_restful.inputs import int_range
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
|
|||||||
@ -3,8 +3,8 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
|
|||||||
@ -5,10 +5,11 @@ from typing import Optional, Union
|
|||||||
from controllers.console.app.error import AppNotFoundError
|
from controllers.console.app.error import AppNotFoundError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.login import current_user
|
from libs.login import current_user
|
||||||
from models import App, AppMode
|
from models import App
|
||||||
|
from models.model import AppMode
|
||||||
|
|
||||||
|
|
||||||
def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode], None] = None):
|
def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode]] = None):
|
||||||
def decorator(view_func):
|
def decorator(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.error import AlreadyActivateError
|
from controllers.console.error import AlreadyActivateError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import StrLen, email, extract_remote_ip, timezone
|
from libs.helper import StrLen, email, extract_remote_ip, timezone
|
||||||
from models.account import AccountStatus
|
from models.account import AccountStatus, Tenant
|
||||||
from services.account_service import AccountService, RegisterService
|
from services.account_service import AccountService, RegisterService
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class ActivateCheckApi(Resource):
|
|||||||
invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token)
|
invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token)
|
||||||
if invitation:
|
if invitation:
|
||||||
data = invitation.get("data", {})
|
data = invitation.get("data", {})
|
||||||
tenant = invitation.get("tenant", None)
|
tenant: Tenant = invitation.get("tenant", None)
|
||||||
workspace_name = tenant.name if tenant else None
|
workspace_name = tenant.name if tenant else None
|
||||||
workspace_id = tenant.id if tenant else None
|
workspace_id = tenant.id if tenant else None
|
||||||
invitee_email = data.get("email") if data else None
|
invitee_email = data.get("email") if data else None
|
||||||
@ -65,7 +65,7 @@ class ActivateApi(Resource):
|
|||||||
account.timezone = args["timezone"]
|
account.timezone = args["timezone"]
|
||||||
account.interface_theme = "light"
|
account.interface_theme = "light"
|
||||||
account.status = AccountStatus.ACTIVE.value
|
account.status = AccountStatus.ACTIVE.value
|
||||||
account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import logging
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import current_app, redirect, request
|
from flask import current_app, redirect, request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource # type: ignore
|
from flask_restful import Resource
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -17,8 +17,8 @@ from ..wraps import account_initialization_required, setup_required
|
|||||||
def get_oauth_providers():
|
def get_oauth_providers():
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
notion_oauth = NotionOAuth(
|
notion_oauth = NotionOAuth(
|
||||||
client_id=dify_config.NOTION_CLIENT_ID or "",
|
client_id=dify_config.NOTION_CLIENT_ID,
|
||||||
client_secret=dify_config.NOTION_CLIENT_SECRET or "",
|
client_secret=dify_config.NOTION_CLIENT_SECRET,
|
||||||
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/data-source/callback/notion",
|
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/data-source/callback/notion",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ class OAuthDataSource(Resource):
|
|||||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider)
|
oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider)
|
||||||
|
print(vars(oauth_provider))
|
||||||
if not oauth_provider:
|
if not oauth_provider:
|
||||||
return {"error": "Invalid provider"}, 400
|
return {"error": "Invalid provider"}, 400
|
||||||
if dify_config.NOTION_INTEGRATION_TYPE == "internal":
|
if dify_config.NOTION_INTEGRATION_TYPE == "internal":
|
||||||
|
|||||||
@ -53,9 +53,3 @@ class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
|
|||||||
error_code = "email_code_login_rate_limit_exceeded"
|
error_code = "email_code_login_rate_limit_exceeded"
|
||||||
description = "Too many login emails have been sent. Please try again in 5 minutes."
|
description = "Too many login emails have been sent. Please try again in 5 minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
|
||||||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
|
||||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
|
||||||
code = 429
|
|
||||||
|
|||||||
@ -2,12 +2,17 @@ import base64
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
|
from controllers.console.auth.error import (
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
EmailCodeError,
|
||||||
|
InvalidEmailError,
|
||||||
|
InvalidTokenError,
|
||||||
|
PasswordMismatchError,
|
||||||
|
)
|
||||||
|
from controllers.console.error import EmailSendIpLimitError, NotAllowedRegister
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -15,7 +20,6 @@ from libs.helper import email, extract_remote_ip
|
|||||||
from libs.password import hash_password, valid_password
|
from libs.password import hash_password, valid_password
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, TenantService
|
from services.account_service import AccountService, TenantService
|
||||||
from services.errors.account import AccountRegisterError
|
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
@ -44,7 +48,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
return {"result": "fail", "data": token, "code": "account_not_found"}
|
||||||
else:
|
else:
|
||||||
raise AccountNotFound()
|
raise NotAllowedRegister()
|
||||||
else:
|
else:
|
||||||
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
|
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
|
||||||
|
|
||||||
@ -118,15 +122,13 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
account = AccountService.create_account_and_tenant(
|
account = AccountService.create_account_and_tenant(
|
||||||
email=reset_data.get("email", ""),
|
email=reset_data.get("email"),
|
||||||
name=reset_data.get("email", ""),
|
name=reset_data.get("email"),
|
||||||
password=password_confirm,
|
password=password_confirm,
|
||||||
interface_language=languages[0],
|
interface_language=languages[0],
|
||||||
)
|
)
|
||||||
except WorkSpaceNotAllowedCreateError:
|
except WorkSpaceNotAllowedCreateError:
|
||||||
pass
|
pass
|
||||||
except AccountRegisterError as are:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
import flask_login # type: ignore
|
import flask_login
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.auth.error import (
|
from controllers.console.auth.error import (
|
||||||
@ -17,10 +16,9 @@ from controllers.console.auth.error import (
|
|||||||
)
|
)
|
||||||
from controllers.console.error import (
|
from controllers.console.error import (
|
||||||
AccountBannedError,
|
AccountBannedError,
|
||||||
AccountInFreezeError,
|
|
||||||
AccountNotFound,
|
|
||||||
EmailSendIpLimitError,
|
EmailSendIpLimitError,
|
||||||
NotAllowedCreateWorkspace,
|
NotAllowedCreateWorkspace,
|
||||||
|
NotAllowedRegister,
|
||||||
)
|
)
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
@ -28,8 +26,6 @@ from libs.helper import email, extract_remote_ip
|
|||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
from services.billing_service import BillingService
|
|
||||||
from services.errors.account import AccountRegisterError
|
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
@ -48,9 +44,6 @@ class LoginApi(Resource):
|
|||||||
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
|
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
|
|
||||||
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"])
|
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"])
|
||||||
if is_login_error_rate_limit:
|
if is_login_error_rate_limit:
|
||||||
raise EmailPasswordLoginLimitError()
|
raise EmailPasswordLoginLimitError()
|
||||||
@ -83,7 +76,7 @@ class LoginApi(Resource):
|
|||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
return {"result": "fail", "data": token, "code": "account_not_found"}
|
||||||
else:
|
else:
|
||||||
raise AccountNotFound()
|
raise NotAllowedRegister()
|
||||||
# SELF_HOSTED only have one workspace
|
# SELF_HOSTED only have one workspace
|
||||||
tenants = TenantService.get_join_tenants(account)
|
tenants = TenantService.get_join_tenants(account)
|
||||||
if len(tenants) == 0:
|
if len(tenants) == 0:
|
||||||
@ -120,15 +113,13 @@ class ResetPasswordSendEmailApi(Resource):
|
|||||||
language = "zh-Hans"
|
language = "zh-Hans"
|
||||||
else:
|
else:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
try:
|
|
||||||
account = AccountService.get_user_through_email(args["email"])
|
account = AccountService.get_user_through_email(args["email"])
|
||||||
except AccountRegisterError as are:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
if account is None:
|
if account is None:
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
if FeatureService.get_system_features().is_allow_register:
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
||||||
else:
|
else:
|
||||||
raise AccountNotFound()
|
raise NotAllowedRegister()
|
||||||
else:
|
else:
|
||||||
token = AccountService.send_reset_password_email(account=account, language=language)
|
token = AccountService.send_reset_password_email(account=account, language=language)
|
||||||
|
|
||||||
@ -151,16 +142,13 @@ class EmailCodeLoginSendEmailApi(Resource):
|
|||||||
language = "zh-Hans"
|
language = "zh-Hans"
|
||||||
else:
|
else:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
try:
|
|
||||||
account = AccountService.get_user_through_email(args["email"])
|
|
||||||
except AccountRegisterError as are:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
|
|
||||||
|
account = AccountService.get_user_through_email(args["email"])
|
||||||
if account is None:
|
if account is None:
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
if FeatureService.get_system_features().is_allow_register:
|
||||||
token = AccountService.send_email_code_login_email(email=args["email"], language=language)
|
token = AccountService.send_email_code_login_email(email=args["email"], language=language)
|
||||||
else:
|
else:
|
||||||
raise AccountNotFound()
|
raise NotAllowedRegister()
|
||||||
else:
|
else:
|
||||||
token = AccountService.send_email_code_login_email(account=account, language=language)
|
token = AccountService.send_email_code_login_email(account=account, language=language)
|
||||||
|
|
||||||
@ -189,10 +177,7 @@ class EmailCodeLoginApi(Resource):
|
|||||||
raise EmailCodeError()
|
raise EmailCodeError()
|
||||||
|
|
||||||
AccountService.revoke_email_code_login_token(args["token"])
|
AccountService.revoke_email_code_login_token(args["token"])
|
||||||
try:
|
account = AccountService.get_user_through_email(user_email)
|
||||||
account = AccountService.get_user_through_email(user_email)
|
|
||||||
except AccountRegisterError as are:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
if account:
|
if account:
|
||||||
tenant = TenantService.get_join_tenants(account)
|
tenant = TenantService.get_join_tenants(account)
|
||||||
if not tenant:
|
if not tenant:
|
||||||
@ -211,8 +196,6 @@ class EmailCodeLoginApi(Resource):
|
|||||||
)
|
)
|
||||||
except WorkSpaceNotAllowedCreateError:
|
except WorkSpaceNotAllowedCreateError:
|
||||||
return NotAllowedCreateWorkspace()
|
return NotAllowedCreateWorkspace()
|
||||||
except AccountRegisterError as are:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
||||||
AccountService.reset_login_error_rate_limit(args["email"])
|
AccountService.reset_login_error_rate_limit(args["email"])
|
||||||
return {"result": "success", "data": token_pair.model_dump()}
|
return {"result": "success", "data": token_pair.model_dump()}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import current_app, redirect, request
|
from flask import current_app, redirect, request
|
||||||
from flask_restful import Resource # type: ignore
|
from flask_restful import Resource
|
||||||
from werkzeug.exceptions import Unauthorized
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -16,7 +16,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
|
|||||||
from models import Account
|
from models import Account
|
||||||
from models.account import AccountStatus
|
from models.account import AccountStatus
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
from services.errors.account import AccountNotFoundError
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
@ -52,6 +52,7 @@ class OAuthLogin(Resource):
|
|||||||
OAUTH_PROVIDERS = get_oauth_providers()
|
OAUTH_PROVIDERS = get_oauth_providers()
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
oauth_provider = OAUTH_PROVIDERS.get(provider)
|
oauth_provider = OAUTH_PROVIDERS.get(provider)
|
||||||
|
print(vars(oauth_provider))
|
||||||
if not oauth_provider:
|
if not oauth_provider:
|
||||||
return {"error": "Invalid provider"}, 400
|
return {"error": "Invalid provider"}, 400
|
||||||
|
|
||||||
@ -76,9 +77,8 @@ class OAuthCallback(Resource):
|
|||||||
try:
|
try:
|
||||||
token = oauth_provider.get_access_token(code)
|
token = oauth_provider.get_access_token(code)
|
||||||
user_info = oauth_provider.get_user_info(token)
|
user_info = oauth_provider.get_user_info(token)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
error_text = e.response.text if e.response else str(e)
|
logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}")
|
||||||
logging.exception(f"An error occurred during the OAuth process with {provider}: {error_text}")
|
|
||||||
return {"error": "OAuth process failed"}, 400
|
return {"error": "OAuth process failed"}, 400
|
||||||
|
|
||||||
if invite_token and RegisterService.is_valid_invite_token(invite_token):
|
if invite_token and RegisterService.is_valid_invite_token(invite_token):
|
||||||
@ -99,8 +99,6 @@ class OAuthCallback(Resource):
|
|||||||
f"{dify_config.CONSOLE_WEB_URL}/signin"
|
f"{dify_config.CONSOLE_WEB_URL}/signin"
|
||||||
"?message=Workspace not found, please contact system admin to invite you to join in a workspace."
|
"?message=Workspace not found, please contact system admin to invite you to join in a workspace."
|
||||||
)
|
)
|
||||||
except AccountRegisterError as e:
|
|
||||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}")
|
|
||||||
|
|
||||||
# Check account status
|
# Check account status
|
||||||
if account.status == AccountStatus.BANNED.value:
|
if account.status == AccountStatus.BANNED.value:
|
||||||
@ -108,7 +106,7 @@ class OAuthCallback(Resource):
|
|||||||
|
|
||||||
if account.status == AccountStatus.PENDING.value:
|
if account.status == AccountStatus.PENDING.value:
|
||||||
account.status = AccountStatus.ACTIVE.value
|
account.status = AccountStatus.ACTIVE.value
|
||||||
account.initialized_at = datetime.now(UTC).replace(tzinfo=None)
|
account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -132,7 +130,7 @@ class OAuthCallback(Resource):
|
|||||||
|
|
||||||
|
|
||||||
def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]:
|
def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]:
|
||||||
account: Optional[Account] = Account.get_by_openid(provider, user_info.id)
|
account = Account.get_by_openid(provider, user_info.id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
account = Account.query.filter_by(email=user_info.email).first()
|
account = Account.query.filter_by(email=user_info.email).first()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required
|
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -83,7 +83,7 @@ class DataSourceApi(Resource):
|
|||||||
if action == "enable":
|
if action == "enable":
|
||||||
if data_source_binding.disabled:
|
if data_source_binding.disabled:
|
||||||
data_source_binding.disabled = False
|
data_source_binding.disabled = False
|
||||||
data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
||||||
db.session.add(data_source_binding)
|
db.session.add(data_source_binding)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
@ -92,7 +92,7 @@ class DataSourceApi(Resource):
|
|||||||
if action == "disable":
|
if action == "disable":
|
||||||
if not data_source_binding.disabled:
|
if not data_source_binding.disabled:
|
||||||
data_source_binding.disabled = True
|
data_source_binding.disabled = True
|
||||||
data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
data_source_binding.updated_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
||||||
db.session.add(data_source_binding)
|
db.session.add(data_source_binding)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
@ -218,7 +218,7 @@ class DataSourceNotionApi(Resource):
|
|||||||
args["doc_form"],
|
args["doc_form"],
|
||||||
args["doc_language"],
|
args["doc_language"],
|
||||||
)
|
)
|
||||||
return response.model_dump(), 200
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
class DataSourceNotionDatasetSyncApi(Resource):
|
class DataSourceNotionDatasetSyncApi(Resource):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import flask_restful # type: ignore
|
import flask_restful
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -10,7 +10,7 @@ from controllers.console import api
|
|||||||
from controllers.console.apikey import api_key_fields, api_key_list
|
from controllers.console.apikey import api_key_fields, api_key_list
|
||||||
from controllers.console.app.error import ProviderNotInitializeError
|
from controllers.console.app.error import ProviderNotInitializeError
|
||||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||||
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||||
from core.indexing_runner import IndexingRunner
|
from core.indexing_runner import IndexingRunner
|
||||||
from core.model_runtime.entities.model_entities import ModelType
|
from core.model_runtime.entities.model_entities import ModelType
|
||||||
@ -44,7 +44,6 @@ class DatasetListApi(Resource):
|
|||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@enterprise_license_required
|
|
||||||
def get(self):
|
def get(self):
|
||||||
page = request.args.get("page", default=1, type=int)
|
page = request.args.get("page", default=1, type=int)
|
||||||
limit = request.args.get("limit", default=20, type=int)
|
limit = request.args.get("limit", default=20, type=int)
|
||||||
@ -464,7 +463,7 @@ class DatasetIndexingEstimateApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IndexingEstimateError(str(e))
|
raise IndexingEstimateError(str(e))
|
||||||
|
|
||||||
return response.model_dump(), 200
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
class DatasetRelatedAppListApi(Resource):
|
class DatasetRelatedAppListApi(Resource):
|
||||||
@ -640,7 +639,6 @@ class DatasetRetrievalSettingApi(Resource):
|
|||||||
| VectorType.MYSCALE
|
| VectorType.MYSCALE
|
||||||
| VectorType.ORACLE
|
| VectorType.ORACLE
|
||||||
| VectorType.ELASTICSEARCH
|
| VectorType.ELASTICSEARCH
|
||||||
| VectorType.ELASTICSEARCH_JA
|
|
||||||
| VectorType.PGVECTOR
|
| VectorType.PGVECTOR
|
||||||
| VectorType.TIDB_ON_QDRANT
|
| VectorType.TIDB_ON_QDRANT
|
||||||
| VectorType.LINDORM
|
| VectorType.LINDORM
|
||||||
@ -684,7 +682,6 @@ class DatasetRetrievalSettingMockApi(Resource):
|
|||||||
| VectorType.MYSCALE
|
| VectorType.MYSCALE
|
||||||
| VectorType.ORACLE
|
| VectorType.ORACLE
|
||||||
| VectorType.ELASTICSEARCH
|
| VectorType.ELASTICSEARCH
|
||||||
| VectorType.ELASTICSEARCH_JA
|
|
||||||
| VectorType.COUCHBASE
|
| VectorType.COUCHBASE
|
||||||
| VectorType.PGVECTOR
|
| VectorType.PGVECTOR
|
||||||
| VectorType.LINDORM
|
| VectorType.LINDORM
|
||||||
@ -735,18 +732,6 @@ class DatasetPermissionUserListApi(Resource):
|
|||||||
}, 200
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
class DatasetAutoDisableLogApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def get(self, dataset_id):
|
|
||||||
dataset_id_str = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
|
||||||
if dataset is None:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
return DatasetService.get_dataset_auto_disable_logs(dataset_id_str), 200
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DatasetListApi, "/datasets")
|
api.add_resource(DatasetListApi, "/datasets")
|
||||||
api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")
|
api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")
|
||||||
api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")
|
api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")
|
||||||
@ -761,4 +746,3 @@ api.add_resource(DatasetApiBaseUrlApi, "/datasets/api-base-info")
|
|||||||
api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")
|
api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")
|
||||||
api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")
|
api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")
|
||||||
api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")
|
api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")
|
||||||
api.add_resource(DatasetAutoDisableLogApi, "/datasets/<uuid:dataset_id>/auto-disable-logs")
|
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from argparse import ArgumentTypeError
|
from argparse import ArgumentTypeError
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime, timezone
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, fields, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, fields, marshal, marshal_with, reqparse
|
||||||
from sqlalchemy import asc, desc
|
from sqlalchemy import asc, desc
|
||||||
from transformers.hf_argparser import string_to_bool # type: ignore
|
from transformers.hf_argparser import string_to_bool
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -52,7 +51,6 @@ from fields.document_fields import (
|
|||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile
|
from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile
|
||||||
from services.dataset_service import DatasetService, DocumentService
|
from services.dataset_service import DatasetService, DocumentService
|
||||||
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
|
|
||||||
from tasks.add_document_to_index_task import add_document_to_index_task
|
from tasks.add_document_to_index_task import add_document_to_index_task
|
||||||
from tasks.remove_document_from_index_task import remove_document_from_index_task
|
from tasks.remove_document_from_index_task import remove_document_from_index_task
|
||||||
|
|
||||||
@ -108,7 +106,6 @@ class GetProcessRuleApi(Resource):
|
|||||||
# get default rules
|
# get default rules
|
||||||
mode = DocumentService.DEFAULT_RULES["mode"]
|
mode = DocumentService.DEFAULT_RULES["mode"]
|
||||||
rules = DocumentService.DEFAULT_RULES["rules"]
|
rules = DocumentService.DEFAULT_RULES["rules"]
|
||||||
limits = DocumentService.DEFAULT_RULES["limits"]
|
|
||||||
if document_id:
|
if document_id:
|
||||||
# get the latest process rule
|
# get the latest process rule
|
||||||
document = Document.query.get_or_404(document_id)
|
document = Document.query.get_or_404(document_id)
|
||||||
@ -135,7 +132,7 @@ class GetProcessRuleApi(Resource):
|
|||||||
mode = dataset_process_rule.mode
|
mode = dataset_process_rule.mode
|
||||||
rules = dataset_process_rule.rules_dict
|
rules = dataset_process_rule.rules_dict
|
||||||
|
|
||||||
return {"mode": mode, "rules": rules, "limits": limits}
|
return {"mode": mode, "rules": rules}
|
||||||
|
|
||||||
|
|
||||||
class DatasetDocumentListApi(Resource):
|
class DatasetDocumentListApi(Resource):
|
||||||
@ -256,23 +253,20 @@ class DatasetDocumentListApi(Resource):
|
|||||||
parser.add_argument("duplicate", type=bool, default=True, nullable=False, location="json")
|
parser.add_argument("duplicate", type=bool, default=True, nullable=False, location="json")
|
||||||
parser.add_argument("original_document_id", type=str, required=False, location="json")
|
parser.add_argument("original_document_id", type=str, required=False, location="json")
|
||||||
parser.add_argument("doc_form", type=str, default="text_model", required=False, nullable=False, location="json")
|
parser.add_argument("doc_form", type=str, default="text_model", required=False, nullable=False, location="json")
|
||||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
|
||||||
parser.add_argument("embedding_model", type=str, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"doc_language", type=str, default="English", required=False, nullable=False, location="json"
|
"doc_language", type=str, default="English", required=False, nullable=False, location="json"
|
||||||
)
|
)
|
||||||
|
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
knowledge_config = KnowledgeConfig(**args)
|
|
||||||
|
|
||||||
if not dataset.indexing_technique and not knowledge_config.indexing_technique:
|
if not dataset.indexing_technique and not args["indexing_technique"]:
|
||||||
raise ValueError("indexing_technique is required.")
|
raise ValueError("indexing_technique is required.")
|
||||||
|
|
||||||
# validate args
|
# validate args
|
||||||
DocumentService.document_create_args_validate(knowledge_config)
|
DocumentService.document_create_args_validate(args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
documents, batch = DocumentService.save_document_with_dataset_id(dataset, knowledge_config, current_user)
|
documents, batch = DocumentService.save_document_with_dataset_id(dataset, args, current_user)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
except QuotaExceededError:
|
except QuotaExceededError:
|
||||||
@ -282,25 +276,6 @@ class DatasetDocumentListApi(Resource):
|
|||||||
|
|
||||||
return {"documents": documents, "batch": batch}
|
return {"documents": documents, "batch": batch}
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def delete(self, dataset_id):
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if dataset is None:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
|
|
||||||
try:
|
|
||||||
document_ids = request.args.getlist("document_id")
|
|
||||||
DocumentService.delete_documents(dataset, document_ids)
|
|
||||||
except services.errors.document.DocumentIndexingError:
|
|
||||||
raise DocumentIndexingError("Cannot delete document during indexing.")
|
|
||||||
|
|
||||||
return {"result": "success"}, 204
|
|
||||||
|
|
||||||
|
|
||||||
class DatasetInitApi(Resource):
|
class DatasetInitApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@ -336,17 +311,14 @@ class DatasetInitApi(Resource):
|
|||||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
# 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:
|
if not current_user.is_dataset_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
knowledge_config = KnowledgeConfig(**args)
|
|
||||||
if knowledge_config.indexing_technique == "high_quality":
|
if args["indexing_technique"] == "high_quality":
|
||||||
if knowledge_config.embedding_model is None or knowledge_config.embedding_model_provider is None:
|
if args["embedding_model"] is None or args["embedding_model_provider"] is None:
|
||||||
raise ValueError("embedding model and embedding model provider are required for high quality indexing.")
|
raise ValueError("embedding model and embedding model provider are required for high quality indexing.")
|
||||||
try:
|
try:
|
||||||
model_manager = ModelManager()
|
model_manager = ModelManager()
|
||||||
model_manager.get_model_instance(
|
model_manager.get_default_model_instance(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id, model_type=ModelType.TEXT_EMBEDDING
|
||||||
provider=args["embedding_model_provider"],
|
|
||||||
model_type=ModelType.TEXT_EMBEDDING,
|
|
||||||
model=args["embedding_model"],
|
|
||||||
)
|
)
|
||||||
except InvokeAuthorizationError:
|
except InvokeAuthorizationError:
|
||||||
raise ProviderNotInitializeError(
|
raise ProviderNotInitializeError(
|
||||||
@ -357,11 +329,11 @@ class DatasetInitApi(Resource):
|
|||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
|
||||||
# validate args
|
# validate args
|
||||||
DocumentService.document_create_args_validate(knowledge_config)
|
DocumentService.document_create_args_validate(args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataset, documents, batch = DocumentService.save_document_without_dataset_id(
|
dataset, documents, batch = DocumentService.save_document_without_dataset_id(
|
||||||
tenant_id=current_user.current_tenant_id, knowledge_config=knowledge_config, account=current_user
|
tenant_id=current_user.current_tenant_id, document_data=args, account=current_user
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@ -414,7 +386,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
|
|||||||
indexing_runner = IndexingRunner()
|
indexing_runner = IndexingRunner()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
estimate_response = indexing_runner.indexing_estimate(
|
response = indexing_runner.indexing_estimate(
|
||||||
current_user.current_tenant_id,
|
current_user.current_tenant_id,
|
||||||
[extract_setting],
|
[extract_setting],
|
||||||
data_process_rule_dict,
|
data_process_rule_dict,
|
||||||
@ -422,7 +394,6 @@ class DocumentIndexingEstimateApi(DocumentResource):
|
|||||||
"English",
|
"English",
|
||||||
dataset_id,
|
dataset_id,
|
||||||
)
|
)
|
||||||
return estimate_response.model_dump(), 200
|
|
||||||
except LLMBadRequestError:
|
except LLMBadRequestError:
|
||||||
raise ProviderNotInitializeError(
|
raise ProviderNotInitializeError(
|
||||||
"No Embedding Model available. Please configure a valid provider "
|
"No Embedding Model available. Please configure a valid provider "
|
||||||
@ -433,7 +404,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IndexingEstimateError(str(e))
|
raise IndexingEstimateError(str(e))
|
||||||
|
|
||||||
return response, 200
|
return response
|
||||||
|
|
||||||
|
|
||||||
class DocumentBatchIndexingEstimateApi(DocumentResource):
|
class DocumentBatchIndexingEstimateApi(DocumentResource):
|
||||||
@ -444,8 +415,9 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
|
|||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
batch = str(batch)
|
batch = str(batch)
|
||||||
documents = self.get_batch_documents(dataset_id, batch)
|
documents = self.get_batch_documents(dataset_id, batch)
|
||||||
|
response = {"tokens": 0, "total_price": 0, "currency": "USD", "total_segments": 0, "preview": []}
|
||||||
if not documents:
|
if not documents:
|
||||||
return {"tokens": 0, "total_price": 0, "currency": "USD", "total_segments": 0, "preview": []}, 200
|
return response
|
||||||
data_process_rule = documents[0].dataset_process_rule
|
data_process_rule = documents[0].dataset_process_rule
|
||||||
data_process_rule_dict = data_process_rule.to_dict()
|
data_process_rule_dict = data_process_rule.to_dict()
|
||||||
info_list = []
|
info_list = []
|
||||||
@ -523,7 +495,6 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
|
|||||||
"English",
|
"English",
|
||||||
dataset_id,
|
dataset_id,
|
||||||
)
|
)
|
||||||
return response.model_dump(), 200
|
|
||||||
except LLMBadRequestError:
|
except LLMBadRequestError:
|
||||||
raise ProviderNotInitializeError(
|
raise ProviderNotInitializeError(
|
||||||
"No Embedding Model available. Please configure a valid provider "
|
"No Embedding Model available. Please configure a valid provider "
|
||||||
@ -533,6 +504,7 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
|
|||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IndexingEstimateError(str(e))
|
raise IndexingEstimateError(str(e))
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class DocumentBatchIndexingStatusApi(DocumentResource):
|
class DocumentBatchIndexingStatusApi(DocumentResource):
|
||||||
@ -605,8 +577,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
if metadata == "only":
|
if metadata == "only":
|
||||||
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata}
|
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata}
|
||||||
elif metadata == "without":
|
elif metadata == "without":
|
||||||
dataset_process_rules = DatasetService.get_process_rules(dataset_id)
|
process_rules = DatasetService.get_process_rules(dataset_id)
|
||||||
document_process_rules = document.dataset_process_rule.to_dict()
|
|
||||||
data_source_info = document.data_source_detail_dict
|
data_source_info = document.data_source_detail_dict
|
||||||
response = {
|
response = {
|
||||||
"id": document.id,
|
"id": document.id,
|
||||||
@ -614,8 +585,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
"data_source_type": document.data_source_type,
|
"data_source_type": document.data_source_type,
|
||||||
"data_source_info": data_source_info,
|
"data_source_info": data_source_info,
|
||||||
"dataset_process_rule_id": document.dataset_process_rule_id,
|
"dataset_process_rule_id": document.dataset_process_rule_id,
|
||||||
"dataset_process_rule": dataset_process_rules,
|
"dataset_process_rule": process_rules,
|
||||||
"document_process_rule": document_process_rules,
|
|
||||||
"name": document.name,
|
"name": document.name,
|
||||||
"created_from": document.created_from,
|
"created_from": document.created_from,
|
||||||
"created_by": document.created_by,
|
"created_by": document.created_by,
|
||||||
@ -638,8 +608,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
"doc_language": document.doc_language,
|
"doc_language": document.doc_language,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
dataset_process_rules = DatasetService.get_process_rules(dataset_id)
|
process_rules = DatasetService.get_process_rules(dataset_id)
|
||||||
document_process_rules = document.dataset_process_rule.to_dict()
|
|
||||||
data_source_info = document.data_source_detail_dict
|
data_source_info = document.data_source_detail_dict
|
||||||
response = {
|
response = {
|
||||||
"id": document.id,
|
"id": document.id,
|
||||||
@ -647,8 +616,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
"data_source_type": document.data_source_type,
|
"data_source_type": document.data_source_type,
|
||||||
"data_source_info": data_source_info,
|
"data_source_info": data_source_info,
|
||||||
"dataset_process_rule_id": document.dataset_process_rule_id,
|
"dataset_process_rule_id": document.dataset_process_rule_id,
|
||||||
"dataset_process_rule": dataset_process_rules,
|
"dataset_process_rule": process_rules,
|
||||||
"document_process_rule": document_process_rules,
|
|
||||||
"name": document.name,
|
"name": document.name,
|
||||||
"created_from": document.created_from,
|
"created_from": document.created_from,
|
||||||
"created_by": document.created_by,
|
"created_by": document.created_by,
|
||||||
@ -694,7 +662,7 @@ class DocumentProcessingApi(DocumentResource):
|
|||||||
raise InvalidActionError("Document not in indexing state.")
|
raise InvalidActionError("Document not in indexing state.")
|
||||||
|
|
||||||
document.paused_by = current_user.id
|
document.paused_by = current_user.id
|
||||||
document.paused_at = datetime.now(UTC).replace(tzinfo=None)
|
document.paused_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
document.is_paused = True
|
document.is_paused = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -761,7 +729,8 @@ class DocumentMetadataApi(DocumentResource):
|
|||||||
|
|
||||||
if not isinstance(doc_metadata, dict):
|
if not isinstance(doc_metadata, dict):
|
||||||
raise ValueError("doc_metadata must be a dictionary.")
|
raise ValueError("doc_metadata must be a dictionary.")
|
||||||
metadata_schema: dict = cast(dict, DocumentService.DOCUMENT_METADATA_SCHEMA[doc_type])
|
|
||||||
|
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[doc_type]
|
||||||
|
|
||||||
document.doc_metadata = {}
|
document.doc_metadata = {}
|
||||||
if doc_type == "others":
|
if doc_type == "others":
|
||||||
@ -773,7 +742,7 @@ class DocumentMetadataApi(DocumentResource):
|
|||||||
document.doc_metadata[key] = value
|
document.doc_metadata[key] = value
|
||||||
|
|
||||||
document.doc_type = doc_type
|
document.doc_type = doc_type
|
||||||
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return {"result": "success", "message": "Document metadata updated."}, 200
|
return {"result": "success", "message": "Document metadata updated."}, 200
|
||||||
@ -784,8 +753,9 @@ class DocumentStatusApi(DocumentResource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@cloud_edition_billing_resource_check("vector_space")
|
@cloud_edition_billing_resource_check("vector_space")
|
||||||
def patch(self, dataset_id, action):
|
def patch(self, dataset_id, document_id, action):
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
|
document_id = str(document_id)
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
if dataset is None:
|
if dataset is None:
|
||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
@ -800,79 +770,84 @@ class DocumentStatusApi(DocumentResource):
|
|||||||
# check user's permission
|
# check user's permission
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
document_ids = request.args.getlist("document_id")
|
document = self.get_document(dataset_id, document_id)
|
||||||
for document_id in document_ids:
|
|
||||||
document = self.get_document(dataset_id, document_id)
|
|
||||||
|
|
||||||
indexing_cache_key = "document_{}_indexing".format(document.id)
|
indexing_cache_key = "document_{}_indexing".format(document.id)
|
||||||
cache_result = redis_client.get(indexing_cache_key)
|
cache_result = redis_client.get(indexing_cache_key)
|
||||||
if cache_result is not None:
|
if cache_result is not None:
|
||||||
raise InvalidActionError(f"Document:{document.name} is being indexed, please try again later")
|
raise InvalidActionError("Document is being indexed, please try again later")
|
||||||
|
|
||||||
if action == "enable":
|
if action == "enable":
|
||||||
if document.enabled:
|
if document.enabled:
|
||||||
continue
|
raise InvalidActionError("Document already enabled.")
|
||||||
document.enabled = True
|
|
||||||
document.disabled_at = None
|
|
||||||
document.disabled_by = None
|
|
||||||
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Set cache to prevent indexing the same document multiple times
|
document.enabled = True
|
||||||
redis_client.setex(indexing_cache_key, 600, 1)
|
document.disabled_at = None
|
||||||
|
document.disabled_by = None
|
||||||
|
document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
add_document_to_index_task.delay(document_id)
|
# Set cache to prevent indexing the same document multiple times
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
|
|
||||||
elif action == "disable":
|
add_document_to_index_task.delay(document_id)
|
||||||
if not document.completed_at or document.indexing_status != "completed":
|
|
||||||
raise InvalidActionError(f"Document: {document.name} is not completed.")
|
|
||||||
if not document.enabled:
|
|
||||||
continue
|
|
||||||
|
|
||||||
document.enabled = False
|
return {"result": "success"}, 200
|
||||||
document.disabled_at = datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
document.disabled_by = current_user.id
|
|
||||||
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
elif action == "disable":
|
||||||
|
if not document.completed_at or document.indexing_status != "completed":
|
||||||
|
raise InvalidActionError("Document is not completed.")
|
||||||
|
if not document.enabled:
|
||||||
|
raise InvalidActionError("Document already disabled.")
|
||||||
|
|
||||||
|
document.enabled = False
|
||||||
|
document.disabled_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
document.disabled_by = current_user.id
|
||||||
|
document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Set cache to prevent indexing the same document multiple times
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
|
|
||||||
|
remove_document_from_index_task.delay(document_id)
|
||||||
|
|
||||||
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
elif action == "archive":
|
||||||
|
if document.archived:
|
||||||
|
raise InvalidActionError("Document already archived.")
|
||||||
|
|
||||||
|
document.archived = True
|
||||||
|
document.archived_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
document.archived_by = current_user.id
|
||||||
|
document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if document.enabled:
|
||||||
# Set cache to prevent indexing the same document multiple times
|
# Set cache to prevent indexing the same document multiple times
|
||||||
redis_client.setex(indexing_cache_key, 600, 1)
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
|
|
||||||
remove_document_from_index_task.delay(document_id)
|
remove_document_from_index_task.delay(document_id)
|
||||||
|
|
||||||
elif action == "archive":
|
return {"result": "success"}, 200
|
||||||
if document.archived:
|
elif action == "un_archive":
|
||||||
continue
|
if not document.archived:
|
||||||
|
raise InvalidActionError("Document is not archived.")
|
||||||
|
|
||||||
document.archived = True
|
document.archived = False
|
||||||
document.archived_at = datetime.now(UTC).replace(tzinfo=None)
|
document.archived_at = None
|
||||||
document.archived_by = current_user.id
|
document.archived_by = None
|
||||||
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
document.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if document.enabled:
|
# Set cache to prevent indexing the same document multiple times
|
||||||
# Set cache to prevent indexing the same document multiple times
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
redis_client.setex(indexing_cache_key, 600, 1)
|
|
||||||
|
|
||||||
remove_document_from_index_task.delay(document_id)
|
add_document_to_index_task.delay(document_id)
|
||||||
|
|
||||||
elif action == "un_archive":
|
return {"result": "success"}, 200
|
||||||
if not document.archived:
|
else:
|
||||||
continue
|
raise InvalidActionError()
|
||||||
document.archived = False
|
|
||||||
document.archived_at = None
|
|
||||||
document.archived_by = None
|
|
||||||
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Set cache to prevent indexing the same document multiple times
|
|
||||||
redis_client.setex(indexing_cache_key, 600, 1)
|
|
||||||
|
|
||||||
add_document_to_index_task.delay(document_id)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidActionError()
|
|
||||||
return {"result": "success"}, 200
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPauseApi(DocumentResource):
|
class DocumentPauseApi(DocumentResource):
|
||||||
@ -969,8 +944,8 @@ class DocumentRetryApi(DocumentResource):
|
|||||||
if document.indexing_status == "completed":
|
if document.indexing_status == "completed":
|
||||||
raise DocumentAlreadyFinishedError()
|
raise DocumentAlreadyFinishedError()
|
||||||
retry_documents.append(document)
|
retry_documents.append(document)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logging.exception(f"Failed to retry document, document id: {document_id}")
|
logging.error(f"Document {document_id} retry failed: {str(e)}")
|
||||||
continue
|
continue
|
||||||
# retry document
|
# retry document
|
||||||
DocumentService.retry_document(dataset_id, retry_documents)
|
DocumentService.retry_document(dataset_id, retry_documents)
|
||||||
@ -1043,7 +1018,7 @@ api.add_resource(
|
|||||||
)
|
)
|
||||||
api.add_resource(DocumentDeleteApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
api.add_resource(DocumentDeleteApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
||||||
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
||||||
api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/status/<string:action>/batch")
|
api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/status/<string:action>")
|
||||||
api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
|
api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
|
||||||
api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
|
api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
|
||||||
api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")
|
api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal, reqparse # type: ignore
|
from flask_restful import Resource, marshal, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import ProviderNotInitializeError
|
from controllers.console.app.error import ProviderNotInitializeError
|
||||||
from controllers.console.datasets.error import (
|
from controllers.console.datasets.error import InvalidActionError, NoFileUploadedError, TooManyFilesError
|
||||||
ChildChunkDeleteIndexError,
|
|
||||||
ChildChunkIndexingError,
|
|
||||||
InvalidActionError,
|
|
||||||
NoFileUploadedError,
|
|
||||||
TooManyFilesError,
|
|
||||||
)
|
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_knowledge_limit_check,
|
cloud_edition_billing_knowledge_limit_check,
|
||||||
@ -25,15 +20,15 @@ from controllers.console.wraps import (
|
|||||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||||
from core.model_manager import ModelManager
|
from core.model_manager import ModelManager
|
||||||
from core.model_runtime.entities.model_entities import ModelType
|
from core.model_runtime.entities.model_entities import ModelType
|
||||||
|
from extensions.ext_database import db
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from fields.segment_fields import child_chunk_fields, segment_fields
|
from fields.segment_fields import segment_fields
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models.dataset import ChildChunk, DocumentSegment
|
from models import DocumentSegment
|
||||||
from services.dataset_service import DatasetService, DocumentService, SegmentService
|
from services.dataset_service import DatasetService, DocumentService, SegmentService
|
||||||
from services.entities.knowledge_entities.knowledge_entities import ChildChunkUpdateArgs, SegmentUpdateArgs
|
|
||||||
from services.errors.chunk import ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError
|
|
||||||
from services.errors.chunk import ChildChunkIndexingError as ChildChunkIndexingServiceError
|
|
||||||
from tasks.batch_create_segment_to_index_task import batch_create_segment_to_index_task
|
from tasks.batch_create_segment_to_index_task import batch_create_segment_to_index_task
|
||||||
|
from tasks.disable_segment_from_index_task import disable_segment_from_index_task
|
||||||
|
from tasks.enable_segment_to_index_task import enable_segment_to_index_task
|
||||||
|
|
||||||
|
|
||||||
class DatasetDocumentSegmentListApi(Resource):
|
class DatasetDocumentSegmentListApi(Resource):
|
||||||
@ -58,16 +53,15 @@ class DatasetDocumentSegmentListApi(Resource):
|
|||||||
raise NotFound("Document not found.")
|
raise NotFound("Document not found.")
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("last_id", type=str, default=None, location="args")
|
||||||
parser.add_argument("limit", type=int, default=20, location="args")
|
parser.add_argument("limit", type=int, default=20, location="args")
|
||||||
parser.add_argument("status", type=str, action="append", default=[], location="args")
|
parser.add_argument("status", type=str, action="append", default=[], location="args")
|
||||||
parser.add_argument("hit_count_gte", type=int, default=None, location="args")
|
parser.add_argument("hit_count_gte", type=int, default=None, location="args")
|
||||||
parser.add_argument("enabled", type=str, default="all", location="args")
|
parser.add_argument("enabled", type=str, default="all", location="args")
|
||||||
parser.add_argument("keyword", type=str, default=None, location="args")
|
parser.add_argument("keyword", type=str, default=None, location="args")
|
||||||
parser.add_argument("page", type=int, default=1, location="args")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
page = args["page"]
|
last_id = args["last_id"]
|
||||||
limit = min(args["limit"], 100)
|
limit = min(args["limit"], 100)
|
||||||
status_list = args["status"]
|
status_list = args["status"]
|
||||||
hit_count_gte = args["hit_count_gte"]
|
hit_count_gte = args["hit_count_gte"]
|
||||||
@ -75,7 +69,14 @@ class DatasetDocumentSegmentListApi(Resource):
|
|||||||
|
|
||||||
query = DocumentSegment.query.filter(
|
query = DocumentSegment.query.filter(
|
||||||
DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
||||||
).order_by(DocumentSegment.position.asc())
|
)
|
||||||
|
|
||||||
|
if last_id is not None:
|
||||||
|
last_segment = db.session.get(DocumentSegment, str(last_id))
|
||||||
|
if last_segment:
|
||||||
|
query = query.filter(DocumentSegment.position > last_segment.position)
|
||||||
|
else:
|
||||||
|
return {"data": [], "has_more": False, "limit": limit}, 200
|
||||||
|
|
||||||
if status_list:
|
if status_list:
|
||||||
query = query.filter(DocumentSegment.status.in_(status_list))
|
query = query.filter(DocumentSegment.status.in_(status_list))
|
||||||
@ -92,44 +93,21 @@ class DatasetDocumentSegmentListApi(Resource):
|
|||||||
elif args["enabled"].lower() == "false":
|
elif args["enabled"].lower() == "false":
|
||||||
query = query.filter(DocumentSegment.enabled == False)
|
query = query.filter(DocumentSegment.enabled == False)
|
||||||
|
|
||||||
segments = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
|
total = query.count()
|
||||||
|
segments = query.order_by(DocumentSegment.position).limit(limit + 1).all()
|
||||||
|
|
||||||
response = {
|
has_more = False
|
||||||
"data": marshal(segments.items, segment_fields),
|
if len(segments) > limit:
|
||||||
|
has_more = True
|
||||||
|
segments = segments[:-1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": marshal(segments, segment_fields),
|
||||||
|
"doc_form": document.doc_form,
|
||||||
|
"has_more": has_more,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"total": segments.total,
|
"total": total,
|
||||||
"total_pages": segments.pages,
|
}, 200
|
||||||
"page": page,
|
|
||||||
}
|
|
||||||
return response, 200
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def delete(self, dataset_id, document_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
segment_ids = request.args.getlist("segment_id")
|
|
||||||
|
|
||||||
# The role of the current user in the ta table must be admin or owner
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
try:
|
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
|
||||||
except services.errors.account.NoPermissionError as e:
|
|
||||||
raise Forbidden(str(e))
|
|
||||||
SegmentService.delete_segments(segment_ids, document, dataset)
|
|
||||||
return {"result": "success"}, 200
|
|
||||||
|
|
||||||
|
|
||||||
class DatasetDocumentSegmentApi(Resource):
|
class DatasetDocumentSegmentApi(Resource):
|
||||||
@ -137,15 +115,11 @@ class DatasetDocumentSegmentApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@cloud_edition_billing_resource_check("vector_space")
|
@cloud_edition_billing_resource_check("vector_space")
|
||||||
def patch(self, dataset_id, document_id, action):
|
def patch(self, dataset_id, segment_id, action):
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
if not dataset:
|
if not dataset:
|
||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check user's model setting
|
# check user's model setting
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
DatasetService.check_dataset_model_setting(dataset)
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
@ -173,17 +147,59 @@ class DatasetDocumentSegmentApi(Resource):
|
|||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
segment_ids = request.args.getlist("segment_id")
|
|
||||||
|
|
||||||
document_indexing_cache_key = "document_{}_indexing".format(document.id)
|
segment = DocumentSegment.query.filter(
|
||||||
|
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not segment:
|
||||||
|
raise NotFound("Segment not found.")
|
||||||
|
|
||||||
|
if segment.status != "completed":
|
||||||
|
raise NotFound("Segment is not completed, enable or disable function is not allowed")
|
||||||
|
|
||||||
|
document_indexing_cache_key = "document_{}_indexing".format(segment.document_id)
|
||||||
cache_result = redis_client.get(document_indexing_cache_key)
|
cache_result = redis_client.get(document_indexing_cache_key)
|
||||||
if cache_result is not None:
|
if cache_result is not None:
|
||||||
raise InvalidActionError("Document is being indexed, please try again later")
|
raise InvalidActionError("Document is being indexed, please try again later")
|
||||||
try:
|
|
||||||
SegmentService.update_segments_status(segment_ids, action, dataset, document)
|
indexing_cache_key = "segment_{}_indexing".format(segment.id)
|
||||||
except Exception as e:
|
cache_result = redis_client.get(indexing_cache_key)
|
||||||
raise InvalidActionError(str(e))
|
if cache_result is not None:
|
||||||
return {"result": "success"}, 200
|
raise InvalidActionError("Segment is being indexed, please try again later")
|
||||||
|
|
||||||
|
if action == "enable":
|
||||||
|
if segment.enabled:
|
||||||
|
raise InvalidActionError("Segment is already enabled.")
|
||||||
|
|
||||||
|
segment.enabled = True
|
||||||
|
segment.disabled_at = None
|
||||||
|
segment.disabled_by = None
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Set cache to prevent indexing the same segment multiple times
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
|
|
||||||
|
enable_segment_to_index_task.delay(segment.id)
|
||||||
|
|
||||||
|
return {"result": "success"}, 200
|
||||||
|
elif action == "disable":
|
||||||
|
if not segment.enabled:
|
||||||
|
raise InvalidActionError("Segment is already disabled.")
|
||||||
|
|
||||||
|
segment.enabled = False
|
||||||
|
segment.disabled_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
segment.disabled_by = current_user.id
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Set cache to prevent indexing the same segment multiple times
|
||||||
|
redis_client.setex(indexing_cache_key, 600, 1)
|
||||||
|
|
||||||
|
disable_segment_from_index_task.delay(segment.id)
|
||||||
|
|
||||||
|
return {"result": "success"}, 200
|
||||||
|
else:
|
||||||
|
raise InvalidActionError()
|
||||||
|
|
||||||
|
|
||||||
class DatasetDocumentSegmentAddApi(Resource):
|
class DatasetDocumentSegmentAddApi(Resource):
|
||||||
@ -291,12 +307,9 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
|||||||
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
|
||||||
parser.add_argument("answer", type=str, required=False, nullable=True, location="json")
|
parser.add_argument("answer", type=str, required=False, nullable=True, location="json")
|
||||||
parser.add_argument("keywords", type=list, required=False, nullable=True, location="json")
|
parser.add_argument("keywords", type=list, required=False, nullable=True, location="json")
|
||||||
parser.add_argument(
|
|
||||||
"regenerate_child_chunks", type=bool, required=False, nullable=True, default=False, location="json"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
SegmentService.segment_create_args_validate(args, document)
|
SegmentService.segment_create_args_validate(args, document)
|
||||||
segment = SegmentService.update_segment(SegmentUpdateArgs(**args), segment, document, dataset)
|
segment = SegmentService.update_segment(args, segment, document, dataset)
|
||||||
return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200
|
return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@ -399,248 +412,8 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
|||||||
return {"job_id": job_id, "job_status": cache_result.decode()}, 200
|
return {"job_id": job_id, "job_status": cache_result.decode()}, 200
|
||||||
|
|
||||||
|
|
||||||
class ChildChunkAddApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@cloud_edition_billing_resource_check("vector_space")
|
|
||||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
|
||||||
def post(self, dataset_id, document_id, segment_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check segment
|
|
||||||
segment_id = str(segment_id)
|
|
||||||
segment = DocumentSegment.query.filter(
|
|
||||||
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not segment:
|
|
||||||
raise NotFound("Segment not found.")
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
# check embedding model setting
|
|
||||||
if dataset.indexing_technique == "high_quality":
|
|
||||||
try:
|
|
||||||
model_manager = ModelManager()
|
|
||||||
model_manager.get_model_instance(
|
|
||||||
tenant_id=current_user.current_tenant_id,
|
|
||||||
provider=dataset.embedding_model_provider,
|
|
||||||
model_type=ModelType.TEXT_EMBEDDING,
|
|
||||||
model=dataset.embedding_model,
|
|
||||||
)
|
|
||||||
except LLMBadRequestError:
|
|
||||||
raise ProviderNotInitializeError(
|
|
||||||
"No Embedding Model available. Please configure a valid provider "
|
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
|
||||||
except ProviderTokenNotInitError as ex:
|
|
||||||
raise ProviderNotInitializeError(ex.description)
|
|
||||||
try:
|
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
|
||||||
except services.errors.account.NoPermissionError as e:
|
|
||||||
raise Forbidden(str(e))
|
|
||||||
# validate args
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
try:
|
|
||||||
child_chunk = SegmentService.create_child_chunk(args.get("content"), segment, document, dataset)
|
|
||||||
except ChildChunkIndexingServiceError as e:
|
|
||||||
raise ChildChunkIndexingError(str(e))
|
|
||||||
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def get(self, dataset_id, document_id, segment_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check segment
|
|
||||||
segment_id = str(segment_id)
|
|
||||||
segment = DocumentSegment.query.filter(
|
|
||||||
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not segment:
|
|
||||||
raise NotFound("Segment not found.")
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("limit", type=int, default=20, location="args")
|
|
||||||
parser.add_argument("keyword", type=str, default=None, location="args")
|
|
||||||
parser.add_argument("page", type=int, default=1, location="args")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
page = args["page"]
|
|
||||||
limit = min(args["limit"], 100)
|
|
||||||
keyword = args["keyword"]
|
|
||||||
|
|
||||||
child_chunks = SegmentService.get_child_chunks(segment_id, document_id, dataset_id, page, limit, keyword)
|
|
||||||
return {
|
|
||||||
"data": marshal(child_chunks.items, child_chunk_fields),
|
|
||||||
"total": child_chunks.total,
|
|
||||||
"total_pages": child_chunks.pages,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
}, 200
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@cloud_edition_billing_resource_check("vector_space")
|
|
||||||
def patch(self, dataset_id, document_id, segment_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check segment
|
|
||||||
segment_id = str(segment_id)
|
|
||||||
segment = DocumentSegment.query.filter(
|
|
||||||
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not segment:
|
|
||||||
raise NotFound("Segment not found.")
|
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
try:
|
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
|
||||||
except services.errors.account.NoPermissionError as e:
|
|
||||||
raise Forbidden(str(e))
|
|
||||||
# validate args
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("chunks", type=list, required=True, nullable=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
try:
|
|
||||||
chunks = [ChildChunkUpdateArgs(**chunk) for chunk in args.get("chunks")]
|
|
||||||
child_chunks = SegmentService.update_child_chunks(chunks, segment, document, dataset)
|
|
||||||
except ChildChunkIndexingServiceError as e:
|
|
||||||
raise ChildChunkIndexingError(str(e))
|
|
||||||
return {"data": marshal(child_chunks, child_chunk_fields)}, 200
|
|
||||||
|
|
||||||
|
|
||||||
class ChildChunkUpdateApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check segment
|
|
||||||
segment_id = str(segment_id)
|
|
||||||
segment = DocumentSegment.query.filter(
|
|
||||||
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not segment:
|
|
||||||
raise NotFound("Segment not found.")
|
|
||||||
# check child chunk
|
|
||||||
child_chunk_id = str(child_chunk_id)
|
|
||||||
child_chunk = ChildChunk.query.filter(
|
|
||||||
ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not child_chunk:
|
|
||||||
raise NotFound("Child chunk not found.")
|
|
||||||
# The role of the current user in the ta table must be admin or owner
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
try:
|
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
|
||||||
except services.errors.account.NoPermissionError as e:
|
|
||||||
raise Forbidden(str(e))
|
|
||||||
try:
|
|
||||||
SegmentService.delete_child_chunk(child_chunk, dataset)
|
|
||||||
except ChildChunkDeleteIndexServiceError as e:
|
|
||||||
raise ChildChunkDeleteIndexError(str(e))
|
|
||||||
return {"result": "success"}, 200
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@cloud_edition_billing_resource_check("vector_space")
|
|
||||||
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check user's model setting
|
|
||||||
DatasetService.check_dataset_model_setting(dataset)
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset_id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check segment
|
|
||||||
segment_id = str(segment_id)
|
|
||||||
segment = DocumentSegment.query.filter(
|
|
||||||
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not segment:
|
|
||||||
raise NotFound("Segment not found.")
|
|
||||||
# check child chunk
|
|
||||||
child_chunk_id = str(child_chunk_id)
|
|
||||||
child_chunk = ChildChunk.query.filter(
|
|
||||||
ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
|
|
||||||
).first()
|
|
||||||
if not child_chunk:
|
|
||||||
raise NotFound("Child chunk not found.")
|
|
||||||
# The role of the current user in the ta table must be admin or owner
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
try:
|
|
||||||
DatasetService.check_dataset_permission(dataset, current_user)
|
|
||||||
except services.errors.account.NoPermissionError as e:
|
|
||||||
raise Forbidden(str(e))
|
|
||||||
# validate args
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
try:
|
|
||||||
child_chunk = SegmentService.update_child_chunk(
|
|
||||||
args.get("content"), child_chunk, segment, document, dataset
|
|
||||||
)
|
|
||||||
except ChildChunkIndexingServiceError as e:
|
|
||||||
raise ChildChunkIndexingError(str(e))
|
|
||||||
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DatasetDocumentSegmentListApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments")
|
api.add_resource(DatasetDocumentSegmentListApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments")
|
||||||
api.add_resource(
|
api.add_resource(DatasetDocumentSegmentApi, "/datasets/<uuid:dataset_id>/segments/<uuid:segment_id>/<string:action>")
|
||||||
DatasetDocumentSegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment/<string:action>"
|
|
||||||
)
|
|
||||||
api.add_resource(DatasetDocumentSegmentAddApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment")
|
api.add_resource(DatasetDocumentSegmentAddApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment")
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
DatasetDocumentSegmentUpdateApi,
|
DatasetDocumentSegmentUpdateApi,
|
||||||
@ -651,11 +424,3 @@ api.add_resource(
|
|||||||
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/batch_import",
|
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/batch_import",
|
||||||
"/datasets/batch_import_status/<uuid:job_id>",
|
"/datasets/batch_import_status/<uuid:job_id>",
|
||||||
)
|
)
|
||||||
api.add_resource(
|
|
||||||
ChildChunkAddApi,
|
|
||||||
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ChildChunkUpdateApi,
|
|
||||||
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks/<uuid:child_chunk_id>",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -89,15 +89,3 @@ class IndexingEstimateError(BaseHTTPException):
|
|||||||
error_code = "indexing_estimate_error"
|
error_code = "indexing_estimate_error"
|
||||||
description = "Knowledge indexing estimate failed: {message}"
|
description = "Knowledge indexing estimate failed: {message}"
|
||||||
code = 500
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
class ChildChunkIndexingError(BaseHTTPException):
|
|
||||||
error_code = "child_chunk_indexing_error"
|
|
||||||
description = "Create child chunk index failed: {message}"
|
|
||||||
code = 500
|
|
||||||
|
|
||||||
|
|
||||||
class ChildChunkDeleteIndexError(BaseHTTPException):
|
|
||||||
error_code = "child_chunk_delete_index_error"
|
|
||||||
description = "Delete child chunk index failed: {message}"
|
|
||||||
code = 500
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, marshal, reqparse # type: ignore
|
from flask_restful import Resource, marshal, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user