mirror of
https://github.com/langgenius/dify.git
synced 2026-02-10 02:03:59 +00:00
Compare commits
370 Commits
fix/versio
...
dev/plugin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086aeea181 | ||
|
|
1d7c4a87d0 | ||
|
|
9042b368e9 | ||
|
|
f1bcd26c69 | ||
|
|
3dcd8b6330 | ||
|
|
10c088029c | ||
|
|
73b1adf862 | ||
|
|
ae76dbd92c | ||
|
|
782df0c383 | ||
|
|
089207240e | ||
|
|
53d30d537f | ||
|
|
53512a4650 | ||
|
|
1fb7dcda24 | ||
|
|
3c3e0a35f4 | ||
|
|
202a246e83 | ||
|
|
08b968eca5 | ||
|
|
b1ac71db3e | ||
|
|
55405c1a26 | ||
|
|
779770dae5 | ||
|
|
002b16e1c6 | ||
|
|
7710d8e83b | ||
|
|
cf75fcdffc | ||
|
|
6e8601b52c | ||
|
|
96cf0ed5af | ||
|
|
ddf9eb1f9a | ||
|
|
46a798bea8 | ||
|
|
bb4fecf3d1 | ||
|
|
9e258c495d | ||
|
|
4fbe52da40 | ||
|
|
1e3197a1ea | ||
|
|
5f692dfce2 | ||
|
|
78a7d7fa21 | ||
|
|
a9dda1554e | ||
|
|
c53786d229 | ||
|
|
17f23f4798 | ||
|
|
67f2c766bc | ||
|
|
9a417bfc5e | ||
|
|
90bc51ed2e | ||
|
|
02dc835721 | ||
|
|
a05e8f0e37 | ||
|
|
b10cbb9b20 | ||
|
|
1aaab741a0 | ||
|
|
bafa46393c | ||
|
|
45d43c41bc | ||
|
|
e944646541 | ||
|
|
21e1443ed5 | ||
|
|
93a5ffb037 | ||
|
|
d5711589cd | ||
|
|
375a359c97 | ||
|
|
3228bac56d | ||
|
|
c66b4e32db | ||
|
|
57b60dd51f | ||
|
|
ff911d0dc5 | ||
|
|
7a71498a3e | ||
|
|
76bcdc2581 | ||
|
|
91a218b29d | ||
|
|
4a6cbda1b4 | ||
|
|
8c08153e33 | ||
|
|
b44b3866a1 | ||
|
|
c242bb372b | ||
|
|
8c9e34133c | ||
|
|
3403ac361a | ||
|
|
07d6cb3f4a | ||
|
|
545aa61cf4 | ||
|
|
9fb78ce827 | ||
|
|
490b6d092e | ||
|
|
42b13bd312 | ||
|
|
28add22f20 | ||
|
|
ce545274a6 | ||
|
|
aa6c951e8c | ||
|
|
c4f4dfc3fb | ||
|
|
548f6ef2b6 | ||
|
|
b15ff4eb8c | ||
|
|
7790214620 | ||
|
|
3942e45cab | ||
|
|
2ace9ae4e4 | ||
|
|
5ac0ef6253 | ||
|
|
f552667312 | ||
|
|
5669a18bd8 | ||
|
|
a97d73ab05 | ||
|
|
252d2c425b | ||
|
|
09fc4bba61 | ||
|
|
5f995fac32 | ||
|
|
79d4db8541 | ||
|
|
9c42626772 | ||
|
|
bbfe83c86b | ||
|
|
55aa4e424a | ||
|
|
8015f5c0c5 | ||
|
|
f3fe14863d | ||
|
|
d96c368660 | ||
|
|
3f34b8b0d1 | ||
|
|
6a58ea9e56 | ||
|
|
23888398d1 | ||
|
|
bfbc5eb91e | ||
|
|
98b0d4169e | ||
|
|
356cd271b2 | ||
|
|
baf7561cf8 | ||
|
|
b09f22961c | ||
|
|
f3ad3a5dfd | ||
|
|
ee49d321c5 | ||
|
|
f88f9d6970 | ||
|
|
3467ad3d02 | ||
|
|
6741604027 | ||
|
|
35312cf96c | ||
|
|
15f028fe59 | ||
|
|
8a2301af56 | ||
|
|
66747a8eef | ||
|
|
19d413ac1e | ||
|
|
4a332ff1af | ||
|
|
dc942db52f | ||
|
|
f535a2aa71 | ||
|
|
dfdd6dfa20 | ||
|
|
2af81d1ee3 | ||
|
|
ece25bce1a | ||
|
|
6fc234183a | ||
|
|
15a56f705f | ||
|
|
899f7e125f | ||
|
|
aa19bb3f30 | ||
|
|
562852a0ae | ||
|
|
a4b992c1ab | ||
|
|
3460c1dfbd | ||
|
|
653f6c2d46 | ||
|
|
ed7851a4b3 | ||
|
|
cb841e5cde | ||
|
|
4dae0e514e | ||
|
|
363c46ace8 | ||
|
|
abe5aca3e2 | ||
|
|
d2cc502c71 | ||
|
|
bea10b4356 | ||
|
|
f5f83f1924 | ||
|
|
403e2d58b9 | ||
|
|
222df44d21 | ||
|
|
566e548713 | ||
|
|
1434d54e7a | ||
|
|
4229d0f9a7 | ||
|
|
7f9eb35e1f | ||
|
|
ed7d7a74ea | ||
|
|
035e54ba4d | ||
|
|
284707c3a8 | ||
|
|
1f63028a83 | ||
|
|
8a0aa91ed7 | ||
|
|
62079991b7 | ||
|
|
4e7e172ff3 | ||
|
|
33a565a719 | ||
|
|
f0b9257387 | ||
|
|
c398c9cb6a | ||
|
|
a3d3e30e3a | ||
|
|
2b86465d4c | ||
|
|
6529240da6 | ||
|
|
0751ad1eeb | ||
|
|
786550bdc9 | ||
|
|
bde756a1ab | ||
|
|
423fb2d7bc | ||
|
|
f96b4f287a | ||
|
|
c00e7d3f65 | ||
|
|
1f38d4846b | ||
|
|
47a64610ca | ||
|
|
f0a845f0f9 | ||
|
|
abec23118d | ||
|
|
0957119550 | ||
|
|
b88194d1c6 | ||
|
|
2b95e54d54 | ||
|
|
f48fa3e4e8 | ||
|
|
5ffc58d6ca | ||
|
|
7d958635f0 | ||
|
|
33990426c1 | ||
|
|
9f3fc7ebf8 | ||
|
|
c8357da13b | ||
|
|
2290f14fb1 | ||
|
|
7796984444 | ||
|
|
75113c26c6 | ||
|
|
939a9ecd21 | ||
|
|
f307c7cd88 | ||
|
|
33ecceb90c | ||
|
|
e0d1cab079 | ||
|
|
811d72a727 | ||
|
|
c3c575c2e1 | ||
|
|
c189629eca | ||
|
|
37117c22d4 | ||
|
|
b05e9d2ab4 | ||
|
|
0451333990 | ||
|
|
ab2e6c19a4 | ||
|
|
f7959bc887 | ||
|
|
45874c699d | ||
|
|
286cdc41ab | ||
|
|
78708eb5d5 | ||
|
|
cf36745770 | ||
|
|
6622c7f98d | ||
|
|
3112b74527 | ||
|
|
b3ae6b634f | ||
|
|
982bca5d40 | ||
|
|
c8dcde6cd0 | ||
|
|
8f9db61688 | ||
|
|
ebdbaf34e6 | ||
|
|
a081b1e79e | ||
|
|
38c31e64db | ||
|
|
ae6f67420c | ||
|
|
ca19bd31d4 | ||
|
|
413dfd5628 | ||
|
|
f9515901cc | ||
|
|
3f42fabff8 | ||
|
|
9bff9b5c9e | ||
|
|
1caa578771 | ||
|
|
b7c11c1818 | ||
|
|
3eb3db0663 | ||
|
|
be46f32056 | ||
|
|
6e5c915f96 | ||
|
|
3dd2c170e7 | ||
|
|
04d13a8116 | ||
|
|
e638ede3f2 | ||
|
|
2348abe4bf | ||
|
|
f7e7a399d9 | ||
|
|
ba91f34636 | ||
|
|
16865d43a8 | ||
|
|
88f41f164f | ||
|
|
0d13aee15c | ||
|
|
49b4144ffd | ||
|
|
186e2d972e | ||
|
|
40dd63ecef | ||
|
|
6d66d6da15 | ||
|
|
03ec3513f3 | ||
|
|
87763fc234 | ||
|
|
f6c44cae2e | ||
|
|
da2ee04fce | ||
|
|
7673c36af3 | ||
|
|
9457b2af2f | ||
|
|
7203991032 | ||
|
|
5a685f7156 | ||
|
|
a6a25030ad | ||
|
|
00458a31d5 | ||
|
|
c6ddf6d6cc | ||
|
|
34b21b3065 | ||
|
|
8fbb355cd2 | ||
|
|
e8b3b7e578 | ||
|
|
59ca44f493 | ||
|
|
9e1457c2c3 | ||
|
|
cd932519b3 | ||
|
|
fac83e14bc | ||
|
|
a97cec57e4 | ||
|
|
38c10b47d3 | ||
|
|
1a2523fd15 | ||
|
|
03243cb422 | ||
|
|
2ad7ee0344 | ||
|
|
2ff2b08739 | ||
|
|
55ce3618ce | ||
|
|
e9e34c1ab2 | ||
|
|
d4c916b496 | ||
|
|
8fbc9c9342 | ||
|
|
1b6fd9dfe8 | ||
|
|
304467e3f5 | ||
|
|
7452032d81 | ||
|
|
87e2048f1b | ||
|
|
d876084392 | ||
|
|
840729afa5 | ||
|
|
941ad03f3c | ||
|
|
d73d191f99 | ||
|
|
c2664e0283 | ||
|
|
ee61cede4e | ||
|
|
b47669b80b | ||
|
|
c0d0c63592 | ||
|
|
b09c39c8dc | ||
|
|
b4b09ddc3c | ||
|
|
d0a21086bd | ||
|
|
d44882c1b5 | ||
|
|
23c68efa2d | ||
|
|
560c5de1b7 | ||
|
|
5d91dbd000 | ||
|
|
6c31ee36cd | ||
|
|
edc29780ed | ||
|
|
aad7e4dd1c | ||
|
|
a6a727e8a4 | ||
|
|
d1fc65fabc | ||
|
|
d4be5ef9de | ||
|
|
1374be5a31 | ||
|
|
b2bbc28580 | ||
|
|
59b3e672aa | ||
|
|
a2f8bce8f5 | ||
|
|
a2b9adb3a2 | ||
|
|
28067640b5 | ||
|
|
da67916843 | ||
|
|
a4a45421cc | ||
|
|
aafab1b59e | ||
|
|
7f49f96c3f | ||
|
|
5673f03db5 | ||
|
|
e54ce479ad | ||
|
|
278adbc10e | ||
|
|
5d4e517397 | ||
|
|
c2671c16a8 | ||
|
|
6024d8a42d | ||
|
|
10991cbc03 | ||
|
|
f565f08aa0 | ||
|
|
3fcf7e88b0 | ||
|
|
ffa5af1356 | ||
|
|
fd4afe09f8 | ||
|
|
dd0904f95c | ||
|
|
4c3076f2a4 | ||
|
|
1e73f63ff8 | ||
|
|
d167d5b1be | ||
|
|
71fa14f791 | ||
|
|
8dd1873e76 | ||
|
|
f91f5c7401 | ||
|
|
c62b7cc679 | ||
|
|
3ee213ddca | ||
|
|
8429877b02 | ||
|
|
05a0faff6a | ||
|
|
e09f6e4987 | ||
|
|
e23f4b0265 | ||
|
|
f582d4a13e | ||
|
|
2f41bd495d | ||
|
|
162a8c4393 | ||
|
|
3d1ce4c53f | ||
|
|
6db3ae9b8e | ||
|
|
6d0cb9dc33 | ||
|
|
46e95e8309 | ||
|
|
a7b9375877 | ||
|
|
0c6a8a130e | ||
|
|
9903f1e703 | ||
|
|
6fad719e42 | ||
|
|
9aaee8ee47 | ||
|
|
166221d784 | ||
|
|
925d69a2ee | ||
|
|
5ff08e241a | ||
|
|
3defd24087 | ||
|
|
9d86147d20 | ||
|
|
80801ac4ab | ||
|
|
210926cd91 | ||
|
|
677a69deed | ||
|
|
8dfdee21ce | ||
|
|
6ea77ab4cd | ||
|
|
e3c996688d | ||
|
|
066516b54d | ||
|
|
49415e5e7f | ||
|
|
bc3a570dda | ||
|
|
0800021a2d | ||
|
|
435eddd867 | ||
|
|
6e0fb055d1 | ||
|
|
1e9ac7ffeb | ||
|
|
b4873ecb43 | ||
|
|
1859d57784 | ||
|
|
69d58fbb50 | ||
|
|
cb34991663 | ||
|
|
c700364e1c | ||
|
|
9a6b1dc3a1 | ||
|
|
54b5b80a07 | ||
|
|
831459b895 | ||
|
|
4e101604c3 | ||
|
|
a6455269f0 | ||
|
|
cd257b91c5 | ||
|
|
d8f57bf899 | ||
|
|
989fb11fd7 | ||
|
|
140965b738 | ||
|
|
14ee51aead | ||
|
|
2e97ba5700 | ||
|
|
f549d53b68 | ||
|
|
a085ad4719 | ||
|
|
f230a9232e | ||
|
|
e84bf35e2a | ||
|
|
20f090537f | ||
|
|
dbe7a7c4fd | ||
|
|
b7a4e3903e | ||
|
|
a697bbdfa7 | ||
|
|
b4c1c2f731 | ||
|
|
1b940e7daa | ||
|
|
d5c31f8728 | ||
|
|
508005b741 | ||
|
|
4f0ecdbb6e | ||
|
|
ab2e69faef | ||
|
|
e46a3343b8 | ||
|
|
47637da734 | ||
|
|
525bde28f6 |
@@ -1,11 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd web && npm install
|
npm add -g pnpm@9.12.2
|
||||||
|
cd web && pnpm install
|
||||||
pipx install poetry
|
pipx install poetry
|
||||||
|
|
||||||
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
|
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.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-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 && pnpm 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
|
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify down"' >> ~/.bashrc
|
||||||
|
|
||||||
|
|||||||
2
.github/actions/setup-poetry/action.yml
vendored
2
.github/actions/setup-poetry/action.yml
vendored
@@ -8,7 +8,7 @@ inputs:
|
|||||||
poetry-version:
|
poetry-version:
|
||||||
description: Poetry version to set up
|
description: Poetry version to set up
|
||||||
required: true
|
required: true
|
||||||
default: '1.8.4'
|
default: '2.0.1'
|
||||||
poetry-lockfile:
|
poetry-lockfile:
|
||||||
description: Path to the Poetry lockfile to restore cache from
|
description: Path to the Poetry lockfile to restore cache from
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
21
.github/workflows/api-tests.yml
vendored
21
.github/workflows/api-tests.yml
vendored
@@ -26,6 +26,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
||||||
uses: ./.github/actions/setup-poetry
|
uses: ./.github/actions/setup-poetry
|
||||||
@@ -42,25 +45,17 @@ jobs:
|
|||||||
run: poetry install -C api --with dev
|
run: poetry install -C api --with dev
|
||||||
|
|
||||||
- name: Check dependencies in pyproject.toml
|
- name: Check dependencies in pyproject.toml
|
||||||
run: poetry run -C api bash dev/pytest/pytest_artifacts.sh
|
run: poetry run -P api bash dev/pytest/pytest_artifacts.sh
|
||||||
|
|
||||||
- name: Run Unit tests
|
- name: Run Unit tests
|
||||||
run: poetry run -C api bash dev/pytest/pytest_unit_tests.sh
|
run: poetry run -P api bash dev/pytest/pytest_unit_tests.sh
|
||||||
|
|
||||||
- name: Run ModelRuntime
|
|
||||||
run: poetry run -C api bash dev/pytest/pytest_model_runtime.sh
|
|
||||||
|
|
||||||
- name: Run dify config tests
|
- name: Run dify config tests
|
||||||
run: poetry run -C api python dev/pytest/pytest_config_tests.py
|
run: poetry run -P api python dev/pytest/pytest_config_tests.py
|
||||||
|
|
||||||
- name: Run Tool
|
|
||||||
run: poetry run -C api bash dev/pytest/pytest_tools.sh
|
|
||||||
|
|
||||||
- name: Run mypy
|
- name: Run mypy
|
||||||
run: |
|
run: |
|
||||||
pushd api
|
poetry run -C api python -m mypy --install-types --non-interactive .
|
||||||
poetry run python -m mypy --install-types --non-interactive .
|
|
||||||
popd
|
|
||||||
|
|
||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
@@ -80,4 +75,4 @@ jobs:
|
|||||||
ssrf_proxy
|
ssrf_proxy
|
||||||
|
|
||||||
- name: Run Workflow
|
- name: Run Workflow
|
||||||
run: poetry run -C api bash dev/pytest/pytest_workflow.sh
|
run: poetry run -P api bash dev/pytest/pytest_workflow.sh
|
||||||
|
|||||||
16
.github/workflows/build-push.yml
vendored
16
.github/workflows/build-push.yml
vendored
@@ -5,6 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
- "deploy/dev"
|
- "deploy/dev"
|
||||||
|
- "dev/plugin-deploy"
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
@@ -79,10 +80,12 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max,scope=${{ matrix.service_name }}
|
cache-to: type=gha,mode=max,scope=${{ matrix.service_name }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
|
env:
|
||||||
|
DIGEST: ${{ steps.build.outputs.digest }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/digests
|
mkdir -p /tmp/digests
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
sanitized_digest=${DIGEST#sha256:}
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${sanitized_digest}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -132,10 +135,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Create manifest list and push
|
- name: Create manifest list and push
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ${{ env[matrix.image_name_env] }}
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
$(printf '${{ env[matrix.image_name_env] }}@sha256:%s ' *)
|
$(printf "$IMAGE_NAME@sha256:%s " *)
|
||||||
|
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ${{ env[matrix.image_name_env] }}
|
||||||
|
IMAGE_VERSION: ${{ steps.meta.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env[matrix.image_name_env] }}:${{ steps.meta.outputs.version }}
|
docker buildx imagetools inspect "$IMAGE_NAME:$IMAGE_VERSION"
|
||||||
|
|||||||
4
.github/workflows/db-migration-test.yml
vendored
4
.github/workflows/db-migration-test.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- plugins/beta
|
||||||
paths:
|
paths:
|
||||||
- api/migrations/**
|
- api/migrations/**
|
||||||
- .github/workflows/db-migration-test.yml
|
- .github/workflows/db-migration-test.yml
|
||||||
@@ -19,6 +20,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Poetry and Python
|
- name: Setup Poetry and Python
|
||||||
uses: ./.github/actions/setup-poetry
|
uses: ./.github/actions/setup-poetry
|
||||||
|
|||||||
47
.github/workflows/docker-build.yml
vendored
Normal file
47
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
paths:
|
||||||
|
- api/Dockerfile
|
||||||
|
- web/Dockerfile
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docker-build-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- service_name: "api-amd64"
|
||||||
|
platform: linux/amd64
|
||||||
|
context: "api"
|
||||||
|
- service_name: "api-arm64"
|
||||||
|
platform: linux/arm64
|
||||||
|
context: "api"
|
||||||
|
- service_name: "web-amd64"
|
||||||
|
platform: linux/amd64
|
||||||
|
context: "web"
|
||||||
|
- service_name: "web-arm64"
|
||||||
|
platform: linux/arm64
|
||||||
|
context: "web"
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
context: "{{defaultContext}}:${{ matrix.context }}"
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
2
.github/workflows/expose_service_ports.sh
vendored
2
.github/workflows/expose_service_ports.sh
vendored
@@ -9,6 +9,6 @@ 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
|
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml
|
||||||
|
|
||||||
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase"
|
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase"
|
||||||
|
|||||||
57
.github/workflows/style.yml
vendored
57
.github/workflows/style.yml
vendored
@@ -17,6 +17,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
@@ -38,12 +41,12 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
poetry run -C api ruff --version
|
poetry run -C api ruff --version
|
||||||
poetry run -C api ruff check ./api
|
poetry run -C api ruff check ./
|
||||||
poetry run -C api ruff format --check ./api
|
poetry run -C api ruff format --check ./
|
||||||
|
|
||||||
- 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 -P api dotenv-linter ./api/.env.example ./web/.env.example
|
||||||
|
|
||||||
- name: Lint hints
|
- name: Lint hints
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -59,6 +62,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
@@ -66,22 +72,58 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: web/**
|
files: web/**
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
run_install: false
|
||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: yarn
|
cache: pnpm
|
||||||
cache-dependency-path: ./web/package.json
|
cache-dependency-path: ./web/package.json
|
||||||
|
|
||||||
- name: Web dependencies
|
- name: Web dependencies
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Web style check
|
- name: Web style check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn run lint
|
run: pnpm run lint
|
||||||
|
|
||||||
|
docker-compose-template:
|
||||||
|
name: Docker Compose Template
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -90,6 +132,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
|
|||||||
9
.github/workflows/tool-test-sdks.yaml
vendored
9
.github/workflows/tool-test-sdks.yaml
vendored
@@ -26,16 +26,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: ''
|
cache: ''
|
||||||
cache-dependency-path: 'yarn.lock'
|
cache-dependency-path: 'pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: pnpm test
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2 # last 2 commits
|
fetch-depth: 2 # last 2 commits
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for file changes in i18n/en-US
|
- name: Check for file changes in i18n/en-US
|
||||||
id: check_files
|
id: check_files
|
||||||
@@ -38,11 +39,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: env.FILES_CHANGED == 'true'
|
if: env.FILES_CHANGED == 'true'
|
||||||
run: yarn install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run npm script
|
- name: Run npm script
|
||||||
if: env.FILES_CHANGED == 'true'
|
if: env.FILES_CHANGED == 'true'
|
||||||
run: npm run auto-gen-i18n
|
run: pnpm run auto-gen-i18n
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: env.FILES_CHANGED == 'true'
|
if: env.FILES_CHANGED == 'true'
|
||||||
|
|||||||
19
.github/workflows/vdb-tests.yml
vendored
19
.github/workflows/vdb-tests.yml
vendored
@@ -28,6 +28,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
||||||
uses: ./.github/actions/setup-poetry
|
uses: ./.github/actions/setup-poetry
|
||||||
@@ -51,7 +54,15 @@ jobs:
|
|||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|
||||||
- name: Set up Vector Stores (TiDB, Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase)
|
- name: Set up Vector Store (TiDB)
|
||||||
|
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
|
with:
|
||||||
|
compose-file: docker/tidb/docker-compose.yaml
|
||||||
|
services: |
|
||||||
|
tidb
|
||||||
|
tiflash
|
||||||
|
|
||||||
|
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase)
|
||||||
uses: hoverkraft-tech/compose-action@v2.0.2
|
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
@@ -67,7 +78,9 @@ jobs:
|
|||||||
pgvector
|
pgvector
|
||||||
chroma
|
chroma
|
||||||
elasticsearch
|
elasticsearch
|
||||||
tidb
|
|
||||||
|
- name: Check TiDB Ready
|
||||||
|
run: poetry run -P api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
|
||||||
|
|
||||||
- name: Test Vector Stores
|
- name: Test Vector Stores
|
||||||
run: poetry run -C api bash dev/pytest/pytest_vdb.sh
|
run: poetry run -P api bash dev/pytest/pytest_vdb.sh
|
||||||
|
|||||||
35
.github/workflows/web-tests.yml
vendored
35
.github/workflows/web-tests.yml
vendored
@@ -22,25 +22,34 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v45
|
uses: tj-actions/changed-files@v45
|
||||||
with:
|
with:
|
||||||
files: web/**
|
files: web/**
|
||||||
|
# to run pnpm, should install package canvas, but it always install failed on amd64 under ubuntu-latest
|
||||||
|
# - name: Install pnpm
|
||||||
|
# uses: pnpm/action-setup@v4
|
||||||
|
# with:
|
||||||
|
# version: 10
|
||||||
|
# run_install: false
|
||||||
|
|
||||||
- name: Setup Node.js
|
# - name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
# uses: actions/setup-node@v4
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
# if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
with:
|
# with:
|
||||||
node-version: 20
|
# node-version: 20
|
||||||
cache: yarn
|
# cache: pnpm
|
||||||
cache-dependency-path: ./web/package.json
|
# cache-dependency-path: ./web/package.json
|
||||||
|
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
# if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn install --frozen-lockfile
|
# run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tests
|
# - name: Run tests
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
# if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn test
|
# run: pnpm test
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -163,6 +163,7 @@ docker/volumes/db/data/*
|
|||||||
docker/volumes/redis/data/*
|
docker/volumes/redis/data/*
|
||||||
docker/volumes/weaviate/*
|
docker/volumes/weaviate/*
|
||||||
docker/volumes/qdrant/*
|
docker/volumes/qdrant/*
|
||||||
|
docker/tidb/volumes/*
|
||||||
docker/volumes/etcd/*
|
docker/volumes/etcd/*
|
||||||
docker/volumes/minio/*
|
docker/volumes/minio/*
|
||||||
docker/volumes/milvus/*
|
docker/volumes/milvus/*
|
||||||
@@ -175,6 +176,7 @@ 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/plugin_daemon/*
|
||||||
!docker/volumes/oceanbase/init.d
|
!docker/volumes/oceanbase/init.d
|
||||||
|
|
||||||
docker/nginx/conf.d/default.conf
|
docker/nginx/conf.d/default.conf
|
||||||
@@ -193,3 +195,9 @@ api/.vscode
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
/.pnpm-store
|
||||||
|
|
||||||
|
# plugin migrate
|
||||||
|
plugins.jsonl
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Dify requires the following dependencies to build, make sure they're installed o
|
|||||||
* [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/)
|
* [pnpm](https://pnpm.io/)
|
||||||
* [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
* [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
||||||
|
|
||||||
### 4. Installations
|
### 4. Installations
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Dify 依赖以下工具和库:
|
|||||||
- [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/)
|
- [pnpm](https://pnpm.io/)
|
||||||
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
||||||
|
|
||||||
### 4. 安装
|
### 4. 安装
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Dify を構築するには次の依存関係が必要です。それらがシス
|
|||||||
- [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/)
|
- [pnpm](https://pnpm.io/)
|
||||||
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
||||||
|
|
||||||
### 4. インストール
|
### 4. インストール
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Dify yêu cầu các phụ thuộc sau để build, hãy đảm bảo chúng đ
|
|||||||
- [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/) phiên bản 8.x.x hoặc [Yarn](https://yarnpkg.com/)
|
- [pnpm](https://pnpm.io/)
|
||||||
- [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.11.x hoặc 3.12.x
|
||||||
|
|
||||||
### 4. Cài đặt
|
### 4. Cài đặt
|
||||||
|
|||||||
19
LICENSE
19
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
# Open Source License
|
# Open Source License
|
||||||
|
|
||||||
Dify is licensed under the Apache License 2.0, with the following additional conditions:
|
Dify is licensed under a modified version of the Apache License 2.0, with the following additional conditions:
|
||||||
|
|
||||||
1. Dify may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer:
|
1. Dify may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer:
|
||||||
|
|
||||||
@@ -21,19 +21,4 @@ Apart from the specific conditions mentioned above, all other rights and restric
|
|||||||
|
|
||||||
The interactive design of this product is protected by appearance patent.
|
The interactive design of this product is protected by appearance patent.
|
||||||
|
|
||||||
© 2024 LangGenius, Inc.
|
© 2025 LangGenius, Inc.
|
||||||
|
|
||||||
|
|
||||||
----------
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -25,6 +25,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -105,6 +108,72 @@ Please refer to our [FAQ](https://docs.dify.ai/getting-started/install-self-host
|
|||||||
**7. Backend-as-a-Service**:
|
**7. Backend-as-a-Service**:
|
||||||
All of Dify's offerings come with corresponding APIs, so you could effortlessly integrate Dify into your own business logic.
|
All of Dify's offerings come with corresponding APIs, so you could effortlessly integrate Dify into your own business logic.
|
||||||
|
|
||||||
|
## Feature Comparison
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th align="center">Feature</th>
|
||||||
|
<th align="center">Dify.AI</th>
|
||||||
|
<th align="center">LangChain</th>
|
||||||
|
<th align="center">Flowise</th>
|
||||||
|
<th align="center">OpenAI Assistants API</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Programming Approach</td>
|
||||||
|
<td align="center">API + App-oriented</td>
|
||||||
|
<td align="center">Python Code</td>
|
||||||
|
<td align="center">App-oriented</td>
|
||||||
|
<td align="center">API-oriented</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Supported LLMs</td>
|
||||||
|
<td align="center">Rich Variety</td>
|
||||||
|
<td align="center">Rich Variety</td>
|
||||||
|
<td align="center">Rich Variety</td>
|
||||||
|
<td align="center">OpenAI-only</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">RAG Engine</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Agent</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Workflow</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Observability</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Enterprise Feature (SSO/Access control)</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Local Deployment</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Using Dify
|
## Using Dify
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="seguir en LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||||
<img alt="Descargas de Docker" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
<img alt="Descargas de Docker" 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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="suivre sur LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||||
<img alt="Tirages Docker" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
<img alt="Tirages Docker" 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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -72,9 +75,7 @@ Dify est une plateforme de développement d'applications LLM open source. Son in
|
|||||||
**4. Pipeline RAG** :
|
**4. Pipeline RAG** :
|
||||||
Des capacités RAG étendues qui couvrent tout, de l'ingestion de documents à la récupération, avec un support prêt à l'emploi pour l'extraction de texte à partir de PDF, PPT et autres formats de document courants.
|
Des capacités RAG étendues qui couvrent tout, de l'ingestion de documents à la récupération, avec un support prêt à l'emploi pour l'extraction de texte à partir de PDF, PPT et autres formats de document courants.
|
||||||
|
|
||||||
**5. Capac
|
**5. Capacités d'agent** :
|
||||||
|
|
||||||
ités d'agent**:
|
|
||||||
Vous pouvez définir des agents basés sur l'appel de fonction LLM ou ReAct, et ajouter des outils pré-construits ou personnalisés pour l'agent. Dify fournit plus de 50 outils intégrés pour les agents d'IA, tels que la recherche Google, DALL·E, Stable Diffusion et WolframAlpha.
|
Vous pouvez définir des agents basés sur l'appel de fonction LLM ou ReAct, et ajouter des outils pré-construits ou personnalisés pour l'agent. Dify fournit plus de 50 outils intégrés pour les agents d'IA, tels que la recherche Google, DALL·E, Stable Diffusion et WolframAlpha.
|
||||||
|
|
||||||
**6. LLMOps** :
|
**6. LLMOps** :
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="LinkedInでフォロー"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -161,7 +164,7 @@ DifyはオープンソースのLLMアプリケーション開発プラットフ
|
|||||||
|
|
||||||
- **企業/組織向けのDify</br>**
|
- **企業/組織向けのDify</br>**
|
||||||
企業中心の機能を提供しています。[メールを送信](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)して企業のニーズについて相談してください。 </br>
|
企業中心の機能を提供しています。[メールを送信](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)して企業のニーズについて相談してください。 </br>
|
||||||
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングどして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
|
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t23mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングとして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
|
||||||
|
|
||||||
|
|
||||||
## 最新の情報を入手
|
## 最新の情報を入手
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -84,9 +87,7 @@ Dify is an open-source LLM app development platform. Its intuitive interface com
|
|||||||
|
|
||||||
## Feature Comparison
|
## Feature Comparison
|
||||||
<table style="width: 100%;">
|
<table style="width: 100%;">
|
||||||
<tr
|
<tr>
|
||||||
|
|
||||||
>
|
|
||||||
<th align="center">Feature</th>
|
<th align="center">Feature</th>
|
||||||
<th align="center">Dify.AI</th>
|
<th align="center">Dify.AI</th>
|
||||||
<th align="center">LangChain</th>
|
<th align="center">LangChain</th>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
70
README_SI.md
70
README_SI.md
@@ -22,6 +22,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="follow on LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -103,6 +106,73 @@ Prosimo, glejte naša pogosta vprašanja [FAQ](https://docs.dify.ai/getting-star
|
|||||||
**7. Backend-as-a-Service**:
|
**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.
|
AVse ponudbe Difyja so opremljene z ustreznimi API-ji, tako da lahko Dify brez težav integrirate v svojo poslovno logiko.
|
||||||
|
|
||||||
|
## Primerjava Funkcij
|
||||||
|
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th align="center">Funkcija</th>
|
||||||
|
<th align="center">Dify.AI</th>
|
||||||
|
<th align="center">LangChain</th>
|
||||||
|
<th align="center">Flowise</th>
|
||||||
|
<th align="center">OpenAI Assistants API</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Programski pristop</td>
|
||||||
|
<td align="center">API + usmerjeno v aplikacije</td>
|
||||||
|
<td align="center">Python koda</td>
|
||||||
|
<td align="center">Usmerjeno v aplikacije</td>
|
||||||
|
<td align="center">Usmerjeno v API</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Podprti LLM-ji</td>
|
||||||
|
<td align="center">Bogata izbira</td>
|
||||||
|
<td align="center">Bogata izbira</td>
|
||||||
|
<td align="center">Bogata izbira</td>
|
||||||
|
<td align="center">Samo OpenAI</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">RAG pogon</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Agent</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Potek dela</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Spremljanje</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Funkcija za podjetja (SSO/nadzor dostopa)</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Lokalna namestitev</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Uporaba Dify
|
## Uporaba Dify
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="LinkedIn'da takip et"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||||
<img alt="Docker Çekmeleri" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
<img alt="Docker Çekmeleri" 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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
@@ -62,8 +65,6 @@ Görsel bir arayüz üzerinde güçlü AI iş akışları oluşturun ve test edi
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
Özür dilerim, haklısınız. Daha anlamlı ve akıcı bir çeviri yapmaya çalışayım. İşte güncellenmiş çeviri:
|
|
||||||
|
|
||||||
**3. Prompt IDE**:
|
**3. Prompt IDE**:
|
||||||
Komut istemlerini oluşturmak, model performansını karşılaştırmak ve sohbet tabanlı uygulamalara metin-konuşma gibi ek özellikler eklemek için kullanıcı dostu bir arayüz.
|
Komut istemlerini oluşturmak, model performansını karşılaştırmak ve sohbet tabanlı uygulamalara metin-konuşma gibi ek özellikler eklemek için kullanıcı dostu bir arayüz.
|
||||||
|
|
||||||
@@ -150,8 +151,6 @@ Görsel bir arayüz üzerinde güçlü AI iş akışları oluşturun ve test edi
|
|||||||
## Dify'ı Kullanma
|
## Dify'ı Kullanma
|
||||||
|
|
||||||
- **Cloud </br>**
|
- **Cloud </br>**
|
||||||
İşte verdiğiniz metnin Türkçe çevirisi, kod bloğu içinde:
|
|
||||||
-
|
|
||||||
Herkesin sıfır kurulumla denemesi için bir [Dify Cloud](https://dify.ai) hizmeti sunuyoruz. Bu hizmet, kendi kendine dağıtılan versiyonun tüm yeteneklerini sağlar ve sandbox planında 200 ücretsiz GPT-4 çağrısı içerir.
|
Herkesin sıfır kurulumla denemesi için bir [Dify Cloud](https://dify.ai) hizmeti sunuyoruz. Bu hizmet, kendi kendine dağıtılan versiyonun tüm yeteneklerini sağlar ve sandbox planında 200 ücretsiz GPT-4 çağrısı içerir.
|
||||||
|
|
||||||
- **Dify Topluluk Sürümünü Kendi Sunucunuzda Barındırma</br>**
|
- **Dify Topluluk Sürümünü Kendi Sunucunuzda Barındırma</br>**
|
||||||
@@ -177,8 +176,6 @@ GitHub'da Dify'a yıldız verin ve yeni sürümlerden anında haberdar olun.
|
|||||||
>- RAM >= 4GB
|
>- RAM >= 4GB
|
||||||
|
|
||||||
</br>
|
</br>
|
||||||
İşte verdiğiniz metnin Türkçe çevirisi, kod bloğu içinde:
|
|
||||||
|
|
||||||
Dify sunucusunu başlatmanın en kolay yolu, [docker-compose.yml](docker/docker-compose.yaml) dosyamızı çalıştırmaktır. Kurulum komutunu çalıştırmadan önce, makinenizde [Docker](https://docs.docker.com/get-docker/) ve [Docker Compose](https://docs.docker.com/compose/install/)'un kurulu olduğundan emin olun:
|
Dify sunucusunu başlatmanın en kolay yolu, [docker-compose.yml](docker/docker-compose.yaml) dosyamızı çalıştırmaktır. Kurulum komutunu çalıştırmadan önce, makinenizde [Docker](https://docs.docker.com/get-docker/) ve [Docker Compose](https://docs.docker.com/compose/install/)'un kurulu olduğundan emin olun:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<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>
|
||||||
|
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||||
|
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||||
|
alt="theo dõi trên LinkedIn"></a>
|
||||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
<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>
|
<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">
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
.env
|
.env
|
||||||
*.env.*
|
*.env.*
|
||||||
|
|
||||||
|
storage/generate_files/*
|
||||||
storage/privkeys/*
|
storage/privkeys/*
|
||||||
|
storage/tools/*
|
||||||
|
storage/upload_files/*
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
@@ -9,6 +12,8 @@ logs
|
|||||||
|
|
||||||
# jetbrains
|
# jetbrains
|
||||||
.idea
|
.idea
|
||||||
|
.mypy_cache
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
# venv
|
# venv
|
||||||
.venv
|
.venv
|
||||||
@@ -409,7 +409,6 @@ MAX_VARIABLE_SIZE=204800
|
|||||||
APP_MAX_EXECUTION_TIME=1200
|
APP_MAX_EXECUTION_TIME=1200
|
||||||
APP_MAX_ACTIVE_REQUESTS=0
|
APP_MAX_ACTIVE_REQUESTS=0
|
||||||
|
|
||||||
|
|
||||||
# Celery beat configuration
|
# Celery beat configuration
|
||||||
CELERY_BEAT_SCHEDULER_TIME=1
|
CELERY_BEAT_SCHEDULER_TIME=1
|
||||||
|
|
||||||
@@ -422,6 +421,22 @@ POSITION_PROVIDER_PINS=
|
|||||||
POSITION_PROVIDER_INCLUDES=
|
POSITION_PROVIDER_INCLUDES=
|
||||||
POSITION_PROVIDER_EXCLUDES=
|
POSITION_PROVIDER_EXCLUDES=
|
||||||
|
|
||||||
|
# Plugin configuration
|
||||||
|
PLUGIN_DAEMON_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi
|
||||||
|
PLUGIN_DAEMON_URL=http://127.0.0.1:5002
|
||||||
|
PLUGIN_REMOTE_INSTALL_PORT=5003
|
||||||
|
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||||
|
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
||||||
|
INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
|
||||||
|
# Marketplace configuration
|
||||||
|
MARKETPLACE_ENABLED=true
|
||||||
|
MARKETPLACE_API_URL=https://marketplace.dify.ai
|
||||||
|
|
||||||
|
# Endpoint configuration
|
||||||
|
ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,12 @@ ignore = [
|
|||||||
"FURB152", # math-constant
|
"FURB152", # math-constant
|
||||||
"UP007", # non-pep604-annotation
|
"UP007", # non-pep604-annotation
|
||||||
"UP032", # f-string
|
"UP032", # f-string
|
||||||
|
"UP045", # non-pep604-annotation-optional
|
||||||
"B005", # strip-with-multi-characters
|
"B005", # strip-with-multi-characters
|
||||||
"B006", # mutable-argument-default
|
"B006", # mutable-argument-default
|
||||||
"B007", # unused-loop-control-variable
|
"B007", # unused-loop-control-variable
|
||||||
"B026", # star-arg-unpacking-after-keyword-arg
|
"B026", # star-arg-unpacking-after-keyword-arg
|
||||||
|
"B903", # class-as-data-structure
|
||||||
"B904", # raise-without-from-inside-except
|
"B904", # raise-without-from-inside-except
|
||||||
"B905", # zip-without-explicit-strict
|
"B905", # zip-without-explicit-strict
|
||||||
"N806", # non-lowercase-variable-in-function
|
"N806", # non-lowercase-variable-in-function
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ FROM python:3.12-slim-bookworm AS base
|
|||||||
WORKDIR /app/api
|
WORKDIR /app/api
|
||||||
|
|
||||||
# Install Poetry
|
# Install Poetry
|
||||||
ENV POETRY_VERSION=1.8.4
|
ENV POETRY_VERSION=2.0.1
|
||||||
|
|
||||||
# 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/
|
||||||
@@ -48,16 +48,20 @@ ENV TZ=UTC
|
|||||||
|
|
||||||
WORKDIR /app/api
|
WORKDIR /app/api
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN \
|
||||||
&& apt-get install -y --no-install-recommends curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
apt-get update \
|
||||||
# if you located in China, you can use aliyun mirror to speed up
|
# Install dependencies
|
||||||
# && echo "deb http://mirrors.aliyun.com/debian testing main" > /etc/apt/sources.list \
|
&& apt-get install -y --no-install-recommends \
|
||||||
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
|
# basic environment
|
||||||
&& apt-get update \
|
curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
||||||
# 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 \
|
expat libldap-2.5-0 perl libsqlite3-0 zlib1g \
|
||||||
# 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 \
|
fonts-noto-cjk \
|
||||||
|
# install a package to improve the accuracy of guessing mime type and file extension
|
||||||
|
media-types \
|
||||||
|
# install libmagic to support the use of python-magic guess MIMETYPE
|
||||||
|
libmagic1 \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -69,6 +73,10 @@ ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
|||||||
# Download nltk data
|
# Download nltk data
|
||||||
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"
|
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"
|
||||||
|
|
||||||
|
ENV TIKTOKEN_CACHE_DIR=/app/api/.tiktoken_cache
|
||||||
|
|
||||||
|
RUN python -c "import tiktoken; tiktoken.encoding_for_model('gpt2')"
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . /app/api/
|
COPY . /app/api/
|
||||||
|
|
||||||
@@ -76,7 +84,6 @@ COPY . /app/api/
|
|||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
ARG COMMIT_SHA
|
ARG COMMIT_SHA
|
||||||
ENV COMMIT_SHA=${COMMIT_SHA}
|
ENV COMMIT_SHA=${COMMIT_SHA}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,13 @@
|
|||||||
|
|
||||||
4. Create environment.
|
4. Create environment.
|
||||||
|
|
||||||
Dify API service uses [Poetry](https://python-poetry.org/docs/) to manage dependencies. You can execute `poetry shell` to activate the environment.
|
Dify API service uses [Poetry](https://python-poetry.org/docs/) to manage dependencies. First, you need to add the poetry shell plugin, if you don't have it already, in order to run in a virtual environment. [Note: Poetry shell is no longer a native command so you need to install the poetry plugin beforehand]
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry self add poetry-plugin-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, You can execute `poetry shell` to activate the environment.
|
||||||
|
|
||||||
5. Install dependencies
|
5. Install dependencies
|
||||||
|
|
||||||
@@ -79,5 +85,5 @@
|
|||||||
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
|
||||||
poetry run -C api bash dev/pytest/pytest_all_tests.sh
|
poetry run -P api bash dev/pytest/pytest_all_tests.sh
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
from contexts.wrapper import RecyclableContextVar
|
||||||
from dify_app import DifyApp
|
from dify_app import DifyApp
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ 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())
|
||||||
|
|
||||||
|
# add before request hook
|
||||||
|
@dify_app.before_request
|
||||||
|
def before_request():
|
||||||
|
# add an unique identifier to each request
|
||||||
|
RecyclableContextVar.increment_thread_recycles()
|
||||||
|
|
||||||
return dify_app
|
return dify_app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ from models.dataset import Document as DatasetDocument
|
|||||||
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
|
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
|
||||||
from models.provider import Provider, ProviderModel
|
from models.provider import Provider, ProviderModel
|
||||||
from services.account_service import RegisterService, TenantService
|
from services.account_service import RegisterService, TenantService
|
||||||
|
from services.plugin.data_migration import PluginDataMigration
|
||||||
|
from services.plugin.plugin_migration import PluginMigration
|
||||||
|
|
||||||
|
|
||||||
@click.command("reset-password", help="Reset the account password.")
|
@click.command("reset-password", help="Reset the account password.")
|
||||||
@@ -524,7 +526,7 @@ def add_qdrant_doc_id_index(field: str):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
click.echo(click.style("Failed to create Qdrant client.", fg="red"))
|
click.echo(click.style("Failed to create Qdrant client.", fg="red"))
|
||||||
|
|
||||||
click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
|
click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
|
||||||
@@ -593,7 +595,7 @@ def upgrade_db():
|
|||||||
|
|
||||||
click.echo(click.style("Database migration successful!", fg="green"))
|
click.echo(click.style("Database migration successful!", fg="green"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Failed to execute database migration")
|
logging.exception("Failed to execute database migration")
|
||||||
finally:
|
finally:
|
||||||
lock.release()
|
lock.release()
|
||||||
@@ -639,7 +641,7 @@ where sites.id is null limit 1000"""
|
|||||||
account = accounts[0]
|
account = accounts[0]
|
||||||
print("Fixing missing site for app {}".format(app.id))
|
print("Fixing missing site for app {}".format(app.id))
|
||||||
app_was_created.send(app, account=account)
|
app_was_created.send(app, account=account)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
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"Failed to fix app related site missing issue, app_id: {app_id}")
|
||||||
@@ -649,3 +651,69 @@ where sites.id is null limit 1000"""
|
|||||||
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"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("migrate-data-for-plugin", help="Migrate data for plugin.")
|
||||||
|
def migrate_data_for_plugin():
|
||||||
|
"""
|
||||||
|
Migrate data for plugin.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting migrate data for plugin.", fg="white"))
|
||||||
|
|
||||||
|
PluginDataMigration.migrate()
|
||||||
|
|
||||||
|
click.echo(click.style("Migrate data for plugin completed.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("extract-plugins", help="Extract plugins.")
|
||||||
|
@click.option("--output_file", prompt=True, help="The file to store the extracted plugins.", default="plugins.jsonl")
|
||||||
|
@click.option("--workers", prompt=True, help="The number of workers to extract plugins.", default=10)
|
||||||
|
def extract_plugins(output_file: str, workers: int):
|
||||||
|
"""
|
||||||
|
Extract plugins.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting extract plugins.", fg="white"))
|
||||||
|
|
||||||
|
PluginMigration.extract_plugins(output_file, workers)
|
||||||
|
|
||||||
|
click.echo(click.style("Extract plugins completed.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("extract-unique-identifiers", help="Extract unique identifiers.")
|
||||||
|
@click.option(
|
||||||
|
"--output_file",
|
||||||
|
prompt=True,
|
||||||
|
help="The file to store the extracted unique identifiers.",
|
||||||
|
default="unique_identifiers.json",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--input_file", prompt=True, help="The file to store the extracted unique identifiers.", default="plugins.jsonl"
|
||||||
|
)
|
||||||
|
def extract_unique_plugins(output_file: str, input_file: str):
|
||||||
|
"""
|
||||||
|
Extract unique plugins.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting extract unique plugins.", fg="white"))
|
||||||
|
|
||||||
|
PluginMigration.extract_unique_plugins_to_file(input_file, output_file)
|
||||||
|
|
||||||
|
click.echo(click.style("Extract unique plugins completed.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("install-plugins", help="Install plugins.")
|
||||||
|
@click.option(
|
||||||
|
"--input_file", prompt=True, help="The file to store the extracted unique identifiers.", default="plugins.jsonl"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output_file", prompt=True, help="The file to store the installed plugins.", default="installed_plugins.jsonl"
|
||||||
|
)
|
||||||
|
@click.option("--workers", prompt=True, help="The number of workers to install plugins.", default=100)
|
||||||
|
def install_plugins(input_file: str, output_file: str, workers: int):
|
||||||
|
"""
|
||||||
|
Install plugins.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting install plugins.", fg="white"))
|
||||||
|
|
||||||
|
PluginMigration.install_plugins(input_file, output_file, workers)
|
||||||
|
|
||||||
|
click.echo(click.style("Install plugins completed.", fg="green"))
|
||||||
|
|||||||
@@ -134,6 +134,60 @@ class CodeExecutionSandboxConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginConfig(BaseSettings):
|
||||||
|
"""
|
||||||
|
Plugin configs
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN_DAEMON_URL: HttpUrl = Field(
|
||||||
|
description="Plugin API URL",
|
||||||
|
default="http://localhost:5002",
|
||||||
|
)
|
||||||
|
|
||||||
|
PLUGIN_DAEMON_KEY: str = Field(
|
||||||
|
description="Plugin API key",
|
||||||
|
default="plugin-api-key",
|
||||||
|
)
|
||||||
|
|
||||||
|
INNER_API_KEY_FOR_PLUGIN: str = Field(description="Inner api key for plugin", default="inner-api-key")
|
||||||
|
|
||||||
|
PLUGIN_REMOTE_INSTALL_HOST: str = Field(
|
||||||
|
description="Plugin Remote Install Host",
|
||||||
|
default="localhost",
|
||||||
|
)
|
||||||
|
|
||||||
|
PLUGIN_REMOTE_INSTALL_PORT: PositiveInt = Field(
|
||||||
|
description="Plugin Remote Install Port",
|
||||||
|
default=5003,
|
||||||
|
)
|
||||||
|
|
||||||
|
PLUGIN_MAX_PACKAGE_SIZE: PositiveInt = Field(
|
||||||
|
description="Maximum allowed size for plugin packages in bytes",
|
||||||
|
default=15728640,
|
||||||
|
)
|
||||||
|
|
||||||
|
PLUGIN_MAX_BUNDLE_SIZE: PositiveInt = Field(
|
||||||
|
description="Maximum allowed size for plugin bundles in bytes",
|
||||||
|
default=15728640 * 12,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceConfig(BaseSettings):
|
||||||
|
"""
|
||||||
|
Configuration for marketplace
|
||||||
|
"""
|
||||||
|
|
||||||
|
MARKETPLACE_ENABLED: bool = Field(
|
||||||
|
description="Enable or disable marketplace",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
MARKETPLACE_API_URL: HttpUrl = Field(
|
||||||
|
description="Marketplace API URL",
|
||||||
|
default="https://marketplace.dify.ai",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EndpointConfig(BaseSettings):
|
class EndpointConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
Configuration for various application endpoints and URLs
|
Configuration for various application endpoints and URLs
|
||||||
@@ -146,7 +200,7 @@ class EndpointConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
CONSOLE_WEB_URL: str = Field(
|
CONSOLE_WEB_URL: str = Field(
|
||||||
description="Base URL for the console web interface," "used for frontend references and CORS configuration",
|
description="Base URL for the console web interface,used for frontend references and CORS configuration",
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,6 +214,10 @@ class EndpointConfig(BaseSettings):
|
|||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENDPOINT_URL_TEMPLATE: str = Field(
|
||||||
|
description="Template url for endpoint plugin", default="http://localhost:5002/e/{hook_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileAccessConfig(BaseSettings):
|
class FileAccessConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
@@ -315,8 +373,8 @@ class HttpConfig(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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 handling of X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Port headers"
|
||||||
" to respect X-* headers to redirect clients",
|
" when the app is behind a single trusted reverse proxy.",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -498,6 +556,11 @@ class AuthConfig(BaseSettings):
|
|||||||
default=86400,
|
default=86400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FORGOT_PASSWORD_LOCKOUT_DURATION: PositiveInt = Field(
|
||||||
|
description="Time (in seconds) a user must wait before retrying password reset after exceeding the rate limit.",
|
||||||
|
default=86400,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModerationConfig(BaseSettings):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
@@ -788,6 +851,8 @@ class FeatureConfig(
|
|||||||
AuthConfig, # Changed from OAuthConfig to AuthConfig
|
AuthConfig, # Changed from OAuthConfig to AuthConfig
|
||||||
BillingConfig,
|
BillingConfig,
|
||||||
CodeExecutionSandboxConfig,
|
CodeExecutionSandboxConfig,
|
||||||
|
PluginConfig,
|
||||||
|
MarketplaceConfig,
|
||||||
DataSetConfig,
|
DataSetConfig,
|
||||||
EndpointConfig,
|
EndpointConfig,
|
||||||
FileAccessConfig,
|
FileAccessConfig,
|
||||||
|
|||||||
@@ -1,9 +1,40 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import Field, NonNegativeInt
|
from pydantic import Field, NonNegativeInt, computed_field
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class HostedCreditConfig(BaseSettings):
|
||||||
|
HOSTED_MODEL_CREDIT_CONFIG: str = Field(
|
||||||
|
description="Model credit configuration in format 'model:credits,model:credits', e.g., 'gpt-4:20,gpt-4o:10'",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_model_credits(self, model_name: str) -> int:
|
||||||
|
"""
|
||||||
|
Get credit value for a specific model name.
|
||||||
|
Returns 1 if model is not found in configuration (default credit).
|
||||||
|
|
||||||
|
:param model_name: The name of the model to search for
|
||||||
|
:return: The credit value for the model
|
||||||
|
"""
|
||||||
|
if not self.HOSTED_MODEL_CREDIT_CONFIG:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
credit_map = dict(
|
||||||
|
item.strip().split(":", 1) for item in self.HOSTED_MODEL_CREDIT_CONFIG.split(",") if ":" in item
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search for matching model pattern
|
||||||
|
for pattern, credit in credit_map.items():
|
||||||
|
if pattern.strip() == model_name:
|
||||||
|
return int(credit)
|
||||||
|
return 1 # Default quota if no match found
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return 1 # Return default quota if parsing fails
|
||||||
|
|
||||||
|
|
||||||
class HostedOpenAiConfig(BaseSettings):
|
class HostedOpenAiConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
Configuration for hosted OpenAI service
|
Configuration for hosted OpenAI service
|
||||||
@@ -181,7 +212,7 @@ class HostedFetchAppTemplateConfig(BaseSettings):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
HOSTED_FETCH_APP_TEMPLATES_MODE: str = Field(
|
HOSTED_FETCH_APP_TEMPLATES_MODE: str = Field(
|
||||||
description="Mode for fetching app templates: remote, db, or builtin" " default to remote,",
|
description="Mode for fetching app templates: remote, db, or builtin default to remote,",
|
||||||
default="remote",
|
default="remote",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,5 +233,7 @@ class HostedServiceConfig(
|
|||||||
HostedZhipuAIConfig,
|
HostedZhipuAIConfig,
|
||||||
# moderation
|
# moderation
|
||||||
HostedModerationConfig,
|
HostedModerationConfig,
|
||||||
|
# credit config
|
||||||
|
HostedCreditConfig,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from typing import Any, Literal, Optional
|
from typing import Any, Literal, Optional
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
@@ -166,6 +167,11 @@ class DatabaseConfig(BaseSettings):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RETRIEVAL_SERVICE_EXECUTORS: NonNegativeInt = Field(
|
||||||
|
description="Number of processes for the retrieval service, default to CPU cores.",
|
||||||
|
default=os.cpu_count(),
|
||||||
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
|
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
COMMIT_SHA: str = Field(
|
COMMIT_SHA: str = Field(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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", "mdx", "pdf", "html", "htm", "xlsx", "xls"]
|
||||||
DOCUMENT_EXTENSIONS.extend(("docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
DOCUMENT_EXTENSIONS.extend(("doc", "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])
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
|
from threading import Lock
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from contexts.wrapper import RecyclableContextVar
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
||||||
|
from core.tools.plugin_tool.provider import PluginToolProviderController
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
|
||||||
|
|
||||||
tenant_id: ContextVar[str] = ContextVar("tenant_id")
|
tenant_id: ContextVar[str] = ContextVar("tenant_id")
|
||||||
|
|
||||||
workflow_variable_pool: ContextVar["VariablePool"] = ContextVar("workflow_variable_pool")
|
workflow_variable_pool: ContextVar["VariablePool"] = ContextVar("workflow_variable_pool")
|
||||||
|
|
||||||
|
"""
|
||||||
|
To avoid race-conditions caused by gunicorn thread recycling, using RecyclableContextVar to replace with
|
||||||
|
"""
|
||||||
|
plugin_tool_providers: RecyclableContextVar[dict[str, "PluginToolProviderController"]] = RecyclableContextVar(
|
||||||
|
ContextVar("plugin_tool_providers")
|
||||||
|
)
|
||||||
|
plugin_tool_providers_lock: RecyclableContextVar[Lock] = RecyclableContextVar(ContextVar("plugin_tool_providers_lock"))
|
||||||
|
|
||||||
|
plugin_model_providers: RecyclableContextVar[list["PluginModelProviderEntity"] | None] = RecyclableContextVar(
|
||||||
|
ContextVar("plugin_model_providers")
|
||||||
|
)
|
||||||
|
plugin_model_providers_lock: RecyclableContextVar[Lock] = RecyclableContextVar(
|
||||||
|
ContextVar("plugin_model_providers_lock")
|
||||||
|
)
|
||||||
|
|||||||
65
api/contexts/wrapper.py
Normal file
65
api/contexts/wrapper.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from contextvars import ContextVar
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class HiddenValue:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_default = HiddenValue()
|
||||||
|
|
||||||
|
|
||||||
|
class RecyclableContextVar(Generic[T]):
|
||||||
|
"""
|
||||||
|
RecyclableContextVar is a wrapper around ContextVar
|
||||||
|
It's safe to use in gunicorn with thread recycling, but features like `reset` are not available for now
|
||||||
|
|
||||||
|
NOTE: you need to call `increment_thread_recycles` before requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
_thread_recycles: ContextVar[int] = ContextVar("thread_recycles")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def increment_thread_recycles(cls):
|
||||||
|
try:
|
||||||
|
recycles = cls._thread_recycles.get()
|
||||||
|
cls._thread_recycles.set(recycles + 1)
|
||||||
|
except LookupError:
|
||||||
|
cls._thread_recycles.set(0)
|
||||||
|
|
||||||
|
def __init__(self, context_var: ContextVar[T]):
|
||||||
|
self._context_var = context_var
|
||||||
|
self._updates = ContextVar[int](context_var.name + "_updates", default=0)
|
||||||
|
|
||||||
|
def get(self, default: T | HiddenValue = _default) -> T:
|
||||||
|
thread_recycles = self._thread_recycles.get(0)
|
||||||
|
self_updates = self._updates.get()
|
||||||
|
if thread_recycles > self_updates:
|
||||||
|
self._updates.set(thread_recycles)
|
||||||
|
|
||||||
|
# check if thread is recycled and should be updated
|
||||||
|
if thread_recycles < self_updates:
|
||||||
|
return self._context_var.get()
|
||||||
|
else:
|
||||||
|
# thread_recycles >= self_updates, means current context is invalid
|
||||||
|
if isinstance(default, HiddenValue) or default is _default:
|
||||||
|
raise LookupError
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def set(self, value: T):
|
||||||
|
# it leads to a situation that self.updates is less than cls.thread_recycles if `set` was never called before
|
||||||
|
# increase it manually
|
||||||
|
thread_recycles = self._thread_recycles.get(0)
|
||||||
|
self_updates = self._updates.get()
|
||||||
|
if thread_recycles > self_updates:
|
||||||
|
self._updates.set(thread_recycles)
|
||||||
|
|
||||||
|
if self._updates.get() == self._thread_recycles.get(0):
|
||||||
|
# after increment,
|
||||||
|
self._updates.set(self._updates.get() + 1)
|
||||||
|
|
||||||
|
# set the context
|
||||||
|
self._context_var.set(value)
|
||||||
@@ -1,12 +1,32 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import warnings
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
try:
|
||||||
|
import magic
|
||||||
|
except ImportError:
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
warnings.warn(
|
||||||
|
"To use python-magic guess MIMETYPE, you need to run `pip install python-magic-bin`", stacklevel=2
|
||||||
|
)
|
||||||
|
elif platform.system() == "Darwin":
|
||||||
|
warnings.warn("To use python-magic guess MIMETYPE, you need to run `brew install libmagic`", stacklevel=2)
|
||||||
|
elif platform.system() == "Linux":
|
||||||
|
warnings.warn(
|
||||||
|
"To use python-magic guess MIMETYPE, you need to run `sudo apt-get install libmagic1`", stacklevel=2
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warnings.warn("To use python-magic guess MIMETYPE, you need to install `libmagic`", stacklevel=2)
|
||||||
|
magic = None # type: ignore
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@@ -47,6 +67,13 @@ def guess_file_info_from_response(response: httpx.Response):
|
|||||||
# If guessing fails, use Content-Type from response headers
|
# If guessing fails, use Content-Type from response headers
|
||||||
mimetype = response.headers.get("Content-Type", "application/octet-stream")
|
mimetype = response.headers.get("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
# Use python-magic to guess MIME type if still unknown or generic
|
||||||
|
if mimetype == "application/octet-stream" and magic is not None:
|
||||||
|
try:
|
||||||
|
mimetype = magic.from_buffer(response.content[:1024], mime=True)
|
||||||
|
except magic.MagicException:
|
||||||
|
pass
|
||||||
|
|
||||||
extension = os.path.splitext(filename)[1]
|
extension = os.path.splitext(filename)[1]
|
||||||
|
|
||||||
# Ensure filename has an extension
|
# Ensure filename has an extension
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from flask import Blueprint
|
|||||||
|
|
||||||
from libs.external_api import ExternalApi
|
from libs.external_api import ExternalApi
|
||||||
|
|
||||||
from .app.app_import import AppImportApi, AppImportConfirmApi
|
from .app.app_import import AppImportApi, AppImportCheckDependenciesApi, AppImportConfirmApi
|
||||||
from .explore.audio import ChatAudioApi, ChatTextApi
|
from .explore.audio import ChatAudioApi, ChatTextApi
|
||||||
from .explore.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi
|
from .explore.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi
|
||||||
from .explore.conversation import (
|
from .explore.conversation import (
|
||||||
@@ -40,6 +40,7 @@ api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
|||||||
# Import App
|
# Import App
|
||||||
api.add_resource(AppImportApi, "/apps/imports")
|
api.add_resource(AppImportApi, "/apps/imports")
|
||||||
api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm")
|
api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm")
|
||||||
|
api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies")
|
||||||
|
|
||||||
# Import other controllers
|
# Import other controllers
|
||||||
from . import admin, apikey, extension, feature, ping, setup, version
|
from . import admin, apikey, extension, feature, ping, setup, version
|
||||||
@@ -166,4 +167,15 @@ api.add_resource(
|
|||||||
from .tag import tags
|
from .tag import tags
|
||||||
|
|
||||||
# Import workspace controllers
|
# Import workspace controllers
|
||||||
from .workspace import account, load_balancing_config, members, model_providers, models, tool_providers, workspace
|
from .workspace import (
|
||||||
|
account,
|
||||||
|
agent_providers,
|
||||||
|
endpoint,
|
||||||
|
load_balancing_config,
|
||||||
|
members,
|
||||||
|
model_providers,
|
||||||
|
models,
|
||||||
|
plugin,
|
||||||
|
tool_providers,
|
||||||
|
workspace,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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 # type: ignore
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@@ -54,9 +56,10 @@ class InsertExploreAppListApi(Resource):
|
|||||||
parser.add_argument("position", type=int, required=True, nullable=False, location="json")
|
parser.add_argument("position", type=int, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app = App.query.filter(App.id == args["app_id"]).first()
|
with Session(db.engine) as session:
|
||||||
|
app = session.execute(select(App).filter(App.id == args["app_id"])).scalar_one_or_none()
|
||||||
if not app:
|
if not app:
|
||||||
raise NotFound(f'App \'{args["app_id"]}\' is not found')
|
raise NotFound(f"App '{args['app_id']}' is not found")
|
||||||
|
|
||||||
site = app.site
|
site = app.site
|
||||||
if not site:
|
if not site:
|
||||||
@@ -70,7 +73,10 @@ class InsertExploreAppListApi(Resource):
|
|||||||
privacy_policy = site.privacy_policy or args["privacy_policy"] or ""
|
privacy_policy = site.privacy_policy or args["privacy_policy"] or ""
|
||||||
custom_disclaimer = site.custom_disclaimer or args["custom_disclaimer"] or ""
|
custom_disclaimer = site.custom_disclaimer or args["custom_disclaimer"] or ""
|
||||||
|
|
||||||
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args["app_id"]).first()
|
with Session(db.engine) as session:
|
||||||
|
recommended_app = session.execute(
|
||||||
|
select(RecommendedApp).filter(RecommendedApp.app_id == args["app_id"])
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
if not recommended_app:
|
if not recommended_app:
|
||||||
recommended_app = RecommendedApp(
|
recommended_app = RecommendedApp(
|
||||||
@@ -110,16 +116,26 @@ class InsertExploreAppApi(Resource):
|
|||||||
@only_edition_cloud
|
@only_edition_cloud
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete(self, app_id):
|
def delete(self, app_id):
|
||||||
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == str(app_id)).first()
|
with Session(db.engine) as session:
|
||||||
|
recommended_app = session.execute(
|
||||||
|
select(RecommendedApp).filter(RecommendedApp.app_id == str(app_id))
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
if not recommended_app:
|
if not recommended_app:
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
app = App.query.filter(App.id == recommended_app.app_id).first()
|
with Session(db.engine) as session:
|
||||||
|
app = session.execute(select(App).filter(App.id == recommended_app.app_id)).scalar_one_or_none()
|
||||||
|
|
||||||
if app:
|
if app:
|
||||||
app.is_public = False
|
app.is_public = False
|
||||||
|
|
||||||
installed_apps = InstalledApp.query.filter(
|
with Session(db.engine) as session:
|
||||||
InstalledApp.app_id == recommended_app.app_id, InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id
|
installed_apps = session.execute(
|
||||||
|
select(InstalledApp).filter(
|
||||||
|
InstalledApp.app_id == recommended_app.app_id,
|
||||||
|
InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id,
|
||||||
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for installed_app in installed_apps:
|
for installed_app in installed_apps:
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from typing import Any
|
|||||||
import flask_restful # type: ignore
|
import flask_restful # type: ignore
|
||||||
from flask_login import current_user # 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 sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@@ -26,7 +28,16 @@ api_key_list = {"data": fields.List(fields.Nested(api_key_fields), attribute="it
|
|||||||
|
|
||||||
|
|
||||||
def _get_resource(resource_id, tenant_id, resource_model):
|
def _get_resource(resource_id, tenant_id, resource_model):
|
||||||
resource = resource_model.query.filter_by(id=resource_id, tenant_id=tenant_id).first()
|
if resource_model == App:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
resource = session.execute(
|
||||||
|
select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
else:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
resource = session.execute(
|
||||||
|
select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
if resource is None:
|
if resource is None:
|
||||||
flask_restful.abort(404, message=f"{resource_model.__name__} not found.")
|
flask_restful.abort(404, message=f"{resource_model.__name__} not found.")
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
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,
|
||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.app_fields import app_import_fields
|
from fields.app_fields import app_import_check_dependencies_fields, app_import_fields
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Account
|
from models import Account
|
||||||
|
from models.model import App
|
||||||
from services.app_dsl_service import AppDslService, ImportStatus
|
from services.app_dsl_service import AppDslService, ImportStatus
|
||||||
|
|
||||||
|
|
||||||
@@ -88,3 +90,20 @@ class AppImportConfirmApi(Resource):
|
|||||||
if result.status == ImportStatus.FAILED.value:
|
if result.status == ImportStatus.FAILED.value:
|
||||||
return result.model_dump(mode="json"), 400
|
return result.model_dump(mode="json"), 400
|
||||||
return result.model_dump(mode="json"), 200
|
return result.model_dump(mode="json"), 200
|
||||||
|
|
||||||
|
|
||||||
|
class AppImportCheckDependenciesApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@get_app_model
|
||||||
|
@account_initialization_required
|
||||||
|
@marshal_with(app_import_check_dependencies_fields)
|
||||||
|
def get(self, app_model: App):
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
import_service = AppDslService(session)
|
||||||
|
result = import_service.check_dependencies(app_model=app_model)
|
||||||
|
|
||||||
|
return result.model_dump(mode="json"), 200
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
|
|||||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||||
from core.model_runtime.errors.invoke import InvokeError
|
from core.model_runtime.errors.invoke import InvokeError
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models.model import AppMode
|
from models import App, AppMode
|
||||||
from services.audio_service import AudioService
|
from services.audio_service import AudioService
|
||||||
from services.errors.audio import (
|
from services.errors.audio import (
|
||||||
AudioTooLargeServiceError,
|
AudioTooLargeServiceError,
|
||||||
@@ -79,7 +79,7 @@ class ChatMessageTextApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
@get_app_model
|
||||||
def post(self, app_model):
|
def post(self, app_model: App):
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -98,9 +98,13 @@ class ChatMessageTextApi(Resource):
|
|||||||
and app_model.workflow.features_dict
|
and app_model.workflow.features_dict
|
||||||
):
|
):
|
||||||
text_to_speech = app_model.workflow.features_dict.get("text_to_speech")
|
text_to_speech = app_model.workflow.features_dict.get("text_to_speech")
|
||||||
|
if text_to_speech is None:
|
||||||
|
raise ValueError("TTS is not enabled")
|
||||||
voice = args.get("voice") or text_to_speech.get("voice")
|
voice = args.get("voice") or text_to_speech.get("voice")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
if app_model.app_model_config is None:
|
||||||
|
raise ValueError("AppModelConfig not found")
|
||||||
voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice")
|
voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice")
|
||||||
except Exception:
|
except Exception:
|
||||||
voice = None
|
voice = None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from datetime import UTC, datetime
|
|||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user # type: ignore
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
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 +51,11 @@ 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()
|
with Session(db.engine) as session:
|
||||||
|
site = session.query(Site).filter(Site.app_id == app_model.id).first()
|
||||||
|
|
||||||
|
if not site:
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
for attr_name in [
|
for attr_name in [
|
||||||
"title",
|
"title",
|
||||||
@@ -76,7 +81,7 @@ class AppSite(Resource):
|
|||||||
|
|
||||||
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(UTC).replace(tzinfo=None)
|
||||||
db.session.commit()
|
session.commit()
|
||||||
|
|
||||||
return site
|
return site
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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.account import Account
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
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
|
||||||
@@ -96,6 +97,9 @@ class DraftWorkflowApi(Resource):
|
|||||||
else:
|
else:
|
||||||
abort(415)
|
abort(415)
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
workflow_service = WorkflowService()
|
workflow_service = WorkflowService()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -139,6 +143,9 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("inputs", type=dict, location="json")
|
parser.add_argument("inputs", type=dict, location="json")
|
||||||
parser.add_argument("query", type=str, required=True, location="json", default="")
|
parser.add_argument("query", type=str, required=True, location="json", default="")
|
||||||
@@ -160,7 +167,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
raise ConversationCompletedError()
|
raise ConversationCompletedError()
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
@@ -178,38 +185,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
if not isinstance(current_user, Account):
|
||||||
parser.add_argument("inputs", type=dict, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = AppGenerateService.generate_single_iteration(
|
|
||||||
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return helper.compact_generate_response(response)
|
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
|
||||||
raise NotFound("Conversation Not Exists.")
|
|
||||||
except services.errors.conversation.ConversationCompletedError:
|
|
||||||
raise ConversationCompletedError()
|
|
||||||
except ValueError as e:
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception("internal server error.")
|
|
||||||
raise InternalServerError()
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowDraftRunIterationNodeApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
|
||||||
def post(self, app_model: App, node_id: str):
|
|
||||||
"""
|
|
||||||
Run draft workflow iteration node
|
|
||||||
"""
|
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@@ -228,7 +204,44 @@ class WorkflowDraftRunIterationNodeApi(Resource):
|
|||||||
raise ConversationCompletedError()
|
raise ConversationCompletedError()
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
|
logging.exception("internal server error.")
|
||||||
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowDraftRunIterationNodeApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||||
|
def post(self, app_model: App, node_id: str):
|
||||||
|
"""
|
||||||
|
Run draft workflow iteration node
|
||||||
|
"""
|
||||||
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("inputs", type=dict, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = AppGenerateService.generate_single_iteration(
|
||||||
|
app_model=app_model, user=current_user, node_id=node_id, args=args, streaming=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return helper.compact_generate_response(response)
|
||||||
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
except services.errors.conversation.ConversationCompletedError:
|
||||||
|
raise ConversationCompletedError()
|
||||||
|
except ValueError as e:
|
||||||
|
raise e
|
||||||
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
@@ -246,6 +259,9 @@ class DraftWorkflowRunApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
|
||||||
parser.add_argument("files", type=list, required=False, location="json")
|
parser.add_argument("files", type=list, required=False, location="json")
|
||||||
@@ -294,13 +310,20 @@ class DraftWorkflowNodeRunApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
inputs = args.get("inputs")
|
||||||
|
if inputs == None:
|
||||||
|
raise ValueError("missing inputs")
|
||||||
|
|
||||||
workflow_service = WorkflowService()
|
workflow_service = WorkflowService()
|
||||||
workflow_node_execution = workflow_service.run_draft_workflow_node(
|
workflow_node_execution = workflow_service.run_draft_workflow_node(
|
||||||
app_model=app_model, node_id=node_id, user_inputs=args.get("inputs"), account=current_user
|
app_model=app_model, node_id=node_id, user_inputs=inputs, account=current_user
|
||||||
)
|
)
|
||||||
|
|
||||||
return workflow_node_execution
|
return workflow_node_execution
|
||||||
@@ -339,6 +362,9 @@ class PublishedWorkflowApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
workflow_service = WorkflowService()
|
workflow_service = WorkflowService()
|
||||||
workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user)
|
workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user)
|
||||||
|
|
||||||
@@ -376,12 +402,17 @@ class DefaultBlockConfigApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("q", type=str, location="args")
|
parser.add_argument("q", type=str, location="args")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
q = args.get("q")
|
||||||
|
|
||||||
filters = None
|
filters = None
|
||||||
if args.get("q"):
|
if q:
|
||||||
try:
|
try:
|
||||||
filters = json.loads(args.get("q", ""))
|
filters = json.loads(args.get("q", ""))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -407,6 +438,9 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
if not current_user.is_editor:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
if request.data:
|
if request.data:
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=False, nullable=True, location="json")
|
parser.add_argument("name", type=str, required=False, nullable=True, location="json")
|
||||||
|
|||||||
@@ -59,3 +59,9 @@ class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
|||||||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
||||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
|
class EmailPasswordResetLimitError(BaseHTTPException):
|
||||||
|
error_code = "email_password_reset_limit"
|
||||||
|
description = "Too many failed password reset attempts. Please try again in 24 hours."
|
||||||
|
code = 429
|
||||||
|
|||||||
@@ -3,10 +3,18 @@ import secrets
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
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 (
|
||||||
|
EmailCodeError,
|
||||||
|
EmailPasswordResetLimitError,
|
||||||
|
InvalidEmailError,
|
||||||
|
InvalidTokenError,
|
||||||
|
PasswordMismatchError,
|
||||||
|
)
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
||||||
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
|
||||||
@@ -37,7 +45,8 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
else:
|
else:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
|
|
||||||
account = Account.query.filter_by(email=args["email"]).first()
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
||||||
token = None
|
token = None
|
||||||
if account is None:
|
if account is None:
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
if FeatureService.get_system_features().is_allow_register:
|
||||||
@@ -62,6 +71,10 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
|
|
||||||
user_email = args["email"]
|
user_email = args["email"]
|
||||||
|
|
||||||
|
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(args["email"])
|
||||||
|
if is_forgot_password_error_rate_limit:
|
||||||
|
raise EmailPasswordResetLimitError()
|
||||||
|
|
||||||
token_data = AccountService.get_reset_password_data(args["token"])
|
token_data = AccountService.get_reset_password_data(args["token"])
|
||||||
if token_data is None:
|
if token_data is None:
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
@@ -70,8 +83,10 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
raise InvalidEmailError()
|
raise InvalidEmailError()
|
||||||
|
|
||||||
if args["code"] != token_data.get("code"):
|
if args["code"] != token_data.get("code"):
|
||||||
|
AccountService.add_forgot_password_error_rate_limit(args["email"])
|
||||||
raise EmailCodeError()
|
raise EmailCodeError()
|
||||||
|
|
||||||
|
AccountService.reset_forgot_password_error_rate_limit(args["email"])
|
||||||
return {"is_valid": True, "email": token_data.get("email")}
|
return {"is_valid": True, "email": token_data.get("email")}
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +119,8 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
password_hashed = hash_password(new_password, salt)
|
password_hashed = hash_password(new_password, salt)
|
||||||
base64_password_hashed = base64.b64encode(password_hashed).decode()
|
base64_password_hashed = base64.b64encode(password_hashed).decode()
|
||||||
|
|
||||||
account = Account.query.filter_by(email=reset_data.get("email")).first()
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=reset_data.get("email"))).scalar_one_or_none()
|
||||||
if account:
|
if account:
|
||||||
account.password = base64_password_hashed
|
account.password = base64_password_hashed
|
||||||
account.password_salt = base64_salt
|
account.password_salt = base64_salt
|
||||||
@@ -125,7 +141,7 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
)
|
)
|
||||||
except WorkSpaceNotAllowedCreateError:
|
except WorkSpaceNotAllowedCreateError:
|
||||||
pass
|
pass
|
||||||
except AccountRegisterError as are:
|
except AccountRegisterError:
|
||||||
raise AccountInFreezeError()
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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 # type: ignore
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Unauthorized
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@@ -135,7 +137,8 @@ def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) ->
|
|||||||
account: Optional[Account] = Account.get_by_openid(provider, user_info.id)
|
account: Optional[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()
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=user_info.email)).scalar_one_or_none()
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import json
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user # type: ignore
|
||||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@@ -76,7 +78,10 @@ class DataSourceApi(Resource):
|
|||||||
def patch(self, binding_id, action):
|
def patch(self, binding_id, action):
|
||||||
binding_id = str(binding_id)
|
binding_id = str(binding_id)
|
||||||
action = str(action)
|
action = str(action)
|
||||||
data_source_binding = DataSourceOauthBinding.query.filter_by(id=binding_id).first()
|
with Session(db.engine) as session:
|
||||||
|
data_source_binding = session.execute(
|
||||||
|
select(DataSourceOauthBinding).filter_by(id=binding_id)
|
||||||
|
).scalar_one_or_none()
|
||||||
if data_source_binding is None:
|
if data_source_binding is None:
|
||||||
raise NotFound("Data source binding not found.")
|
raise NotFound("Data source binding not found.")
|
||||||
# enable binding
|
# enable binding
|
||||||
@@ -108,6 +113,7 @@ class DataSourceNotionListApi(Resource):
|
|||||||
def get(self):
|
def get(self):
|
||||||
dataset_id = request.args.get("dataset_id", default=None, type=str)
|
dataset_id = request.args.get("dataset_id", default=None, type=str)
|
||||||
exist_page_ids = []
|
exist_page_ids = []
|
||||||
|
with Session(db.engine) as session:
|
||||||
# import notion in the exist dataset
|
# import notion in the exist dataset
|
||||||
if dataset_id:
|
if dataset_id:
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
@@ -115,19 +121,24 @@ class DataSourceNotionListApi(Resource):
|
|||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
if dataset.data_source_type != "notion_import":
|
if dataset.data_source_type != "notion_import":
|
||||||
raise ValueError("Dataset is not notion type.")
|
raise ValueError("Dataset is not notion type.")
|
||||||
documents = Document.query.filter_by(
|
|
||||||
|
documents = session.execute(
|
||||||
|
select(Document).filter_by(
|
||||||
dataset_id=dataset_id,
|
dataset_id=dataset_id,
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
data_source_type="notion_import",
|
data_source_type="notion_import",
|
||||||
enabled=True,
|
enabled=True,
|
||||||
|
)
|
||||||
).all()
|
).all()
|
||||||
if documents:
|
if documents:
|
||||||
for document in documents:
|
for document in documents:
|
||||||
data_source_info = json.loads(document.data_source_info)
|
data_source_info = json.loads(document.data_source_info)
|
||||||
exist_page_ids.append(data_source_info["notion_page_id"])
|
exist_page_ids.append(data_source_info["notion_page_id"])
|
||||||
# get all authorized pages
|
# get all authorized pages
|
||||||
data_source_bindings = DataSourceOauthBinding.query.filter_by(
|
data_source_bindings = session.scalars(
|
||||||
|
select(DataSourceOauthBinding).filter_by(
|
||||||
tenant_id=current_user.current_tenant_id, provider="notion", disabled=False
|
tenant_id=current_user.current_tenant_id, provider="notion", disabled=False
|
||||||
|
)
|
||||||
).all()
|
).all()
|
||||||
if not data_source_bindings:
|
if not data_source_bindings:
|
||||||
return {"notion_info": []}, 200
|
return {"notion_info": []}, 200
|
||||||
@@ -158,14 +169,17 @@ class DataSourceNotionApi(Resource):
|
|||||||
def get(self, workspace_id, page_id, page_type):
|
def get(self, workspace_id, page_id, page_type):
|
||||||
workspace_id = str(workspace_id)
|
workspace_id = str(workspace_id)
|
||||||
page_id = str(page_id)
|
page_id = str(page_id)
|
||||||
data_source_binding = DataSourceOauthBinding.query.filter(
|
with Session(db.engine) as session:
|
||||||
|
data_source_binding = session.execute(
|
||||||
|
select(DataSourceOauthBinding).filter(
|
||||||
db.and_(
|
db.and_(
|
||||||
DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
|
DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
|
||||||
DataSourceOauthBinding.provider == "notion",
|
DataSourceOauthBinding.provider == "notion",
|
||||||
DataSourceOauthBinding.disabled == False,
|
DataSourceOauthBinding.disabled == False,
|
||||||
DataSourceOauthBinding.source_info["workspace_id"] == f'"{workspace_id}"',
|
DataSourceOauthBinding.source_info["workspace_id"] == f'"{workspace_id}"',
|
||||||
)
|
)
|
||||||
).first()
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
if not data_source_binding:
|
if not data_source_binding:
|
||||||
raise NotFound("Data source binding not found.")
|
raise NotFound("Data source binding not found.")
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from controllers.console.wraps import account_initialization_required, enterpris
|
|||||||
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
|
||||||
|
from core.plugin.entities.plugin import ModelProviderID
|
||||||
from core.provider_manager import ProviderManager
|
from core.provider_manager import ProviderManager
|
||||||
from core.rag.datasource.vdb.vector_type import VectorType
|
from core.rag.datasource.vdb.vector_type import VectorType
|
||||||
from core.rag.extractor.entity.extract_setting import ExtractSetting
|
from core.rag.extractor.entity.extract_setting import ExtractSetting
|
||||||
@@ -52,12 +53,12 @@ class DatasetListApi(Resource):
|
|||||||
# provider = request.args.get("provider", default="vendor")
|
# provider = request.args.get("provider", default="vendor")
|
||||||
search = request.args.get("keyword", default=None, type=str)
|
search = request.args.get("keyword", default=None, type=str)
|
||||||
tag_ids = request.args.getlist("tag_ids")
|
tag_ids = request.args.getlist("tag_ids")
|
||||||
|
include_all = request.args.get("include_all", default="false").lower() == "true"
|
||||||
if ids:
|
if ids:
|
||||||
datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id)
|
datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id)
|
||||||
else:
|
else:
|
||||||
datasets, total = DatasetService.get_datasets(
|
datasets, total = DatasetService.get_datasets(
|
||||||
page, limit, current_user.current_tenant_id, current_user, search, tag_ids
|
page, limit, current_user.current_tenant_id, current_user, search, tag_ids, include_all
|
||||||
)
|
)
|
||||||
|
|
||||||
# check embedding setting
|
# check embedding setting
|
||||||
@@ -72,7 +73,9 @@ class DatasetListApi(Resource):
|
|||||||
|
|
||||||
data = marshal(datasets, dataset_detail_fields)
|
data = marshal(datasets, dataset_detail_fields)
|
||||||
for item in data:
|
for item in data:
|
||||||
|
# convert embedding_model_provider to plugin standard format
|
||||||
if item["indexing_technique"] == "high_quality":
|
if item["indexing_technique"] == "high_quality":
|
||||||
|
item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"]))
|
||||||
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
||||||
if item_model in model_names:
|
if item_model in model_names:
|
||||||
item["embedding_available"] = True
|
item["embedding_available"] = True
|
||||||
@@ -457,7 +460,7 @@ class DatasetIndexingEstimateApi(Resource):
|
|||||||
)
|
)
|
||||||
except LLMBadRequestError:
|
except LLMBadRequestError:
|
||||||
raise ProviderNotInitializeError(
|
raise ProviderNotInitializeError(
|
||||||
"No Embedding Model available. Please configure a valid provider " "in the Settings -> Model Provider."
|
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -619,9 +622,7 @@ class DatasetRetrievalSettingApi(Resource):
|
|||||||
vector_type = dify_config.VECTOR_STORE
|
vector_type = dify_config.VECTOR_STORE
|
||||||
match vector_type:
|
match vector_type:
|
||||||
case (
|
case (
|
||||||
VectorType.MILVUS
|
VectorType.RELYT
|
||||||
| VectorType.RELYT
|
|
||||||
| VectorType.PGVECTOR
|
|
||||||
| VectorType.TIDB_VECTOR
|
| VectorType.TIDB_VECTOR
|
||||||
| VectorType.CHROMA
|
| VectorType.CHROMA
|
||||||
| VectorType.TENCENT
|
| VectorType.TENCENT
|
||||||
@@ -645,6 +646,7 @@ class DatasetRetrievalSettingApi(Resource):
|
|||||||
| VectorType.TIDB_ON_QDRANT
|
| VectorType.TIDB_ON_QDRANT
|
||||||
| VectorType.LINDORM
|
| VectorType.LINDORM
|
||||||
| VectorType.COUCHBASE
|
| VectorType.COUCHBASE
|
||||||
|
| VectorType.MILVUS
|
||||||
):
|
):
|
||||||
return {
|
return {
|
||||||
"retrieval_method": [
|
"retrieval_method": [
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from flask import request
|
|||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user # type: ignore
|
||||||
from flask_restful import Resource, fields, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import Resource, fields, marshal, marshal_with, reqparse # type: ignore
|
||||||
from sqlalchemy import asc, desc
|
from sqlalchemy import asc, desc
|
||||||
from transformers.hf_argparser import string_to_bool # type: ignore
|
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@@ -40,6 +39,7 @@ from core.indexing_runner import IndexingRunner
|
|||||||
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 core.model_runtime.errors.invoke import InvokeAuthorizationError
|
from core.model_runtime.errors.invoke import InvokeAuthorizationError
|
||||||
|
from core.plugin.manager.exc import PluginDaemonClientSideError
|
||||||
from core.rag.extractor.entity.extract_setting import ExtractSetting
|
from core.rag.extractor.entity.extract_setting import ExtractSetting
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
@@ -150,8 +150,20 @@ class DatasetDocumentListApi(Resource):
|
|||||||
sort = request.args.get("sort", default="-created_at", type=str)
|
sort = request.args.get("sort", default="-created_at", type=str)
|
||||||
# "yes", "true", "t", "y", "1" convert to True, while others convert to False.
|
# "yes", "true", "t", "y", "1" convert to True, while others convert to False.
|
||||||
try:
|
try:
|
||||||
fetch = string_to_bool(request.args.get("fetch", default="false"))
|
fetch_val = request.args.get("fetch", default="false")
|
||||||
except (ArgumentTypeError, ValueError, Exception) as e:
|
if isinstance(fetch_val, bool):
|
||||||
|
fetch = fetch_val
|
||||||
|
else:
|
||||||
|
if fetch_val.lower() in ("yes", "true", "t", "y", "1"):
|
||||||
|
fetch = True
|
||||||
|
elif fetch_val.lower() in ("no", "false", "f", "n", "0"):
|
||||||
|
fetch = False
|
||||||
|
else:
|
||||||
|
raise ArgumentTypeError(
|
||||||
|
f"Truthy value expected: got {fetch_val} but expected one of yes/no, true/false, t/f, y/n, 1/0 "
|
||||||
|
f"(case insensitive)."
|
||||||
|
)
|
||||||
|
except (ArgumentTypeError, ValueError, Exception):
|
||||||
fetch = False
|
fetch = False
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
if not dataset:
|
if not dataset:
|
||||||
@@ -350,8 +362,7 @@ class DatasetInitApi(Resource):
|
|||||||
)
|
)
|
||||||
except InvokeAuthorizationError:
|
except InvokeAuthorizationError:
|
||||||
raise ProviderNotInitializeError(
|
raise ProviderNotInitializeError(
|
||||||
"No Embedding Model available. Please configure a valid provider "
|
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -430,6 +441,8 @@ class DocumentIndexingEstimateApi(DocumentResource):
|
|||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
except PluginDaemonClientSideError as ex:
|
||||||
|
raise ProviderNotInitializeError(ex.description)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IndexingEstimateError(str(e))
|
raise IndexingEstimateError(str(e))
|
||||||
|
|
||||||
@@ -526,11 +539,12 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
|
|||||||
return response.model_dump(), 200
|
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
except PluginDaemonClientSideError as ex:
|
||||||
|
raise ProviderNotInitializeError(ex.description)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IndexingEstimateError(str(e))
|
raise IndexingEstimateError(str(e))
|
||||||
|
|
||||||
@@ -603,7 +617,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
raise InvalidMetadataError(f"Invalid metadata value: {metadata}")
|
raise InvalidMetadataError(f"Invalid metadata value: {metadata}")
|
||||||
|
|
||||||
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_details}
|
||||||
elif metadata == "without":
|
elif metadata == "without":
|
||||||
dataset_process_rules = DatasetService.get_process_rules(dataset_id)
|
dataset_process_rules = DatasetService.get_process_rules(dataset_id)
|
||||||
document_process_rules = document.dataset_process_rule.to_dict()
|
document_process_rules = document.dataset_process_rule.to_dict()
|
||||||
@@ -664,7 +678,7 @@ class DocumentDetailApi(DocumentResource):
|
|||||||
"disabled_by": document.disabled_by,
|
"disabled_by": document.disabled_by,
|
||||||
"archived": document.archived,
|
"archived": document.archived,
|
||||||
"doc_type": document.doc_type,
|
"doc_type": document.doc_type,
|
||||||
"doc_metadata": document.doc_metadata,
|
"doc_metadata": document.doc_metadata_details,
|
||||||
"segment_count": document.segment_count,
|
"segment_count": document.segment_count,
|
||||||
"average_segment_length": document.average_segment_length,
|
"average_segment_length": document.average_segment_length,
|
||||||
"hit_count": document.hit_count,
|
"hit_count": document.hit_count,
|
||||||
|
|||||||
@@ -168,8 +168,7 @@ class DatasetDocumentSegmentApi(Resource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -217,8 +216,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -267,8 +265,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -368,9 +365,9 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
|||||||
result = []
|
result = []
|
||||||
for index, row in df.iterrows():
|
for index, row in df.iterrows():
|
||||||
if document.doc_form == "qa_model":
|
if document.doc_form == "qa_model":
|
||||||
data = {"content": row[0], "answer": row[1]}
|
data = {"content": row.iloc[0], "answer": row.iloc[1]}
|
||||||
else:
|
else:
|
||||||
data = {"content": row[0]}
|
data = {"content": row.iloc[0]}
|
||||||
result.append(data)
|
result.append(data)
|
||||||
if len(result) == 0:
|
if len(result) == 0:
|
||||||
raise ValueError("The CSV file is empty.")
|
raise ValueError("The CSV file is empty.")
|
||||||
@@ -437,8 +434,7 @@ class ChildChunkAddApi(Resource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
|||||||
143
api/controllers/console/datasets/metadata.py
Normal file
143
api/controllers/console/datasets/metadata.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
from flask_login import current_user # type: ignore # type: ignore
|
||||||
|
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
|
||||||
|
from fields.dataset_fields import dataset_metadata_fields
|
||||||
|
from libs.login import login_required
|
||||||
|
from services.dataset_service import DatasetService
|
||||||
|
from services.entities.knowledge_entities.knowledge_entities import (
|
||||||
|
MetadataArgs,
|
||||||
|
MetadataOperationData,
|
||||||
|
)
|
||||||
|
from services.metadata_service import MetadataService
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_name(name):
|
||||||
|
if not name or len(name) < 1 or len(name) > 40:
|
||||||
|
raise ValueError("Name must be between 1 to 40 characters.")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_description_length(description):
|
||||||
|
if len(description) > 400:
|
||||||
|
raise ValueError("Description cannot exceed 400 characters.")
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
@marshal_with(dataset_metadata_fields)
|
||||||
|
def post(self, dataset_id):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("type", type=str, required=True, nullable=True, location="json")
|
||||||
|
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
metadata_args = MetadataArgs(**args)
|
||||||
|
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
metadata = MetadataService.create_metadata(dataset_id_str, metadata_args)
|
||||||
|
return metadata, 201
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
def patch(self, dataset_id, metadata_id):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
metadata_id_str = str(metadata_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name"))
|
||||||
|
return metadata, 200
|
||||||
|
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
def delete(self, dataset_id, metadata_id):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
metadata_id_str = str(metadata_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
MetadataService.delete_metadata(dataset_id_str, metadata_id_str)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataBuiltInFieldApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
def get(self):
|
||||||
|
built_in_fields = MetadataService.get_built_in_fields()
|
||||||
|
return built_in_fields, 200
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataBuiltInFieldActionApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
def post(self, dataset_id, action):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
if action == "enable":
|
||||||
|
MetadataService.enable_built_in_field(dataset)
|
||||||
|
elif action == "disable":
|
||||||
|
MetadataService.disable_built_in_field(dataset)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentMetadataApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@enterprise_license_required
|
||||||
|
def post(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.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("operation_data", type=list, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
metadata_args = MetadataOperationData(**args)
|
||||||
|
|
||||||
|
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||||
|
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(DatasetListApi, "/datasets/<uuid:dataset_id>/metadata")
|
||||||
|
api.add_resource(DatasetMetadataApi, "/datasets/<uuid:dataset_id>/metadata/<uuid:metadata_id>")
|
||||||
|
api.add_resource(DatasetMetadataBuiltInFieldApi, "/datasets/metadata/built-in")
|
||||||
|
api.add_resource(DatasetMetadataBuiltInFieldActionApi, "/datasets/metadata/built-in/<string:action>")
|
||||||
|
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/metadata")
|
||||||
@@ -32,7 +32,7 @@ class ConversationListApi(InstalledAppResource):
|
|||||||
|
|
||||||
pinned = None
|
pinned = None
|
||||||
if "pinned" in args and args["pinned"] is not None:
|
if "pinned" in args and args["pinned"] is not None:
|
||||||
pinned = True if args["pinned"] == "true" else False
|
pinned = args["pinned"] == "true"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class MessageListApi(InstalledAppResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
|
||||||
)
|
)
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import os
|
|||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
from extensions.ext_database import db
|
||||||
from libs.helper import StrLen
|
from libs.helper import StrLen
|
||||||
from models.model import DifySetup
|
from models.model import DifySetup
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
@@ -42,7 +45,11 @@ class InitValidateAPI(Resource):
|
|||||||
def get_init_validate_status():
|
def get_init_validate_status():
|
||||||
if dify_config.EDITION == "SELF_HOSTED":
|
if dify_config.EDITION == "SELF_HOSTED":
|
||||||
if os.environ.get("INIT_PASSWORD"):
|
if os.environ.get("INIT_PASSWORD"):
|
||||||
return session.get("is_init_validated") or DifySetup.query.first()
|
if session.get("is_init_validated"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
with Session(db.engine) as db_session:
|
||||||
|
return db_session.execute(select(DifySetup)).scalar_one_or_none()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from flask_restful import Resource, reqparse # type: ignore
|
|||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from libs.helper import StrLen, email, extract_remote_ip
|
from libs.helper import StrLen, email, extract_remote_ip
|
||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
from models.model import DifySetup
|
from models.model import DifySetup, db
|
||||||
from services.account_service import RegisterService, TenantService
|
from services.account_service import RegisterService, TenantService
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
@@ -52,7 +52,8 @@ class SetupApi(Resource):
|
|||||||
|
|
||||||
def get_setup_status():
|
def get_setup_status():
|
||||||
if dify_config.EDITION == "SELF_HOSTED":
|
if dify_config.EDITION == "SELF_HOSTED":
|
||||||
return DifySetup.query.first()
|
return db.session.query(DifySetup).first()
|
||||||
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.account import TenantPluginPermission
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_permission_required(
|
||||||
|
install_required: bool = False,
|
||||||
|
debug_required: bool = False,
|
||||||
|
):
|
||||||
|
def interceptor(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
user = current_user
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
permission = (
|
||||||
|
session.query(TenantPluginPermission)
|
||||||
|
.filter(
|
||||||
|
TenantPluginPermission.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not permission:
|
||||||
|
# no permission set, allow access for everyone
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
if install_required:
|
||||||
|
if permission.install_permission == TenantPluginPermission.InstallPermission.NOBODY:
|
||||||
|
raise Forbidden()
|
||||||
|
if permission.install_permission == TenantPluginPermission.InstallPermission.ADMINS:
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
if permission.install_permission == TenantPluginPermission.InstallPermission.EVERYONE:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if debug_required:
|
||||||
|
if permission.debug_permission == TenantPluginPermission.DebugPermission.NOBODY:
|
||||||
|
raise Forbidden()
|
||||||
|
if permission.debug_permission == TenantPluginPermission.DebugPermission.ADMINS:
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
if permission.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
return interceptor
|
||||||
|
|||||||
36
api/controllers/console/workspace/agent_providers.py
Normal file
36
api/controllers/console/workspace/agent_providers.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
from flask_restful import Resource # type: ignore
|
||||||
|
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from libs.login import login_required
|
||||||
|
from services.agent_service import AgentService
|
||||||
|
|
||||||
|
|
||||||
|
class AgentProviderListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
|
return jsonable_encoder(AgentService.list_agent_providers(user_id, tenant_id))
|
||||||
|
|
||||||
|
|
||||||
|
class AgentProviderApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, provider_name: str):
|
||||||
|
user = current_user
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
return jsonable_encoder(AgentService.get_agent_provider(user_id, tenant_id, provider_name))
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(AgentProviderListApi, "/workspaces/current/agent-providers")
|
||||||
|
api.add_resource(AgentProviderApi, "/workspaces/current/agent-provider/<path:provider_name>")
|
||||||
205
api/controllers/console/workspace/endpoint.py
Normal file
205
api/controllers/console/workspace/endpoint.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from libs.login import login_required
|
||||||
|
from services.plugin.endpoint_service import EndpointService
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointCreateApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("plugin_unique_identifier", type=str, required=True)
|
||||||
|
parser.add_argument("settings", type=dict, required=True)
|
||||||
|
parser.add_argument("name", type=str, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
plugin_unique_identifier = args["plugin_unique_identifier"]
|
||||||
|
settings = args["settings"]
|
||||||
|
name = args["name"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": EndpointService.create_endpoint(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
user_id=user.id,
|
||||||
|
plugin_unique_identifier=plugin_unique_identifier,
|
||||||
|
name=name,
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("page", type=int, required=True, location="args")
|
||||||
|
parser.add_argument("page_size", type=int, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
page = args["page"]
|
||||||
|
page_size = args["page_size"]
|
||||||
|
|
||||||
|
return jsonable_encoder(
|
||||||
|
{
|
||||||
|
"endpoints": EndpointService.list_endpoints(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
user_id=user.id,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointListForSinglePluginApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("page", type=int, required=True, location="args")
|
||||||
|
parser.add_argument("page_size", type=int, required=True, location="args")
|
||||||
|
parser.add_argument("plugin_id", type=str, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
page = args["page"]
|
||||||
|
page_size = args["page_size"]
|
||||||
|
plugin_id = args["plugin_id"]
|
||||||
|
|
||||||
|
return jsonable_encoder(
|
||||||
|
{
|
||||||
|
"endpoints": EndpointService.list_endpoints_for_single_plugin(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
user_id=user.id,
|
||||||
|
plugin_id=plugin_id,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointDeleteApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("endpoint_id", type=str, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
endpoint_id = args["endpoint_id"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": EndpointService.delete_endpoint(
|
||||||
|
tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointUpdateApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("endpoint_id", type=str, required=True)
|
||||||
|
parser.add_argument("settings", type=dict, required=True)
|
||||||
|
parser.add_argument("name", type=str, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
endpoint_id = args["endpoint_id"]
|
||||||
|
settings = args["settings"]
|
||||||
|
name = args["name"]
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": EndpointService.update_endpoint(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
user_id=user.id,
|
||||||
|
endpoint_id=endpoint_id,
|
||||||
|
name=name,
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointEnableApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("endpoint_id", type=str, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
endpoint_id = args["endpoint_id"]
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": EndpointService.enable_endpoint(
|
||||||
|
tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointDisableApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("endpoint_id", type=str, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
endpoint_id = args["endpoint_id"]
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": EndpointService.disable_endpoint(
|
||||||
|
tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(EndpointCreateApi, "/workspaces/current/endpoints/create")
|
||||||
|
api.add_resource(EndpointListApi, "/workspaces/current/endpoints/list")
|
||||||
|
api.add_resource(EndpointListForSinglePluginApi, "/workspaces/current/endpoints/list/plugin")
|
||||||
|
api.add_resource(EndpointDeleteApi, "/workspaces/current/endpoints/delete")
|
||||||
|
api.add_resource(EndpointUpdateApi, "/workspaces/current/endpoints/update")
|
||||||
|
api.add_resource(EndpointEnableApi, "/workspaces/current/endpoints/enable")
|
||||||
|
api.add_resource(EndpointDisableApi, "/workspaces/current/endpoints/disable")
|
||||||
@@ -112,10 +112,10 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
|
|||||||
# Load Balancing Config
|
# Load Balancing Config
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
LoadBalancingCredentialsValidateApi,
|
LoadBalancingCredentialsValidateApi,
|
||||||
"/workspaces/current/model-providers/<string:provider>/models/load-balancing-configs/credentials-validate",
|
"/workspaces/current/model-providers/<path:provider>/models/load-balancing-configs/credentials-validate",
|
||||||
)
|
)
|
||||||
|
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
LoadBalancingConfigCredentialsValidateApi,
|
LoadBalancingConfigCredentialsValidateApi,
|
||||||
"/workspaces/current/model-providers/<string:provider>/models/load-balancing-configs/<string:config_id>/credentials-validate",
|
"/workspaces/current/model-providers/<path:provider>/models/load-balancing-configs/<string:config_id>/credentials-validate",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class ModelProviderValidateApi(Resource):
|
|||||||
response = {"result": "success" if result else "error"}
|
response = {"result": "success" if result else "error"}
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
response["error"] = error
|
response["error"] = error or "Unknown error"
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -125,9 +125,10 @@ class ModelProviderIconApi(Resource):
|
|||||||
Get model provider icon
|
Get model provider icon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, provider: str, icon_type: str, lang: str):
|
def get(self, tenant_id: str, provider: str, icon_type: str, lang: str):
|
||||||
model_provider_service = ModelProviderService()
|
model_provider_service = ModelProviderService()
|
||||||
icon, mimetype = model_provider_service.get_model_provider_icon(
|
icon, mimetype = model_provider_service.get_model_provider_icon(
|
||||||
|
tenant_id=tenant_id,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
icon_type=icon_type,
|
icon_type=icon_type,
|
||||||
lang=lang,
|
lang=lang,
|
||||||
@@ -183,53 +184,17 @@ class ModelProviderPaymentCheckoutUrlApi(Resource):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ModelProviderFreeQuotaSubmitApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def post(self, provider: str):
|
|
||||||
model_provider_service = ModelProviderService()
|
|
||||||
result = model_provider_service.free_quota_submit(tenant_id=current_user.current_tenant_id, provider=provider)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class ModelProviderFreeQuotaQualificationVerifyApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
def get(self, provider: str):
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("token", type=str, required=False, nullable=True, location="args")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
model_provider_service = ModelProviderService()
|
|
||||||
result = model_provider_service.free_quota_qualification_verify(
|
|
||||||
tenant_id=current_user.current_tenant_id, provider=provider, token=args["token"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ModelProviderListApi, "/workspaces/current/model-providers")
|
api.add_resource(ModelProviderListApi, "/workspaces/current/model-providers")
|
||||||
|
|
||||||
api.add_resource(ModelProviderCredentialApi, "/workspaces/current/model-providers/<string:provider>/credentials")
|
api.add_resource(ModelProviderCredentialApi, "/workspaces/current/model-providers/<path:provider>/credentials")
|
||||||
api.add_resource(ModelProviderValidateApi, "/workspaces/current/model-providers/<string:provider>/credentials/validate")
|
api.add_resource(ModelProviderValidateApi, "/workspaces/current/model-providers/<path:provider>/credentials/validate")
|
||||||
api.add_resource(ModelProviderApi, "/workspaces/current/model-providers/<string:provider>")
|
api.add_resource(ModelProviderApi, "/workspaces/current/model-providers/<path:provider>")
|
||||||
api.add_resource(
|
|
||||||
ModelProviderIconApi, "/workspaces/current/model-providers/<string:provider>/<string:icon_type>/<string:lang>"
|
|
||||||
)
|
|
||||||
|
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
PreferredProviderTypeUpdateApi, "/workspaces/current/model-providers/<string:provider>/preferred-provider-type"
|
PreferredProviderTypeUpdateApi, "/workspaces/current/model-providers/<path:provider>/preferred-provider-type"
|
||||||
)
|
)
|
||||||
|
api.add_resource(ModelProviderPaymentCheckoutUrlApi, "/workspaces/current/model-providers/<path:provider>/checkout-url")
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderPaymentCheckoutUrlApi, "/workspaces/current/model-providers/<string:provider>/checkout-url"
|
ModelProviderIconApi,
|
||||||
)
|
"/workspaces/<string:tenant_id>/model-providers/<path:provider>/<string:icon_type>/<string:lang>",
|
||||||
api.add_resource(
|
|
||||||
ModelProviderFreeQuotaSubmitApi, "/workspaces/current/model-providers/<string:provider>/free-quota-submit"
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ModelProviderFreeQuotaQualificationVerifyApi,
|
|
||||||
"/workspaces/current/model-providers/<string:provider>/free-quota-qualification-verify",
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ class ModelProviderModelValidateApi(Resource):
|
|||||||
response = {"result": "success" if result else "error"}
|
response = {"result": "success" if result else "error"}
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
response["error"] = error
|
response["error"] = error or ""
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -362,26 +362,26 @@ class ModelProviderAvailableModelApi(Resource):
|
|||||||
return jsonable_encoder({"data": models})
|
return jsonable_encoder({"data": models})
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ModelProviderModelApi, "/workspaces/current/model-providers/<string:provider>/models")
|
api.add_resource(ModelProviderModelApi, "/workspaces/current/model-providers/<path:provider>/models")
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderModelEnableApi,
|
ModelProviderModelEnableApi,
|
||||||
"/workspaces/current/model-providers/<string:provider>/models/enable",
|
"/workspaces/current/model-providers/<path:provider>/models/enable",
|
||||||
endpoint="model-provider-model-enable",
|
endpoint="model-provider-model-enable",
|
||||||
)
|
)
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderModelDisableApi,
|
ModelProviderModelDisableApi,
|
||||||
"/workspaces/current/model-providers/<string:provider>/models/disable",
|
"/workspaces/current/model-providers/<path:provider>/models/disable",
|
||||||
endpoint="model-provider-model-disable",
|
endpoint="model-provider-model-disable",
|
||||||
)
|
)
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderModelCredentialApi, "/workspaces/current/model-providers/<string:provider>/models/credentials"
|
ModelProviderModelCredentialApi, "/workspaces/current/model-providers/<path:provider>/models/credentials"
|
||||||
)
|
)
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderModelValidateApi, "/workspaces/current/model-providers/<string:provider>/models/credentials/validate"
|
ModelProviderModelValidateApi, "/workspaces/current/model-providers/<path:provider>/models/credentials/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ModelProviderModelParameterRuleApi, "/workspaces/current/model-providers/<string:provider>/models/parameter-rules"
|
ModelProviderModelParameterRuleApi, "/workspaces/current/model-providers/<path:provider>/models/parameter-rules"
|
||||||
)
|
)
|
||||||
api.add_resource(ModelProviderAvailableModelApi, "/workspaces/current/models/model-types/<string:model_type>")
|
api.add_resource(ModelProviderAvailableModelApi, "/workspaces/current/models/model-types/<string:model_type>")
|
||||||
api.add_resource(DefaultModelApi, "/workspaces/current/default-model")
|
api.add_resource(DefaultModelApi, "/workspaces/current/default-model")
|
||||||
|
|||||||
475
api/controllers/console/workspace/plugin.py
Normal file
475
api/controllers/console/workspace/plugin.py
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
from flask import request, send_file
|
||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.workspace import plugin_permission_required
|
||||||
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from core.plugin.manager.exc import PluginDaemonClientSideError
|
||||||
|
from libs.login import login_required
|
||||||
|
from models.account import TenantPluginPermission
|
||||||
|
from services.plugin.plugin_permission_service import PluginPermissionService
|
||||||
|
from services.plugin.plugin_service import PluginService
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDebuggingKeyApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def get(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"key": PluginService.get_debugging_key(tenant_id),
|
||||||
|
"host": dify_config.PLUGIN_REMOTE_INSTALL_HOST,
|
||||||
|
"port": dify_config.PLUGIN_REMOTE_INSTALL_PORT,
|
||||||
|
}
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginListApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
try:
|
||||||
|
plugins = PluginService.list(tenant_id)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder({"plugins": plugins})
|
||||||
|
|
||||||
|
|
||||||
|
class PluginListInstallationsFromIdsApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("plugin_ids", type=list, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
plugins = PluginService.list_installations_from_ids(tenant_id, args["plugin_ids"])
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder({"plugins": plugins})
|
||||||
|
|
||||||
|
|
||||||
|
class PluginIconApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
def get(self):
|
||||||
|
req = reqparse.RequestParser()
|
||||||
|
req.add_argument("tenant_id", type=str, required=True, location="args")
|
||||||
|
req.add_argument("filename", type=str, required=True, location="args")
|
||||||
|
args = req.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
icon_bytes, mimetype = PluginService.get_asset(args["tenant_id"], args["filename"])
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
icon_cache_max_age = dify_config.TOOL_ICON_CACHE_MAX_AGE
|
||||||
|
return send_file(io.BytesIO(icon_bytes), mimetype=mimetype, max_age=icon_cache_max_age)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUploadFromPkgApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
file = request.files["pkg"]
|
||||||
|
|
||||||
|
# check file size
|
||||||
|
if file.content_length > dify_config.PLUGIN_MAX_PACKAGE_SIZE:
|
||||||
|
raise ValueError("File size exceeds the maximum allowed size")
|
||||||
|
|
||||||
|
content = file.read()
|
||||||
|
try:
|
||||||
|
response = PluginService.upload_pkg(tenant_id, content)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUploadFromGithubApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("repo", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("version", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("package", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = PluginService.upload_pkg_from_github(tenant_id, args["repo"], args["version"], args["package"])
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUploadFromBundleApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
file = request.files["bundle"]
|
||||||
|
|
||||||
|
# check file size
|
||||||
|
if file.content_length > dify_config.PLUGIN_MAX_BUNDLE_SIZE:
|
||||||
|
raise ValueError("File size exceeds the maximum allowed size")
|
||||||
|
|
||||||
|
content = file.read()
|
||||||
|
try:
|
||||||
|
response = PluginService.upload_bundle(tenant_id, content)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallFromPkgApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("plugin_unique_identifiers", type=list, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# check if all plugin_unique_identifiers are valid string
|
||||||
|
for plugin_unique_identifier in args["plugin_unique_identifiers"]:
|
||||||
|
if not isinstance(plugin_unique_identifier, str):
|
||||||
|
raise ValueError("Invalid plugin unique identifier")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = PluginService.install_from_local_pkg(tenant_id, args["plugin_unique_identifiers"])
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallFromGithubApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("repo", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("version", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("package", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("plugin_unique_identifier", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = PluginService.install_from_github(
|
||||||
|
tenant_id,
|
||||||
|
args["plugin_unique_identifier"],
|
||||||
|
args["repo"],
|
||||||
|
args["version"],
|
||||||
|
args["package"],
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallFromMarketplaceApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(install_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("plugin_unique_identifiers", type=list, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# check if all plugin_unique_identifiers are valid string
|
||||||
|
for plugin_unique_identifier in args["plugin_unique_identifiers"]:
|
||||||
|
if not isinstance(plugin_unique_identifier, str):
|
||||||
|
raise ValueError("Invalid plugin unique identifier")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = PluginService.install_from_marketplace_pkg(tenant_id, args["plugin_unique_identifiers"])
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder(response)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFetchManifestApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def get(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("plugin_unique_identifier", type=str, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
{
|
||||||
|
"manifest": PluginService.fetch_plugin_manifest(
|
||||||
|
tenant_id, args["plugin_unique_identifier"]
|
||||||
|
).model_dump()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFetchInstallTasksApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def get(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("page", type=int, required=True, location="args")
|
||||||
|
parser.add_argument("page_size", type=int, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
{"tasks": PluginService.fetch_install_tasks(tenant_id, args["page"], args["page_size"])}
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFetchInstallTaskApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def get(self, task_id: str):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonable_encoder({"task": PluginService.fetch_install_task(tenant_id, task_id)})
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDeleteInstallTaskApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self, task_id: str):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {"success": PluginService.delete_install_task(tenant_id, task_id)}
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDeleteAllInstallTaskItemsApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {"success": PluginService.delete_all_install_task_items(tenant_id)}
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDeleteInstallTaskItemApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self, task_id: str, identifier: str):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {"success": PluginService.delete_install_task_item(tenant_id, task_id, identifier)}
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUpgradeFromMarketplaceApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("original_plugin_unique_identifier", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("new_plugin_unique_identifier", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
PluginService.upgrade_plugin_with_marketplace(
|
||||||
|
tenant_id, args["original_plugin_unique_identifier"], args["new_plugin_unique_identifier"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUpgradeFromGithubApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("original_plugin_unique_identifier", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("new_plugin_unique_identifier", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("repo", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("version", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("package", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
PluginService.upgrade_plugin_with_github(
|
||||||
|
tenant_id,
|
||||||
|
args["original_plugin_unique_identifier"],
|
||||||
|
args["new_plugin_unique_identifier"],
|
||||||
|
args["repo"],
|
||||||
|
args["version"],
|
||||||
|
args["package"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUninstallApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@plugin_permission_required(debug_required=True)
|
||||||
|
def post(self):
|
||||||
|
req = reqparse.RequestParser()
|
||||||
|
req.add_argument("plugin_installation_id", type=str, required=True, location="json")
|
||||||
|
args = req.parse_args()
|
||||||
|
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {"success": PluginService.uninstall(tenant_id, args["plugin_installation_id"])}
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginChangePermissionApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
user = current_user
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
req = reqparse.RequestParser()
|
||||||
|
req.add_argument("install_permission", type=str, required=True, location="json")
|
||||||
|
req.add_argument("debug_permission", type=str, required=True, location="json")
|
||||||
|
args = req.parse_args()
|
||||||
|
|
||||||
|
install_permission = TenantPluginPermission.InstallPermission(args["install_permission"])
|
||||||
|
debug_permission = TenantPluginPermission.DebugPermission(args["debug_permission"])
|
||||||
|
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
|
return {"success": PluginPermissionService.change_permission(tenant_id, install_permission, debug_permission)}
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFetchPermissionApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self):
|
||||||
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
permission = PluginPermissionService.get_permission(tenant_id)
|
||||||
|
if not permission:
|
||||||
|
return jsonable_encoder(
|
||||||
|
{
|
||||||
|
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||||
|
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonable_encoder(
|
||||||
|
{
|
||||||
|
"install_permission": permission.install_permission,
|
||||||
|
"debug_permission": permission.debug_permission,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
|
||||||
|
api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
|
||||||
|
api.add_resource(PluginListInstallationsFromIdsApi, "/workspaces/current/plugin/list/installations/ids")
|
||||||
|
api.add_resource(PluginIconApi, "/workspaces/current/plugin/icon")
|
||||||
|
api.add_resource(PluginUploadFromPkgApi, "/workspaces/current/plugin/upload/pkg")
|
||||||
|
api.add_resource(PluginUploadFromGithubApi, "/workspaces/current/plugin/upload/github")
|
||||||
|
api.add_resource(PluginUploadFromBundleApi, "/workspaces/current/plugin/upload/bundle")
|
||||||
|
api.add_resource(PluginInstallFromPkgApi, "/workspaces/current/plugin/install/pkg")
|
||||||
|
api.add_resource(PluginInstallFromGithubApi, "/workspaces/current/plugin/install/github")
|
||||||
|
api.add_resource(PluginUpgradeFromMarketplaceApi, "/workspaces/current/plugin/upgrade/marketplace")
|
||||||
|
api.add_resource(PluginUpgradeFromGithubApi, "/workspaces/current/plugin/upgrade/github")
|
||||||
|
api.add_resource(PluginInstallFromMarketplaceApi, "/workspaces/current/plugin/install/marketplace")
|
||||||
|
api.add_resource(PluginFetchManifestApi, "/workspaces/current/plugin/fetch-manifest")
|
||||||
|
api.add_resource(PluginFetchInstallTasksApi, "/workspaces/current/plugin/tasks")
|
||||||
|
api.add_resource(PluginFetchInstallTaskApi, "/workspaces/current/plugin/tasks/<task_id>")
|
||||||
|
api.add_resource(PluginDeleteInstallTaskApi, "/workspaces/current/plugin/tasks/<task_id>/delete")
|
||||||
|
api.add_resource(PluginDeleteAllInstallTaskItemsApi, "/workspaces/current/plugin/tasks/delete_all")
|
||||||
|
api.add_resource(PluginDeleteInstallTaskItemApi, "/workspaces/current/plugin/tasks/<task_id>/delete/<path:identifier>")
|
||||||
|
api.add_resource(PluginUninstallApi, "/workspaces/current/plugin/uninstall")
|
||||||
|
|
||||||
|
api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change")
|
||||||
|
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")
|
||||||
@@ -25,8 +25,10 @@ class ToolProviderListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
req = reqparse.RequestParser()
|
req = reqparse.RequestParser()
|
||||||
req.add_argument(
|
req.add_argument(
|
||||||
@@ -47,28 +49,43 @@ class ToolBuiltinProviderListToolsApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self, provider):
|
def get(self, provider):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
BuiltinToolManageService.list_builtin_tool_provider_tools(
|
BuiltinToolManageService.list_builtin_tool_provider_tools(
|
||||||
user_id,
|
|
||||||
tenant_id,
|
tenant_id,
|
||||||
provider,
|
provider,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBuiltinProviderInfoApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def get(self, provider):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
|
return jsonable_encoder(BuiltinToolManageService.get_builtin_tool_provider_info(user_id, tenant_id, provider))
|
||||||
|
|
||||||
|
|
||||||
class ToolBuiltinProviderDeleteApi(Resource):
|
class ToolBuiltinProviderDeleteApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider):
|
def post(self, provider):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
return BuiltinToolManageService.delete_builtin_tool_provider(
|
return BuiltinToolManageService.delete_builtin_tool_provider(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -82,11 +99,13 @@ class ToolBuiltinProviderUpdateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider):
|
def post(self, provider):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||||
@@ -131,11 +150,13 @@ class ToolApiProviderAddApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||||
@@ -168,6 +189,11 @@ class ToolApiProviderGetRemoteSchemaApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
|
user = current_user
|
||||||
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
|
|
||||||
parser.add_argument("url", type=str, required=True, nullable=False, location="args")
|
parser.add_argument("url", type=str, required=True, nullable=False, location="args")
|
||||||
@@ -175,8 +201,8 @@ class ToolApiProviderGetRemoteSchemaApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
return ApiToolManageService.get_api_tool_provider_remote_schema(
|
return ApiToolManageService.get_api_tool_provider_remote_schema(
|
||||||
current_user.id,
|
user_id,
|
||||||
current_user.current_tenant_id,
|
tenant_id,
|
||||||
args["url"],
|
args["url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -186,8 +212,10 @@ class ToolApiProviderListToolsApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
|
|
||||||
@@ -209,11 +237,13 @@ class ToolApiProviderUpdateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||||
@@ -248,11 +278,13 @@ class ToolApiProviderDeleteApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
|
|
||||||
@@ -272,8 +304,10 @@ class ToolApiProviderGetApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
|
|
||||||
@@ -293,7 +327,11 @@ class ToolBuiltinProviderCredentialsSchemaApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self, provider):
|
def get(self, provider):
|
||||||
return BuiltinToolManageService.list_builtin_provider_credentials_schema(provider)
|
user = current_user
|
||||||
|
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
|
return BuiltinToolManageService.list_builtin_provider_credentials_schema(provider, tenant_id)
|
||||||
|
|
||||||
|
|
||||||
class ToolApiProviderSchemaApi(Resource):
|
class ToolApiProviderSchemaApi(Resource):
|
||||||
@@ -344,11 +382,13 @@ class ToolWorkflowProviderCreateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
reqparser = reqparse.RequestParser()
|
reqparser = reqparse.RequestParser()
|
||||||
reqparser.add_argument("workflow_app_id", type=uuid_value, required=True, nullable=False, location="json")
|
reqparser.add_argument("workflow_app_id", type=uuid_value, required=True, nullable=False, location="json")
|
||||||
@@ -381,11 +421,13 @@ class ToolWorkflowProviderUpdateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
reqparser = reqparse.RequestParser()
|
reqparser = reqparse.RequestParser()
|
||||||
reqparser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="json")
|
reqparser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="json")
|
||||||
@@ -421,11 +463,13 @@ class ToolWorkflowProviderDeleteApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
if not current_user.is_admin_or_owner:
|
user = current_user
|
||||||
|
|
||||||
|
if not user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = user.id
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
reqparser = reqparse.RequestParser()
|
reqparser = reqparse.RequestParser()
|
||||||
reqparser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="json")
|
reqparser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="json")
|
||||||
@@ -444,8 +488,10 @@ class ToolWorkflowProviderGetApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("workflow_tool_id", type=uuid_value, required=False, nullable=True, location="args")
|
parser.add_argument("workflow_tool_id", type=uuid_value, required=False, nullable=True, location="args")
|
||||||
@@ -476,8 +522,10 @@ class ToolWorkflowProviderListToolApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="args")
|
parser.add_argument("workflow_tool_id", type=uuid_value, required=True, nullable=False, location="args")
|
||||||
@@ -498,8 +546,10 @@ class ToolBuiltinListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
[
|
[
|
||||||
@@ -517,8 +567,10 @@ class ToolApiListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
[
|
[
|
||||||
@@ -536,8 +588,10 @@ class ToolWorkflowListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
user_id = current_user.id
|
user = current_user
|
||||||
tenant_id = current_user.current_tenant_id
|
|
||||||
|
user_id = user.id
|
||||||
|
tenant_id = user.current_tenant_id
|
||||||
|
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
[
|
[
|
||||||
@@ -563,16 +617,18 @@ class ToolLabelsApi(Resource):
|
|||||||
api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers")
|
api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers")
|
||||||
|
|
||||||
# builtin tool provider
|
# builtin tool provider
|
||||||
api.add_resource(ToolBuiltinProviderListToolsApi, "/workspaces/current/tool-provider/builtin/<provider>/tools")
|
api.add_resource(ToolBuiltinProviderListToolsApi, "/workspaces/current/tool-provider/builtin/<path:provider>/tools")
|
||||||
api.add_resource(ToolBuiltinProviderDeleteApi, "/workspaces/current/tool-provider/builtin/<provider>/delete")
|
api.add_resource(ToolBuiltinProviderInfoApi, "/workspaces/current/tool-provider/builtin/<path:provider>/info")
|
||||||
api.add_resource(ToolBuiltinProviderUpdateApi, "/workspaces/current/tool-provider/builtin/<provider>/update")
|
api.add_resource(ToolBuiltinProviderDeleteApi, "/workspaces/current/tool-provider/builtin/<path:provider>/delete")
|
||||||
|
api.add_resource(ToolBuiltinProviderUpdateApi, "/workspaces/current/tool-provider/builtin/<path:provider>/update")
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ToolBuiltinProviderGetCredentialsApi, "/workspaces/current/tool-provider/builtin/<provider>/credentials"
|
ToolBuiltinProviderGetCredentialsApi, "/workspaces/current/tool-provider/builtin/<path:provider>/credentials"
|
||||||
)
|
)
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
ToolBuiltinProviderCredentialsSchemaApi, "/workspaces/current/tool-provider/builtin/<provider>/credentials_schema"
|
ToolBuiltinProviderCredentialsSchemaApi,
|
||||||
|
"/workspaces/current/tool-provider/builtin/<path:provider>/credentials_schema",
|
||||||
)
|
)
|
||||||
api.add_resource(ToolBuiltinProviderIconApi, "/workspaces/current/tool-provider/builtin/<provider>/icon")
|
api.add_resource(ToolBuiltinProviderIconApi, "/workspaces/current/tool-provider/builtin/<path:provider>/icon")
|
||||||
|
|
||||||
# api tool provider
|
# api tool provider
|
||||||
api.add_resource(ToolApiProviderAddApi, "/workspaces/current/tool-provider/api/add")
|
api.add_resource(ToolApiProviderAddApi, "/workspaces/current/tool-provider/api/add")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from flask_login import current_user # type: ignore
|
|||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console.workspace.error import AccountNotInitializedError
|
from controllers.console.workspace.error import AccountNotInitializedError
|
||||||
|
from extensions.ext_database import db
|
||||||
from models.model import DifySetup
|
from models.model import DifySetup
|
||||||
from services.feature_service import FeatureService, LicenseStatus
|
from services.feature_service import FeatureService, LicenseStatus
|
||||||
from services.operation_service import OperationService
|
from services.operation_service import OperationService
|
||||||
@@ -134,9 +135,13 @@ def setup_required(view):
|
|||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
# check setup
|
# check setup
|
||||||
if dify_config.EDITION == "SELF_HOSTED" and os.environ.get("INIT_PASSWORD") and not DifySetup.query.first():
|
if (
|
||||||
|
dify_config.EDITION == "SELF_HOSTED"
|
||||||
|
and os.environ.get("INIT_PASSWORD")
|
||||||
|
and not db.session.query(DifySetup).first()
|
||||||
|
):
|
||||||
raise NotInitValidateError()
|
raise NotInitValidateError()
|
||||||
elif dify_config.EDITION == "SELF_HOSTED" and not DifySetup.query.first():
|
elif dify_config.EDITION == "SELF_HOSTED" and not db.session.query(DifySetup).first():
|
||||||
raise NotSetupError()
|
raise NotSetupError()
|
||||||
|
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ bp = Blueprint("files", __name__)
|
|||||||
api = ExternalApi(bp)
|
api = ExternalApi(bp)
|
||||||
|
|
||||||
|
|
||||||
from . import image_preview, tool_files
|
from . import image_preview, tool_files, upload
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from flask import Response, request
|
from flask import Response, request
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
@@ -71,7 +73,8 @@ class FilePreviewApi(Resource):
|
|||||||
if upload_file.size > 0:
|
if upload_file.size > 0:
|
||||||
response.headers["Content-Length"] = str(upload_file.size)
|
response.headers["Content-Length"] = str(upload_file.size)
|
||||||
if args["as_attachment"]:
|
if args["as_attachment"]:
|
||||||
response.headers["Content-Disposition"] = f"attachment; filename={upload_file.name}"
|
encoded_filename = quote(upload_file.name)
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
69
api/controllers/files/upload.py
Normal file
69
api/controllers/files/upload.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from flask import request
|
||||||
|
from flask_restful import Resource, marshal_with # type: ignore
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
import services
|
||||||
|
from controllers.console.wraps import setup_required
|
||||||
|
from controllers.files import api
|
||||||
|
from controllers.files.error import UnsupportedFileTypeError
|
||||||
|
from controllers.inner_api.plugin.wraps import get_user
|
||||||
|
from controllers.service_api.app.error import FileTooLargeError
|
||||||
|
from core.file.helpers import verify_plugin_file_signature
|
||||||
|
from fields.file_fields import file_fields
|
||||||
|
from services.file_service import FileService
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUploadFileApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@marshal_with(file_fields)
|
||||||
|
def post(self):
|
||||||
|
# get file from request
|
||||||
|
file = request.files["file"]
|
||||||
|
|
||||||
|
timestamp = request.args.get("timestamp")
|
||||||
|
nonce = request.args.get("nonce")
|
||||||
|
sign = request.args.get("sign")
|
||||||
|
tenant_id = request.args.get("tenant_id")
|
||||||
|
if not tenant_id:
|
||||||
|
raise Forbidden("Invalid request.")
|
||||||
|
|
||||||
|
user_id = request.args.get("user_id")
|
||||||
|
user = get_user(tenant_id, user_id)
|
||||||
|
|
||||||
|
filename = file.filename
|
||||||
|
mimetype = file.mimetype
|
||||||
|
|
||||||
|
if not filename or not mimetype:
|
||||||
|
raise Forbidden("Invalid request.")
|
||||||
|
|
||||||
|
if not timestamp or not nonce or not sign:
|
||||||
|
raise Forbidden("Invalid request.")
|
||||||
|
|
||||||
|
if not verify_plugin_file_signature(
|
||||||
|
filename=filename,
|
||||||
|
mimetype=mimetype,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
timestamp=timestamp,
|
||||||
|
nonce=nonce,
|
||||||
|
sign=sign,
|
||||||
|
):
|
||||||
|
raise Forbidden("Invalid request.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
upload_file = FileService.upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=file.read(),
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=user,
|
||||||
|
source=None,
|
||||||
|
)
|
||||||
|
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||||
|
raise FileTooLargeError(file_too_large_error.description)
|
||||||
|
except services.errors.file.UnsupportedFileTypeError:
|
||||||
|
raise UnsupportedFileTypeError()
|
||||||
|
|
||||||
|
return upload_file, 201
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(PluginUploadFileApi, "/files/upload/for-plugin")
|
||||||
@@ -5,4 +5,5 @@ from libs.external_api import ExternalApi
|
|||||||
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
|
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
|
||||||
api = ExternalApi(bp)
|
api = ExternalApi(bp)
|
||||||
|
|
||||||
|
from .plugin import plugin
|
||||||
from .workspace import workspace
|
from .workspace import workspace
|
||||||
|
|||||||
293
api/controllers/inner_api/plugin/plugin.py
Normal file
293
api/controllers/inner_api/plugin/plugin.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
from flask_restful import Resource # type: ignore
|
||||||
|
|
||||||
|
from controllers.console.wraps import setup_required
|
||||||
|
from controllers.inner_api import api
|
||||||
|
from controllers.inner_api.plugin.wraps import get_user_tenant, plugin_data
|
||||||
|
from controllers.inner_api.wraps import plugin_inner_api_only
|
||||||
|
from core.file.helpers import get_signed_file_url_for_plugin
|
||||||
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from core.plugin.backwards_invocation.app import PluginAppBackwardsInvocation
|
||||||
|
from core.plugin.backwards_invocation.base import BaseBackwardsInvocationResponse
|
||||||
|
from core.plugin.backwards_invocation.encrypt import PluginEncrypter
|
||||||
|
from core.plugin.backwards_invocation.model import PluginModelBackwardsInvocation
|
||||||
|
from core.plugin.backwards_invocation.node import PluginNodeBackwardsInvocation
|
||||||
|
from core.plugin.backwards_invocation.tool import PluginToolBackwardsInvocation
|
||||||
|
from core.plugin.entities.request import (
|
||||||
|
RequestInvokeApp,
|
||||||
|
RequestInvokeEncrypt,
|
||||||
|
RequestInvokeLLM,
|
||||||
|
RequestInvokeModeration,
|
||||||
|
RequestInvokeParameterExtractorNode,
|
||||||
|
RequestInvokeQuestionClassifierNode,
|
||||||
|
RequestInvokeRerank,
|
||||||
|
RequestInvokeSpeech2Text,
|
||||||
|
RequestInvokeSummary,
|
||||||
|
RequestInvokeTextEmbedding,
|
||||||
|
RequestInvokeTool,
|
||||||
|
RequestInvokeTTS,
|
||||||
|
RequestRequestUploadFile,
|
||||||
|
)
|
||||||
|
from core.tools.entities.tool_entities import ToolProviderType
|
||||||
|
from libs.helper import compact_generate_response
|
||||||
|
from models.account import Account, Tenant
|
||||||
|
from models.model import EndUser
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeLLMApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeLLM)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeLLM):
|
||||||
|
def generator():
|
||||||
|
response = PluginModelBackwardsInvocation.invoke_llm(user_model.id, tenant_model, payload)
|
||||||
|
return PluginModelBackwardsInvocation.convert_to_event_stream(response)
|
||||||
|
|
||||||
|
return compact_generate_response(generator())
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeTextEmbeddingApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeTextEmbedding)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTextEmbedding):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginModelBackwardsInvocation.invoke_text_embedding(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeRerankApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeRerank)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeRerank):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginModelBackwardsInvocation.invoke_rerank(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeTTSApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeTTS)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTTS):
|
||||||
|
def generator():
|
||||||
|
response = PluginModelBackwardsInvocation.invoke_tts(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
return PluginModelBackwardsInvocation.convert_to_event_stream(response)
|
||||||
|
|
||||||
|
return compact_generate_response(generator())
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeSpeech2TextApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeSpeech2Text)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeSpeech2Text):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginModelBackwardsInvocation.invoke_speech2text(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeModerationApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeModeration)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeModeration):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginModelBackwardsInvocation.invoke_moderation(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeToolApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeTool)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTool):
|
||||||
|
def generator():
|
||||||
|
return PluginToolBackwardsInvocation.convert_to_event_stream(
|
||||||
|
PluginToolBackwardsInvocation.invoke_tool(
|
||||||
|
tenant_id=tenant_model.id,
|
||||||
|
user_id=user_model.id,
|
||||||
|
tool_type=ToolProviderType.value_of(payload.tool_type),
|
||||||
|
provider=payload.provider,
|
||||||
|
tool_name=payload.tool,
|
||||||
|
tool_parameters=payload.tool_parameters,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return compact_generate_response(generator())
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeParameterExtractorNodeApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeParameterExtractorNode)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeParameterExtractorNode):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginNodeBackwardsInvocation.invoke_parameter_extractor(
|
||||||
|
tenant_id=tenant_model.id,
|
||||||
|
user_id=user_model.id,
|
||||||
|
parameters=payload.parameters,
|
||||||
|
model_config=payload.model,
|
||||||
|
instruction=payload.instruction,
|
||||||
|
query=payload.query,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeQuestionClassifierNodeApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeQuestionClassifierNode)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeQuestionClassifierNode):
|
||||||
|
try:
|
||||||
|
return jsonable_encoder(
|
||||||
|
BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginNodeBackwardsInvocation.invoke_question_classifier(
|
||||||
|
tenant_id=tenant_model.id,
|
||||||
|
user_id=user_model.id,
|
||||||
|
query=payload.query,
|
||||||
|
model_config=payload.model,
|
||||||
|
classes=payload.classes,
|
||||||
|
instruction=payload.instruction,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeAppApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeApp)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeApp):
|
||||||
|
response = PluginAppBackwardsInvocation.invoke_app(
|
||||||
|
app_id=payload.app_id,
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant_id=tenant_model.id,
|
||||||
|
conversation_id=payload.conversation_id,
|
||||||
|
query=payload.query,
|
||||||
|
stream=payload.response_mode == "streaming",
|
||||||
|
inputs=payload.inputs,
|
||||||
|
files=payload.files,
|
||||||
|
)
|
||||||
|
|
||||||
|
return compact_generate_response(PluginAppBackwardsInvocation.convert_to_event_stream(response))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeEncryptApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeEncrypt)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeEncrypt):
|
||||||
|
"""
|
||||||
|
encrypt or decrypt data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return BaseBackwardsInvocationResponse(
|
||||||
|
data=PluginEncrypter.invoke_encrypt(tenant_model, payload)
|
||||||
|
).model_dump()
|
||||||
|
except Exception as e:
|
||||||
|
return BaseBackwardsInvocationResponse(error=str(e)).model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInvokeSummaryApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestInvokeSummary)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeSummary):
|
||||||
|
try:
|
||||||
|
return BaseBackwardsInvocationResponse(
|
||||||
|
data={
|
||||||
|
"summary": PluginModelBackwardsInvocation.invoke_summary(
|
||||||
|
user_id=user_model.id,
|
||||||
|
tenant=tenant_model,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).model_dump()
|
||||||
|
except Exception as e:
|
||||||
|
return BaseBackwardsInvocationResponse(error=str(e)).model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
class PluginUploadFileRequestApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@plugin_inner_api_only
|
||||||
|
@get_user_tenant
|
||||||
|
@plugin_data(payload_type=RequestRequestUploadFile)
|
||||||
|
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestRequestUploadFile):
|
||||||
|
# generate signed url
|
||||||
|
url = get_signed_file_url_for_plugin(payload.filename, payload.mimetype, tenant_model.id, user_model.id)
|
||||||
|
return BaseBackwardsInvocationResponse(data={"url": url}).model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(PluginInvokeLLMApi, "/invoke/llm")
|
||||||
|
api.add_resource(PluginInvokeTextEmbeddingApi, "/invoke/text-embedding")
|
||||||
|
api.add_resource(PluginInvokeRerankApi, "/invoke/rerank")
|
||||||
|
api.add_resource(PluginInvokeTTSApi, "/invoke/tts")
|
||||||
|
api.add_resource(PluginInvokeSpeech2TextApi, "/invoke/speech2text")
|
||||||
|
api.add_resource(PluginInvokeModerationApi, "/invoke/moderation")
|
||||||
|
api.add_resource(PluginInvokeToolApi, "/invoke/tool")
|
||||||
|
api.add_resource(PluginInvokeParameterExtractorNodeApi, "/invoke/parameter-extractor")
|
||||||
|
api.add_resource(PluginInvokeQuestionClassifierNodeApi, "/invoke/question-classifier")
|
||||||
|
api.add_resource(PluginInvokeAppApi, "/invoke/app")
|
||||||
|
api.add_resource(PluginInvokeEncryptApi, "/invoke/encrypt")
|
||||||
|
api.add_resource(PluginInvokeSummaryApi, "/invoke/summary")
|
||||||
|
api.add_resource(PluginUploadFileRequestApi, "/upload/file/request")
|
||||||
116
api/controllers/inner_api/plugin/wraps.py
Normal file
116
api/controllers/inner_api/plugin/wraps.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
from flask_restful import reqparse # type: ignore
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.account import Account, Tenant
|
||||||
|
from models.model import EndUser
|
||||||
|
from services.account_service import AccountService
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(tenant_id: str, user_id: str | None) -> Account | EndUser:
|
||||||
|
try:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
if not user_id:
|
||||||
|
user_id = "DEFAULT-USER"
|
||||||
|
|
||||||
|
if user_id == "DEFAULT-USER":
|
||||||
|
user_model = session.query(EndUser).filter(EndUser.session_id == "DEFAULT-USER").first()
|
||||||
|
if not user_model:
|
||||||
|
user_model = EndUser(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
type="service_api",
|
||||||
|
is_anonymous=True if user_id == "DEFAULT-USER" else False,
|
||||||
|
session_id=user_id,
|
||||||
|
)
|
||||||
|
session.add(user_model)
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
user_model = AccountService.load_user(user_id)
|
||||||
|
if not user_model:
|
||||||
|
user_model = session.query(EndUser).filter(EndUser.id == user_id).first()
|
||||||
|
if not user_model:
|
||||||
|
raise ValueError("user not found")
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("user not found")
|
||||||
|
|
||||||
|
return user_model
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_tenant(view: Optional[Callable] = None):
|
||||||
|
def decorator(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
# fetch json body
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("tenant_id", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("user_id", type=str, required=True, location="json")
|
||||||
|
|
||||||
|
kwargs = parser.parse_args()
|
||||||
|
|
||||||
|
user_id = kwargs.get("user_id")
|
||||||
|
tenant_id = kwargs.get("tenant_id")
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
raise ValueError("tenant_id is required")
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
user_id = "DEFAULT-USER"
|
||||||
|
|
||||||
|
del kwargs["tenant_id"]
|
||||||
|
del kwargs["user_id"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_model = (
|
||||||
|
db.session.query(Tenant)
|
||||||
|
.filter(
|
||||||
|
Tenant.id == tenant_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("tenant not found")
|
||||||
|
|
||||||
|
if not tenant_model:
|
||||||
|
raise ValueError("tenant not found")
|
||||||
|
|
||||||
|
kwargs["tenant_model"] = tenant_model
|
||||||
|
kwargs["user_model"] = get_user(tenant_id, user_id)
|
||||||
|
|
||||||
|
return view_func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
if view is None:
|
||||||
|
return decorator
|
||||||
|
else:
|
||||||
|
return decorator(view)
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_data(view: Optional[Callable] = None, *, payload_type: type[BaseModel]):
|
||||||
|
def decorator(view_func):
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("invalid json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = payload_type(**data)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"invalid payload: {str(e)}")
|
||||||
|
|
||||||
|
kwargs["payload"] = payload
|
||||||
|
return view_func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
if view is None:
|
||||||
|
return decorator
|
||||||
|
else:
|
||||||
|
return decorator(view)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from flask_restful import Resource, reqparse # type: ignore
|
from flask_restful import Resource, reqparse # type: ignore
|
||||||
|
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import setup_required
|
||||||
from controllers.inner_api import api
|
from controllers.inner_api import api
|
||||||
from controllers.inner_api.wraps import inner_api_only
|
from controllers.inner_api.wraps import enterprise_inner_api_only
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
@@ -10,7 +12,7 @@ from services.account_service import TenantService
|
|||||||
|
|
||||||
class EnterpriseWorkspace(Resource):
|
class EnterpriseWorkspace(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@inner_api_only
|
@enterprise_inner_api_only
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=True, location="json")
|
parser.add_argument("name", type=str, required=True, location="json")
|
||||||
@@ -29,4 +31,34 @@ class EnterpriseWorkspace(Resource):
|
|||||||
return {"message": "enterprise workspace created."}
|
return {"message": "enterprise workspace created."}
|
||||||
|
|
||||||
|
|
||||||
|
class EnterpriseWorkspaceNoOwnerEmail(Resource):
|
||||||
|
@setup_required
|
||||||
|
@enterprise_inner_api_only
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("name", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
tenant = TenantService.create_tenant(args["name"], is_from_dashboard=True)
|
||||||
|
|
||||||
|
tenant_was_created.send(tenant)
|
||||||
|
|
||||||
|
resp = {
|
||||||
|
"id": tenant.id,
|
||||||
|
"name": tenant.name,
|
||||||
|
"encrypt_public_key": tenant.encrypt_public_key,
|
||||||
|
"plan": tenant.plan,
|
||||||
|
"status": tenant.status,
|
||||||
|
"custom_config": json.loads(tenant.custom_config) if tenant.custom_config else {},
|
||||||
|
"created_at": tenant.created_at.isoformat() + "Z" if tenant.created_at else None,
|
||||||
|
"updated_at": tenant.updated_at.isoformat() + "Z" if tenant.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "enterprise workspace created.",
|
||||||
|
"tenant": resp,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(EnterpriseWorkspace, "/enterprise/workspace")
|
api.add_resource(EnterpriseWorkspace, "/enterprise/workspace")
|
||||||
|
api.add_resource(EnterpriseWorkspaceNoOwnerEmail, "/enterprise/workspace/ownerless")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from extensions.ext_database import db
|
|||||||
from models.model import EndUser
|
from models.model import EndUser
|
||||||
|
|
||||||
|
|
||||||
def inner_api_only(view):
|
def enterprise_inner_api_only(view):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if not dify_config.INNER_API:
|
if not dify_config.INNER_API:
|
||||||
@@ -18,7 +18,7 @@ def inner_api_only(view):
|
|||||||
|
|
||||||
# get header 'X-Inner-Api-Key'
|
# get header 'X-Inner-Api-Key'
|
||||||
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
||||||
if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY:
|
if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY_FOR_PLUGIN:
|
||||||
abort(401)
|
abort(401)
|
||||||
|
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
@@ -26,7 +26,7 @@ def inner_api_only(view):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def inner_api_user_auth(view):
|
def enterprise_inner_api_user_auth(view):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if not dify_config.INNER_API:
|
if not dify_config.INNER_API:
|
||||||
@@ -60,3 +60,19 @@ def inner_api_user_auth(view):
|
|||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_inner_api_only(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not dify_config.PLUGIN_DAEMON_KEY:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# get header 'X-Inner-Api-Key'
|
||||||
|
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
||||||
|
if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY_FOR_PLUGIN:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ api = ExternalApi(bp)
|
|||||||
|
|
||||||
from . import index
|
from . import index
|
||||||
from .app import app, audio, completion, conversation, file, message, workflow
|
from .app import app, audio, completion, conversation, file, message, workflow
|
||||||
from .dataset import dataset, document, hit_testing, segment
|
from .dataset import dataset, document, hit_testing, segment, upload_file
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from controllers.service_api.app.error import NotChatAppError
|
|||||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from fields.conversation_fields import message_file_fields
|
from fields.conversation_fields import message_file_fields
|
||||||
|
from fields.message_fields import feedback_fields, retriever_resource_fields
|
||||||
from fields.raws import FilesContainedField
|
from fields.raws import FilesContainedField
|
||||||
from libs.helper import TimestampField, uuid_value
|
from libs.helper import TimestampField, uuid_value
|
||||||
from models.model import App, AppMode, EndUser
|
from models.model import App, AppMode, EndUser
|
||||||
@@ -18,26 +19,6 @@ from services.message_service import MessageService
|
|||||||
|
|
||||||
|
|
||||||
class MessageListApi(Resource):
|
class MessageListApi(Resource):
|
||||||
feedback_fields = {"rating": fields.String}
|
|
||||||
retriever_resource_fields = {
|
|
||||||
"id": fields.String,
|
|
||||||
"message_id": fields.String,
|
|
||||||
"position": fields.Integer,
|
|
||||||
"dataset_id": fields.String,
|
|
||||||
"dataset_name": fields.String,
|
|
||||||
"document_id": fields.String,
|
|
||||||
"document_name": fields.String,
|
|
||||||
"data_source_type": fields.String,
|
|
||||||
"segment_id": fields.String,
|
|
||||||
"score": fields.Float,
|
|
||||||
"hit_count": fields.Integer,
|
|
||||||
"word_count": fields.Integer,
|
|
||||||
"segment_position": fields.Integer,
|
|
||||||
"index_node_hash": fields.String,
|
|
||||||
"content": fields.String,
|
|
||||||
"created_at": TimestampField,
|
|
||||||
}
|
|
||||||
|
|
||||||
agent_thought_fields = {
|
agent_thought_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"chain_id": fields.String,
|
"chain_id": fields.String,
|
||||||
@@ -89,7 +70,7 @@ class MessageListApi(Resource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
|
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
||||||
)
|
)
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ class DatasetListApi(DatasetApiResource):
|
|||||||
# provider = request.args.get("provider", default="vendor")
|
# provider = request.args.get("provider", default="vendor")
|
||||||
search = request.args.get("keyword", default=None, type=str)
|
search = request.args.get("keyword", default=None, type=str)
|
||||||
tag_ids = request.args.getlist("tag_ids")
|
tag_ids = request.args.getlist("tag_ids")
|
||||||
|
include_all = request.args.get("include_all", default="false").lower() == "true"
|
||||||
|
|
||||||
datasets, total = DatasetService.get_datasets(page, limit, tenant_id, current_user, search, tag_ids)
|
datasets, total = DatasetService.get_datasets(
|
||||||
|
page, limit, tenant_id, current_user, search, tag_ids, include_all
|
||||||
|
)
|
||||||
# check embedding setting
|
# check embedding setting
|
||||||
provider_manager = ProviderManager()
|
provider_manager = ProviderManager()
|
||||||
configurations = provider_manager.get_configurations(tenant_id=current_user.current_tenant_id)
|
configurations = provider_manager.get_configurations(tenant_id=current_user.current_tenant_id)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from controllers.service_api.app.error import (
|
|||||||
from controllers.service_api.dataset.error import (
|
from controllers.service_api.dataset.error import (
|
||||||
ArchivedDocumentImmutableError,
|
ArchivedDocumentImmutableError,
|
||||||
DocumentIndexingError,
|
DocumentIndexingError,
|
||||||
|
InvalidMetadataError,
|
||||||
)
|
)
|
||||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||||
from core.errors.error import ProviderTokenNotInitError
|
from core.errors.error import ProviderTokenNotInitError
|
||||||
@@ -50,6 +51,9 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||||||
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
|
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
|
||||||
)
|
)
|
||||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||||
|
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
||||||
|
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@@ -61,6 +65,28 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||||||
if not dataset.indexing_technique and not args["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 metadata if provided
|
||||||
|
if args.get("doc_type") or args.get("doc_metadata"):
|
||||||
|
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||||
|
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||||
|
|
||||||
|
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||||
|
raise InvalidMetadataError(
|
||||||
|
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(args["doc_metadata"], dict):
|
||||||
|
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||||
|
|
||||||
|
# Validate metadata schema based on doc_type
|
||||||
|
if args["doc_type"] != "others":
|
||||||
|
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||||
|
for key, value in args["doc_metadata"].items():
|
||||||
|
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||||
|
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||||
|
# set to MetaDataConfig
|
||||||
|
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||||
|
|
||||||
text = args.get("text")
|
text = args.get("text")
|
||||||
name = args.get("name")
|
name = args.get("name")
|
||||||
if text is None or name is None:
|
if text is None or name is None:
|
||||||
@@ -107,6 +133,8 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||||||
"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")
|
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||||
|
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
||||||
|
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@@ -115,6 +143,32 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||||||
if not dataset:
|
if not dataset:
|
||||||
raise ValueError("Dataset is not exist.")
|
raise ValueError("Dataset is not exist.")
|
||||||
|
|
||||||
|
# indexing_technique is already set in dataset since this is an update
|
||||||
|
args["indexing_technique"] = dataset.indexing_technique
|
||||||
|
|
||||||
|
# Validate metadata if provided
|
||||||
|
if args.get("doc_type") or args.get("doc_metadata"):
|
||||||
|
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||||
|
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||||
|
|
||||||
|
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||||
|
raise InvalidMetadataError(
|
||||||
|
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(args["doc_metadata"], dict):
|
||||||
|
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||||
|
|
||||||
|
# Validate metadata schema based on doc_type
|
||||||
|
if args["doc_type"] != "others":
|
||||||
|
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||||
|
for key, value in args["doc_metadata"].items():
|
||||||
|
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||||
|
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||||
|
|
||||||
|
# set to MetaDataConfig
|
||||||
|
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||||
|
|
||||||
if args["text"]:
|
if args["text"]:
|
||||||
text = args.get("text")
|
text = args.get("text")
|
||||||
name = args.get("name")
|
name = args.get("name")
|
||||||
@@ -161,6 +215,30 @@ class DocumentAddByFileApi(DatasetApiResource):
|
|||||||
args["doc_form"] = "text_model"
|
args["doc_form"] = "text_model"
|
||||||
if "doc_language" not in args:
|
if "doc_language" not in args:
|
||||||
args["doc_language"] = "English"
|
args["doc_language"] = "English"
|
||||||
|
|
||||||
|
# Validate metadata if provided
|
||||||
|
if args.get("doc_type") or args.get("doc_metadata"):
|
||||||
|
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||||
|
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||||
|
|
||||||
|
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||||
|
raise InvalidMetadataError(
|
||||||
|
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(args["doc_metadata"], dict):
|
||||||
|
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||||
|
|
||||||
|
# Validate metadata schema based on doc_type
|
||||||
|
if args["doc_type"] != "others":
|
||||||
|
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||||
|
for key, value in args["doc_metadata"].items():
|
||||||
|
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||||
|
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||||
|
|
||||||
|
# set to MetaDataConfig
|
||||||
|
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||||
|
|
||||||
# get dataset info
|
# get dataset info
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@@ -228,6 +306,29 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||||||
if "doc_language" not in args:
|
if "doc_language" not in args:
|
||||||
args["doc_language"] = "English"
|
args["doc_language"] = "English"
|
||||||
|
|
||||||
|
# Validate metadata if provided
|
||||||
|
if args.get("doc_type") or args.get("doc_metadata"):
|
||||||
|
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||||
|
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||||
|
|
||||||
|
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||||
|
raise InvalidMetadataError(
|
||||||
|
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(args["doc_metadata"], dict):
|
||||||
|
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||||
|
|
||||||
|
# Validate metadata schema based on doc_type
|
||||||
|
if args["doc_type"] != "others":
|
||||||
|
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||||
|
for key, value in args["doc_metadata"].items():
|
||||||
|
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||||
|
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||||
|
|
||||||
|
# set to MetaDataConfig
|
||||||
|
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||||
|
|
||||||
# get dataset info
|
# get dataset info
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@@ -235,6 +336,10 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||||||
|
|
||||||
if not dataset:
|
if not dataset:
|
||||||
raise ValueError("Dataset is not exist.")
|
raise ValueError("Dataset is not exist.")
|
||||||
|
|
||||||
|
# indexing_technique is already set in dataset since this is an update
|
||||||
|
args["indexing_technique"] = dataset.indexing_technique
|
||||||
|
|
||||||
if "file" in request.files:
|
if "file" in request.files:
|
||||||
# save file info
|
# save file info
|
||||||
file = request.files["file"]
|
file = request.files["file"]
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ class SegmentApi(DatasetApiResource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -95,8 +94,7 @@ class SegmentApi(DatasetApiResource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
@@ -175,8 +173,7 @@ class DatasetSegmentApi(DatasetApiResource):
|
|||||||
)
|
)
|
||||||
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 in the Settings -> Model Provider."
|
||||||
"in the Settings -> Model Provider."
|
|
||||||
)
|
)
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ProviderNotInitializeError(ex.description)
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
|||||||
54
api/controllers/service_api/dataset/upload_file.py
Normal file
54
api/controllers/service_api/dataset/upload_file.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from controllers.service_api import api
|
||||||
|
from controllers.service_api.wraps import (
|
||||||
|
DatasetApiResource,
|
||||||
|
)
|
||||||
|
from core.file import helpers as file_helpers
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.dataset import Dataset
|
||||||
|
from models.model import UploadFile
|
||||||
|
from services.dataset_service import DocumentService
|
||||||
|
|
||||||
|
|
||||||
|
class UploadFileApi(DatasetApiResource):
|
||||||
|
def get(self, tenant_id, dataset_id, document_id):
|
||||||
|
"""Get upload file."""
|
||||||
|
# check dataset
|
||||||
|
dataset_id = str(dataset_id)
|
||||||
|
tenant_id = str(tenant_id)
|
||||||
|
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
|
||||||
|
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 upload file
|
||||||
|
if document.data_source_type != "upload_file":
|
||||||
|
raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
|
||||||
|
data_source_info = document.data_source_info_dict
|
||||||
|
if data_source_info and "upload_file_id" in data_source_info:
|
||||||
|
file_id = data_source_info["upload_file_id"]
|
||||||
|
upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
|
||||||
|
if not upload_file:
|
||||||
|
raise NotFound("UploadFile not found.")
|
||||||
|
else:
|
||||||
|
raise ValueError("Upload file id not found in document data source info.")
|
||||||
|
|
||||||
|
url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
|
||||||
|
return {
|
||||||
|
"id": upload_file.id,
|
||||||
|
"name": upload_file.name,
|
||||||
|
"size": upload_file.size,
|
||||||
|
"extension": upload_file.extension,
|
||||||
|
"url": url,
|
||||||
|
"download_url": f"{url}&as_attachment=true",
|
||||||
|
"mime_type": upload_file.mime_type,
|
||||||
|
"created_by": upload_file.created_by,
|
||||||
|
"created_at": upload_file.created_at.timestamp(),
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(UploadFileApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/upload-file")
|
||||||
@@ -154,7 +154,7 @@ def validate_dataset_token(view=None):
|
|||||||
) # TODO: only owner information is required, so only one is returned.
|
) # TODO: only owner information is required, so only one is returned.
|
||||||
if tenant_account_join:
|
if tenant_account_join:
|
||||||
tenant, ta = tenant_account_join
|
tenant, ta = tenant_account_join
|
||||||
account = Account.query.filter_by(id=ta.account_id).first()
|
account = db.session.query(Account).filter(Account.id == ta.account_id).first()
|
||||||
# Login admin
|
# Login admin
|
||||||
if account:
|
if account:
|
||||||
account.current_tenant = tenant
|
account.current_tenant = tenant
|
||||||
@@ -195,7 +195,11 @@ def validate_and_get_api_token(scope: str | None = None):
|
|||||||
with Session(db.engine, expire_on_commit=False) as session:
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
update_stmt = (
|
update_stmt = (
|
||||||
update(ApiToken)
|
update(ApiToken)
|
||||||
.where(ApiToken.token == auth_token, ApiToken.last_used_at < cutoff_time, ApiToken.type == scope)
|
.where(
|
||||||
|
ApiToken.token == auth_token,
|
||||||
|
(ApiToken.last_used_at.is_(None) | (ApiToken.last_used_at < cutoff_time)),
|
||||||
|
ApiToken.type == scope,
|
||||||
|
)
|
||||||
.values(last_used_at=current_time)
|
.values(last_used_at=current_time)
|
||||||
.returning(ApiToken)
|
.returning(ApiToken)
|
||||||
)
|
)
|
||||||
@@ -236,7 +240,7 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
|
|||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
app_id=app_model.id,
|
app_id=app_model.id,
|
||||||
type="service_api",
|
type="service_api",
|
||||||
is_anonymous=True if user_id == "DEFAULT-USER" else False,
|
is_anonymous=user_id == "DEFAULT-USER",
|
||||||
session_id=user_id,
|
session_id=user_id,
|
||||||
)
|
)
|
||||||
db.session.add(end_user)
|
db.session.add(end_user)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ConversationListApi(WebApiResource):
|
|||||||
|
|
||||||
pinned = None
|
pinned = None
|
||||||
if "pinned" in args and args["pinned"] is not None:
|
if "pinned" in args and args["pinned"] is not None:
|
||||||
pinned = True if args["pinned"] == "true" else False
|
pinned = args["pinned"] == "true"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom
|
|||||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||||
from core.model_runtime.errors.invoke import InvokeError
|
from core.model_runtime.errors.invoke import InvokeError
|
||||||
from fields.conversation_fields import message_file_fields
|
from fields.conversation_fields import message_file_fields
|
||||||
from fields.message_fields import agent_thought_fields
|
from fields.message_fields import agent_thought_fields, feedback_fields, retriever_resource_fields
|
||||||
from fields.raws import FilesContainedField
|
from fields.raws import FilesContainedField
|
||||||
from libs import helper
|
from libs import helper
|
||||||
from libs.helper import TimestampField, uuid_value
|
from libs.helper import TimestampField, uuid_value
|
||||||
@@ -34,27 +34,6 @@ from services.message_service import MessageService
|
|||||||
|
|
||||||
|
|
||||||
class MessageListApi(WebApiResource):
|
class MessageListApi(WebApiResource):
|
||||||
feedback_fields = {"rating": fields.String}
|
|
||||||
|
|
||||||
retriever_resource_fields = {
|
|
||||||
"id": fields.String,
|
|
||||||
"message_id": fields.String,
|
|
||||||
"position": fields.Integer,
|
|
||||||
"dataset_id": fields.String,
|
|
||||||
"dataset_name": fields.String,
|
|
||||||
"document_id": fields.String,
|
|
||||||
"document_name": fields.String,
|
|
||||||
"data_source_type": fields.String,
|
|
||||||
"segment_id": fields.String,
|
|
||||||
"score": fields.Float,
|
|
||||||
"hit_count": fields.Integer,
|
|
||||||
"word_count": fields.Integer,
|
|
||||||
"segment_position": fields.Integer,
|
|
||||||
"index_node_hash": fields.String,
|
|
||||||
"content": fields.String,
|
|
||||||
"created_at": TimestampField,
|
|
||||||
}
|
|
||||||
|
|
||||||
message_fields = {
|
message_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"conversation_id": fields.String,
|
"conversation_id": fields.String,
|
||||||
@@ -91,7 +70,7 @@ class MessageListApi(WebApiResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
|
||||||
)
|
)
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
|
||||||
from typing import Optional, Union, cast
|
from typing import Optional, Union, cast
|
||||||
|
|
||||||
from core.agent.entities import AgentEntity, AgentToolEntity
|
from core.agent.entities import AgentEntity, AgentToolEntity
|
||||||
@@ -32,19 +31,16 @@ from core.model_runtime.entities import (
|
|||||||
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||||
from core.model_runtime.entities.model_entities import ModelFeature
|
from core.model_runtime.entities.model_entities import ModelFeature
|
||||||
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
|
||||||
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
||||||
|
from core.tools.__base.tool import Tool
|
||||||
from core.tools.entities.tool_entities import (
|
from core.tools.entities.tool_entities import (
|
||||||
ToolParameter,
|
ToolParameter,
|
||||||
ToolRuntimeVariablePool,
|
|
||||||
)
|
)
|
||||||
from core.tools.tool.dataset_retriever_tool import DatasetRetrieverTool
|
|
||||||
from core.tools.tool.tool import Tool
|
|
||||||
from core.tools.tool_manager import ToolManager
|
from core.tools.tool_manager import ToolManager
|
||||||
|
from core.tools.utils.dataset_retriever_tool import DatasetRetrieverTool
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from factories import file_factory
|
from factories import file_factory
|
||||||
from models.model import Conversation, Message, MessageAgentThought, MessageFile
|
from models.model import Conversation, Message, MessageAgentThought, MessageFile
|
||||||
from models.tools import ToolConversationVariables
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -62,11 +58,9 @@ class BaseAgentRunner(AppRunner):
|
|||||||
queue_manager: AppQueueManager,
|
queue_manager: AppQueueManager,
|
||||||
message: Message,
|
message: Message,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
model_instance: ModelInstance,
|
||||||
memory: Optional[TokenBufferMemory] = None,
|
memory: Optional[TokenBufferMemory] = None,
|
||||||
prompt_messages: Optional[list[PromptMessage]] = None,
|
prompt_messages: Optional[list[PromptMessage]] = None,
|
||||||
variables_pool: Optional[ToolRuntimeVariablePool] = None,
|
|
||||||
db_variables: Optional[ToolConversationVariables] = None,
|
|
||||||
model_instance: ModelInstance,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.tenant_id = tenant_id
|
self.tenant_id = tenant_id
|
||||||
self.application_generate_entity = application_generate_entity
|
self.application_generate_entity = application_generate_entity
|
||||||
@@ -79,8 +73,6 @@ class BaseAgentRunner(AppRunner):
|
|||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.history_prompt_messages = self.organize_agent_history(prompt_messages=prompt_messages or [])
|
self.history_prompt_messages = self.organize_agent_history(prompt_messages=prompt_messages or [])
|
||||||
self.variables_pool = variables_pool
|
|
||||||
self.db_variables_pool = db_variables
|
|
||||||
self.model_instance = model_instance
|
self.model_instance = model_instance
|
||||||
|
|
||||||
# init callback
|
# init callback
|
||||||
@@ -141,11 +133,10 @@ class BaseAgentRunner(AppRunner):
|
|||||||
agent_tool=tool,
|
agent_tool=tool,
|
||||||
invoke_from=self.application_generate_entity.invoke_from,
|
invoke_from=self.application_generate_entity.invoke_from,
|
||||||
)
|
)
|
||||||
tool_entity.load_variables(self.variables_pool)
|
assert tool_entity.entity.description
|
||||||
|
|
||||||
message_tool = PromptMessageTool(
|
message_tool = PromptMessageTool(
|
||||||
name=tool.tool_name,
|
name=tool.tool_name,
|
||||||
description=tool_entity.description.llm if tool_entity.description else "",
|
description=tool_entity.entity.description.llm,
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
@@ -153,7 +144,7 @@ class BaseAgentRunner(AppRunner):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = tool_entity.get_all_runtime_parameters()
|
parameters = tool_entity.get_merged_runtime_parameters()
|
||||||
for parameter in parameters:
|
for parameter in parameters:
|
||||||
if parameter.form != ToolParameter.ToolParameterForm.LLM:
|
if parameter.form != ToolParameter.ToolParameterForm.LLM:
|
||||||
continue
|
continue
|
||||||
@@ -186,9 +177,11 @@ class BaseAgentRunner(AppRunner):
|
|||||||
"""
|
"""
|
||||||
convert dataset retriever tool to prompt message tool
|
convert dataset retriever tool to prompt message tool
|
||||||
"""
|
"""
|
||||||
|
assert tool.entity.description
|
||||||
|
|
||||||
prompt_tool = PromptMessageTool(
|
prompt_tool = PromptMessageTool(
|
||||||
name=tool.identity.name if tool.identity else "unknown",
|
name=tool.entity.identity.name,
|
||||||
description=tool.description.llm if tool.description else "",
|
description=tool.entity.description.llm,
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
@@ -234,8 +227,7 @@ class BaseAgentRunner(AppRunner):
|
|||||||
# save prompt tool
|
# save prompt tool
|
||||||
prompt_messages_tools.append(prompt_tool)
|
prompt_messages_tools.append(prompt_tool)
|
||||||
# save tool entity
|
# save tool entity
|
||||||
if dataset_tool.identity is not None:
|
tool_instances[dataset_tool.entity.identity.name] = dataset_tool
|
||||||
tool_instances[dataset_tool.identity.name] = dataset_tool
|
|
||||||
|
|
||||||
return tool_instances, prompt_messages_tools
|
return tool_instances, prompt_messages_tools
|
||||||
|
|
||||||
@@ -320,24 +312,24 @@ class BaseAgentRunner(AppRunner):
|
|||||||
def save_agent_thought(
|
def save_agent_thought(
|
||||||
self,
|
self,
|
||||||
agent_thought: MessageAgentThought,
|
agent_thought: MessageAgentThought,
|
||||||
tool_name: str,
|
tool_name: str | None,
|
||||||
tool_input: Union[str, dict],
|
tool_input: Union[str, dict, None],
|
||||||
thought: str,
|
thought: str | None,
|
||||||
observation: Union[str, dict, None],
|
observation: Union[str, dict, None],
|
||||||
tool_invoke_meta: Union[str, dict, None],
|
tool_invoke_meta: Union[str, dict, None],
|
||||||
answer: str,
|
answer: str | None,
|
||||||
messages_ids: list[str],
|
messages_ids: list[str],
|
||||||
llm_usage: LLMUsage | None = None,
|
llm_usage: LLMUsage | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Save agent thought
|
Save agent thought
|
||||||
"""
|
"""
|
||||||
queried_thought = (
|
updated_agent_thought = (
|
||||||
db.session.query(MessageAgentThought).filter(MessageAgentThought.id == agent_thought.id).first()
|
db.session.query(MessageAgentThought).filter(MessageAgentThought.id == agent_thought.id).first()
|
||||||
)
|
)
|
||||||
if not queried_thought:
|
if not updated_agent_thought:
|
||||||
raise ValueError(f"Agent thought {agent_thought.id} not found")
|
raise ValueError("agent thought not found")
|
||||||
agent_thought = queried_thought
|
agent_thought = updated_agent_thought
|
||||||
|
|
||||||
if thought:
|
if thought:
|
||||||
agent_thought.thought = thought
|
agent_thought.thought = thought
|
||||||
@@ -349,39 +341,39 @@ class BaseAgentRunner(AppRunner):
|
|||||||
if isinstance(tool_input, dict):
|
if isinstance(tool_input, dict):
|
||||||
try:
|
try:
|
||||||
tool_input = json.dumps(tool_input, ensure_ascii=False)
|
tool_input = json.dumps(tool_input, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
tool_input = json.dumps(tool_input)
|
tool_input = json.dumps(tool_input)
|
||||||
|
|
||||||
agent_thought.tool_input = tool_input
|
updated_agent_thought.tool_input = tool_input
|
||||||
|
|
||||||
if observation:
|
if observation:
|
||||||
if isinstance(observation, dict):
|
if isinstance(observation, dict):
|
||||||
try:
|
try:
|
||||||
observation = json.dumps(observation, ensure_ascii=False)
|
observation = json.dumps(observation, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
observation = json.dumps(observation)
|
observation = json.dumps(observation)
|
||||||
|
|
||||||
agent_thought.observation = observation
|
updated_agent_thought.observation = observation
|
||||||
|
|
||||||
if answer:
|
if answer:
|
||||||
agent_thought.answer = answer
|
agent_thought.answer = answer
|
||||||
|
|
||||||
if messages_ids is not None and len(messages_ids) > 0:
|
if messages_ids is not None and len(messages_ids) > 0:
|
||||||
agent_thought.message_files = json.dumps(messages_ids)
|
updated_agent_thought.message_files = json.dumps(messages_ids)
|
||||||
|
|
||||||
if llm_usage:
|
if llm_usage:
|
||||||
agent_thought.message_token = llm_usage.prompt_tokens
|
updated_agent_thought.message_token = llm_usage.prompt_tokens
|
||||||
agent_thought.message_price_unit = llm_usage.prompt_price_unit
|
updated_agent_thought.message_price_unit = llm_usage.prompt_price_unit
|
||||||
agent_thought.message_unit_price = llm_usage.prompt_unit_price
|
updated_agent_thought.message_unit_price = llm_usage.prompt_unit_price
|
||||||
agent_thought.answer_token = llm_usage.completion_tokens
|
updated_agent_thought.answer_token = llm_usage.completion_tokens
|
||||||
agent_thought.answer_price_unit = llm_usage.completion_price_unit
|
updated_agent_thought.answer_price_unit = llm_usage.completion_price_unit
|
||||||
agent_thought.answer_unit_price = llm_usage.completion_unit_price
|
updated_agent_thought.answer_unit_price = llm_usage.completion_unit_price
|
||||||
agent_thought.tokens = llm_usage.total_tokens
|
updated_agent_thought.tokens = llm_usage.total_tokens
|
||||||
agent_thought.total_price = llm_usage.total_price
|
updated_agent_thought.total_price = llm_usage.total_price
|
||||||
|
|
||||||
# check if tool labels is not empty
|
# check if tool labels is not empty
|
||||||
labels = agent_thought.tool_labels or {}
|
labels = updated_agent_thought.tool_labels or {}
|
||||||
tools = agent_thought.tool.split(";") if agent_thought.tool else []
|
tools = updated_agent_thought.tool.split(";") if updated_agent_thought.tool else []
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
if not tool:
|
if not tool:
|
||||||
continue
|
continue
|
||||||
@@ -392,42 +384,20 @@ class BaseAgentRunner(AppRunner):
|
|||||||
else:
|
else:
|
||||||
labels[tool] = {"en_US": tool, "zh_Hans": tool}
|
labels[tool] = {"en_US": tool, "zh_Hans": tool}
|
||||||
|
|
||||||
agent_thought.tool_labels_str = json.dumps(labels)
|
updated_agent_thought.tool_labels_str = json.dumps(labels)
|
||||||
|
|
||||||
if tool_invoke_meta is not None:
|
if tool_invoke_meta is not None:
|
||||||
if isinstance(tool_invoke_meta, dict):
|
if isinstance(tool_invoke_meta, dict):
|
||||||
try:
|
try:
|
||||||
tool_invoke_meta = json.dumps(tool_invoke_meta, ensure_ascii=False)
|
tool_invoke_meta = json.dumps(tool_invoke_meta, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
tool_invoke_meta = json.dumps(tool_invoke_meta)
|
tool_invoke_meta = json.dumps(tool_invoke_meta)
|
||||||
|
|
||||||
agent_thought.tool_meta_str = tool_invoke_meta
|
updated_agent_thought.tool_meta_str = tool_invoke_meta
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
def update_db_variables(self, tool_variables: ToolRuntimeVariablePool, db_variables: ToolConversationVariables):
|
|
||||||
"""
|
|
||||||
convert tool variables to db variables
|
|
||||||
"""
|
|
||||||
queried_variables = (
|
|
||||||
db.session.query(ToolConversationVariables)
|
|
||||||
.filter(
|
|
||||||
ToolConversationVariables.conversation_id == self.message.conversation_id,
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not queried_variables:
|
|
||||||
return
|
|
||||||
|
|
||||||
db_variables = queried_variables
|
|
||||||
|
|
||||||
db_variables.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
db_variables.variables_str = json.dumps(jsonable_encoder(tool_variables.pool))
|
|
||||||
db.session.commit()
|
|
||||||
db.session.close()
|
|
||||||
|
|
||||||
def organize_agent_history(self, prompt_messages: list[PromptMessage]) -> list[PromptMessage]:
|
def organize_agent_history(self, prompt_messages: list[PromptMessage]) -> list[PromptMessage]:
|
||||||
"""
|
"""
|
||||||
Organize agent history
|
Organize agent history
|
||||||
@@ -464,11 +434,11 @@ class BaseAgentRunner(AppRunner):
|
|||||||
tool_call_response: list[ToolPromptMessage] = []
|
tool_call_response: list[ToolPromptMessage] = []
|
||||||
try:
|
try:
|
||||||
tool_inputs = json.loads(agent_thought.tool_input)
|
tool_inputs = json.loads(agent_thought.tool_input)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
tool_inputs = {tool: {} for tool in tools}
|
tool_inputs = {tool: {} for tool in tools}
|
||||||
try:
|
try:
|
||||||
tool_responses = json.loads(agent_thought.observation)
|
tool_responses = json.loads(agent_thought.observation)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
tool_responses = dict.fromkeys(tools, agent_thought.observation)
|
tool_responses = dict.fromkeys(tools, agent_thought.observation)
|
||||||
|
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
@@ -515,7 +485,11 @@ class BaseAgentRunner(AppRunner):
|
|||||||
files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all()
|
files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all()
|
||||||
if not files:
|
if not files:
|
||||||
return UserPromptMessage(content=message.query)
|
return UserPromptMessage(content=message.query)
|
||||||
|
if message.app_model_config:
|
||||||
file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict())
|
file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict())
|
||||||
|
else:
|
||||||
|
file_extra_config = None
|
||||||
|
|
||||||
if not file_extra_config:
|
if not file_extra_config:
|
||||||
return UserPromptMessage(content=message.query)
|
return UserPromptMessage(content=message.query)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Generator, Mapping
|
from collections.abc import Generator, Mapping, Sequence
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from core.agent.base_agent_runner import BaseAgentRunner
|
from core.agent.base_agent_runner import BaseAgentRunner
|
||||||
@@ -18,8 +18,8 @@ from core.model_runtime.entities.message_entities import (
|
|||||||
)
|
)
|
||||||
from core.ops.ops_trace_manager import TraceQueueManager
|
from core.ops.ops_trace_manager import TraceQueueManager
|
||||||
from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform
|
from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform
|
||||||
|
from core.tools.__base.tool import Tool
|
||||||
from core.tools.entities.tool_entities import ToolInvokeMeta
|
from core.tools.entities.tool_entities import ToolInvokeMeta
|
||||||
from core.tools.tool.tool import Tool
|
|
||||||
from core.tools.tool_engine import ToolEngine
|
from core.tools.tool_engine import ToolEngine
|
||||||
from models.model import Message
|
from models.model import Message
|
||||||
|
|
||||||
@@ -27,11 +27,11 @@ from models.model import Message
|
|||||||
class CotAgentRunner(BaseAgentRunner, ABC):
|
class CotAgentRunner(BaseAgentRunner, ABC):
|
||||||
_is_first_iteration = True
|
_is_first_iteration = True
|
||||||
_ignore_observation_providers = ["wenxin"]
|
_ignore_observation_providers = ["wenxin"]
|
||||||
_historic_prompt_messages: list[PromptMessage] | None = None
|
_historic_prompt_messages: list[PromptMessage]
|
||||||
_agent_scratchpad: list[AgentScratchpadUnit] | None = None
|
_agent_scratchpad: list[AgentScratchpadUnit]
|
||||||
_instruction: str = "" # FIXME this must be str for now
|
_instruction: str
|
||||||
_query: str | None = None
|
_query: str
|
||||||
_prompt_messages_tools: list[PromptMessageTool] = []
|
_prompt_messages_tools: Sequence[PromptMessageTool]
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
@@ -42,6 +42,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
"""
|
"""
|
||||||
Run Cot agent application
|
Run Cot agent application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app_generate_entity = self.application_generate_entity
|
app_generate_entity = self.application_generate_entity
|
||||||
self._repack_app_generate_entity(app_generate_entity)
|
self._repack_app_generate_entity(app_generate_entity)
|
||||||
self._init_react_state(query)
|
self._init_react_state(query)
|
||||||
@@ -54,17 +55,19 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
app_generate_entity.model_conf.stop.append("Observation")
|
app_generate_entity.model_conf.stop.append("Observation")
|
||||||
|
|
||||||
app_config = self.app_config
|
app_config = self.app_config
|
||||||
|
assert app_config.agent
|
||||||
|
|
||||||
# init instruction
|
# init instruction
|
||||||
inputs = inputs or {}
|
inputs = inputs or {}
|
||||||
instruction = app_config.prompt_template.simple_prompt_template
|
instruction = app_config.prompt_template.simple_prompt_template or ""
|
||||||
self._instruction = self._fill_in_inputs_from_external_data_tools(instruction=instruction or "", inputs=inputs)
|
self._instruction = self._fill_in_inputs_from_external_data_tools(instruction, inputs)
|
||||||
|
|
||||||
iteration_step = 1
|
iteration_step = 1
|
||||||
max_iteration_steps = min(app_config.agent.max_iteration if app_config.agent else 5, 5) + 1
|
max_iteration_steps = min(app_config.agent.max_iteration if app_config.agent else 5, 5) + 1
|
||||||
|
|
||||||
# convert tools into ModelRuntime Tool format
|
# convert tools into ModelRuntime Tool format
|
||||||
tool_instances, self._prompt_messages_tools = self._init_prompt_tools()
|
tool_instances, prompt_messages_tools = self._init_prompt_tools()
|
||||||
|
self._prompt_messages_tools = prompt_messages_tools
|
||||||
|
|
||||||
function_call_state = True
|
function_call_state = True
|
||||||
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
||||||
@@ -116,14 +119,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
callbacks=[],
|
callbacks=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not isinstance(chunks, Generator):
|
usage_dict: dict[str, Optional[LLMUsage]] = {}
|
||||||
raise ValueError("Expected streaming response from LLM")
|
|
||||||
|
|
||||||
# check llm result
|
|
||||||
if not chunks:
|
|
||||||
raise ValueError("failed to invoke llm")
|
|
||||||
|
|
||||||
usage_dict: dict[str, Optional[LLMUsage]] = {"usage": None}
|
|
||||||
react_chunks = CotAgentOutputParser.handle_react_stream_output(chunks, usage_dict)
|
react_chunks = CotAgentOutputParser.handle_react_stream_output(chunks, usage_dict)
|
||||||
scratchpad = AgentScratchpadUnit(
|
scratchpad = AgentScratchpadUnit(
|
||||||
agent_response="",
|
agent_response="",
|
||||||
@@ -143,14 +139,14 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
if isinstance(chunk, AgentScratchpadUnit.Action):
|
if isinstance(chunk, AgentScratchpadUnit.Action):
|
||||||
action = chunk
|
action = chunk
|
||||||
# detect action
|
# detect action
|
||||||
if scratchpad.agent_response is not None:
|
assert scratchpad.agent_response is not None
|
||||||
scratchpad.agent_response += json.dumps(chunk.model_dump())
|
scratchpad.agent_response += json.dumps(chunk.model_dump())
|
||||||
scratchpad.action_str = json.dumps(chunk.model_dump())
|
scratchpad.action_str = json.dumps(chunk.model_dump())
|
||||||
scratchpad.action = action
|
scratchpad.action = action
|
||||||
else:
|
else:
|
||||||
if scratchpad.agent_response is not None:
|
assert scratchpad.agent_response is not None
|
||||||
scratchpad.agent_response += chunk
|
scratchpad.agent_response += chunk
|
||||||
if scratchpad.thought is not None:
|
assert scratchpad.thought is not None
|
||||||
scratchpad.thought += chunk
|
scratchpad.thought += chunk
|
||||||
yield LLMResultChunk(
|
yield LLMResultChunk(
|
||||||
model=self.model_config.model,
|
model=self.model_config.model,
|
||||||
@@ -158,9 +154,9 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
system_fingerprint="",
|
system_fingerprint="",
|
||||||
delta=LLMResultChunkDelta(index=0, message=AssistantPromptMessage(content=chunk), usage=None),
|
delta=LLMResultChunkDelta(index=0, message=AssistantPromptMessage(content=chunk), usage=None),
|
||||||
)
|
)
|
||||||
if scratchpad.thought is not None:
|
|
||||||
|
assert scratchpad.thought is not None
|
||||||
scratchpad.thought = scratchpad.thought.strip() or "I am thinking about how to help you"
|
scratchpad.thought = scratchpad.thought.strip() or "I am thinking about how to help you"
|
||||||
if self._agent_scratchpad is not None:
|
|
||||||
self._agent_scratchpad.append(scratchpad)
|
self._agent_scratchpad.append(scratchpad)
|
||||||
|
|
||||||
# get llm usage
|
# get llm usage
|
||||||
@@ -172,7 +168,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
|
|
||||||
self.save_agent_thought(
|
self.save_agent_thought(
|
||||||
agent_thought=agent_thought,
|
agent_thought=agent_thought,
|
||||||
tool_name=scratchpad.action.action_name if scratchpad.action else "",
|
tool_name=(scratchpad.action.action_name if scratchpad.action and not scratchpad.is_final() else ""),
|
||||||
tool_input={scratchpad.action.action_name: scratchpad.action.action_input} if scratchpad.action else {},
|
tool_input={scratchpad.action.action_name: scratchpad.action.action_input} if scratchpad.action else {},
|
||||||
tool_invoke_meta={},
|
tool_invoke_meta={},
|
||||||
thought=scratchpad.thought or "",
|
thought=scratchpad.thought or "",
|
||||||
@@ -256,8 +252,6 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
answer=final_answer,
|
answer=final_answer,
|
||||||
messages_ids=[],
|
messages_ids=[],
|
||||||
)
|
)
|
||||||
if self.variables_pool is not None and self.db_variables_pool is not None:
|
|
||||||
self.update_db_variables(self.variables_pool, self.db_variables_pool)
|
|
||||||
# publish end event
|
# publish end event
|
||||||
self.queue_manager.publish(
|
self.queue_manager.publish(
|
||||||
QueueMessageEndEvent(
|
QueueMessageEndEvent(
|
||||||
@@ -275,7 +269,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
def _handle_invoke_action(
|
def _handle_invoke_action(
|
||||||
self,
|
self,
|
||||||
action: AgentScratchpadUnit.Action,
|
action: AgentScratchpadUnit.Action,
|
||||||
tool_instances: dict[str, Tool],
|
tool_instances: Mapping[str, Tool],
|
||||||
message_file_ids: list[str],
|
message_file_ids: list[str],
|
||||||
trace_manager: Optional[TraceQueueManager] = None,
|
trace_manager: Optional[TraceQueueManager] = None,
|
||||||
) -> tuple[str, ToolInvokeMeta]:
|
) -> tuple[str, ToolInvokeMeta]:
|
||||||
@@ -315,11 +309,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# publish files
|
# publish files
|
||||||
for message_file_id, save_as in message_files:
|
for message_file_id in message_files:
|
||||||
if save_as is not None and self.variables_pool:
|
|
||||||
# FIXME the save_as type is confusing, it should be a string or not
|
|
||||||
self.variables_pool.set_file(tool_name=tool_call_name, value=message_file_id, name=str(save_as))
|
|
||||||
|
|
||||||
# publish message file
|
# publish message file
|
||||||
self.queue_manager.publish(
|
self.queue_manager.publish(
|
||||||
QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
|
QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
|
||||||
@@ -342,7 +332,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
for key, value in inputs.items():
|
for key, value in inputs.items():
|
||||||
try:
|
try:
|
||||||
instruction = instruction.replace(f"{{{{{key}}}}}", str(value))
|
instruction = instruction.replace(f"{{{{{key}}}}}", str(value))
|
||||||
except Exception as e:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return instruction
|
return instruction
|
||||||
@@ -379,7 +369,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
return message
|
return message
|
||||||
|
|
||||||
def _organize_historic_prompt_messages(
|
def _organize_historic_prompt_messages(
|
||||||
self, current_session_messages: Optional[list[PromptMessage]] = None
|
self, current_session_messages: list[PromptMessage] | None = None
|
||||||
) -> list[PromptMessage]:
|
) -> list[PromptMessage]:
|
||||||
"""
|
"""
|
||||||
organize historic prompt messages
|
organize historic prompt messages
|
||||||
@@ -391,8 +381,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
for message in self.history_prompt_messages:
|
for message in self.history_prompt_messages:
|
||||||
if isinstance(message, AssistantPromptMessage):
|
if isinstance(message, AssistantPromptMessage):
|
||||||
if not current_scratchpad:
|
if not current_scratchpad:
|
||||||
if not isinstance(message.content, str | None):
|
assert isinstance(message.content, str)
|
||||||
raise NotImplementedError("expected str type")
|
|
||||||
current_scratchpad = AgentScratchpadUnit(
|
current_scratchpad = AgentScratchpadUnit(
|
||||||
agent_response=message.content,
|
agent_response=message.content,
|
||||||
thought=message.content or "I am thinking about how to help you",
|
thought=message.content or "I am thinking about how to help you",
|
||||||
@@ -411,9 +400,8 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
elif isinstance(message, ToolPromptMessage):
|
elif isinstance(message, ToolPromptMessage):
|
||||||
if not current_scratchpad:
|
if current_scratchpad:
|
||||||
continue
|
assert isinstance(message.content, str)
|
||||||
if isinstance(message.content, str):
|
|
||||||
current_scratchpad.observation = message.content
|
current_scratchpad.observation = message.content
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("expected str type")
|
raise NotImplementedError("expected str type")
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ class CotChatAgentRunner(CotAgentRunner):
|
|||||||
"""
|
"""
|
||||||
Organize system prompt
|
Organize system prompt
|
||||||
"""
|
"""
|
||||||
if not self.app_config.agent:
|
assert self.app_config.agent
|
||||||
raise ValueError("Agent configuration is not set")
|
assert self.app_config.agent.prompt
|
||||||
|
|
||||||
prompt_entity = self.app_config.agent.prompt
|
prompt_entity = self.app_config.agent.prompt
|
||||||
if not prompt_entity:
|
if not prompt_entity:
|
||||||
@@ -83,8 +83,10 @@ class CotChatAgentRunner(CotAgentRunner):
|
|||||||
assistant_message.content = "" # FIXME: type check tell mypy that assistant_message.content is str
|
assistant_message.content = "" # FIXME: type check tell mypy that assistant_message.content is str
|
||||||
for unit in agent_scratchpad:
|
for unit in agent_scratchpad:
|
||||||
if unit.is_final():
|
if unit.is_final():
|
||||||
|
assert isinstance(assistant_message.content, str)
|
||||||
assistant_message.content += f"Final Answer: {unit.agent_response}"
|
assistant_message.content += f"Final Answer: {unit.agent_response}"
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(assistant_message.content, str)
|
||||||
assistant_message.content += f"Thought: {unit.thought}\n\n"
|
assistant_message.content += f"Thought: {unit.thought}\n\n"
|
||||||
if unit.action_str:
|
if unit.action_str:
|
||||||
assistant_message.content += f"Action: {unit.action_str}\n\n"
|
assistant_message.content += f"Action: {unit.action_str}\n\n"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
from typing import Any, Literal, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolProviderType
|
||||||
|
|
||||||
|
|
||||||
class AgentToolEntity(BaseModel):
|
class AgentToolEntity(BaseModel):
|
||||||
@@ -9,10 +11,11 @@ class AgentToolEntity(BaseModel):
|
|||||||
Agent Tool Entity.
|
Agent Tool Entity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
provider_type: Literal["builtin", "api", "workflow"]
|
provider_type: ToolProviderType
|
||||||
provider_id: str
|
provider_id: str
|
||||||
tool_name: str
|
tool_name: str
|
||||||
tool_parameters: dict[str, Any] = {}
|
tool_parameters: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
plugin_unique_identifier: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AgentPromptEntity(BaseModel):
|
class AgentPromptEntity(BaseModel):
|
||||||
@@ -66,7 +69,7 @@ class AgentEntity(BaseModel):
|
|||||||
Agent Entity.
|
Agent Entity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Strategy(Enum):
|
class Strategy(StrEnum):
|
||||||
"""
|
"""
|
||||||
Agent Strategy.
|
Agent Strategy.
|
||||||
"""
|
"""
|
||||||
@@ -78,5 +81,13 @@ class AgentEntity(BaseModel):
|
|||||||
model: str
|
model: str
|
||||||
strategy: Strategy
|
strategy: Strategy
|
||||||
prompt: Optional[AgentPromptEntity] = None
|
prompt: Optional[AgentPromptEntity] = None
|
||||||
tools: list[AgentToolEntity] | None = None
|
tools: Optional[list[AgentToolEntity]] = None
|
||||||
max_iteration: int = 5
|
max_iteration: int = 5
|
||||||
|
|
||||||
|
|
||||||
|
class AgentInvokeMessage(ToolInvokeMessage):
|
||||||
|
"""
|
||||||
|
Agent Invoke Message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|||||||
@@ -46,18 +46,20 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
# convert tools into ModelRuntime Tool format
|
# convert tools into ModelRuntime Tool format
|
||||||
tool_instances, prompt_messages_tools = self._init_prompt_tools()
|
tool_instances, prompt_messages_tools = self._init_prompt_tools()
|
||||||
|
|
||||||
|
assert app_config.agent
|
||||||
|
|
||||||
iteration_step = 1
|
iteration_step = 1
|
||||||
max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1
|
max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1
|
||||||
|
|
||||||
# continue to run until there is not any tool call
|
# continue to run until there is not any tool call
|
||||||
function_call_state = True
|
function_call_state = True
|
||||||
llm_usage: dict[str, LLMUsage] = {"usage": LLMUsage.empty_usage()}
|
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
||||||
final_answer = ""
|
final_answer = ""
|
||||||
|
|
||||||
# get tracing instance
|
# get tracing instance
|
||||||
trace_manager = app_generate_entity.trace_manager
|
trace_manager = app_generate_entity.trace_manager
|
||||||
|
|
||||||
def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage):
|
def increase_usage(final_llm_usage_dict: dict[str, Optional[LLMUsage]], usage: LLMUsage):
|
||||||
if not final_llm_usage_dict["usage"]:
|
if not final_llm_usage_dict["usage"]:
|
||||||
final_llm_usage_dict["usage"] = usage
|
final_llm_usage_dict["usage"] = usage
|
||||||
else:
|
else:
|
||||||
@@ -107,7 +109,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
|
|
||||||
current_llm_usage = None
|
current_llm_usage = None
|
||||||
|
|
||||||
if self.stream_tool_call and isinstance(chunks, Generator):
|
if isinstance(chunks, Generator):
|
||||||
is_first_chunk = True
|
is_first_chunk = True
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
if is_first_chunk:
|
if is_first_chunk:
|
||||||
@@ -124,7 +126,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
tool_call_inputs = json.dumps(
|
tool_call_inputs = json.dumps(
|
||||||
{tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
|
{tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError:
|
||||||
# ensure ascii to avoid encoding error
|
# ensure ascii to avoid encoding error
|
||||||
tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
|
tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
|
||||||
|
|
||||||
@@ -140,7 +142,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
current_llm_usage = chunk.delta.usage
|
current_llm_usage = chunk.delta.usage
|
||||||
|
|
||||||
yield chunk
|
yield chunk
|
||||||
elif not self.stream_tool_call and isinstance(chunks, LLMResult):
|
else:
|
||||||
result = chunks
|
result = chunks
|
||||||
# check if there is any tool call
|
# check if there is any tool call
|
||||||
if self.check_blocking_tool_calls(result):
|
if self.check_blocking_tool_calls(result):
|
||||||
@@ -151,7 +153,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
tool_call_inputs = json.dumps(
|
tool_call_inputs = json.dumps(
|
||||||
{tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
|
{tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError:
|
||||||
# ensure ascii to avoid encoding error
|
# ensure ascii to avoid encoding error
|
||||||
tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
|
tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
|
||||||
|
|
||||||
@@ -183,8 +185,6 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
usage=result.usage,
|
usage=result.usage,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
raise RuntimeError(f"invalid chunks type: {type(chunks)}")
|
|
||||||
|
|
||||||
assistant_message = AssistantPromptMessage(content="", tool_calls=[])
|
assistant_message = AssistantPromptMessage(content="", tool_calls=[])
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
@@ -243,15 +243,12 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
invoke_from=self.application_generate_entity.invoke_from,
|
invoke_from=self.application_generate_entity.invoke_from,
|
||||||
agent_tool_callback=self.agent_callback,
|
agent_tool_callback=self.agent_callback,
|
||||||
trace_manager=trace_manager,
|
trace_manager=trace_manager,
|
||||||
|
app_id=self.application_generate_entity.app_config.app_id,
|
||||||
|
message_id=self.message.id,
|
||||||
|
conversation_id=self.conversation.id,
|
||||||
)
|
)
|
||||||
# publish files
|
# publish files
|
||||||
for message_file_id, save_as in message_files:
|
for message_file_id in message_files:
|
||||||
if save_as:
|
|
||||||
if self.variables_pool:
|
|
||||||
self.variables_pool.set_file(
|
|
||||||
tool_name=tool_call_name, value=message_file_id, name=save_as
|
|
||||||
)
|
|
||||||
|
|
||||||
# publish message file
|
# publish message file
|
||||||
self.queue_manager.publish(
|
self.queue_manager.publish(
|
||||||
QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
|
QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
|
||||||
@@ -303,8 +300,6 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
|
|
||||||
iteration_step += 1
|
iteration_step += 1
|
||||||
|
|
||||||
if self.variables_pool and self.db_variables_pool:
|
|
||||||
self.update_db_variables(self.variables_pool, self.db_variables_pool)
|
|
||||||
# publish end event
|
# publish end event
|
||||||
self.queue_manager.publish(
|
self.queue_manager.publish(
|
||||||
QueueMessageEndEvent(
|
QueueMessageEndEvent(
|
||||||
@@ -335,9 +330,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def extract_tool_calls(
|
def extract_tool_calls(self, llm_result_chunk: LLMResultChunk) -> list[tuple[str, str, dict[str, Any]]]:
|
||||||
self, llm_result_chunk: LLMResultChunk
|
|
||||||
) -> Union[None, list[tuple[str, str, dict[str, Any]]]]:
|
|
||||||
"""
|
"""
|
||||||
Extract tool calls from llm result chunk
|
Extract tool calls from llm result chunk
|
||||||
|
|
||||||
@@ -360,7 +353,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
|
|
||||||
return tool_calls
|
return tool_calls
|
||||||
|
|
||||||
def extract_blocking_tool_calls(self, llm_result: LLMResult) -> Union[None, list[tuple[str, str, dict[str, Any]]]]:
|
def extract_blocking_tool_calls(self, llm_result: LLMResult) -> list[tuple[str, str, dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
Extract blocking tool calls from llm result
|
Extract blocking tool calls from llm result
|
||||||
|
|
||||||
@@ -383,9 +376,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
|
|
||||||
return tool_calls
|
return tool_calls
|
||||||
|
|
||||||
def _init_system_message(
|
def _init_system_message(self, prompt_template: str, prompt_messages: list[PromptMessage]) -> list[PromptMessage]:
|
||||||
self, prompt_template: str, prompt_messages: Optional[list[PromptMessage]] = None
|
|
||||||
) -> list[PromptMessage]:
|
|
||||||
"""
|
"""
|
||||||
Initialize system message
|
Initialize system message
|
||||||
"""
|
"""
|
||||||
|
|||||||
89
api/core/agent/plugin_entities.py
Normal file
89
api/core/agent/plugin_entities.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
|
||||||
|
|
||||||
|
from core.entities.parameter_entities import CommonParameterType
|
||||||
|
from core.plugin.entities.parameters import (
|
||||||
|
PluginParameter,
|
||||||
|
as_normal_type,
|
||||||
|
cast_parameter_value,
|
||||||
|
init_frontend_parameter,
|
||||||
|
)
|
||||||
|
from core.tools.entities.common_entities import I18nObject
|
||||||
|
from core.tools.entities.tool_entities import (
|
||||||
|
ToolIdentity,
|
||||||
|
ToolProviderIdentity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStrategyProviderIdentity(ToolProviderIdentity):
|
||||||
|
"""
|
||||||
|
Inherits from ToolProviderIdentity, without any additional fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStrategyParameter(PluginParameter):
|
||||||
|
class AgentStrategyParameterType(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Keep all the types from PluginParameterType
|
||||||
|
"""
|
||||||
|
|
||||||
|
STRING = CommonParameterType.STRING.value
|
||||||
|
NUMBER = CommonParameterType.NUMBER.value
|
||||||
|
BOOLEAN = CommonParameterType.BOOLEAN.value
|
||||||
|
SELECT = CommonParameterType.SELECT.value
|
||||||
|
SECRET_INPUT = CommonParameterType.SECRET_INPUT.value
|
||||||
|
FILE = CommonParameterType.FILE.value
|
||||||
|
FILES = CommonParameterType.FILES.value
|
||||||
|
APP_SELECTOR = CommonParameterType.APP_SELECTOR.value
|
||||||
|
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value
|
||||||
|
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value
|
||||||
|
|
||||||
|
# deprecated, should not use.
|
||||||
|
SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value
|
||||||
|
|
||||||
|
def as_normal_type(self):
|
||||||
|
return as_normal_type(self)
|
||||||
|
|
||||||
|
def cast_value(self, value: Any):
|
||||||
|
return cast_parameter_value(self, value)
|
||||||
|
|
||||||
|
type: AgentStrategyParameterType = Field(..., description="The type of the parameter")
|
||||||
|
|
||||||
|
def init_frontend_parameter(self, value: Any):
|
||||||
|
return init_frontend_parameter(self, self.type, value)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStrategyProviderEntity(BaseModel):
|
||||||
|
identity: AgentStrategyProviderIdentity
|
||||||
|
plugin_id: Optional[str] = Field(None, description="The id of the plugin")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStrategyIdentity(ToolIdentity):
|
||||||
|
"""
|
||||||
|
Inherits from ToolIdentity, without any additional fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AgentStrategyEntity(BaseModel):
|
||||||
|
identity: AgentStrategyIdentity
|
||||||
|
parameters: list[AgentStrategyParameter] = Field(default_factory=list)
|
||||||
|
description: I18nObject = Field(..., description="The description of the agent strategy")
|
||||||
|
output_schema: Optional[dict] = None
|
||||||
|
|
||||||
|
# pydantic configs
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
@field_validator("parameters", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def set_parameters(cls, v, validation_info: ValidationInfo) -> list[AgentStrategyParameter]:
|
||||||
|
return v or []
|
||||||
|
|
||||||
|
|
||||||
|
class AgentProviderEntityWithPlugin(AgentStrategyProviderEntity):
|
||||||
|
strategies: list[AgentStrategyEntity] = Field(default_factory=list)
|
||||||
42
api/core/agent/strategy/base.py
Normal file
42
api/core/agent/strategy/base.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Generator, Sequence
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from core.agent.entities import AgentInvokeMessage
|
||||||
|
from core.agent.plugin_entities import AgentStrategyParameter
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgentStrategy(ABC):
|
||||||
|
"""
|
||||||
|
Agent Strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
def invoke(
|
||||||
|
self,
|
||||||
|
params: dict[str, Any],
|
||||||
|
user_id: str,
|
||||||
|
conversation_id: Optional[str] = None,
|
||||||
|
app_id: Optional[str] = None,
|
||||||
|
message_id: Optional[str] = None,
|
||||||
|
) -> Generator[AgentInvokeMessage, None, None]:
|
||||||
|
"""
|
||||||
|
Invoke the agent strategy.
|
||||||
|
"""
|
||||||
|
yield from self._invoke(params, user_id, conversation_id, app_id, message_id)
|
||||||
|
|
||||||
|
def get_parameters(self) -> Sequence[AgentStrategyParameter]:
|
||||||
|
"""
|
||||||
|
Get the parameters for the agent strategy.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _invoke(
|
||||||
|
self,
|
||||||
|
params: dict[str, Any],
|
||||||
|
user_id: str,
|
||||||
|
conversation_id: Optional[str] = None,
|
||||||
|
app_id: Optional[str] = None,
|
||||||
|
message_id: Optional[str] = None,
|
||||||
|
) -> Generator[AgentInvokeMessage, None, None]:
|
||||||
|
pass
|
||||||
59
api/core/agent/strategy/plugin.py
Normal file
59
api/core/agent/strategy/plugin.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from collections.abc import Generator, Sequence
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from core.agent.entities import AgentInvokeMessage
|
||||||
|
from core.agent.plugin_entities import AgentStrategyEntity, AgentStrategyParameter
|
||||||
|
from core.agent.strategy.base import BaseAgentStrategy
|
||||||
|
from core.plugin.manager.agent import PluginAgentManager
|
||||||
|
from core.plugin.utils.converter import convert_parameters_to_plugin_format
|
||||||
|
|
||||||
|
|
||||||
|
class PluginAgentStrategy(BaseAgentStrategy):
|
||||||
|
"""
|
||||||
|
Agent Strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
tenant_id: str
|
||||||
|
declaration: AgentStrategyEntity
|
||||||
|
|
||||||
|
def __init__(self, tenant_id: str, declaration: AgentStrategyEntity):
|
||||||
|
self.tenant_id = tenant_id
|
||||||
|
self.declaration = declaration
|
||||||
|
|
||||||
|
def get_parameters(self) -> Sequence[AgentStrategyParameter]:
|
||||||
|
return self.declaration.parameters
|
||||||
|
|
||||||
|
def initialize_parameters(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Initialize the parameters for the agent strategy.
|
||||||
|
"""
|
||||||
|
for parameter in self.declaration.parameters:
|
||||||
|
params[parameter.name] = parameter.init_frontend_parameter(params.get(parameter.name))
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _invoke(
|
||||||
|
self,
|
||||||
|
params: dict[str, Any],
|
||||||
|
user_id: str,
|
||||||
|
conversation_id: Optional[str] = None,
|
||||||
|
app_id: Optional[str] = None,
|
||||||
|
message_id: Optional[str] = None,
|
||||||
|
) -> Generator[AgentInvokeMessage, None, None]:
|
||||||
|
"""
|
||||||
|
Invoke the agent strategy.
|
||||||
|
"""
|
||||||
|
manager = PluginAgentManager()
|
||||||
|
|
||||||
|
initialized_params = self.initialize_parameters(params)
|
||||||
|
params = convert_parameters_to_plugin_format(initialized_params)
|
||||||
|
|
||||||
|
yield from manager.invoke(
|
||||||
|
tenant_id=self.tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_provider=self.declaration.identity.provider,
|
||||||
|
agent_strategy=self.declaration.identity.name,
|
||||||
|
agent_params=params,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
app_id=app_id,
|
||||||
|
message_id=message_id,
|
||||||
|
)
|
||||||
@@ -4,7 +4,8 @@ from core.app.app_config.entities import EasyUIBasedAppConfig
|
|||||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||||
from core.entities.model_entities import ModelStatus
|
from core.entities.model_entities import ModelStatus
|
||||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||||
from core.model_runtime.entities.model_entities import ModelType
|
from core.model_runtime.entities.llm_entities import LLMMode
|
||||||
|
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
|
||||||
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||||
from core.provider_manager import ProviderManager
|
from core.provider_manager import ProviderManager
|
||||||
|
|
||||||
@@ -63,14 +64,14 @@ class ModelConfigConverter:
|
|||||||
stop = completion_params["stop"]
|
stop = completion_params["stop"]
|
||||||
del completion_params["stop"]
|
del completion_params["stop"]
|
||||||
|
|
||||||
|
model_schema = model_type_instance.get_model_schema(model_config.model, model_credentials)
|
||||||
|
|
||||||
# get model mode
|
# get model mode
|
||||||
model_mode = model_config.mode
|
model_mode = model_config.mode
|
||||||
if not model_mode:
|
if not model_mode:
|
||||||
mode_enum = model_type_instance.get_model_mode(model=model_config.model, credentials=model_credentials)
|
model_mode = LLMMode.CHAT.value
|
||||||
|
if model_schema and model_schema.model_properties.get(ModelPropertyKey.MODE):
|
||||||
model_mode = mode_enum.value
|
model_mode = LLMMode.value_of(model_schema.model_properties[ModelPropertyKey.MODE]).value
|
||||||
|
|
||||||
model_schema = model_type_instance.get_model_schema(model_config.model, model_credentials)
|
|
||||||
|
|
||||||
if not model_schema:
|
if not model_schema:
|
||||||
raise ValueError(f"Model {model_name} not exist.")
|
raise ValueError(f"Model {model_name} not exist.")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user