mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
Compare commits
403 Commits
v1.10.2-lt
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847755b1c2 | ||
|
|
b57455d0ee | ||
|
|
2a257edff9 | ||
|
|
d47699331c | ||
|
|
90c64d77dd | ||
|
|
e1ada3ffe2 | ||
|
|
b62c79fda6 | ||
|
|
3d9e7dd4b1 | ||
|
|
8ff15865a7 | ||
|
|
48899d55d1 | ||
|
|
1cc4107bfe | ||
|
|
b13cd03706 | ||
|
|
69f024492b | ||
|
|
a9c46cd7e0 | ||
|
|
a9e9f5b085 | ||
|
|
e12b1fe14e | ||
|
|
7ce66a7bf3 | ||
|
|
decd3395db | ||
|
|
9d7a383348 | ||
|
|
8f7d91798b | ||
|
|
81a3af2c8b | ||
|
|
2ec0d22b14 | ||
|
|
27a77dc657 | ||
|
|
187e9c1e4e | ||
|
|
e5bab10824 | ||
|
|
347f4a0b03 | ||
|
|
289ebf42a6 | ||
|
|
71fdce08d7 | ||
|
|
adc5af9cef | ||
|
|
ce2ab322f6 | ||
|
|
a7e31b94c7 | ||
|
|
8498687794 | ||
|
|
190ca3e198 | ||
|
|
c1ddec1a61 | ||
|
|
1ba8077e95 | ||
|
|
9a42bd2302 | ||
|
|
35b662a52d | ||
|
|
a4faf52261 | ||
|
|
a30316d87a | ||
|
|
a4d10cbe3b | ||
|
|
ceccf9f1fa | ||
|
|
3964db20dc | ||
|
|
57ada0708f | ||
|
|
a1a92a833a | ||
|
|
5e0d8048f9 | ||
|
|
8903b35aec | ||
|
|
fa4f7e99fd | ||
|
|
b0630b3ddd | ||
|
|
b2bf69740c | ||
|
|
8cf66b9eca | ||
|
|
1db8577ca6 | ||
|
|
949e4dea9e | ||
|
|
c12988bc8a | ||
|
|
2728453f6c | ||
|
|
ccf43bbcd9 | ||
|
|
8d8de53e38 | ||
|
|
7faf556771 | ||
|
|
76ec8ad6f6 | ||
|
|
0cf05a76a0 | ||
|
|
357edbfbe0 | ||
|
|
0609a9afc8 | ||
|
|
96e59a018f | ||
|
|
79b2de8893 | ||
|
|
0c7cca035e | ||
|
|
00a3e5ddc3 | ||
|
|
a6533c0db7 | ||
|
|
704077d066 | ||
|
|
59ee0c1270 | ||
|
|
b37cc3ba1c | ||
|
|
dfe6d0a91b | ||
|
|
5813eedd4f | ||
|
|
363150380d | ||
|
|
47f9c04664 | ||
|
|
17cd88edda | ||
|
|
d33b620dc8 | ||
|
|
b95cb20704 | ||
|
|
4ff1944b60 | ||
|
|
1c6b0f8a86 | ||
|
|
d85801fe58 | ||
|
|
e79e7d505d | ||
|
|
33b1cd65b0 | ||
|
|
8d503c8bf8 | ||
|
|
df7f922013 | ||
|
|
a0541203e4 | ||
|
|
fb64731cd8 | ||
|
|
d960a18711 | ||
|
|
ee35cc21e9 | ||
|
|
e4a60daa17 | ||
|
|
7bcb770ee5 | ||
|
|
b5b09dc8b4 | ||
|
|
b1f6092620 | ||
|
|
c8441cfd73 | ||
|
|
7e4b147576 | ||
|
|
9cd082089a | ||
|
|
d4541e23f9 | ||
|
|
5e7e91cced | ||
|
|
d9787bb548 | ||
|
|
131b5b3bbe | ||
|
|
e58f95832b | ||
|
|
f24337d5f3 | ||
|
|
d6f1d25b59 | ||
|
|
0ec198fa43 | ||
|
|
77e96624ee | ||
|
|
5e02809db2 | ||
|
|
6fe001fcf8 | ||
|
|
f646102262 | ||
|
|
b97f4e16ba | ||
|
|
0c14306889 | ||
|
|
f1d043f67b | ||
|
|
c39a6e81d7 | ||
|
|
9c56c7e198 | ||
|
|
6484fef8ea | ||
|
|
8a194481ac | ||
|
|
072b817792 | ||
|
|
d32f7d36a6 | ||
|
|
54c9d4e725 | ||
|
|
d2637c3de2 | ||
|
|
2550324003 | ||
|
|
bf52dd8174 | ||
|
|
2ecec57d2f | ||
|
|
b5fda0e020 | ||
|
|
45a60cd9a7 | ||
|
|
2b82675853 | ||
|
|
1d3bf1ca73 | ||
|
|
04cb9c96fe | ||
|
|
45d8ac2eee | ||
|
|
eb60331c88 | ||
|
|
8fc9b0a22d | ||
|
|
ec5fd9e343 | ||
|
|
1e49939c38 | ||
|
|
5a7b23aa00 | ||
|
|
791505b7b8 | ||
|
|
da10649adb | ||
|
|
a025e3960d | ||
|
|
98ed348de9 | ||
|
|
b9dcc36b31 | ||
|
|
7a3d3844ae | ||
|
|
6ea2cf149a | ||
|
|
c30677d8b0 | ||
|
|
a1a2fb5628 | ||
|
|
f9cb0e24d6 | ||
|
|
2dc42183cb | ||
|
|
c781c11d26 | ||
|
|
e178cfe5c0 | ||
|
|
3b24373cd0 | ||
|
|
125ed8aa7a | ||
|
|
0b60a03e5d | ||
|
|
bb3f17ebfe | ||
|
|
ce7efd4758 | ||
|
|
1695710cbe | ||
|
|
b0366b18b6 | ||
|
|
a4f27249ed | ||
|
|
ebe8506c67 | ||
|
|
5243e42100 | ||
|
|
e5738f3b31 | ||
|
|
3c3bd9884f | ||
|
|
0213aff12a | ||
|
|
8dc793a128 | ||
|
|
0861eb4cdc | ||
|
|
d78c1459b7 | ||
|
|
f188383fea | ||
|
|
7421caba9b | ||
|
|
bbab359813 | ||
|
|
deb3844b4c | ||
|
|
26f36ccee1 | ||
|
|
8381ca5287 | ||
|
|
e8c1cdf959 | ||
|
|
675d2366da | ||
|
|
bbb63a5928 | ||
|
|
f5282bf1e7 | ||
|
|
4ae02c8d3e | ||
|
|
c0ffc0aaf5 | ||
|
|
3557ea50fa | ||
|
|
43702e42b8 | ||
|
|
7a3a975645 | ||
|
|
9da7a553bf | ||
|
|
2d6d16e046 | ||
|
|
919a3eee5d | ||
|
|
1a704f1c25 | ||
|
|
7ca0a7bd02 | ||
|
|
2681d02728 | ||
|
|
5e43bb9d2a | ||
|
|
ecd5fafbaa | ||
|
|
23b47657c0 | ||
|
|
76d050bea4 | ||
|
|
add9d1bab8 | ||
|
|
9c36d8f30a | ||
|
|
189e2a6c63 | ||
|
|
867c53984b | ||
|
|
560890f717 | ||
|
|
675adeeb63 | ||
|
|
6bc00eb869 | ||
|
|
1eccb54199 | ||
|
|
86e11baeb2 | ||
|
|
69ae1cafab | ||
|
|
9d6451b95b | ||
|
|
add1cba8cb | ||
|
|
927f0c784a | ||
|
|
ac5a9d01a8 | ||
|
|
7b213f547d | ||
|
|
7e4e2e98bb | ||
|
|
21d2a44090 | ||
|
|
e364d6e373 | ||
|
|
15feca802a | ||
|
|
2686e76c8a | ||
|
|
6cf91098d6 | ||
|
|
678a5ae4a5 | ||
|
|
a71c844ef4 | ||
|
|
74d10b61bc | ||
|
|
27bc01d442 | ||
|
|
bd900118f4 | ||
|
|
44c1d35b1f | ||
|
|
f6994e16b9 | ||
|
|
6d7b5eb219 | ||
|
|
27d4603b02 | ||
|
|
339e18d837 | ||
|
|
fb0fdb9c85 | ||
|
|
5b2baaf04d | ||
|
|
739b977ce3 | ||
|
|
0315fe91df | ||
|
|
02611588fc | ||
|
|
2991f0b640 | ||
|
|
6fde8ec80f | ||
|
|
081fcab7eb | ||
|
|
0e98c7783b | ||
|
|
812dc142c8 | ||
|
|
26946d0afb | ||
|
|
ec6657177a | ||
|
|
4b9cecd4d1 | ||
|
|
06867d33cb | ||
|
|
b53a933327 | ||
|
|
596b13711f | ||
|
|
e7c3169898 | ||
|
|
2612894557 | ||
|
|
7afc1da0af | ||
|
|
6aa0e9b5e4 | ||
|
|
24763427eb | ||
|
|
da03442be2 | ||
|
|
9750c6d605 | ||
|
|
a2b6620b10 | ||
|
|
b0a4e9e78f | ||
|
|
36809b3314 | ||
|
|
165c27164e | ||
|
|
1566ae7fbe | ||
|
|
b3feb243d3 | ||
|
|
cb104cc211 | ||
|
|
7a99c78840 | ||
|
|
a07df46f9d | ||
|
|
4fa3fec103 | ||
|
|
a2265cf357 | ||
|
|
0b1990f8b3 | ||
|
|
bf91579b4e | ||
|
|
cc2789f37f | ||
|
|
3fe47e0fb3 | ||
|
|
46e464b126 | ||
|
|
54c4293482 | ||
|
|
a799026d52 | ||
|
|
5ceb8a7ff9 | ||
|
|
e7e4570aeb | ||
|
|
f69346f2bc | ||
|
|
dcc80a4dca | ||
|
|
2fe4248785 | ||
|
|
bdaeb1bec4 | ||
|
|
89be3e317d | ||
|
|
2b079a4144 | ||
|
|
4ffec85e9b | ||
|
|
bd098e68e5 | ||
|
|
d282795644 | ||
|
|
5ba802482f | ||
|
|
378de21fa2 | ||
|
|
5eee6bfb6c | ||
|
|
d47295aa36 | ||
|
|
0a3b9ee02b | ||
|
|
97fb4a5cea | ||
|
|
80cd3ff7ec | ||
|
|
c66f79ad5a | ||
|
|
3797613182 | ||
|
|
7145f303da | ||
|
|
754f7cb87c | ||
|
|
8a7e41be61 | ||
|
|
532ea4941a | ||
|
|
286552d54b | ||
|
|
f82ba3c4b8 | ||
|
|
0e29ce28cf | ||
|
|
eed4d6857e | ||
|
|
601b03d84e | ||
|
|
8252febe22 | ||
|
|
1a57e5897d | ||
|
|
1e8e3a90aa | ||
|
|
2971406909 | ||
|
|
db772b1d1c | ||
|
|
ca12d653a6 | ||
|
|
a0ee5c9441 | ||
|
|
b1aa1f7a53 | ||
|
|
579604bd81 | ||
|
|
a303f24974 | ||
|
|
fadbd2fde0 | ||
|
|
bd7dbb13f3 | ||
|
|
394d96980b | ||
|
|
d27e5a4c01 | ||
|
|
a6703c9889 | ||
|
|
545e80d655 | ||
|
|
436e43dd04 | ||
|
|
b8562ec736 | ||
|
|
1b6b021226 | ||
|
|
55cdd0a708 | ||
|
|
f9d536f5a2 | ||
|
|
ba4e55d3e8 | ||
|
|
3594fdadfa | ||
|
|
83140b5f1d | ||
|
|
26cb55adfb | ||
|
|
440e2ba695 | ||
|
|
f19316639e | ||
|
|
564e781ba2 | ||
|
|
9b46d29e73 | ||
|
|
6630589e8e | ||
|
|
2f20868ca6 | ||
|
|
64df9cf437 | ||
|
|
66b84a77b9 | ||
|
|
d058d6d176 | ||
|
|
5f289885f7 | ||
|
|
82b06d130a | ||
|
|
0f0b6b976e | ||
|
|
f681cb9b23 | ||
|
|
ee5c8a455d | ||
|
|
b01172b242 | ||
|
|
19b9e52a45 | ||
|
|
563516f835 | ||
|
|
2d6ac806ff | ||
|
|
f9c4e96f97 | ||
|
|
66f674f651 | ||
|
|
98ef6e5775 | ||
|
|
ad452afa52 | ||
|
|
0a148bff4b | ||
|
|
9189a2ff2b | ||
|
|
c0255beca2 | ||
|
|
c526a27796 | ||
|
|
13ce966caa | ||
|
|
dcee1b6d55 | ||
|
|
0ce6dd0795 | ||
|
|
259d1c872b | ||
|
|
e7c2c9710a | ||
|
|
0eebbb094c | ||
|
|
2faabbe392 | ||
|
|
e22fc95ee9 | ||
|
|
72398408c5 | ||
|
|
18e4647211 | ||
|
|
642a284b33 | ||
|
|
3951a15e9d | ||
|
|
27561410e5 | ||
|
|
47849fc1a5 | ||
|
|
f19ad24907 | ||
|
|
5baa141b89 | ||
|
|
ea0ab5e3d2 | ||
|
|
e995a663c8 | ||
|
|
4a681297e0 | ||
|
|
a65fc39ae4 | ||
|
|
bcd41d0c19 | ||
|
|
96562b9f16 | ||
|
|
470105f895 | ||
|
|
7b51d08ea5 | ||
|
|
9e62e81158 | ||
|
|
b7accc54e7 | ||
|
|
4b4b84a220 | ||
|
|
5ec94860b2 | ||
|
|
263c18ebca | ||
|
|
4f06cfe1ab | ||
|
|
8b52927b4f | ||
|
|
1a7f484a62 | ||
|
|
aab6a2cdf3 | ||
|
|
3709d34343 | ||
|
|
bf6cfface6 | ||
|
|
075646481d | ||
|
|
f6586a481b | ||
|
|
a3cd92c503 | ||
|
|
11f63b4556 | ||
|
|
000a3970e0 | ||
|
|
be31989ab9 | ||
|
|
460de60019 | ||
|
|
3a9780dc4f | ||
|
|
9620817a8f | ||
|
|
03a705fc93 | ||
|
|
d7ef3ba67b | ||
|
|
80e7dac0c8 | ||
|
|
03274d9ee5 | ||
|
|
6a8d2c1f9c | ||
|
|
510fae6bf1 | ||
|
|
ad0b032384 | ||
|
|
a09f5c0577 | ||
|
|
b8960d57c8 | ||
|
|
f6e089daee | ||
|
|
7bd1dfbdaa | ||
|
|
175a80191e | ||
|
|
518202ae0e | ||
|
|
f1a1c40724 | ||
|
|
e11c550fc2 | ||
|
|
82b566d580 | ||
|
|
8b9998a53d | ||
|
|
96ab89adf3 | ||
|
|
d99c9ad1ee | ||
|
|
d918f96c66 | ||
|
|
8b17afc6b3 | ||
|
|
c5e4bbf2ce |
|
|
@ -0,0 +1,17 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
timezone: "Asia/Shanghai"
|
||||
day: "friday"
|
||||
target-branch: "v2"
|
||||
groups:
|
||||
python-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
# ignore:
|
||||
# - dependency-name: "pymupdf"
|
||||
# versions: ["*"]
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ on:
|
|||
inputs:
|
||||
dockerImageTag:
|
||||
description: 'Image Tag'
|
||||
default: 'v1.10.0-dev'
|
||||
default: 'v1.10.7-dev'
|
||||
required: true
|
||||
dockerImageTagWithLatest:
|
||||
description: '是否发布latest tag(正式发版时选择,测试版本切勿选择)'
|
||||
|
|
@ -36,7 +36,7 @@ on:
|
|||
jobs:
|
||||
build-and-push-to-fit2cloud-registry:
|
||||
if: ${{ contains(github.event.inputs.registry, 'fit2cloud') }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: df -h
|
||||
|
|
@ -64,18 +64,15 @@ jobs:
|
|||
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
|
||||
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
|
||||
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
|
||||
else
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
|
||||
fi
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=${GITHUB_SHA::8} --no-cache \
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
|
||||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
# Until https://github.com/tonistiigi/binfmt/issues/215
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
|
|
@ -92,11 +89,12 @@ jobs:
|
|||
password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }}
|
||||
- name: Docker Buildx (build-and-push)
|
||||
run: |
|
||||
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
|
||||
|
||||
build-and-push-to-dockerhub:
|
||||
if: ${{ contains(github.event.inputs.registry, 'dockerhub') }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: df -h
|
||||
|
|
@ -124,18 +122,15 @@ jobs:
|
|||
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
|
||||
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
|
||||
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
|
||||
else
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
|
||||
fi
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=${GITHUB_SHA::8} --no-cache \
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
|
||||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
# Until https://github.com/tonistiigi/binfmt/issues/215
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
|
|
@ -151,4 +146,5 @@ jobs:
|
|||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Docker Buildx (build-and-push)
|
||||
run: |
|
||||
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
|
||||
|
|
|
|||
|
|
@ -183,4 +183,5 @@ apps/xpack
|
|||
data
|
||||
.dev
|
||||
poetry.lock
|
||||
apps/setting/models_provider/impl/*/icon/
|
||||
apps/setting/models_provider/impl/*/icon/
|
||||
tmp/
|
||||
12
README.md
12
README.md
|
|
@ -1,6 +1,6 @@
|
|||
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
|
||||
<h3 align="center">Ready-to-use, flexible RAG Chatbot</h3>
|
||||
<h3 align="center">基于大模型和 RAG 的开源知识库问答系统</h3>
|
||||
<h3 align="center">Open-source platform for building enterprise-grade agents</h3>
|
||||
<h3 align="center">强大易用的企业级智能体平台</h3>
|
||||
<p align="center"><a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
</p>
|
||||
<hr/>
|
||||
|
||||
MaxKB = Max Knowledge Base, it is a chatbot based on Large Language Models (LLM) and Retrieval-Augmented Generation (RAG). MaxKB is widely applied in scenarios such as intelligent customer service, corporate internal knowledge bases, academic research, and education.
|
||||
MaxKB = Max Knowledge Brain, it is an open-source platform for building enterprise-grade agents. MaxKB integrates Retrieval-Augmented Generation (RAG) pipelines, supports robust workflows, and provides advanced MCP tool-use capabilities. MaxKB is widely applied in scenarios such as intelligent customer service, corporate internal knowledge bases, academic research, and education.
|
||||
|
||||
- **Ready-to-Use**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization, and RAG (Retrieval-Augmented Generation). This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience.
|
||||
- **Flexible Orchestration**: Equipped with a powerful workflow engine and function library, enabling the orchestration of AI processes to meet the needs of complex business scenarios.
|
||||
- **RAG Pipeline**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization. This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience.
|
||||
- **Agentic Workflow**: Equipped with a powerful workflow engine, function library and MCP tool-use, enabling the orchestration of AI processes to meet the needs of complex business scenarios.
|
||||
- **Seamless Integration**: Facilitates zero-coding rapid integration into third-party business systems, quickly equipping existing systems with intelligent Q&A capabilities to enhance user satisfaction.
|
||||
- **Model-Agnostic**: Supports various large models, including private models (such as DeepSeek, Llama, Qwen, etc.) and public models (like OpenAI, Claude, Gemini, etc.).
|
||||
- **Multi Modal**: Native support for input and output text, image, audio and video.
|
||||
|
|
@ -56,8 +56,6 @@ Access MaxKB web interface at `http://your_server_ip:8080` with default admin cr
|
|||
|
||||
## Feature Comparison
|
||||
|
||||
MaxKB is positioned as an Ready-to-use RAG (Retrieval-Augmented Generation) intelligent Q&A application, rather than a middleware platform for building large model applications. The following table is merely a comparison from a functional perspective.
|
||||
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th align="center">Feature</th>
|
||||
|
|
|
|||
24
README_CN.md
24
README_CN.md
|
|
@ -1,25 +1,25 @@
|
|||
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
|
||||
<h3 align="center">基于大模型和 RAG 的知识库问答系统</h3>
|
||||
<h4 align="center">Ready-to-use, flexible RAG Chatbot</h4>
|
||||
<h3 align="center">强大易用的企业级智能体平台</h3>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: auto;" /></a>
|
||||
<a href="https://market.aliyun.com/products/53690006/cmjj00067609.html?userCode=kmemb8jp" target="_blank"><img src="https://img.alicdn.com/imgextra/i2/O1CN01H5JIwY1rZ0OobDjnJ_!!6000000005644-2-tps-1000-216.png" alt="1Panel-dev%2FMaxKB | Aliyun" style="width: 250px; height: auto;" /></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="README_EN.md"><img src="https://img.shields.io/badge/English_README-blue" alt="English README"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb" alt="License: GPL v3"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb/releases/latest"><img src="https://img.shields.io/github/v/release/1Panel-dev/maxkb" alt="Latest release"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?style=flat-square" alt="Stars"></a>
|
||||
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?style=flat-square" alt="Stars"></a>
|
||||
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
|
||||
<a href="https://gitee.com/fit2cloud-feizhiyun/MaxKB"><img src="https://gitee.com/fit2cloud-feizhiyun/MaxKB/badge/star.svg?theme=gvp" alt="Gitee Stars"></a>
|
||||
<a href="https://gitcode.com/feizhiyun/MaxKB"><img src="https://gitcode.com/feizhiyun/MaxKB/star/badge.svg" alt="GitCode Stars"></a>
|
||||
</p>
|
||||
<hr/>
|
||||
|
||||
MaxKB = Max Knowledge Base,是一款基于大语言模型和 RAG 的开源知识库问答系统,广泛应用于智能客服、企业内部知识库、学术研究与教育等场景。
|
||||
MaxKB = Max Knowledge Brain,是一款强大易用的企业级智能体平台,支持 RAG 检索增强生成、工作流编排、MCP 工具调用能力。MaxKB 支持对接各种主流大语言模型,广泛应用于智能客服、企业内部知识库问答、员工助手、学术研究与教育等场景。
|
||||
|
||||
- **开箱即用**:支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化和 RAG(检索增强生成),有效减少大模型幻觉,智能问答交互体验好;
|
||||
- **模型中立**:支持对接各种大模型,包括本地私有大模型(DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等);
|
||||
- **灵活编排**:内置强大的工作流引擎和函数库,支持编排 AI 工作过程,满足复杂业务场景下的需求;
|
||||
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度。
|
||||
- **RAG 检索增强生成**:高效搭建本地 AI 知识库,支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化,有效减少大模型幻觉,提升问答效果;
|
||||
- **灵活编排**:内置强大的工作流引擎、函数库和 MCP 工具调用能力,支持编排 AI 工作过程,满足复杂业务场景下的需求;
|
||||
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度;
|
||||
- **模型中立**:支持对接各种大模型,包括本地私有大模型(DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等)。
|
||||
|
||||
MaxKB 三分钟视频介绍:https://www.bilibili.com/video/BV18JypYeEkj/
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/var/lib/po
|
|||
- 你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB;
|
||||
- 如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署;
|
||||
- MaxKB 产品版本分为社区版和专业版,详情请参见:[MaxKB 产品版本对比](https://maxkb.cn/pricing.html);
|
||||
- 如果您需要向团队介绍 MaxKB,可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202501.pdf)。
|
||||
- 如果您需要向团队介绍 MaxKB,可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202503.pdf)。
|
||||
|
||||
如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。
|
||||
|
||||
|
|
|
|||
19
USE-CASES.md
19
USE-CASES.md
|
|
@ -18,3 +18,22 @@
|
|||
- [MaxKB 应用案例:产品使用指南AI助手,新手小白也能轻松搞定!](https://www.bilibili.com/video/BV1Yz6gYtEqX)
|
||||
- [MaxKB 应用案例:生物医药AI客服智能体验!](https://www.bilibili.com/video/BV13JzvYsE3e)
|
||||
- [MaxKB 应用案例:高校行政管理AI小助手](https://www.bilibili.com/video/BV1yvBMYvEdy)
|
||||
- [MaxKB 应用案例:岳阳市人民医院-OA小助手](https://mp.weixin.qq.com/s/O94Qo3UH-MiUtDdWCVg8sQ)
|
||||
- [MaxKB 应用案例:常熟市第一人民医院](https://mp.weixin.qq.com/s/s5XXGTR3_MUo41NbJ8WzZQ)
|
||||
- [MaxKB 应用案例:华北水利水电大学](https://mp.weixin.qq.com/s/PoOFAcMCr9qJdvSj8c08qg)
|
||||
- [MaxKB 应用案例:唐山海事局-“小海”AI语音助手](https://news.qq.com/rain/a/20250223A030BE00)
|
||||
- [MaxKB 应用案例:湖南汉寿政务](http://hsds.hsdj.gov.cn:19999/ui/chat/a2c976736739aadc)
|
||||
- [MaxKB 应用案例:广州市妇女儿童医疗中心-AI医疗数据分类分级小助手](https://mp.weixin.qq.com/s/YHUMkUOAaUomBV8bswpK3g)
|
||||
- [MaxKB 应用案例:苏州热工研究院有限公司-维修大纲评估质量自查AI小助手](https://mp.weixin.qq.com/s/Ts5FQdnv7Tu9Jp7bvofCVA)
|
||||
- [MaxKB 应用案例:国核自仪系统工程有限公司-NuCON AI帮](https://mp.weixin.qq.com/s/HNPc7u5xVfGLJr8IQz3vjQ)
|
||||
- [MaxKB 应用案例:深圳通开启Deep Seek智能应用新篇章](https://mp.weixin.qq.com/s/SILN0GSescH9LyeQqYP0VQ)
|
||||
- [MaxKB 应用案例:南通智慧出行领跑长三角!首款接入DeepSeek的"畅行南通"APP上线AI新场景](https://mp.weixin.qq.com/s/WEC9UQ6msY0VS8LhTZh-Ew)
|
||||
- [MaxKB 应用案例:中船动力人工智能"智慧动力云助手"及首批数字员工正式上线](https://mp.weixin.qq.com/s/OGcEkjh9DzGO1Tkc9nr7qg)
|
||||
- [MaxKB 应用案例:AI+矿山:DeepSeek助力绿色智慧矿山智慧“升级”](https://mp.weixin.qq.com/s/SZstxTvVoLZg0ECbZbfpIA)
|
||||
- [MaxKB 应用案例:DeepSeek落地弘盛铜业:国产大模型点亮"黑灯工厂"新引擎](https://mp.weixin.qq.com/s/Eczdx574MS5RMF7WfHN7_A)
|
||||
- [MaxKB 应用案例:拥抱智能时代!中国五矿以 “AI+”赋能企业发展](https://mp.weixin.qq.com/s/D5vBtlX2E81pWE3_2OgWSw)
|
||||
- [MaxKB 应用案例:DeepSeek赋能中冶武勘AI智能体](https://mp.weixin.qq.com/s/8m0vxGcWXNdZazziQrLyxg)
|
||||
- [MaxKB 应用案例:重磅!陕西广电网络“秦岭云”平台实现DeepSeek本地化部署](https://mp.weixin.qq.com/s/ZKmEU_wWShK1YDomKJHQeA)
|
||||
- [MaxKB 应用案例:粤海集团完成DeepSeek私有化部署,助力集团智能化管理](https://mp.weixin.qq.com/s/2JbVp0-kr9Hfp-0whH4cvg)
|
||||
- [MaxKB 应用案例:建筑材料工业信息中心完成DeepSeek本地化部署,推动行业数智化转型新发展](https://mp.weixin.qq.com/s/HThGSnND3qDF8ySEqiM4jw)
|
||||
- [MaxKB 应用案例:一起DeepSeek!福建设计以AI大模型开启新篇章](https://mp.weixin.qq.com/s/m67e-H7iQBg3d24NM82UjA)
|
||||
|
|
|
|||
|
|
@ -124,9 +124,11 @@ def event_content(response,
|
|||
request_token = 0
|
||||
response_token = 0
|
||||
write_context(step, manage, request_token, response_token, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, step, padding_problem_text, client_id,
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else '')
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else ''
|
||||
, asker=asker)
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], '', True,
|
||||
request_token, response_token,
|
||||
|
|
@ -135,16 +137,21 @@ def event_content(response,
|
|||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
except Exception as e:
|
||||
logging.getLogger("max_kb_error").error(f'{str(e)}:{traceback.format_exc()}')
|
||||
all_text = '异常' + str(e)
|
||||
all_text = 'Exception:' + str(e)
|
||||
write_context(step, manage, 0, 0, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, step, padding_problem_text, client_id)
|
||||
all_text, manage, step, padding_problem_text, client_id, reasoning_content='',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), all_text,
|
||||
'ai-chat-node',
|
||||
[], True, 0, 0,
|
||||
{'node_is_end': True, 'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node'})
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], all_text,
|
||||
False,
|
||||
0, 0, {'node_is_end': False,
|
||||
'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node',
|
||||
'real_node_id': 'ai-chat-node',
|
||||
'reasoning_content': ''})
|
||||
|
||||
|
||||
class BaseChatStep(IChatStep):
|
||||
|
|
@ -300,9 +307,11 @@ class BaseChatStep(IChatStep):
|
|||
else:
|
||||
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get(
|
||||
'reasoning_content')
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
content, manage, self, padding_problem_text, client_id,
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else '')
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else '',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id),
|
||||
content, True,
|
||||
|
|
@ -316,8 +325,10 @@ class BaseChatStep(IChatStep):
|
|||
except Exception as e:
|
||||
all_text = 'Exception:' + str(e)
|
||||
write_context(self, manage, 0, 0, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, self, padding_problem_text, client_id)
|
||||
all_text, manage, self, padding_problem_text, client_id, reasoning_content='',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id), all_text, True, 0,
|
||||
0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ class WorkFlowPostHandler:
|
|||
answer_text_list=answer_text_list,
|
||||
run_time=time.time() - workflow.context['start_time'],
|
||||
index=0)
|
||||
self.chat_info.append_chat_record(chat_record, self.client_id)
|
||||
asker = workflow.context.get('asker', None)
|
||||
self.chat_info.append_chat_record(chat_record, self.client_id, asker)
|
||||
# 重新设置缓存
|
||||
chat_cache.set(chat_id,
|
||||
self.chat_info, timeout=60 * 30)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@ from .speech_to_text_step_node import BaseSpeechToTextNode
|
|||
from .start_node import *
|
||||
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
|
||||
from .variable_assign_node import BaseVariableAssignNode
|
||||
from .mcp_node import BaseMcpNode
|
||||
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
|
||||
BaseConditionNode, BaseReplyNode,
|
||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||
BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||
BaseImageGenerateNode, BaseVariableAssignNode]
|
||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ class ChatNodeSerializer(serializers.Serializer):
|
|||
error_messages=ErrMessage.dict('Model settings'))
|
||||
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Context Type")))
|
||||
mcp_enable = serializers.BooleanField(required=False,
|
||||
error_messages=ErrMessage.boolean(_("Whether to enable MCP")))
|
||||
mcp_servers = serializers.JSONField(required=False, error_messages=ErrMessage.list(_("MCP Server")))
|
||||
|
||||
|
||||
class IChatNode(INode):
|
||||
|
|
@ -49,5 +52,7 @@ class IChatNode(INode):
|
|||
model_params_setting=None,
|
||||
dialogue_type=None,
|
||||
model_setting=None,
|
||||
mcp_enable=False,
|
||||
mcp_servers=None,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,14 +6,19 @@
|
|||
@date:2024/6/4 14:30
|
||||
@desc:
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from functools import reduce
|
||||
from types import AsyncGeneratorType
|
||||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
from langchain_core.messages import BaseMessage, AIMessage
|
||||
from langchain_core.messages import BaseMessage, AIMessage, AIMessageChunk, ToolMessage
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
|
||||
|
|
@ -22,6 +27,19 @@ from setting.models import Model
|
|||
from setting.models_provider import get_model_credential
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
tool_message_template = """
|
||||
<details>
|
||||
<summary>
|
||||
<strong>Called MCP Tool: <em>%s</em></strong>
|
||||
</summary>
|
||||
|
||||
```json
|
||||
%s
|
||||
```
|
||||
</details>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
|
||||
reasoning_content: str):
|
||||
|
|
@ -56,6 +74,7 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||
reasoning = Reasoning(model_setting.get('reasoning_content_start', '<think>'),
|
||||
model_setting.get('reasoning_content_end', '</think>'))
|
||||
response_reasoning_content = False
|
||||
|
||||
for chunk in response:
|
||||
reasoning_chunk = reasoning.get_reasoning_content(chunk)
|
||||
content_chunk = reasoning_chunk.get('content')
|
||||
|
|
@ -84,6 +103,39 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||
|
||||
|
||||
async def _yield_mcp_response(chat_model, message_list, mcp_servers):
|
||||
async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
|
||||
agent = create_react_agent(chat_model, client.get_tools())
|
||||
response = agent.astream({"messages": message_list}, stream_mode='messages')
|
||||
async for chunk in response:
|
||||
if isinstance(chunk[0], ToolMessage):
|
||||
content = tool_message_template % (chunk[0].name, chunk[0].content)
|
||||
chunk[0].content = content
|
||||
yield chunk[0]
|
||||
if isinstance(chunk[0], AIMessageChunk):
|
||||
yield chunk[0]
|
||||
|
||||
|
||||
def mcp_response_generator(chat_model, message_list, mcp_servers):
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
async_gen = _yield_mcp_response(chat_model, message_list, mcp_servers)
|
||||
while True:
|
||||
try:
|
||||
chunk = loop.run_until_complete(anext_async(async_gen))
|
||||
yield chunk
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'exception: {e}')
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def anext_async(agen):
|
||||
return await agen.__anext__()
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据
|
||||
|
|
@ -136,12 +188,15 @@ class BaseChatNode(IChatNode):
|
|||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['reasoning_content'] = details.get('reasoning_content')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
dialogue_type=None,
|
||||
model_setting=None,
|
||||
mcp_enable=False,
|
||||
mcp_servers=None,
|
||||
**kwargs) -> NodeResult:
|
||||
if dialogue_type is None:
|
||||
dialogue_type = 'WORKFLOW'
|
||||
|
|
@ -163,6 +218,14 @@ class BaseChatNode(IChatNode):
|
|||
self.context['system'] = system
|
||||
message_list = self.generate_message_list(system, prompt, history_message)
|
||||
self.context['message_list'] = message_list
|
||||
|
||||
if mcp_enable and mcp_servers is not None and '"stdio"' not in mcp_servers:
|
||||
r = mcp_response_generator(chat_model, message_list, mcp_servers)
|
||||
return NodeResult(
|
||||
{'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context_stream)
|
||||
|
||||
if stream:
|
||||
r = chat_model.stream(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
|
|
|
|||
|
|
@ -168,7 +168,8 @@ class BaseApplicationNode(IApplicationNode):
|
|||
self.context['question'] = details.get('question')
|
||||
self.context['type'] = details.get('type')
|
||||
self.context['reasoning_content'] = details.get('reasoning_content')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
|
||||
app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None,
|
||||
|
|
@ -178,7 +179,8 @@ class BaseApplicationNode(IApplicationNode):
|
|||
current_chat_id = string_to_uuid(chat_id + application_id)
|
||||
Chat.objects.get_or_create(id=current_chat_id, defaults={
|
||||
'application_id': application_id,
|
||||
'abstract': message[0:1024]
|
||||
'abstract': message[0:1024],
|
||||
'client_id': client_id,
|
||||
})
|
||||
if app_document_list is None:
|
||||
app_document_list = []
|
||||
|
|
|
|||
|
|
@ -9,20 +9,22 @@
|
|||
|
||||
from .contain_compare import *
|
||||
from .equal_compare import *
|
||||
from .gt_compare import *
|
||||
from .ge_compare import *
|
||||
from .gt_compare import *
|
||||
from .is_not_null_compare import *
|
||||
from .is_not_true import IsNotTrueCompare
|
||||
from .is_null_compare import *
|
||||
from .is_true import IsTrueCompare
|
||||
from .le_compare import *
|
||||
from .lt_compare import *
|
||||
from .len_equal_compare import *
|
||||
from .len_ge_compare import *
|
||||
from .len_gt_compare import *
|
||||
from .len_le_compare import *
|
||||
from .len_lt_compare import *
|
||||
from .len_equal_compare import *
|
||||
from .is_not_null_compare import *
|
||||
from .is_null_compare import *
|
||||
from .lt_compare import *
|
||||
from .not_contain_compare import *
|
||||
|
||||
compare_handle_list = [GECompare(), GTCompare(), ContainCompare(), EqualCompare(), LTCompare(), LECompare(),
|
||||
LenLECompare(), LenGECompare(), LenEqualCompare(), LenGTCompare(), LenLTCompare(),
|
||||
IsNullCompare(),
|
||||
IsNotNullCompare(), NotContainCompare()]
|
||||
IsNotNullCompare(), NotContainCompare(), IsTrueCompare(), IsNotTrueCompare()]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: is_not_true.py
|
||||
@date:2025/4/7 13:44
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from application.flow.step_node.condition_node.compare import Compare
|
||||
|
||||
|
||||
class IsNotTrueCompare(Compare):
|
||||
|
||||
def support(self, node_id, fields: List[str], source_value, compare, target_value):
|
||||
if compare == 'is_not_true':
|
||||
return True
|
||||
|
||||
def compare(self, source_value, compare, target_value):
|
||||
try:
|
||||
return source_value is False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: IsTrue.py
|
||||
@date:2025/4/7 13:38
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from application.flow.step_node.condition_node.compare import Compare
|
||||
|
||||
|
||||
class IsTrueCompare(Compare):
|
||||
|
||||
def support(self, node_id, fields: List[str], source_value, compare, target_value):
|
||||
if compare == 'is_true':
|
||||
return True
|
||||
|
||||
def compare(self, source_value, compare, target_value):
|
||||
try:
|
||||
return source_value is True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
|
@ -40,7 +40,11 @@ class BaseConditionNode(IConditionNode):
|
|||
value = self.workflow_manage.generate_prompt(value)
|
||||
except Exception as e:
|
||||
pass
|
||||
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
|
||||
field_value = None
|
||||
try:
|
||||
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
|
||||
except Exception as e:
|
||||
pass
|
||||
for compare_handler in compare_handle_list:
|
||||
if compare_handler.support(field_list[0], field_list[1:], field_value, compare, value):
|
||||
return compare_handler.compare(field_value, compare, value)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ from application.flow.step_node.direct_reply_node.i_reply_node import IReplyNode
|
|||
class BaseReplyNode(IReplyNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, reply_type, stream, fields=None, content=None, **kwargs) -> NodeResult:
|
||||
if reply_type == 'referencing':
|
||||
result = self.get_reference_content(fields)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ class BaseFormNode(IFormNode):
|
|||
self.context['start_time'] = details.get('start_time')
|
||||
self.context['form_data'] = form_data
|
||||
self.context['is_submit'] = details.get('is_submit')
|
||||
self.answer_text = details.get('result')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('result')
|
||||
if form_data is not None:
|
||||
for key in form_data:
|
||||
self.context[key] = form_data[key]
|
||||
|
|
@ -70,7 +71,7 @@ class BaseFormNode(IFormNode):
|
|||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting,ensure_ascii=False)}</form_rander>'
|
||||
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
|
||||
context = self.workflow_manage.get_workflow_content()
|
||||
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
|
|
@ -85,7 +86,7 @@ class BaseFormNode(IFormNode):
|
|||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting,ensure_ascii=False)}</form_rander>'
|
||||
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
|
||||
context = self.workflow_manage.get_workflow_content()
|
||||
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import time
|
|||
from typing import Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from common.util.rsa_util import rsa_long_decrypt
|
||||
from function_lib.models.function import FunctionLib
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
|
|
@ -38,15 +40,15 @@ def get_field_value(debug_field_list, name, is_required):
|
|||
if len(result) > 0:
|
||||
return result[-1]['value']
|
||||
if is_required:
|
||||
raise AppApiException(500, f"{name}字段未设置值")
|
||||
raise AppApiException(500, _('Field: {name} No value set').format(name=name))
|
||||
return None
|
||||
|
||||
|
||||
def valid_reference_value(_type, value, name):
|
||||
if _type == 'int':
|
||||
instance_type = int
|
||||
instance_type = int | float
|
||||
elif _type == 'float':
|
||||
instance_type = float
|
||||
instance_type = float | int
|
||||
elif _type == 'dict':
|
||||
instance_type = dict
|
||||
elif _type == 'array':
|
||||
|
|
@ -54,13 +56,16 @@ def valid_reference_value(_type, value, name):
|
|||
elif _type == 'string':
|
||||
instance_type = str
|
||||
else:
|
||||
raise Exception(500, f'字段:{name}类型:{_type} 不支持的类型')
|
||||
raise Exception(_('Field: {name} Type: {_type} Value: {value} Unsupported types').format(name=name,
|
||||
_type=_type))
|
||||
if not isinstance(value, instance_type):
|
||||
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
|
||||
raise Exception(
|
||||
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
|
||||
value=value))
|
||||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and value is None:
|
||||
if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
|
||||
return None
|
||||
if not is_required and source == 'reference' and (value is None or len(value) == 0):
|
||||
return None
|
||||
|
|
@ -69,6 +74,10 @@ def convert_value(name: str, value, _type, is_required, source, node):
|
|||
value[0],
|
||||
value[1:])
|
||||
valid_reference_value(_type, value, name)
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
|
|
@ -79,26 +88,37 @@ def convert_value(name: str, value, _type, is_required, source, node):
|
|||
v = json.loads(value)
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
raise Exception("类型错误")
|
||||
raise Exception(_('type error'))
|
||||
if _type == 'array':
|
||||
v = json.loads(value)
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
raise Exception("类型错误")
|
||||
raise Exception(_('type error'))
|
||||
return value
|
||||
except Exception as e:
|
||||
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
|
||||
raise Exception(
|
||||
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
|
||||
value=value))
|
||||
|
||||
|
||||
def valid_function(function_lib, user_id):
|
||||
if function_lib is None:
|
||||
raise Exception(_('Function does not exist'))
|
||||
if function_lib.permission_type == 'PRIVATE' and str(function_lib.user_id) != str(user_id):
|
||||
raise Exception(_('No permission to use this function {name}').format(name=function_lib.name))
|
||||
if not function_lib.is_active:
|
||||
raise Exception(_('Function {name} is unavailable').format(name=function_lib.name))
|
||||
|
||||
|
||||
class BaseFunctionLibNodeNode(IFunctionLibNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.answer_text = str(details.get('result'))
|
||||
if self.node_params.get('is_result'):
|
||||
self.answer_text = str(details.get('result'))
|
||||
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
|
||||
if not function_lib.is_active:
|
||||
raise Exception(f'函数:{function_lib.name} 不可用')
|
||||
valid_function(function_lib, self.flow_params_serializer.data.get('user_id'))
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'),
|
||||
field.get('source'), self)
|
||||
|
|
@ -107,8 +127,14 @@ class BaseFunctionLibNodeNode(IFunctionLibNode):
|
|||
), **field}
|
||||
for field in
|
||||
function_lib.input_field_list]}
|
||||
|
||||
self.context['params'] = params
|
||||
result = function_executor.exec_code(function_lib.code, params)
|
||||
# 合并初始化参数
|
||||
if function_lib.init_params is not None:
|
||||
all_params = json.loads(rsa_long_decrypt(function_lib.init_params)) | params
|
||||
else:
|
||||
all_params = params
|
||||
result = function_executor.exec_code(function_lib.code, all_params)
|
||||
return NodeResult({'result': result}, {}, _write_context=write_context)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
|||
|
||||
def valid_reference_value(_type, value, name):
|
||||
if _type == 'int':
|
||||
instance_type = int
|
||||
instance_type = int | float
|
||||
elif _type == 'float':
|
||||
instance_type = float
|
||||
instance_type = float | int
|
||||
elif _type == 'dict':
|
||||
instance_type = dict
|
||||
elif _type == 'array':
|
||||
|
|
@ -49,13 +49,17 @@ def valid_reference_value(_type, value, name):
|
|||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and value is None:
|
||||
if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
|
||||
return None
|
||||
if source == 'reference':
|
||||
value = node.workflow_manage.get_reference_field(
|
||||
value[0],
|
||||
value[1:])
|
||||
valid_reference_value(_type, value, name)
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
|
|
@ -80,7 +84,8 @@ def convert_value(name: str, value, _type, is_required, source, node):
|
|||
class BaseFunctionNodeNode(IFunctionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.answer_text = str(details.get('result'))
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = str(details.get('result'))
|
||||
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ class BaseImageGenerateNode(IImageGenerateNode):
|
|||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_type, history_chat_record, chat_id,
|
||||
model_params_setting,
|
||||
|
|
@ -24,7 +25,8 @@ class BaseImageGenerateNode(IImageGenerateNode):
|
|||
**kwargs) -> NodeResult:
|
||||
print(model_params_setting)
|
||||
application = self.workflow_manage.work_flow_post_handler.chat_info.application
|
||||
tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'), **model_params_setting)
|
||||
tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
|
||||
**model_params_setting)
|
||||
history_message = self.get_history_message(history_chat_record, dialogue_number)
|
||||
self.context['history_message'] = history_message
|
||||
question = self.generate_prompt_question(prompt)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
|
|||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, history_chat_record, stream, chat_id,
|
||||
model_params_setting,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class McpNodeSerializer(serializers.Serializer):
|
||||
mcp_servers = serializers.JSONField(required=True,
|
||||
error_messages=ErrMessage.char(_("Mcp servers")))
|
||||
|
||||
mcp_server = serializers.CharField(required=True,
|
||||
error_messages=ErrMessage.char(_("Mcp server")))
|
||||
|
||||
mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
|
||||
|
||||
tool_params = serializers.DictField(required=True,
|
||||
error_messages=ErrMessage.char(_("Tool parameters")))
|
||||
|
||||
|
||||
class IMcpNode(INode):
|
||||
type = 'mcp-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return McpNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_mcp_node import BaseMcpNode
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# coding=utf-8
|
||||
import asyncio
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode
|
||||
|
||||
|
||||
class BaseMcpNode(IMcpNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.context['tool_params'] = details.get('tool_params')
|
||||
self.context['mcp_tool'] = details.get('mcp_tool')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('result')
|
||||
|
||||
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||
servers = json.loads(mcp_servers)
|
||||
params = json.loads(json.dumps(tool_params))
|
||||
params = self.handle_variables(params)
|
||||
|
||||
async def call_tool(s, session, t, a):
|
||||
async with MultiServerMCPClient(s) as client:
|
||||
s = await client.sessions[session].call_tool(t, a)
|
||||
return s
|
||||
|
||||
res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
|
||||
return NodeResult(
|
||||
{'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})
|
||||
|
||||
def handle_variables(self, tool_params):
|
||||
# 处理参数中的变量
|
||||
for k, v in tool_params.items():
|
||||
if type(v) == str:
|
||||
tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k])
|
||||
if type(v) == dict:
|
||||
self.handle_variables(v)
|
||||
if (type(v) == list) and (type(v[0]) == str):
|
||||
tool_params[k] = self.get_reference_content(v)
|
||||
return tool_params
|
||||
|
||||
def get_reference_content(self, fields: List[str]):
|
||||
return str(self.workflow_manage.get_reference_field(
|
||||
fields[0],
|
||||
fields[1:]))
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'type': self.node.type,
|
||||
'mcp_tool': self.context.get('mcp_tool'),
|
||||
'tool_params': self.context.get('tool_params'),
|
||||
'result': self.context.get('result'),
|
||||
}
|
||||
|
|
@ -80,7 +80,8 @@ class BaseQuestionNode(IQuestionNode):
|
|||
self.context['answer'] = details.get('answer')
|
||||
self.context['message_tokens'] = details.get('message_tokens')
|
||||
self.context['answer_tokens'] = details.get('answer_tokens')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
|||
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
|
||||
'data': '\n'.join(
|
||||
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
|
||||
paragraph_list])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
|
||||
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
|
||||
'directly_return': '\n'.join(
|
||||
[paragraph.get('content') for paragraph in
|
||||
result if
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ class BaseSpeechToTextNode(ISpeechToTextNode):
|
|||
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, stt_model_id, chat_id, audio, **kwargs) -> NodeResult:
|
||||
stt_model = get_model_instance_by_model_user_id(stt_model_id, self.flow_params_serializer.data.get('user_id'))
|
||||
|
|
|
|||
|
|
@ -40,10 +40,13 @@ class BaseStartStepNode(IStarNode):
|
|||
self.context['document'] = details.get('document_list')
|
||||
self.context['image'] = details.get('image_list')
|
||||
self.context['audio'] = details.get('audio_list')
|
||||
self.context['other'] = details.get('other_list')
|
||||
self.status = details.get('status')
|
||||
self.err_message = details.get('err_message')
|
||||
for key, value in workflow_variable.items():
|
||||
workflow_manage.context[key] = value
|
||||
for item in details.get('global_fields', []):
|
||||
workflow_manage.context[item.get('key')] = item.get('value')
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
pass
|
||||
|
|
@ -59,7 +62,8 @@ class BaseStartStepNode(IStarNode):
|
|||
'question': question,
|
||||
'image': self.workflow_manage.image_list,
|
||||
'document': self.workflow_manage.document_list,
|
||||
'audio': self.workflow_manage.audio_list
|
||||
'audio': self.workflow_manage.audio_list,
|
||||
'other': self.workflow_manage.other_list,
|
||||
}
|
||||
return NodeResult(node_variable, workflow_variable)
|
||||
|
||||
|
|
@ -83,5 +87,6 @@ class BaseStartStepNode(IStarNode):
|
|||
'image_list': self.context.get('image'),
|
||||
'document_list': self.context.get('document'),
|
||||
'audio_list': self.context.get('audio'),
|
||||
'other_list': self.context.get('other'),
|
||||
'global_fields': global_fields
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
|
|||
class BaseTextToSpeechNode(ITextToSpeechNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.answer_text = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, tts_model_id, chat_id,
|
||||
content, model_params_setting=None,
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ class WorkflowManage:
|
|||
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None,
|
||||
document_list=None,
|
||||
audio_list=None,
|
||||
other_list=None,
|
||||
start_node_id=None,
|
||||
start_node_data=None, chat_record=None, child_node=None):
|
||||
if form_data is None:
|
||||
|
|
@ -248,12 +249,15 @@ class WorkflowManage:
|
|||
document_list = []
|
||||
if audio_list is None:
|
||||
audio_list = []
|
||||
if other_list is None:
|
||||
other_list = []
|
||||
self.start_node_id = start_node_id
|
||||
self.start_node = None
|
||||
self.form_data = form_data
|
||||
self.image_list = image_list
|
||||
self.document_list = document_list
|
||||
self.audio_list = audio_list
|
||||
self.other_list = other_list
|
||||
self.params = params
|
||||
self.flow = flow
|
||||
self.context = {}
|
||||
|
|
@ -269,11 +273,36 @@ class WorkflowManage:
|
|||
self.child_node = child_node
|
||||
self.future_list = []
|
||||
self.lock = threading.Lock()
|
||||
self.field_list = []
|
||||
self.global_field_list = []
|
||||
self.init_fields()
|
||||
if start_node_id is not None:
|
||||
self.load_node(chat_record, start_node_id, start_node_data)
|
||||
else:
|
||||
self.node_context = []
|
||||
|
||||
def init_fields(self):
|
||||
field_list = []
|
||||
global_field_list = []
|
||||
for node in self.flow.nodes:
|
||||
properties = node.properties
|
||||
node_name = properties.get('stepName')
|
||||
node_id = node.id
|
||||
node_config = properties.get('config')
|
||||
if node_config is not None:
|
||||
fields = node_config.get('fields')
|
||||
if fields is not None:
|
||||
for field in fields:
|
||||
field_list.append({**field, 'node_id': node_id, 'node_name': node_name})
|
||||
global_fields = node_config.get('globalFields')
|
||||
if global_fields is not None:
|
||||
for global_field in global_fields:
|
||||
global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name})
|
||||
field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
|
||||
global_field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
|
||||
self.field_list = field_list
|
||||
self.global_field_list = global_field_list
|
||||
|
||||
def append_answer(self, content):
|
||||
self.answer += content
|
||||
self.answer_list[-1] += content
|
||||
|
|
@ -739,23 +768,15 @@ class WorkflowManage:
|
|||
|
||||
def reset_prompt(self, prompt: str):
|
||||
placeholder = "{}"
|
||||
for node in self.flow.nodes:
|
||||
properties = node.properties
|
||||
node_config = properties.get('config')
|
||||
if node_config is not None:
|
||||
fields = node_config.get('fields')
|
||||
if fields is not None:
|
||||
for field in fields:
|
||||
globeLabel = f"{properties.get('stepName')}.{field.get('value')}"
|
||||
globeValue = f"context.get('{node.id}',{placeholder}).get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
global_fields = node_config.get('globalFields')
|
||||
if global_fields is not None:
|
||||
for field in global_fields:
|
||||
globeLabel = f"全局变量.{field.get('value')}"
|
||||
globeLabelNew = f"global.{field.get('value')}"
|
||||
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
|
||||
for field in self.field_list:
|
||||
globeLabel = f"{field.get('node_name')}.{field.get('value')}"
|
||||
globeValue = f"context.get('{field.get('node_id')}',{placeholder}).get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
for field in self.global_field_list:
|
||||
globeLabel = f"全局变量.{field.get('value')}"
|
||||
globeLabelNew = f"global.{field.get('value')}"
|
||||
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
|
||||
return prompt
|
||||
|
||||
def generate_prompt(self, prompt: str):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-15 15:52
|
||||
|
||||
import application.models.application
|
||||
from django.db import migrations, models
|
||||
|
||||
import common.encoder.encoder
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('application', '0009_application_type_application_work_flow_and_more'),
|
||||
]
|
||||
|
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='chatrecord',
|
||||
name='details',
|
||||
field=models.JSONField(default=dict, encoder=application.models.application.DateEncoder, verbose_name='对话详情'),
|
||||
field=models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.18 on 2025-03-18 06:05
|
||||
|
||||
import application.models.application
|
||||
import common.encoder.encoder
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('application', '0025_alter_application_prologue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='asker',
|
||||
field=models.JSONField(default=application.models.application.default_asker, encoder=common.encoder.encoder.SystemEncoder, verbose_name='访问者'),
|
||||
),
|
||||
]
|
||||
|
|
@ -6,15 +6,13 @@
|
|||
@date:2023/9/25 14:24
|
||||
@desc:
|
||||
"""
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from langchain.schema import HumanMessage, AIMessage
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from common.encoder.encoder import SystemEncoder
|
||||
from common.mixins.app_model_mixin import AppModelMixin
|
||||
from dataset.models.data_set import DataSet
|
||||
from setting.models.model_management import Model
|
||||
|
|
@ -117,10 +115,15 @@ class ApplicationDatasetMapping(AppModelMixin):
|
|||
db_table = "application_dataset_mapping"
|
||||
|
||||
|
||||
def default_asker():
|
||||
return {'user_name': '游客'}
|
||||
|
||||
|
||||
class Chat(AppModelMixin):
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
|
||||
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||
abstract = models.CharField(max_length=1024, verbose_name="摘要")
|
||||
asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder)
|
||||
client_id = models.UUIDField(verbose_name="客户端id", default=None, null=True)
|
||||
is_deleted = models.BooleanField(verbose_name="", default=False)
|
||||
|
||||
|
|
@ -135,18 +138,6 @@ class VoteChoices(models.TextChoices):
|
|||
TRAMPLE = 1, '反对'
|
||||
|
||||
|
||||
class DateEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return float(obj)
|
||||
else:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class ChatRecord(AppModelMixin):
|
||||
"""
|
||||
对话日志 详情
|
||||
|
|
@ -163,7 +154,7 @@ class ChatRecord(AppModelMixin):
|
|||
message_tokens = models.IntegerField(verbose_name="请求token数量", default=0)
|
||||
answer_tokens = models.IntegerField(verbose_name="响应token数量", default=0)
|
||||
const = models.IntegerField(verbose_name="总费用", default=0)
|
||||
details = models.JSONField(verbose_name="对话详情", default=dict, encoder=DateEncoder)
|
||||
details = models.JSONField(verbose_name="对话详情", default=dict, encoder=SystemEncoder)
|
||||
improve_paragraph_id_list = ArrayField(verbose_name="改进标注列表",
|
||||
base_field=models.UUIDField(max_length=128, blank=True)
|
||||
, default=list)
|
||||
|
|
@ -176,7 +167,11 @@ class ChatRecord(AppModelMixin):
|
|||
return HumanMessage(content=self.problem_text)
|
||||
|
||||
def get_ai_message(self):
|
||||
return AIMessage(content=self.answer_text)
|
||||
answer_text = self.answer_text
|
||||
if answer_text is None or len(str(answer_text).strip()) == 0:
|
||||
answer_text = _(
|
||||
'Sorry, no relevant content was found. Please re-describe your problem or provide more information. ')
|
||||
return AIMessage(content=answer_text)
|
||||
|
||||
def get_node_details_runtime_node_id(self, runtime_node_id):
|
||||
return self.details.get(runtime_node_id, None)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
@date:2023/11/7 10:02
|
||||
@desc:
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
|
|
@ -23,6 +24,8 @@ from django.db.models import QuerySet
|
|||
from django.db.models.expressions import RawSQL
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from mcp.client.sse import sse_client
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
|
||||
|
|
@ -39,13 +42,13 @@ from common.exception.app_exception import AppApiException, NotFound404, AppUnau
|
|||
from common.field.common import UploadedImageField, UploadedFileField
|
||||
from common.models.db_model_manage import DBModelManage
|
||||
from common.response import result
|
||||
from common.util.common import valid_license, password_encrypt
|
||||
from common.util.common import valid_license, password_encrypt, restricted_loads
|
||||
from common.util.field_message import ErrMessage
|
||||
from common.util.file_util import get_file_content
|
||||
from dataset.models import DataSet, Document, Image
|
||||
from dataset.serializers.common_serializers import list_paragraph, get_embedding_model_by_dataset_id_list
|
||||
from embedding.models import SearchMode
|
||||
from function_lib.models.function import FunctionLib, PermissionType
|
||||
from function_lib.models.function import FunctionLib, PermissionType, FunctionType
|
||||
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer, FunctionLibModelSerializer
|
||||
from setting.models import AuthOperate, TeamMemberPermission
|
||||
from setting.models.model_management import Model
|
||||
|
|
@ -60,6 +63,7 @@ chat_cache = cache.caches['chat_cache']
|
|||
|
||||
|
||||
class MKInstance:
|
||||
|
||||
def __init__(self, application: dict, function_lib_list: List[dict], version: str):
|
||||
self.application = application
|
||||
self.function_lib_list = function_lib_list
|
||||
|
|
@ -144,10 +148,12 @@ class ModelSettingSerializer(serializers.Serializer):
|
|||
error_messages=ErrMessage.char(_("Thinking process switch")))
|
||||
reasoning_content_start = serializers.CharField(required=False, allow_null=True, default="<think>",
|
||||
allow_blank=True, max_length=256,
|
||||
trim_whitespace=False,
|
||||
error_messages=ErrMessage.char(
|
||||
_("The thinking process begins to mark")))
|
||||
reasoning_content_end = serializers.CharField(required=False, allow_null=True, allow_blank=True, default="</think>",
|
||||
max_length=256,
|
||||
trim_whitespace=False,
|
||||
error_messages=ErrMessage.char(_("End of thinking process marker")))
|
||||
|
||||
|
||||
|
|
@ -158,7 +164,7 @@ class ApplicationWorkflowSerializer(serializers.Serializer):
|
|||
max_length=256, min_length=1,
|
||||
error_messages=ErrMessage.char(_("Application Description")))
|
||||
work_flow = serializers.DictField(required=False, error_messages=ErrMessage.dict(_("Workflow Objects")))
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
|
||||
error_messages=ErrMessage.char(_("Opening remarks")))
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -221,7 +227,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
min_value=0,
|
||||
max_value=1024,
|
||||
error_messages=ErrMessage.integer(_("Historical chat records")))
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
|
||||
error_messages=ErrMessage.char(_("Opening remarks")))
|
||||
dataset_id_list = serializers.ListSerializer(required=False, child=serializers.UUIDField(required=True),
|
||||
allow_null=True,
|
||||
|
|
@ -328,7 +334,8 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
for field in input_field_list:
|
||||
if field['assignment_method'] == 'api_input' and field['variable'] in params:
|
||||
query += f"&{field['variable']}={params[field['variable']]}"
|
||||
|
||||
if 'asker' in params:
|
||||
query += f"&asker={params.get('asker')}"
|
||||
return query
|
||||
|
||||
class AccessTokenSerializer(serializers.Serializer):
|
||||
|
|
@ -488,7 +495,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
min_value=0,
|
||||
max_value=1024,
|
||||
error_messages=ErrMessage.integer(_("Historical chat records")))
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
|
||||
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
|
||||
error_messages=ErrMessage.char(_("Opening remarks")))
|
||||
dataset_id_list = serializers.ListSerializer(required=False, child=serializers.UUIDField(required=True),
|
||||
error_messages=ErrMessage.list(_("Related Knowledge Base"))
|
||||
|
|
@ -727,7 +734,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
user_id = self.data.get('user_id')
|
||||
mk_instance_bytes = self.data.get('file').read()
|
||||
try:
|
||||
mk_instance = pickle.loads(mk_instance_bytes)
|
||||
mk_instance = restricted_loads(mk_instance_bytes)
|
||||
except Exception as e:
|
||||
raise AppApiException(1001, _("Unsupported file format"))
|
||||
application = mk_instance.application
|
||||
|
|
@ -810,8 +817,10 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
application = QuerySet(Application).filter(id=self.data.get("application_id")).first()
|
||||
return FunctionLibSerializer.Query(data={'user_id': application.user_id, 'is_active': True}).list(
|
||||
with_valid=True)
|
||||
return FunctionLibSerializer.Query(
|
||||
data={'user_id': application.user_id, 'is_active': True,
|
||||
'function_type': FunctionType.PUBLIC}
|
||||
).list(with_valid=True)
|
||||
|
||||
def get_function_lib(self, function_lib_id, with_valid=True):
|
||||
if with_valid:
|
||||
|
|
@ -981,6 +990,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
'draggable': application_setting.draggable,
|
||||
'show_guide': application_setting.show_guide,
|
||||
'avatar': application_setting.avatar,
|
||||
'show_avatar': application_setting.show_avatar,
|
||||
'float_icon': application_setting.float_icon,
|
||||
'authentication': application_setting.authentication,
|
||||
'authentication_type': application_setting.authentication_value.get(
|
||||
|
|
@ -989,6 +999,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
'disclaimer_value': application_setting.disclaimer_value,
|
||||
'custom_theme': application_setting.custom_theme,
|
||||
'user_avatar': application_setting.user_avatar,
|
||||
'show_user_avatar': application_setting.show_user_avatar,
|
||||
'float_location': application_setting.float_location}
|
||||
return ApplicationSerializer.Query.reset_application(
|
||||
{**ApplicationSerializer.ApplicationModel(application).data,
|
||||
|
|
@ -1001,7 +1012,8 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
'stt_autosend': application.stt_autosend,
|
||||
'file_upload_enable': application.file_upload_enable,
|
||||
'file_upload_setting': application.file_upload_setting,
|
||||
'work_flow': application.work_flow,
|
||||
'work_flow': {'nodes': [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
|
||||
node.get('id') == 'base-node']},
|
||||
'show_source': application_access_token.show_source,
|
||||
'language': application_access_token.language,
|
||||
**application_setting_dict})
|
||||
|
|
@ -1062,6 +1074,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
for update_key in update_keys:
|
||||
if update_key in instance and instance.get(update_key) is not None:
|
||||
application.__setattr__(update_key, instance.get(update_key))
|
||||
print(application.name)
|
||||
application.save()
|
||||
|
||||
if 'dataset_id_list' in instance:
|
||||
|
|
@ -1080,6 +1093,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
chat_cache.clear_by_application_id(application_id)
|
||||
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
|
||||
# 更新缓存数据
|
||||
print(application.name)
|
||||
get_application_access_token(application_access_token.access_token, False)
|
||||
return self.one(with_valid=False)
|
||||
|
||||
|
|
@ -1132,6 +1146,8 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
instance['file_upload_enable'] = node_data['file_upload_enable']
|
||||
if 'file_upload_setting' in node_data:
|
||||
instance['file_upload_setting'] = node_data['file_upload_setting']
|
||||
if 'name' in node_data:
|
||||
instance['name'] = node_data['name']
|
||||
break
|
||||
|
||||
def speech_to_text(self, file, with_valid=True):
|
||||
|
|
@ -1201,7 +1217,9 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
self.is_valid(raise_exception=True)
|
||||
if with_valid:
|
||||
self.is_valid()
|
||||
embed_application = QuerySet(Application).get(id=app_id)
|
||||
embed_application = QuerySet(Application).filter(id=app_id).first()
|
||||
if embed_application is None:
|
||||
raise AppApiException(500, _('Application does not exist'))
|
||||
if embed_application.type == ApplicationTypeChoices.WORK_FLOW:
|
||||
work_flow_version = QuerySet(WorkFlowVersion).filter(application_id=embed_application.id).order_by(
|
||||
'-create_time')[0:1].first()
|
||||
|
|
@ -1300,3 +1318,29 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
application_api_key.save()
|
||||
# 写入缓存
|
||||
get_application_api_key(application_api_key.secret_key, False)
|
||||
|
||||
class McpServers(serializers.Serializer):
|
||||
mcp_servers = serializers.JSONField(required=True)
|
||||
|
||||
def get_mcp_servers(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
if '"stdio"' in self.data.get('mcp_servers'):
|
||||
raise AppApiException(500, _('stdio is not supported'))
|
||||
servers = json.loads(self.data.get('mcp_servers'))
|
||||
|
||||
async def get_mcp_tools(servers):
|
||||
async with MultiServerMCPClient(servers) as client:
|
||||
return client.get_tools()
|
||||
|
||||
tools = []
|
||||
for server in servers:
|
||||
tools += [
|
||||
{
|
||||
'server': server,
|
||||
'name': tool.name,
|
||||
'description': tool.description,
|
||||
'args_schema': tool.args_schema,
|
||||
}
|
||||
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
|
||||
return tools
|
||||
|
|
|
|||
|
|
@ -116,13 +116,15 @@ class ChatInfo:
|
|||
}
|
||||
|
||||
def to_pipeline_manage_params(self, problem_text: str, post_response_handler: PostResponseHandler,
|
||||
exclude_paragraph_id_list, client_id: str, client_type, stream=True):
|
||||
exclude_paragraph_id_list, client_id: str, client_type, stream=True, form_data=None):
|
||||
if form_data is None:
|
||||
form_data = {}
|
||||
params = self.to_base_pipeline_manage_params()
|
||||
return {**params, 'problem_text': problem_text, 'post_response_handler': post_response_handler,
|
||||
'exclude_paragraph_id_list': exclude_paragraph_id_list, 'stream': stream, 'client_id': client_id,
|
||||
'client_type': client_type}
|
||||
'client_type': client_type, 'form_data': form_data}
|
||||
|
||||
def append_chat_record(self, chat_record: ChatRecord, client_id=None):
|
||||
def append_chat_record(self, chat_record: ChatRecord, client_id=None, asker=None):
|
||||
chat_record.problem_text = chat_record.problem_text[0:10240] if chat_record.problem_text is not None else ""
|
||||
chat_record.answer_text = chat_record.answer_text[0:40960] if chat_record.problem_text is not None else ""
|
||||
is_save = True
|
||||
|
|
@ -137,8 +139,17 @@ class ChatInfo:
|
|||
if self.application.id is not None:
|
||||
# 插入数据库
|
||||
if not QuerySet(Chat).filter(id=self.chat_id).exists():
|
||||
asker_dict = {'user_name': '游客'}
|
||||
if asker is not None:
|
||||
if isinstance(asker, str):
|
||||
asker_dict = {
|
||||
'user_name': asker
|
||||
}
|
||||
elif isinstance(asker, dict):
|
||||
asker_dict = asker
|
||||
|
||||
Chat(id=self.chat_id, application_id=self.application.id, abstract=chat_record.problem_text[0:1024],
|
||||
client_id=client_id, update_time=datetime.now()).save()
|
||||
client_id=client_id, asker=asker_dict, update_time=datetime.now()).save()
|
||||
else:
|
||||
Chat.objects.filter(id=self.chat_id).update(update_time=datetime.now())
|
||||
# 插入会话记录
|
||||
|
|
@ -171,7 +182,8 @@ def get_post_handler(chat_info: ChatInfo):
|
|||
answer_text_list=answer_list,
|
||||
run_time=manage.context['run_time'],
|
||||
index=len(chat_info.chat_record_list) + 1)
|
||||
chat_info.append_chat_record(chat_record, client_id)
|
||||
asker = kwargs.get("asker", None)
|
||||
chat_info.append_chat_record(chat_record, client_id, asker=asker)
|
||||
# 重新设置缓存
|
||||
chat_cache.set(chat_id,
|
||||
chat_info, timeout=60 * 30)
|
||||
|
|
@ -201,12 +213,21 @@ class OpenAIChatSerializer(serializers.Serializer):
|
|||
return instance.get('messages')[-1].get('content')
|
||||
|
||||
@staticmethod
|
||||
def generate_chat(chat_id, application_id, message, client_id):
|
||||
def generate_chat(chat_id, application_id, message, client_id, asker=None):
|
||||
if chat_id is None:
|
||||
chat_id = str(uuid.uuid1())
|
||||
chat = QuerySet(Chat).filter(id=chat_id).first()
|
||||
if chat is None:
|
||||
Chat(id=chat_id, application_id=application_id, abstract=message[0:1024], client_id=client_id).save()
|
||||
asker_dict = {'user_name': '游客'}
|
||||
if asker is not None:
|
||||
if isinstance(asker, str):
|
||||
asker_dict = {
|
||||
'user_name': asker
|
||||
}
|
||||
elif isinstance(asker, dict):
|
||||
asker_dict = asker
|
||||
Chat(id=chat_id, application_id=application_id, abstract=message[0:1024], client_id=client_id,
|
||||
asker=asker_dict).save()
|
||||
return chat_id
|
||||
|
||||
def chat(self, instance: Dict, with_valid=True):
|
||||
|
|
@ -220,7 +241,8 @@ class OpenAIChatSerializer(serializers.Serializer):
|
|||
application_id = self.data.get('application_id')
|
||||
client_id = self.data.get('client_id')
|
||||
client_type = self.data.get('client_type')
|
||||
chat_id = self.generate_chat(chat_id, application_id, message, client_id)
|
||||
chat_id = self.generate_chat(chat_id, application_id, message, client_id,
|
||||
asker=instance.get('form_data', {}).get("asker"))
|
||||
return ChatMessageSerializer(
|
||||
data={
|
||||
'chat_id': chat_id, 'message': message,
|
||||
|
|
@ -233,6 +255,7 @@ class OpenAIChatSerializer(serializers.Serializer):
|
|||
'image_list': instance.get('image_list', []),
|
||||
'document_list': instance.get('document_list', []),
|
||||
'audio_list': instance.get('audio_list', []),
|
||||
'other_list': instance.get('other_list', []),
|
||||
}
|
||||
).chat(base_to_response=OpenaiToResponse())
|
||||
|
||||
|
|
@ -262,6 +285,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
|
||||
document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
|
||||
audio_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Audio")))
|
||||
other_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Other")))
|
||||
child_node = serializers.DictField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.dict(_("Child Nodes")))
|
||||
|
||||
|
|
@ -310,6 +334,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
stream = self.data.get('stream')
|
||||
client_id = self.data.get('client_id')
|
||||
client_type = self.data.get('client_type')
|
||||
form_data = self.data.get("form_data")
|
||||
pipeline_manage_builder = PipelineManage.builder()
|
||||
# 如果开启了问题优化,则添加上问题优化步骤
|
||||
if chat_info.application.problem_optimization:
|
||||
|
|
@ -331,7 +356,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
exclude_paragraph_id_list = list(set(paragraph_id_list))
|
||||
# 构建运行参数
|
||||
params = chat_info.to_pipeline_manage_params(message, get_post_handler(chat_info), exclude_paragraph_id_list,
|
||||
client_id, client_type, stream)
|
||||
client_id, client_type, stream, form_data)
|
||||
# 运行流水线作业
|
||||
pipeline_message.run(params)
|
||||
return pipeline_message.context['chat_result']
|
||||
|
|
@ -359,6 +384,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
image_list = self.data.get('image_list')
|
||||
document_list = self.data.get('document_list')
|
||||
audio_list = self.data.get('audio_list')
|
||||
other_list = self.data.get('other_list')
|
||||
user_id = chat_info.application.user_id
|
||||
chat_record_id = self.data.get('chat_record_id')
|
||||
chat_record = None
|
||||
|
|
@ -375,7 +401,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
'client_id': client_id,
|
||||
'client_type': client_type,
|
||||
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
|
||||
base_to_response, form_data, image_list, document_list, audio_list,
|
||||
base_to_response, form_data, image_list, document_list, audio_list, other_list,
|
||||
self.data.get('runtime_node_id'),
|
||||
self.data.get('node_data'), chat_record, self.data.get('child_node'))
|
||||
r = work_flow_manage.run()
|
||||
|
|
|
|||
|
|
@ -174,7 +174,14 @@ class ChatSerializers(serializers.Serializer):
|
|||
condition = base_condition & min_trample_query
|
||||
else:
|
||||
condition = base_condition
|
||||
return query_set.filter(condition).order_by("-application_chat.update_time")
|
||||
inner_queryset = QuerySet(Chat).filter(application_id=self.data.get("application_id"))
|
||||
if 'abstract' in self.data and self.data.get('abstract') is not None:
|
||||
inner_queryset = inner_queryset.filter(abstract__icontains=self.data.get('abstract'))
|
||||
|
||||
return {
|
||||
'inner_queryset': inner_queryset,
|
||||
'default_queryset': query_set.filter(condition).order_by("-application_chat.update_time")
|
||||
}
|
||||
|
||||
def list(self, with_valid=True):
|
||||
if with_valid:
|
||||
|
|
@ -216,6 +223,7 @@ class ChatSerializers(serializers.Serializer):
|
|||
"\n".join([
|
||||
f"{improve_paragraph_list[index].get('title')}\n{improve_paragraph_list[index].get('content')}"
|
||||
for index in range(len(improve_paragraph_list))]),
|
||||
row.get('asker').get('user_name'),
|
||||
row.get('message_tokens') + row.get('answer_tokens'), row.get('run_time'),
|
||||
str(row.get('create_time').astimezone(pytz.timezone(TIME_ZONE)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
)]
|
||||
|
|
@ -242,7 +250,8 @@ class ChatSerializers(serializers.Serializer):
|
|||
gettext('answer'), gettext('User feedback'),
|
||||
gettext('Reference segment number'),
|
||||
gettext('Section title + content'),
|
||||
gettext('Annotation'), gettext('Consuming tokens'), gettext('Time consumed (s)'),
|
||||
gettext('Annotation'), gettext('USER'), gettext('Consuming tokens'),
|
||||
gettext('Time consumed (s)'),
|
||||
gettext('Question Time')]
|
||||
for col_idx, header in enumerate(headers, 1):
|
||||
cell = worksheet.cell(row=1, column=col_idx)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ SELECT
|
|||
application_chat_record_temp."index" as "index",
|
||||
application_chat_record_temp.improve_paragraph_list as improve_paragraph_list,
|
||||
application_chat_record_temp.vote_status as vote_status,
|
||||
application_chat_record_temp.create_time as create_time
|
||||
application_chat_record_temp.create_time as create_time,
|
||||
to_json(application_chat.asker) as asker
|
||||
FROM
|
||||
application_chat application_chat
|
||||
LEFT JOIN (
|
||||
|
|
@ -22,6 +23,8 @@ FROM
|
|||
chat_id
|
||||
FROM
|
||||
application_chat_record
|
||||
WHERE chat_id IN (
|
||||
SELECT id FROM application_chat ${inner_queryset})
|
||||
GROUP BY
|
||||
application_chat_record.chat_id
|
||||
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
|
||||
|
|
@ -34,4 +37,5 @@ FROM
|
|||
END as improve_paragraph_list
|
||||
FROM
|
||||
application_chat_record application_chat_record
|
||||
) application_chat_record_temp ON application_chat_record_temp.chat_id = application_chat."id"
|
||||
) application_chat_record_temp ON application_chat_record_temp.chat_id = application_chat."id"
|
||||
${default_queryset}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
SELECT
|
||||
*
|
||||
*,to_json(asker) as asker
|
||||
FROM
|
||||
application_chat application_chat
|
||||
LEFT JOIN (
|
||||
|
|
@ -11,6 +11,9 @@ FROM
|
|||
chat_id
|
||||
FROM
|
||||
application_chat_record
|
||||
WHERE chat_id IN (
|
||||
SELECT id FROM application_chat ${inner_queryset})
|
||||
GROUP BY
|
||||
application_chat_record.chat_id
|
||||
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
|
||||
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
|
||||
${default_queryset}
|
||||
|
|
@ -38,6 +38,15 @@ class ApplicationApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_("Application authentication token"),
|
||||
description=_("Application authentication token"),
|
||||
default="token"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
|
|
@ -133,6 +142,27 @@ class ApplicationApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
|
||||
description=_("Primary key id")),
|
||||
'secret_key': openapi.Schema(type=openapi.TYPE_STRING, title=_("Secret key"),
|
||||
description=_("Secret key")),
|
||||
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Is activation"),
|
||||
description=_("Is activation")),
|
||||
'application_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application ID"),
|
||||
description=_("Application ID")),
|
||||
'allow_cross_domain': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title=_("Is cross-domain allowed"),
|
||||
description=_("Is cross-domain allowed")),
|
||||
'cross_domain_list': openapi.Schema(type=openapi.TYPE_ARRAY, title=_('Cross-domain list'),
|
||||
items=openapi.Schema(type=openapi.TYPE_STRING))
|
||||
}
|
||||
)
|
||||
|
||||
class AccessToken(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
|
|
@ -171,6 +201,37 @@ class ApplicationApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[],
|
||||
properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
|
||||
description=_("Primary key id")),
|
||||
'access_token': openapi.Schema(type=openapi.TYPE_STRING, title=_("Access Token"),
|
||||
description=_("Access Token")),
|
||||
'access_token_reset': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Reset Token"),
|
||||
description=_("Reset Token")),
|
||||
|
||||
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Is activation"),
|
||||
description=_("Is activation")),
|
||||
'access_num': openapi.Schema(type=openapi.TYPE_NUMBER, title=_("Number of visits"),
|
||||
description=_("Number of visits")),
|
||||
'white_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Whether to enable whitelist"),
|
||||
description=_("Whether to enable whitelist")),
|
||||
'white_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(type=openapi.TYPE_STRING), title=_("Whitelist"),
|
||||
description=_("Whitelist")),
|
||||
'show_source': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title=_("Whether to display knowledge sources"),
|
||||
description=_("Whether to display knowledge sources")),
|
||||
'language': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title=_("language"),
|
||||
description=_("language"))
|
||||
}
|
||||
)
|
||||
|
||||
class Edit(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
|
|
@ -367,6 +428,56 @@ class ApplicationApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['id', 'name', 'desc', 'model_id', 'dialogue_number', 'dataset_setting', 'model_setting',
|
||||
'problem_optimization', 'stt_model_enable', 'stt_model_enable', 'tts_type',
|
||||
'work_flow'],
|
||||
properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
|
||||
description=_("Primary key id")),
|
||||
'name': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Name"),
|
||||
description=_("Application Name")),
|
||||
'desc': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Description"),
|
||||
description=_("Application Description")),
|
||||
'model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Model id"),
|
||||
description=_("Model id")),
|
||||
"dialogue_number": openapi.Schema(type=openapi.TYPE_NUMBER,
|
||||
title=_("Number of multi-round conversations"),
|
||||
description=_("Number of multi-round conversations")),
|
||||
'prologue': openapi.Schema(type=openapi.TYPE_STRING, title=_("Opening remarks"),
|
||||
description=_("Opening remarks")),
|
||||
'dataset_id_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||
title=_("List of associated knowledge base IDs"),
|
||||
description=_("List of associated knowledge base IDs")),
|
||||
'dataset_setting': ApplicationApi.DatasetSetting.get_request_body_api(),
|
||||
'model_setting': ApplicationApi.ModelSetting.get_request_body_api(),
|
||||
'problem_optimization': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Problem Optimization"),
|
||||
description=_("Problem Optimization"), default=True),
|
||||
'type': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Type"),
|
||||
description=_("Application Type SIMPLE | WORK_FLOW")),
|
||||
'problem_optimization_prompt': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title=_('Question optimization tips'),
|
||||
description=_("Question optimization tips"),
|
||||
default=_(
|
||||
"() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the <data></data> tag")),
|
||||
'tts_model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Text-to-speech model ID"),
|
||||
description=_("Text-to-speech model ID")),
|
||||
'stt_model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Speech-to-text model id"),
|
||||
description=_("Speech-to-text model id")),
|
||||
'stt_model_enable': openapi.Schema(type=openapi.TYPE_STRING, title=_("Is speech-to-text enabled"),
|
||||
description=_("Is speech-to-text enabled")),
|
||||
'tts_model_enable': openapi.Schema(type=openapi.TYPE_STRING, title=_("Is text-to-speech enabled"),
|
||||
description=_("Is text-to-speech enabled")),
|
||||
'tts_type': openapi.Schema(type=openapi.TYPE_STRING, title=_("Text-to-speech type"),
|
||||
description=_("Text-to-speech type")),
|
||||
'work_flow': ApplicationApi.WorkFlow.get_request_body_api(),
|
||||
}
|
||||
)
|
||||
|
||||
class Query(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
|
|
|
|||
|
|
@ -53,6 +53,85 @@ class ChatClientHistoryApi(ApiMixin):
|
|||
|
||||
|
||||
class OpenAIChatApi(ApiMixin):
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Responses(responses={
|
||||
200: openapi.Response(description=_('response parameters'),
|
||||
schema=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=['id',
|
||||
'choices'],
|
||||
properties={
|
||||
'id': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_(
|
||||
"Conversation ID")),
|
||||
'choices': openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[
|
||||
'message'],
|
||||
properties={
|
||||
'finish_reason': openapi.Schema(
|
||||
type=openapi.TYPE_STRING, ),
|
||||
'index': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER),
|
||||
'answer_list': openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[
|
||||
'content'],
|
||||
properties={
|
||||
'content': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'view_type': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'runtime_node_id': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'chat_record_id': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'reasoning_content': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
}
|
||||
)),
|
||||
'message': openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[
|
||||
'content'],
|
||||
properties={
|
||||
'content': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'role': openapi.Schema(
|
||||
type=openapi.TYPE_STRING)
|
||||
|
||||
}),
|
||||
|
||||
}
|
||||
)),
|
||||
'created': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER),
|
||||
'model': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'object': openapi.Schema(
|
||||
type=openapi.TYPE_STRING),
|
||||
'usage': openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[
|
||||
'completion_tokens',
|
||||
'prompt_tokens',
|
||||
'total_tokens'],
|
||||
properties={
|
||||
'completion_tokens': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER),
|
||||
'prompt_tokens': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER),
|
||||
'total_tokens': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER)
|
||||
})
|
||||
|
||||
}))})
|
||||
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
|
|
@ -240,6 +319,15 @@ class ChatApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_("Conversation ID"),
|
||||
description=_("Conversation ID"),
|
||||
default="chat_id"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
return [openapi.Parameter(name='application_id',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ urlpatterns = [
|
|||
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
|
||||
path('application/embed', views.Application.Embed.as_view()),
|
||||
path('application/authentication', views.Application.Authentication.as_view()),
|
||||
path('application/mcp_servers', views.Application.McpServers.as_view()),
|
||||
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
|
||||
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
|
||||
path('application/<str:application_id>/export', views.Application.Export.as_view()),
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ from rest_framework.views import APIView
|
|||
|
||||
from application.serializers.application_version_serializers import ApplicationVersionSerializer
|
||||
from application.swagger_api.application_version_api import ApplicationVersionApi
|
||||
from application.views import get_application_operation_object
|
||||
from common.auth import has_permissions, TokenAuth
|
||||
from common.constants.permission_constants import PermissionConstants, CompareConstants, ViewPermission, RoleConstants, \
|
||||
Permission, Group, Operate
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
|
@ -82,6 +84,8 @@ class ApplicationVersionView(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="Modify application version information",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str, work_flow_version_id: str):
|
||||
return result.success(
|
||||
ApplicationVersionSerializer.Operate(
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@
|
|||
|
||||
from django.core import cache
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, gettext
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
|
|
@ -20,10 +21,12 @@ from application.serializers.application_serializers import ApplicationSerialize
|
|||
from application.serializers.application_statistics_serializers import ApplicationStatisticsSerializer
|
||||
from application.swagger_api.application_api import ApplicationApi
|
||||
from application.swagger_api.application_statistics_api import ApplicationStatisticsApi
|
||||
from application.views.common import get_application_operation_object
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import CompareConstants, PermissionConstants, Permission, Group, Operate, \
|
||||
ViewPermission, RoleConstants
|
||||
from common.exception.app_exception import AppAuthenticationFailed
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.swagger_api.common_api import CommonApi
|
||||
from common.util.common import query_params_to_single_dict
|
||||
|
|
@ -152,6 +155,8 @@ class Application(APIView):
|
|||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT,
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Application', operate="Modify application icon",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.IconOperate(
|
||||
|
|
@ -168,6 +173,7 @@ class Application(APIView):
|
|||
tags=[_("Application")]
|
||||
)
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
@log(menu='Application', operate="Import Application")
|
||||
def post(self, request: Request):
|
||||
return result.success(ApplicationSerializer.Import(
|
||||
data={'user_id': request.user.id, 'file': request.FILES.get('file')}).import_())
|
||||
|
|
@ -182,6 +188,8 @@ class Application(APIView):
|
|||
)
|
||||
@has_permissions(lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id')))
|
||||
@log(menu='Application', operate="Export Application",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def get(self, request: Request, application_id: str):
|
||||
return ApplicationSerializer.Operate(
|
||||
data={'application_id': application_id, 'user_id': request.user.id}).export()
|
||||
|
|
@ -335,6 +343,8 @@ class Application(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="Add ApiKey",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def post(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.ApplicationKeySerializer(
|
||||
|
|
@ -363,13 +373,16 @@ class Application(APIView):
|
|||
operation_id=_("Modify application API_KEY"),
|
||||
tags=[_('Application/API_KEY')],
|
||||
manual_parameters=ApplicationApi.ApiKey.Operate.get_request_params_api(),
|
||||
request_body=ApplicationApi.ApiKey.Operate.get_request_body_api())
|
||||
request_body=ApplicationApi.ApiKey.Operate.get_request_body_api(),
|
||||
responses=result.get_api_response(ApplicationApi.ApiKey.Operate.get_response_body_api()))
|
||||
@has_permissions(ViewPermission(
|
||||
[RoleConstants.ADMIN, RoleConstants.USER],
|
||||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT,
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Application', operate="Modify application API_KEY",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str, api_key_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.ApplicationKeySerializer.Operate(
|
||||
|
|
@ -387,6 +400,8 @@ class Application(APIView):
|
|||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND), PermissionConstants.APPLICATION_DELETE,
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Application', operate="Delete Application API_KEY",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def delete(self, request: Request, application_id: str, api_key_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.ApplicationKeySerializer.Operate(
|
||||
|
|
@ -401,12 +416,15 @@ class Application(APIView):
|
|||
operation_id=_("Modify Application AccessToken"),
|
||||
tags=[_('Application/Public Access')],
|
||||
manual_parameters=ApplicationApi.AccessToken.get_request_params_api(),
|
||||
request_body=ApplicationApi.AccessToken.get_request_body_api())
|
||||
request_body=ApplicationApi.AccessToken.get_request_body_api(),
|
||||
responses=result.get_api_response(ApplicationApi.AccessToken.get_response_body_api()))
|
||||
@has_permissions(ViewPermission(
|
||||
[RoleConstants.ADMIN, RoleConstants.USER],
|
||||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="Modify Application AccessToken",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.AccessTokenSerializer(data={'application_id': application_id}).edit(
|
||||
|
|
@ -439,6 +457,7 @@ class Application(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("Application Certification"),
|
||||
operation_id=_("Application Certification"),
|
||||
request_body=ApplicationApi.Authentication.get_request_body_api(),
|
||||
responses=result.get_api_response(ApplicationApi.Authentication.get_response_body_api()),
|
||||
tags=[_("Application/Certification")],
|
||||
security=[])
|
||||
def post(self, request: Request):
|
||||
|
|
@ -456,8 +475,11 @@ class Application(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("Create an application"),
|
||||
operation_id=_("Create an application"),
|
||||
request_body=ApplicationApi.Create.get_request_body_api(),
|
||||
responses=result.get_api_response(ApplicationApi.Create.get_response_body_api()),
|
||||
tags=[_('Application')])
|
||||
@has_permissions(PermissionConstants.APPLICATION_CREATE, compare=CompareConstants.AND)
|
||||
@log(menu='Application', operate="Create an application",
|
||||
get_operation_object=lambda r, k: {'name': r.data.get('name')})
|
||||
def post(self, request: Request):
|
||||
return result.success(ApplicationSerializer.Create(data={'user_id': request.user.id}).insert(request.data))
|
||||
|
||||
|
|
@ -512,6 +534,8 @@ class Application(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="Publishing an application",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.Operate(
|
||||
|
|
@ -533,6 +557,8 @@ class Application(APIView):
|
|||
compare=CompareConstants.AND),
|
||||
lambda r, k: Permission(group=Group.APPLICATION, operate=Operate.DELETE,
|
||||
dynamic_tag=k.get('application_id')), compare=CompareConstants.AND)
|
||||
@log(menu='Application', operate="Deleting application",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def delete(self, request: Request, application_id: str):
|
||||
return result.success(ApplicationSerializer.Operate(
|
||||
data={'application_id': application_id, 'user_id': request.user.id}).delete(
|
||||
|
|
@ -550,6 +576,8 @@ class Application(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="Modify the application",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.Operate(
|
||||
|
|
@ -660,8 +688,19 @@ class Application(APIView):
|
|||
dynamic_tag=keywords.get(
|
||||
'application_id'))],
|
||||
compare=CompareConstants.AND))
|
||||
@log(menu='Application', operate="trial listening",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def post(self, request: Request, application_id: str):
|
||||
byte_data = ApplicationSerializer.Operate(
|
||||
data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data)
|
||||
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
|
||||
'Content-Disposition': 'attachment; filename="abc.mp3"'})
|
||||
|
||||
class McpServers(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
@has_permissions(PermissionConstants.APPLICATION_READ, compare=CompareConstants.AND)
|
||||
def get(self, request: Request):
|
||||
return result.success(ApplicationSerializer.McpServers(
|
||||
data={'mcp_servers': request.query_params.get('mcp_servers')}).get_mcp_servers())
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ from application.serializers.chat_message_serializers import ChatMessageSerializ
|
|||
from application.serializers.chat_serializers import ChatSerializers, ChatRecordSerializer
|
||||
from application.swagger_api.chat_api import ChatApi, VoteApi, ChatRecordApi, ImproveApi, ChatRecordImproveApi, \
|
||||
ChatClientHistoryApi, OpenAIChatApi
|
||||
from application.views import get_application_operation_object
|
||||
from common.auth import TokenAuth, has_permissions, OpenAIKeyAuth
|
||||
from common.constants.authentication_type import AuthenticationType
|
||||
from common.constants.permission_constants import Permission, Group, Operate, \
|
||||
RoleConstants, ViewPermission, CompareConstants
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.util.common import query_params_to_single_dict
|
||||
from dataset.serializers.file_serializers import FileSerializer
|
||||
|
|
@ -35,6 +37,7 @@ class Openai(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("OpenAI Interface Dialogue"),
|
||||
operation_id=_("OpenAI Interface Dialogue"),
|
||||
request_body=OpenAIChatApi.get_request_body_api(),
|
||||
responses=OpenAIChatApi.get_response_body_api(),
|
||||
tags=[_("OpenAI Dialogue")])
|
||||
def post(self, request: Request, application_id: str):
|
||||
return OpenAIChatSerializer(data={'application_id': application_id, 'client_id': request.auth.client_id,
|
||||
|
|
@ -58,6 +61,8 @@ class ChatView(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
|
||||
dynamic_tag=keywords.get('application_id'))])
|
||||
)
|
||||
@log(menu='Conversation Log', operate="Export conversation",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def post(self, request: Request, application_id: str):
|
||||
return ChatSerializers.Query(
|
||||
data={**query_params_to_single_dict(request.query_params), 'application_id': application_id,
|
||||
|
|
@ -89,10 +94,11 @@ class ChatView(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("Get the workflow temporary session id"),
|
||||
operation_id=_("Get the workflow temporary session id"),
|
||||
request_body=ChatApi.OpenWorkFlowTemp.get_request_body_api(),
|
||||
responses=result.get_api_response(ChatApi.OpenTempChat.get_response_body_api()),
|
||||
tags=[_("Application/Chat")])
|
||||
def post(self, request: Request):
|
||||
return result.success(ChatSerializers.OpenWorkFlowChat(
|
||||
data={**request.data, 'user_id': request.user.id}).open())
|
||||
data={'user_id': request.user.id, **request.data}).open())
|
||||
|
||||
class OpenTemp(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
|
@ -101,6 +107,7 @@ class ChatView(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("Get a temporary session id"),
|
||||
operation_id=_("Get a temporary session id"),
|
||||
request_body=ChatApi.OpenTempChat.get_request_body_api(),
|
||||
responses=result.get_api_response(ChatApi.OpenTempChat.get_response_body_api()),
|
||||
tags=[_("Application/Chat")])
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def post(self, request: Request):
|
||||
|
|
@ -139,6 +146,8 @@ class ChatView(APIView):
|
|||
'document_list') if 'document_list' in request.data else [],
|
||||
'audio_list': request.data.get(
|
||||
'audio_list') if 'audio_list' in request.data else [],
|
||||
'other_list': request.data.get(
|
||||
'other_list') if 'other_list' in request.data else [],
|
||||
'client_type': request.auth.client_type,
|
||||
'node_id': request.data.get('node_id', None),
|
||||
'runtime_node_id': request.data.get('runtime_node_id', None),
|
||||
|
|
@ -177,6 +186,8 @@ class ChatView(APIView):
|
|||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND),
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Delete a conversation",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def delete(self, request: Request, application_id: str, chat_id: str):
|
||||
return result.success(
|
||||
ChatSerializers.Operate(
|
||||
|
|
@ -218,6 +229,8 @@ class ChatView(APIView):
|
|||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND),
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Client deletes conversation",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def delete(self, request: Request, application_id: str, chat_id: str):
|
||||
return result.success(
|
||||
ChatSerializers.Operate(
|
||||
|
|
@ -228,13 +241,16 @@ class ChatView(APIView):
|
|||
@swagger_auto_schema(operation_summary=_("Client modifies dialogue summary"),
|
||||
operation_id=_("Client modifies dialogue summary"),
|
||||
request_body=ChatClientHistoryApi.Operate.ReAbstract.get_request_body_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=[_("Application/Conversation Log")])
|
||||
@has_permissions(ViewPermission(
|
||||
[RoleConstants.APPLICATION_ACCESS_TOKEN],
|
||||
[RoleConstants.APPLICATION_ACCESS_TOKEN, RoleConstants.ADMIN, RoleConstants.USER],
|
||||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND),
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Client modifies dialogue summary",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str, chat_id: str):
|
||||
return result.success(
|
||||
ChatSerializers.Operate(
|
||||
|
|
@ -343,6 +359,8 @@ class ChatView(APIView):
|
|||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
|
||||
dynamic_tag=keywords.get('application_id'))])
|
||||
)
|
||||
@log(menu='Conversation Log', operate="Like, Dislike",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str, chat_id: str, chat_record_id: str):
|
||||
return result.success(ChatRecordSerializer.Vote(
|
||||
data={'vote_status': request.data.get('vote_status'), 'chat_id': chat_id,
|
||||
|
|
@ -390,6 +408,8 @@ class ChatView(APIView):
|
|||
'dataset_id'))],
|
||||
compare=CompareConstants.AND
|
||||
), compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Annotation",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def put(self, request: Request, application_id: str, chat_id: str, chat_record_id: str, dataset_id: str,
|
||||
document_id: str):
|
||||
return result.success(ChatRecordSerializer.Improve(
|
||||
|
|
@ -401,6 +421,7 @@ class ChatView(APIView):
|
|||
operation_id=_("Add to Knowledge Base"),
|
||||
manual_parameters=ImproveApi.get_request_params_api_post(),
|
||||
request_body=ImproveApi.get_request_body_api_post(),
|
||||
responses=result.get_default_response(),
|
||||
tags=[_("Application/Conversation Log/Add to Knowledge Base")]
|
||||
)
|
||||
@has_permissions(
|
||||
|
|
@ -415,6 +436,8 @@ class ChatView(APIView):
|
|||
'dataset_id'))],
|
||||
compare=CompareConstants.AND
|
||||
), compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Add to Knowledge Base",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def post(self, request: Request, application_id: str, dataset_id: str):
|
||||
return result.success(ChatRecordSerializer.PostImprove().post_improve(request.data))
|
||||
|
||||
|
|
@ -440,6 +463,8 @@ class ChatView(APIView):
|
|||
'dataset_id'))],
|
||||
compare=CompareConstants.AND
|
||||
), compare=CompareConstants.AND)
|
||||
@log(menu='Conversation Log', operate="Delete a Annotation",
|
||||
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
|
||||
def delete(self, request: Request, application_id: str, chat_id: str, chat_record_id: str,
|
||||
dataset_id: str,
|
||||
document_id: str, paragraph_id: str):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: common.py
|
||||
@date:2025/3/25 16:56
|
||||
@desc:
|
||||
"""
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from application.models import Application
|
||||
|
||||
|
||||
def get_application_operation_object(application_id):
|
||||
application_model = QuerySet(model=Application).filter(id=application_id).first()
|
||||
if application_model is not None:
|
||||
return {
|
||||
"name": application_model.name
|
||||
}
|
||||
return {}
|
||||
|
|
@ -11,35 +11,50 @@ import time
|
|||
|
||||
from common.cache.mem_cache import MemCache
|
||||
|
||||
lock = threading.Lock()
|
||||
_lock = threading.Lock()
|
||||
locks = {}
|
||||
|
||||
|
||||
class ModelManage:
|
||||
cache = MemCache('model', {})
|
||||
up_clear_time = time.time()
|
||||
|
||||
@staticmethod
|
||||
def _get_lock(_id):
|
||||
lock = locks.get(_id)
|
||||
if lock is None:
|
||||
with _lock:
|
||||
lock = locks.get(_id)
|
||||
if lock is None:
|
||||
lock = threading.Lock()
|
||||
locks[_id] = lock
|
||||
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def get_model(_id, get_model):
|
||||
# 获取锁
|
||||
lock.acquire()
|
||||
try:
|
||||
model_instance = ModelManage.cache.get(_id)
|
||||
if model_instance is None or not model_instance.is_cache_model():
|
||||
model_instance = ModelManage.cache.get(_id)
|
||||
if model_instance is None:
|
||||
lock = ModelManage._get_lock(_id)
|
||||
with lock:
|
||||
model_instance = ModelManage.cache.get(_id)
|
||||
if model_instance is None:
|
||||
model_instance = get_model(_id)
|
||||
ModelManage.cache.set(_id, model_instance, timeout=60 * 60 * 8)
|
||||
else:
|
||||
if model_instance.is_cache_model():
|
||||
ModelManage.cache.touch(_id, timeout=60 * 60 * 8)
|
||||
else:
|
||||
model_instance = get_model(_id)
|
||||
ModelManage.cache.set(_id, model_instance, timeout=60 * 30)
|
||||
return model_instance
|
||||
# 续期
|
||||
ModelManage.cache.touch(_id, timeout=60 * 30)
|
||||
ModelManage.clear_timeout_cache()
|
||||
return model_instance
|
||||
finally:
|
||||
# 释放锁
|
||||
lock.release()
|
||||
ModelManage.cache.set(_id, model_instance, timeout=60 * 60 * 8)
|
||||
ModelManage.clear_timeout_cache()
|
||||
return model_instance
|
||||
|
||||
@staticmethod
|
||||
def clear_timeout_cache():
|
||||
if time.time() - ModelManage.up_clear_time > 60:
|
||||
ModelManage.cache.clear_timeout_data()
|
||||
if time.time() - ModelManage.up_clear_time > 60 * 60:
|
||||
threading.Thread(target=lambda: ModelManage.cache.clear_timeout_data()).start()
|
||||
ModelManage.up_clear_time = time.time()
|
||||
|
||||
@staticmethod
|
||||
def delete_key(_id):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
@date:2023/9/5 14:01
|
||||
@desc: 用于swagger 分组
|
||||
"""
|
||||
|
||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||
from drf_yasg.inspectors import SwaggerAutoSchema
|
||||
|
||||
tags_dict = {
|
||||
|
|
@ -20,10 +20,10 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
|||
if "api" in tags and operation_keys:
|
||||
return [tags_dict.get(operation_keys[1]) if operation_keys[1] in tags_dict else operation_keys[1]]
|
||||
return tags
|
||||
|
||||
|
||||
class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
|
||||
def get_schema(self, request=None, public=False):
|
||||
schema = super().get_schema(request, public)
|
||||
if request.is_secure():
|
||||
schema.schemes = ['https']
|
||||
else:
|
||||
schema.schemes = ['http']
|
||||
return schema
|
||||
schema.schemes = ['https', 'http']
|
||||
return schema
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: SystemEncoder.py
|
||||
@date:2025/3/17 16:38
|
||||
@desc:
|
||||
"""
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
|
||||
|
||||
|
||||
class SystemEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return float(obj)
|
||||
if isinstance(obj, InMemoryUploadedFile):
|
||||
return {'name': obj.name, 'size': obj.size}
|
||||
if isinstance(obj, TemporaryUploadedFile):
|
||||
return {'name': obj.name, 'size': obj.size}
|
||||
else:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
|
@ -12,15 +12,22 @@ from .listener_manage import *
|
|||
from django.utils.translation import gettext as _
|
||||
|
||||
from ..db.sql_execute import update_execute
|
||||
from common.lock.impl.file_lock import FileLock
|
||||
|
||||
lock = FileLock()
|
||||
update_document_status_sql = """
|
||||
UPDATE "public"."document"
|
||||
SET status ="replace"("replace"("replace"(status, '1', '3'), '0', '3'), '4', '3')
|
||||
WHERE status ~ '1|0|4'
|
||||
"""
|
||||
|
||||
|
||||
def run():
|
||||
# QuerySet(Document).filter(status__in=[Status.embedding, Status.queue_up]).update(**{'status': Status.error})
|
||||
QuerySet(Model).filter(status=setting.models.Status.DOWNLOAD).update(status=setting.models.Status.ERROR,
|
||||
meta={'message': _('The download process was interrupted, please try again')})
|
||||
update_execute(update_document_status_sql, [])
|
||||
if lock.try_lock('event_init', 30 * 30):
|
||||
try:
|
||||
QuerySet(Model).filter(status=setting.models.Status.DOWNLOAD).update(status=setting.models.Status.ERROR,
|
||||
meta={'message': _(
|
||||
'The download process was interrupted, please try again')})
|
||||
update_execute(update_document_status_sql, [])
|
||||
finally:
|
||||
lock.un_lock('event_init')
|
||||
|
|
|
|||
|
|
@ -238,11 +238,8 @@ class ListenerManagement:
|
|||
for key in params_dict:
|
||||
_value_ = params_dict[key]
|
||||
exec_sql = exec_sql.replace(key, str(_value_))
|
||||
lock.acquire()
|
||||
try:
|
||||
with lock:
|
||||
native_update(query_set, exec_sql)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
@staticmethod
|
||||
def embedding_by_document(document_id, embedding_model: Embeddings, state_list=None):
|
||||
|
|
@ -272,9 +269,6 @@ class ListenerManagement:
|
|||
ListenerManagement.update_status(QuerySet(Document).filter(id=document_id), TaskType.EMBEDDING,
|
||||
State.STARTED)
|
||||
|
||||
# 删除文档向量数据
|
||||
VectorStore.get_embedding_vector().delete_by_document_id(document_id)
|
||||
|
||||
# 根据段落进行向量化处理
|
||||
page_desc(QuerySet(Paragraph)
|
||||
.annotate(
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ from .table_checkbox import *
|
|||
from .radio_card_field import *
|
||||
from .label import *
|
||||
from .slider_field import *
|
||||
from .switch_field import *
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ class SwitchField(BaseField):
|
|||
@param props_info:
|
||||
"""
|
||||
|
||||
super().__init__('Switch', label, required, default_value, relation_show_field_dict,
|
||||
super().__init__('SwitchInput', label, required, default_value, relation_show_field_dict,
|
||||
{},
|
||||
TriggerType.OPTION_LIST, attrs, props_info)
|
||||
|
|
|
|||
|
|
@ -110,24 +110,47 @@ def get_image_id_func():
|
|||
return get_image_id
|
||||
|
||||
|
||||
title_font_list = [
|
||||
[36, 100],
|
||||
[30, 36]
|
||||
]
|
||||
|
||||
|
||||
def get_title_level(paragraph: Paragraph):
|
||||
try:
|
||||
if paragraph.style is not None:
|
||||
psn = paragraph.style.name
|
||||
if psn.startswith('Heading') or psn.startswith('TOC 标题') or psn.startswith('标题'):
|
||||
return int(psn.replace("Heading ", '').replace('TOC 标题', '').replace('标题',
|
||||
''))
|
||||
if len(paragraph.runs) == 1:
|
||||
font_size = paragraph.runs[0].font.size
|
||||
pt = font_size.pt
|
||||
if pt >= 30:
|
||||
for _value, index in zip(title_font_list, range(len(title_font_list))):
|
||||
if pt >= _value[0] and pt < _value[1]:
|
||||
return index + 1
|
||||
except Exception as e:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class DocSplitHandle(BaseSplitHandle):
|
||||
@staticmethod
|
||||
def paragraph_to_md(paragraph: Paragraph, doc: Document, images_list, get_image_id):
|
||||
try:
|
||||
psn = paragraph.style.name
|
||||
if psn.startswith('Heading') or psn.startswith('TOC 标题') or psn.startswith('标题'):
|
||||
title = "".join(["#" for i in range(
|
||||
int(psn.replace("Heading ", '').replace('TOC 标题', '').replace('标题',
|
||||
'')))]) + " " + paragraph.text
|
||||
title_level = get_title_level(paragraph)
|
||||
if title_level is not None:
|
||||
title = "".join(["#" for i in range(title_level)]) + " " + paragraph.text
|
||||
images = reduce(lambda x, y: [*x, *y],
|
||||
[get_paragraph_element_images(e, doc, images_list, get_image_id) for e in
|
||||
paragraph._element],
|
||||
[])
|
||||
|
||||
if len(images) > 0:
|
||||
return title + '\n' + images_to_string(images, doc, images_list, get_image_id) if len(
|
||||
paragraph.text) > 0 else images_to_string(images, doc, images_list, get_image_id)
|
||||
return title
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return paragraph.text
|
||||
|
|
|
|||
|
|
@ -173,14 +173,15 @@ class PdfSplitHandle(BaseSplitHandle):
|
|||
|
||||
# Null characters are not allowed.
|
||||
chapter_text = chapter_text.replace('\0', '')
|
||||
|
||||
# 限制标题长度
|
||||
real_chapter_title = chapter_title[:256]
|
||||
# 限制章节内容长度
|
||||
if 0 < limit < len(chapter_text):
|
||||
split_text = PdfSplitHandle.split_text(chapter_text, limit)
|
||||
for text in split_text:
|
||||
chapters.append({"title": chapter_title, "content": text})
|
||||
chapters.append({"title": real_chapter_title, "content": text})
|
||||
else:
|
||||
chapters.append({"title": chapter_title, "content": chapter_text if chapter_text else chapter_title})
|
||||
chapters.append({"title": real_chapter_title, "content": chapter_text if chapter_text else real_chapter_title})
|
||||
# 保存章节内容和章节标题
|
||||
return chapters
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,8 @@ class ZipParseQAHandle(BaseParseQAHandle):
|
|||
files = zip_ref.namelist()
|
||||
# 读取压缩包中的文件内容
|
||||
for file in files:
|
||||
if file.endswith('/'):
|
||||
# 跳过 macOS 特有的元数据目录和文件
|
||||
if file.endswith('/') or file.startswith('__MACOSX'):
|
||||
continue
|
||||
with zip_ref.open(file) as f:
|
||||
# 对文件内容进行处理
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ class XlsSplitHandle(BaseParseTableHandle):
|
|||
for row in data:
|
||||
# 将每个单元格中的内容替换换行符为 <br> 以保留原始格式
|
||||
md_table += '| ' + ' | '.join(
|
||||
[str(cell).replace('\n', '<br>') if cell else '' for cell in row]) + ' |\n'
|
||||
[str(cell)
|
||||
.replace('\r\n', '<br>')
|
||||
.replace('\n', '<br>')
|
||||
if cell else '' for cell in row]) + ' |\n'
|
||||
md_tables += md_table + '\n\n'
|
||||
|
||||
return md_tables
|
||||
|
|
|
|||
|
|
@ -19,36 +19,24 @@ class XlsxSplitHandle(BaseParseTableHandle):
|
|||
|
||||
def fill_merged_cells(self, sheet, image_dict):
|
||||
data = []
|
||||
|
||||
# 获取第一行作为标题行
|
||||
headers = []
|
||||
for idx, cell in enumerate(sheet[1]):
|
||||
if cell.value is None:
|
||||
headers.append(' ' * (idx + 1))
|
||||
else:
|
||||
headers.append(cell.value)
|
||||
|
||||
# 从第二行开始遍历每一行
|
||||
for row in sheet.iter_rows(min_row=2, values_only=False):
|
||||
row_data = {}
|
||||
for row in sheet.iter_rows(values_only=False):
|
||||
row_data = []
|
||||
for col_idx, cell in enumerate(row):
|
||||
cell_value = cell.value
|
||||
|
||||
# 如果单元格为空,并且该单元格在合并单元格内,获取合并单元格的值
|
||||
if cell_value is None:
|
||||
for merged_range in sheet.merged_cells.ranges:
|
||||
if cell.coordinate in merged_range:
|
||||
cell_value = sheet[merged_range.min_row][merged_range.min_col - 1].value
|
||||
break
|
||||
|
||||
image = image_dict.get(cell_value, None)
|
||||
if image is not None:
|
||||
cell_value = f''
|
||||
|
||||
# 使用标题作为键,单元格的值作为值存入字典
|
||||
row_data[headers[col_idx]] = cell_value
|
||||
row_data.insert(col_idx, cell_value)
|
||||
data.append(row_data)
|
||||
|
||||
for merged_range in sheet.merged_cells.ranges:
|
||||
cell_value = data[merged_range.min_row - 1][merged_range.min_col - 1]
|
||||
for row_index in range(merged_range.min_row, merged_range.max_row + 1):
|
||||
for col_index in range(merged_range.min_col, merged_range.max_col + 1):
|
||||
data[row_index - 1][col_index - 1] = cell_value
|
||||
return data
|
||||
|
||||
def handle(self, file, get_buffer, save_image):
|
||||
|
|
@ -65,11 +53,13 @@ class XlsxSplitHandle(BaseParseTableHandle):
|
|||
paragraphs = []
|
||||
ws = wb[sheetname]
|
||||
data = self.fill_merged_cells(ws, image_dict)
|
||||
|
||||
for row in data:
|
||||
row_output = "; ".join([f"{key}: {value}" for key, value in row.items()])
|
||||
# print(row_output)
|
||||
paragraphs.append({'title': '', 'content': row_output})
|
||||
if len(data) >= 2:
|
||||
head_list = data[0]
|
||||
for row_index in range(1, len(data)):
|
||||
row_output = "; ".join(
|
||||
[f"{head_list[col_index]}: {data[row_index][col_index]}" for col_index in
|
||||
range(0, len(data[row_index]))])
|
||||
paragraphs.append({'title': '', 'content': row_output})
|
||||
|
||||
result.append({'name': sheetname, 'paragraphs': paragraphs})
|
||||
|
||||
|
|
@ -78,7 +68,6 @@ class XlsxSplitHandle(BaseParseTableHandle):
|
|||
return [{'name': file.name, 'paragraphs': []}]
|
||||
return result
|
||||
|
||||
|
||||
def get_content(self, file, save_image):
|
||||
try:
|
||||
# 加载 Excel 文件
|
||||
|
|
@ -94,18 +83,18 @@ class XlsxSplitHandle(BaseParseTableHandle):
|
|||
# 如果未指定 sheet_name,则使用第一个工作表
|
||||
for sheetname in workbook.sheetnames:
|
||||
sheet = workbook[sheetname] if sheetname else workbook.active
|
||||
rows = self.fill_merged_cells(sheet, image_dict)
|
||||
if len(rows) == 0:
|
||||
data = self.fill_merged_cells(sheet, image_dict)
|
||||
if len(data) == 0:
|
||||
continue
|
||||
# 提取表头和内容
|
||||
|
||||
headers = [f"{key}" for key, value in rows[0].items()]
|
||||
headers = [f"{value}" for value in data[0]]
|
||||
|
||||
# 构建 Markdown 表格
|
||||
md_table = '| ' + ' | '.join(headers) + ' |\n'
|
||||
md_table += '| ' + ' | '.join(['---'] * len(headers)) + ' |\n'
|
||||
for row in rows:
|
||||
r = [f'{value}' for key, value in row.items()]
|
||||
for row_index in range(1, len(data)):
|
||||
r = [f'{value}' for value in data[row_index]]
|
||||
md_table += '| ' + ' | '.join(
|
||||
[str(cell).replace('\n', '<br>') if cell is not None else '' for cell in r]) + ' |\n'
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from common.handle.base_split_handle import BaseSplitHandle
|
|||
|
||||
|
||||
def post_cell(cell_value):
|
||||
return cell_value.replace('\n', '<br>').replace('|', '|')
|
||||
return cell_value.replace('\r\n', '<br>').replace('\n', '<br>').replace('|', '|')
|
||||
|
||||
|
||||
def row_to_md(row):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import zipfile
|
|||
from typing import List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from charset_normalizer import detect
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from common.handle.base_split_handle import BaseSplitHandle
|
||||
|
|
@ -28,6 +29,7 @@ from common.util.common import parse_md_image
|
|||
from dataset.models import Image
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class FileBufferHandle:
|
||||
buffer = None
|
||||
|
||||
|
|
@ -75,6 +77,7 @@ def get_image_list(result_list: list, zip_files: List[str]):
|
|||
if search:
|
||||
new_image_id = str(uuid.uuid1())
|
||||
source_image_path = search.group().replace('(', '').replace(')', '')
|
||||
source_image_path = source_image_path.strip().split(" ")[0]
|
||||
image_path = urljoin(result.get('name'), '.' + source_image_path if source_image_path.startswith(
|
||||
'/') else source_image_path)
|
||||
if not zip_files.__contains__(image_path):
|
||||
|
|
@ -98,6 +101,15 @@ def get_image_list(result_list: list, zip_files: List[str]):
|
|||
return image_file_list
|
||||
|
||||
|
||||
def get_file_name(file_name):
|
||||
try:
|
||||
file_name_code = file_name.encode('cp437')
|
||||
charset = detect(file_name_code)['encoding']
|
||||
return file_name_code.decode(charset)
|
||||
except Exception as e:
|
||||
return file_name
|
||||
|
||||
|
||||
def filter_image_file(result_list: list, image_list):
|
||||
image_source_file_list = [image.get('source_file') for image in image_list]
|
||||
return [r for r in result_list if not image_source_file_list.__contains__(r.get('name', ''))]
|
||||
|
|
@ -114,11 +126,13 @@ class ZipSplitHandle(BaseSplitHandle):
|
|||
files = zip_ref.namelist()
|
||||
# 读取压缩包中的文件内容
|
||||
for file in files:
|
||||
if file.endswith('/'):
|
||||
if file.endswith('/') or file.startswith('__MACOSX'):
|
||||
continue
|
||||
with zip_ref.open(file) as f:
|
||||
# 对文件内容进行处理
|
||||
try:
|
||||
# 处理一下文件名
|
||||
f.name = get_file_name(f.name)
|
||||
value = file_to_paragraph(f, pattern_list, with_filter, limit)
|
||||
if isinstance(value, list):
|
||||
result = [*result, *value]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: log.py
|
||||
@date:2025/3/14 16:09
|
||||
@desc:
|
||||
"""
|
||||
from gettext import gettext
|
||||
|
||||
from setting.models.log_management import Log
|
||||
|
||||
|
||||
def _get_ip_address(request):
|
||||
"""
|
||||
获取ip地址
|
||||
@param request:
|
||||
@return:
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
|
||||
def _get_user(request):
|
||||
"""
|
||||
获取用户
|
||||
@param request:
|
||||
@return:
|
||||
"""
|
||||
user = request.user
|
||||
if user is None:
|
||||
return {
|
||||
|
||||
}
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"nick_name": user.nick_name,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
|
||||
def _get_details(request):
|
||||
path = request.path
|
||||
body = request.data
|
||||
query = request.query_params
|
||||
return {
|
||||
'path': path,
|
||||
'body': body,
|
||||
'query': query
|
||||
}
|
||||
|
||||
|
||||
def log(menu: str, operate, get_user=_get_user, get_ip_address=_get_ip_address, get_details=_get_details,
|
||||
get_operation_object=None):
|
||||
"""
|
||||
记录审计日志
|
||||
@param menu: 操作菜单 str
|
||||
@param operate: 操作 str|func 如果是一个函数 入参将是一个request 响应为str def operate(request): return "操作菜单"
|
||||
@param get_user: 获取用户
|
||||
@param get_ip_address:获取IP地址
|
||||
@param get_details: 获取执行详情
|
||||
@param get_operation_object: 获取操作对象
|
||||
@return:
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
def run(view, request, **kwargs):
|
||||
status = 200
|
||||
operation_object = {}
|
||||
try:
|
||||
if get_operation_object is not None:
|
||||
operation_object = get_operation_object(request, kwargs)
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
return func(view, request, **kwargs)
|
||||
except Exception as e:
|
||||
status = 500
|
||||
raise e
|
||||
finally:
|
||||
ip = get_ip_address(request)
|
||||
user = get_user(request)
|
||||
details = get_details(request)
|
||||
_operate = operate
|
||||
if callable(operate):
|
||||
_operate = operate(request)
|
||||
# 插入审计日志
|
||||
Log(menu=menu, operate=_operate, user=user, status=status, ip_address=ip, details=details,
|
||||
operation_object=operation_object).save()
|
||||
|
||||
return run
|
||||
|
||||
return inner
|
||||
|
|
@ -24,12 +24,13 @@ class GunicornLocalModelService(BaseService):
|
|||
os.environ.setdefault('SERVER_NAME', 'local_model')
|
||||
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
|
||||
bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}'
|
||||
worker = CONFIG.get("LOCAL_MODEL_HOST_WORKER", 1)
|
||||
cmd = [
|
||||
'gunicorn', 'smartdoc.wsgi:application',
|
||||
'-b', bind,
|
||||
'-k', 'gthread',
|
||||
'--threads', '200',
|
||||
'-w', "1",
|
||||
'-w', str(worker),
|
||||
'--max-requests', '10240',
|
||||
'--max-requests-jitter', '2048',
|
||||
'--access-logformat', log_format,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: static_headers_middleware.py
|
||||
@date:2024/3/13 18:26
|
||||
@desc:
|
||||
"""
|
||||
from django.http import HttpResponse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
content = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', '/api/user', true)
|
||||
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
const token = localStorage.getItem('token')
|
||||
const pathname = window.location.pathname
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', token)
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
window.location.href = pathname
|
||||
}
|
||||
if (xhr.status === 401) {
|
||||
window.location.href = '/ui/login'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
} else {
|
||||
window.location.href = '/ui/login'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class DocHeadersMiddleware(MiddlewareMixin):
|
||||
def process_response(self, request, response):
|
||||
if request.path.startswith('/doc/') or request.path.startswith('/doc/chat/'):
|
||||
HTTP_REFERER = request.META.get('HTTP_REFERER')
|
||||
if HTTP_REFERER is None:
|
||||
return HttpResponse(content)
|
||||
if HTTP_REFERER == request._current_scheme_host + request.path:
|
||||
return response
|
||||
return response
|
||||
|
|
@ -10,6 +10,8 @@ import hashlib
|
|||
import importlib
|
||||
import io
|
||||
import mimetypes
|
||||
import pickle
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
from functools import reduce
|
||||
|
|
@ -23,6 +25,51 @@ from pydub import AudioSegment
|
|||
from ..exception.app_exception import AppApiException
|
||||
from ..models.db_model_manage import DBModelManage
|
||||
|
||||
safe_builtins = {
|
||||
'MKInstance'
|
||||
}
|
||||
|
||||
ALLOWED_CLASSES = {
|
||||
("builtins", "dict"),
|
||||
('uuid', 'UUID'),
|
||||
("application.serializers.application_serializers", "MKInstance"),
|
||||
("function_lib.serializers.function_lib_serializer", "FlibInstance")
|
||||
}
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
|
||||
def find_class(self, module, name):
|
||||
if (module, name) in ALLOWED_CLASSES:
|
||||
return super().find_class(module, name)
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
(module, name))
|
||||
|
||||
|
||||
def restricted_loads(s):
|
||||
"""Helper function analogous to pickle.loads()."""
|
||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||
|
||||
|
||||
def encryption(message: str):
|
||||
"""
|
||||
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890
|
||||
:param message:
|
||||
:return:
|
||||
"""
|
||||
max_pre_len = 8
|
||||
max_post_len = 4
|
||||
message_len = len(message)
|
||||
pre_len = int(message_len / 5 * 2)
|
||||
post_len = int(message_len / 5 * 1)
|
||||
pre_str = "".join([message[index] for index in
|
||||
range(0, max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(pre_len))])
|
||||
end_str = "".join(
|
||||
[message[index] for index in
|
||||
range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len), message_len)])
|
||||
content = "***************"
|
||||
return pre_str + content + end_str
|
||||
|
||||
|
||||
def sub_array(array: List, item_num=10):
|
||||
result = []
|
||||
|
|
@ -251,3 +298,14 @@ def markdown_to_plain_text(md: str) -> str:
|
|||
# 去除首尾空格
|
||||
text = text.strip()
|
||||
return text
|
||||
|
||||
|
||||
SAFE_CHAR_SET = (
|
||||
[chr(i) for i in range(65, 91) if chr(i) not in {'I', 'O'}] + # 大写字母 A-H, J-N, P-Z
|
||||
[chr(i) for i in range(97, 123) if chr(i) not in {'i', 'l', 'o'}] + # 小写字母 a-h, j-n, p-z
|
||||
[str(i) for i in range(10) if str(i) not in {'0', '1', '7'}] # 数字 2-6, 8-9
|
||||
)
|
||||
|
||||
|
||||
def get_random_chars(number=4):
|
||||
return ''.join(random.choices(SAFE_CHAR_SET, k=number))
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@
|
|||
@desc:
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from textwrap import dedent
|
||||
|
||||
from diskcache import Cache
|
||||
|
||||
from smartdoc.const import BASE_DIR
|
||||
from smartdoc.const import PROJECT_DIR
|
||||
|
||||
|
|
@ -37,6 +36,8 @@ class FunctionExecutor:
|
|||
old_mask = os.umask(0o077)
|
||||
try:
|
||||
os.makedirs(self.sandbox_path, 0o700, exist_ok=True)
|
||||
os.makedirs(os.path.join(self.sandbox_path, 'execute'), 0o700, exist_ok=True)
|
||||
os.makedirs(os.path.join(self.sandbox_path, 'result'), 0o700, exist_ok=True)
|
||||
finally:
|
||||
os.umask(old_mask)
|
||||
|
||||
|
|
@ -44,10 +45,11 @@ class FunctionExecutor:
|
|||
_id = str(uuid.uuid1())
|
||||
success = '{"code":200,"msg":"成功","data":exec_result}'
|
||||
err = '{"code":500,"msg":str(e),"data":None}'
|
||||
path = r'' + self.sandbox_path + ''
|
||||
result_path = f'{self.sandbox_path}/result/{_id}.result'
|
||||
_exec_code = f"""
|
||||
try:
|
||||
import os
|
||||
import pickle
|
||||
env = dict(os.environ)
|
||||
for key in list(env.keys()):
|
||||
if key in os.environ and (key.startswith('MAXKB') or key.startswith('POSTGRES') or key.startswith('PG')):
|
||||
|
|
@ -60,13 +62,11 @@ try:
|
|||
for local in locals_v:
|
||||
globals_v[local] = locals_v[local]
|
||||
exec_result=f(**keywords)
|
||||
from diskcache import Cache
|
||||
cache = Cache({path!a})
|
||||
cache.set({_id!a},{success})
|
||||
with open({result_path!a}, 'wb') as file:
|
||||
file.write(pickle.dumps({success}))
|
||||
except Exception as e:
|
||||
from diskcache import Cache
|
||||
cache = Cache({path!a})
|
||||
cache.set({_id!a},{err})
|
||||
with open({result_path!a}, 'wb') as file:
|
||||
file.write(pickle.dumps({err}))
|
||||
"""
|
||||
if self.sandbox:
|
||||
subprocess_result = self._exec_sandbox(_exec_code, _id)
|
||||
|
|
@ -74,21 +74,21 @@ except Exception as e:
|
|||
subprocess_result = self._exec(_exec_code)
|
||||
if subprocess_result.returncode == 1:
|
||||
raise Exception(subprocess_result.stderr)
|
||||
cache = Cache(self.sandbox_path)
|
||||
result = cache.get(_id)
|
||||
cache.delete(_id)
|
||||
with open(result_path, 'rb') as file:
|
||||
result = pickle.loads(file.read())
|
||||
os.remove(result_path)
|
||||
if result.get('code') == 200:
|
||||
return result.get('data')
|
||||
raise Exception(result.get('msg'))
|
||||
|
||||
def _exec_sandbox(self, _code, _id):
|
||||
exec_python_file = f'{self.sandbox_path}/{_id}.py'
|
||||
exec_python_file = f'{self.sandbox_path}/execute/{_id}.py'
|
||||
with open(exec_python_file, 'w') as file:
|
||||
file.write(_code)
|
||||
os.system(f"chown {self.user}:{self.user} {exec_python_file}")
|
||||
os.system(f"chown {self.user}:root {exec_python_file}")
|
||||
kwargs = {'cwd': BASE_DIR}
|
||||
subprocess_result = subprocess.run(
|
||||
['su', '-c', python_directory + ' ' + exec_python_file, self.user],
|
||||
['su', '-s', python_directory, '-c', "exec(open('" + exec_python_file + "').read())", self.user],
|
||||
text=True,
|
||||
capture_output=True, **kwargs)
|
||||
os.remove(exec_python_file)
|
||||
|
|
|
|||
|
|
@ -40,15 +40,12 @@ def generate():
|
|||
def get_key_pair():
|
||||
rsa_value = rsa_cache.get(cache_key)
|
||||
if rsa_value is None:
|
||||
lock.acquire()
|
||||
rsa_value = rsa_cache.get(cache_key)
|
||||
if rsa_value is not None:
|
||||
return rsa_value
|
||||
try:
|
||||
with lock:
|
||||
rsa_value = rsa_cache.get(cache_key)
|
||||
if rsa_value is not None:
|
||||
return rsa_value
|
||||
rsa_value = get_key_pair_by_sql()
|
||||
rsa_cache.set(cache_key, rsa_value)
|
||||
finally:
|
||||
lock.release()
|
||||
return rsa_value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -339,13 +339,14 @@ class SplitModel:
|
|||
for e in result:
|
||||
if len(e['content']) > 4096:
|
||||
pass
|
||||
return [item for item in [self.post_reset_paragraph(row) for row in result] if
|
||||
title_list = list(set([row.get('title') for row in result]))
|
||||
return [item for item in [self.post_reset_paragraph(row, title_list) for row in result] if
|
||||
'content' in item and len(item.get('content').strip()) > 0]
|
||||
|
||||
def post_reset_paragraph(self, paragraph: Dict):
|
||||
result = self.filter_title_special_characters(paragraph)
|
||||
def post_reset_paragraph(self, paragraph: Dict, title_list: List[str]):
|
||||
result = self.content_is_null(paragraph, title_list)
|
||||
result = self.filter_title_special_characters(result)
|
||||
result = self.sub_title(result)
|
||||
result = self.content_is_null(result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -357,11 +358,14 @@ class SplitModel:
|
|||
return paragraph
|
||||
|
||||
@staticmethod
|
||||
def content_is_null(paragraph: Dict):
|
||||
def content_is_null(paragraph: Dict, title_list: List[str]):
|
||||
if 'title' in paragraph:
|
||||
title = paragraph.get('title')
|
||||
content = paragraph.get('content')
|
||||
if (content is None or len(content.strip()) == 0) and (title is not None and len(title) > 0):
|
||||
find = [t for t in title_list if t.__contains__(title) and t != title]
|
||||
if find:
|
||||
return {'title': '', 'content': ''}
|
||||
return {'title': '', 'content': title}
|
||||
return paragraph
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ from typing import List
|
|||
|
||||
import jieba
|
||||
import jieba.posseg
|
||||
from jieba import analyse
|
||||
|
||||
from common.util.split_model import group_by
|
||||
|
||||
jieba_word_list_cache = [chr(item) for item in range(38, 84)]
|
||||
|
||||
|
|
@ -80,37 +77,12 @@ def get_key_by_word_dict(key, word_dict):
|
|||
|
||||
|
||||
def to_ts_vector(text: str):
|
||||
# 获取不分词的数据
|
||||
word_list = get_word_list(text)
|
||||
# 获取关键词关系
|
||||
word_dict = to_word_dict(word_list, text)
|
||||
# 替换字符串
|
||||
text = replace_word(word_dict, text)
|
||||
# 分词
|
||||
filter_word = jieba.analyse.extract_tags(text, topK=100)
|
||||
result = jieba.lcut(text, HMM=True, use_paddle=True)
|
||||
# 过滤标点符号
|
||||
result = [item for item in result if filter_word.__contains__(item) and len(item) < 10]
|
||||
result_ = [{'word': get_key_by_word_dict(result[index], word_dict), 'index': index} for index in
|
||||
range(len(result))]
|
||||
result_group = group_by(result_, lambda r: r['word'])
|
||||
return " ".join(
|
||||
[f"{key.lower()}:{','.join([str(item['index'] + 1) for item in result_group[key]][:20])}" for key in
|
||||
result_group if
|
||||
not remove_chars.__contains__(key) and len(key.strip()) >= 0])
|
||||
result = jieba.lcut(text, cut_all=True)
|
||||
return " ".join(result)
|
||||
|
||||
|
||||
def to_query(text: str):
|
||||
# 获取不分词的数据
|
||||
word_list = get_word_list(text)
|
||||
# 获取关键词关系
|
||||
word_dict = to_word_dict(word_list, text)
|
||||
# 替换字符串
|
||||
text = replace_word(word_dict, text)
|
||||
extract_tags = analyse.extract_tags(text, topK=5, withWeight=True, allowPOS=('ns', 'n', 'vn', 'v', 'eng'))
|
||||
result = " ".join([get_key_by_word_dict(word, word_dict) for word, score in extract_tags if
|
||||
not remove_chars.__contains__(word)])
|
||||
# 删除词库
|
||||
for word in word_list:
|
||||
jieba.del_word(word)
|
||||
extract_tags = jieba.lcut(text, cut_all=True)
|
||||
result = " ".join(extract_tags)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ class Type(models.TextChoices):
|
|||
|
||||
web = 1, 'web站点类型'
|
||||
|
||||
lark = 2, '飞书类型'
|
||||
yuque = 3, '语雀类型'
|
||||
|
||||
|
||||
class HitHandlingMethod(models.TextChoices):
|
||||
optimization = 'optimization', '模型优化'
|
||||
|
|
|
|||
|
|
@ -40,6 +40,14 @@ def zip_dir(zip_path, output=None):
|
|||
zip.close()
|
||||
|
||||
|
||||
def is_valid_uuid(s):
|
||||
try:
|
||||
uuid.UUID(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def write_image(zip_path: str, image_list: List[str]):
|
||||
for image in image_list:
|
||||
search = re.search("\(.*\)", image)
|
||||
|
|
@ -47,6 +55,9 @@ def write_image(zip_path: str, image_list: List[str]):
|
|||
text = search.group()
|
||||
if text.startswith('(/api/file/'):
|
||||
r = text.replace('(/api/file/', '').replace(')', '')
|
||||
r = r.strip().split(" ")[0]
|
||||
if not is_valid_uuid(r):
|
||||
break
|
||||
file = QuerySet(File).filter(id=r).first()
|
||||
if file is None:
|
||||
break
|
||||
|
|
@ -58,6 +69,9 @@ def write_image(zip_path: str, image_list: List[str]):
|
|||
f.write(file.get_byte())
|
||||
else:
|
||||
r = text.replace('(/api/image/', '').replace(')', '')
|
||||
r = r.strip().split(" ")[0]
|
||||
if not is_valid_uuid(r):
|
||||
break
|
||||
image_model = QuerySet(Image).filter(id=r).first()
|
||||
if image_model is None:
|
||||
break
|
||||
|
|
@ -208,3 +222,26 @@ def get_embedding_model_id_by_dataset_id_list(dataset_id_list: List):
|
|||
if len(dataset_list) == 0:
|
||||
raise Exception(_('Knowledge base setting error, please reset the knowledge base'))
|
||||
return str(dataset_list[0].embedding_mode_id)
|
||||
|
||||
|
||||
class GenerateRelatedSerializer(ApiMixin, serializers.Serializer):
|
||||
model_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('Model id')))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.uuid(_('Prompt word')))
|
||||
state_list = serializers.ListField(required=False, child=serializers.CharField(required=True),
|
||||
error_messages=ErrMessage.list("state list"))
|
||||
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'model_id': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title=_('Model id'),
|
||||
description=_('Model id')),
|
||||
'prompt': openapi.Schema(type=openapi.TYPE_STRING, title=_('Prompt word'),
|
||||
description=_("Prompt word")),
|
||||
'state_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||
title=_('state list'))
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from django.contrib.postgres.fields import ArrayField
|
|||
from django.core import validators
|
||||
from django.db import transaction, models
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.functions import Reverse, Substr
|
||||
from django.http import HttpResponse
|
||||
from drf_yasg import openapi
|
||||
from rest_framework import serializers
|
||||
|
|
@ -42,9 +43,10 @@ from common.util.split_model import get_split_model
|
|||
from dataset.models.data_set import DataSet, Document, Paragraph, Problem, Type, ProblemParagraphMapping, TaskType, \
|
||||
State, File, Image
|
||||
from dataset.serializers.common_serializers import list_paragraph, MetaSerializer, ProblemParagraphManage, \
|
||||
get_embedding_model_by_dataset_id, get_embedding_model_id_by_dataset_id, write_image, zip_dir
|
||||
get_embedding_model_by_dataset_id, get_embedding_model_id_by_dataset_id, write_image, zip_dir, \
|
||||
GenerateRelatedSerializer
|
||||
from dataset.serializers.document_serializers import DocumentSerializers, DocumentInstanceSerializer
|
||||
from dataset.task import sync_web_dataset, sync_replace_web_dataset
|
||||
from dataset.task import sync_web_dataset, sync_replace_web_dataset, generate_related_by_dataset_id
|
||||
from embedding.models import SearchMode
|
||||
from embedding.task import embedding_by_dataset, delete_embedding_by_dataset
|
||||
from setting.models import AuthOperate, Model
|
||||
|
|
@ -453,11 +455,13 @@ class DataSetSerializers(serializers.ModelSerializer):
|
|||
# 批量插入关联问题
|
||||
QuerySet(ProblemParagraphMapping).bulk_create(problem_paragraph_mapping_list) if len(
|
||||
problem_paragraph_mapping_list) > 0 else None
|
||||
|
||||
# 响应数据
|
||||
return {**DataSetSerializers(dataset).data,
|
||||
'document_list': DocumentSerializers.Query(data={'dataset_id': dataset_id}).list(
|
||||
with_valid=True)}, dataset_id
|
||||
'user_id': user_id,
|
||||
'document_list': document_model_list,
|
||||
"document_count": len(document_model_list),
|
||||
"char_length": reduce(lambda x, y: x + y, [d.char_length for d in document_model_list],
|
||||
0)}, dataset_id
|
||||
|
||||
@staticmethod
|
||||
def get_last_url_path(url):
|
||||
|
|
@ -814,6 +818,31 @@ class DataSetSerializers(serializers.ModelSerializer):
|
|||
except AlreadyQueued as e:
|
||||
raise AppApiException(500, _('Failed to send the vectorization task, please try again later!'))
|
||||
|
||||
def generate_related(self, instance: Dict, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
GenerateRelatedSerializer(data=instance).is_valid(raise_exception=True)
|
||||
dataset_id = self.data.get('id')
|
||||
model_id = instance.get("model_id")
|
||||
prompt = instance.get("prompt")
|
||||
state_list = instance.get('state_list')
|
||||
ListenerManagement.update_status(QuerySet(Document).filter(dataset_id=dataset_id),
|
||||
TaskType.GENERATE_PROBLEM,
|
||||
State.PENDING)
|
||||
ListenerManagement.update_status(QuerySet(Paragraph).annotate(
|
||||
reversed_status=Reverse('status'),
|
||||
task_type_status=Substr('reversed_status', TaskType.GENERATE_PROBLEM.value,
|
||||
1),
|
||||
).filter(task_type_status__in=state_list, dataset_id=dataset_id)
|
||||
.values('id'),
|
||||
TaskType.GENERATE_PROBLEM,
|
||||
State.PENDING)
|
||||
ListenerManagement.get_aggregation_document_status_by_dataset_id(dataset_id)()
|
||||
try:
|
||||
generate_related_by_dataset_id.delay(dataset_id, model_id, prompt, state_list)
|
||||
except AlreadyQueued as e:
|
||||
raise AppApiException(500, _('Failed to send the vectorization task, please try again later!'))
|
||||
|
||||
def list_application(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ from django.db import transaction, models
|
|||
from django.db.models import QuerySet, Count
|
||||
from django.db.models.functions import Substr, Reverse
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import gettext_lazy as _, gettext, to_locale
|
||||
from drf_yasg import openapi
|
||||
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
|
||||
from rest_framework import serializers
|
||||
|
|
@ -64,8 +66,6 @@ from embedding.task.embedding import embedding_by_document, delete_embedding_by_
|
|||
embedding_by_document_list
|
||||
from setting.models import Model
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
from django.utils.translation import gettext_lazy as _, gettext, to_locale
|
||||
from django.utils.translation import get_language
|
||||
|
||||
parse_qa_handle_list = [XlsParseQAHandle(), CsvParseQAHandle(), XlsxParseQAHandle(), ZipParseQAHandle()]
|
||||
parse_table_handle_list = [CsvSplitTableHandle(), XlsSplitTableHandle(), XlsxSplitTableHandle()]
|
||||
|
|
@ -661,6 +661,8 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
|
|||
cell = worksheet.cell(row=row_idx + 1, column=col_idx + 1)
|
||||
if isinstance(col, str):
|
||||
col = re.sub(ILLEGAL_CHARACTERS_RE, '', col)
|
||||
if col.startswith(('=', '+', '-', '@')):
|
||||
col = '\ufeff' + col
|
||||
cell.value = col
|
||||
# 创建HttpResponse对象返回Excel文件
|
||||
return workbook
|
||||
|
|
@ -703,6 +705,8 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
|
|||
|
||||
@staticmethod
|
||||
def reset_document_name(document_name):
|
||||
if document_name is not None:
|
||||
document_name = document_name.strip()[0:29]
|
||||
if document_name is None or not Utils.valid_sheet_name(document_name):
|
||||
return "Sheet"
|
||||
return document_name.strip()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ mime_types = {"html": "text/html", "htm": "text/html", "shtml": "text/html", "cs
|
|||
"woff2": "font/woff2", "jar": "application/java-archive", "war": "application/java-archive",
|
||||
"ear": "application/java-archive", "json": "application/json", "hqx": "application/mac-binhex40",
|
||||
"doc": "application/msword", "pdf": "application/pdf", "ps": "application/postscript",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"eps": "application/postscript", "ai": "application/postscript", "rtf": "application/rtf",
|
||||
"m3u8": "application/vnd.apple.mpegurl", "kml": "application/vnd.google-earth.kml+xml",
|
||||
"kmz": "application/vnd.google-earth.kmz", "xls": "application/vnd.ms-excel",
|
||||
|
|
@ -87,4 +90,4 @@ class FileSerializer(serializers.Serializer):
|
|||
'Content-Disposition': 'attachment; filename="{}"'.format(
|
||||
file.file_name)})
|
||||
return HttpResponse(file.get_byte(), status=200,
|
||||
headers={'Content-Type': mime_types.get(file.file_name.split(".")[-1], 'text/plain')})
|
||||
headers={'Content-Type': mime_types.get(file_type, 'text/plain')})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ UPDATE "document"
|
|||
SET "char_length" = ( SELECT CASE WHEN
|
||||
"sum" ( "char_length" ( "content" ) ) IS NULL THEN
|
||||
0 ELSE "sum" ( "char_length" ( "content" ) )
|
||||
END FROM paragraph WHERE "document_id" = %s )
|
||||
END FROM paragraph WHERE "document_id" = %s ),
|
||||
"update_time" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
"id" = %s
|
||||
|
|
@ -64,6 +64,17 @@ def get_is_the_task_interrupted(document_id):
|
|||
return is_the_task_interrupted
|
||||
|
||||
|
||||
@celery_app.task(base=QueueOnce, once={'keys': ['dataset_id']},
|
||||
name='celery:generate_related_by_dataset')
|
||||
def generate_related_by_dataset_id(dataset_id, model_id, prompt, state_list=None):
|
||||
document_list = QuerySet(Document).filter(dataset_id=dataset_id)
|
||||
for document in document_list:
|
||||
try:
|
||||
generate_related_by_document_id.delay(document.id, model_id, prompt, state_list)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
@celery_app.task(base=QueueOnce, once={'keys': ['document_id']},
|
||||
name='celery:generate_related_by_document')
|
||||
def generate_related_by_document_id(document_id, model_id, prompt, state_list=None):
|
||||
|
|
|
|||
|
|
@ -54,5 +54,8 @@ def sync_replace_web_dataset(dataset_id: str, url: str, selector: str):
|
|||
def sync_web_document(dataset_id, source_url_list: List[str], selector: str):
|
||||
handler = get_sync_web_document_handler(dataset_id)
|
||||
for source_url in source_url_list:
|
||||
result = Fork(base_fork_url=source_url, selector_list=selector.split(' ')).fork()
|
||||
handler(source_url, selector, result)
|
||||
try:
|
||||
result = Fork(base_fork_url=source_url, selector_list=selector.split(' ')).fork()
|
||||
handler(source_url, selector, result)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Section title (optional), Section content (required,question answer,no more than 4096 characters), Question (optional,one per line in the cell)
|
||||
Section title (optional), Section content (required,question answer), Question (optional,one per line in the cell)
|
||||
MaxKB product introduction,"MaxKB is a knowledge base question-answering system based on the LLM large language model. MaxKB = Max Knowledge Base,aims to become the most powerful brain of the enterprise。Out-of-the-box: supports direct document upload、automatic crawling of online documents、automatic text splitting and vectorization、and good intelligent question-answering interactive experience;Seamless embedding: supports zero-coding and rapid embedding into third-party business systems;Multi-model support: supports docking with mainstream large models,including Ollama local private large models (such as Llama 2、Llama 3、qwen)、Tongyi Qianwen、OpenAI、Azure OpenAI、Kimi、Zhipu AI、iFlytek Spark and Baidu Qianfan large models、etc.","What is MaxKB?
|
||||
MaxKB product introduction
|
||||
Large language model supported by MaxKB
|
||||
|
|
|
|||
|
|
|
@ -1,4 +1,4 @@
|
|||
分段标题(选填),分段内容(必填,问题答案,最长不超过4096个字符)),问题(选填,单元格内一行一个)
|
||||
分段标题(选填),分段内容(必填,问题答案)),问题(选填,单元格内一行一个)
|
||||
MaxKB产品介绍,"MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB = Max Knowledge Base,旨在成为企业的最强大脑。
|
||||
开箱即用:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好;
|
||||
无缝嵌入:支持零编码快速嵌入到第三方业务系统;
|
||||
|
|
|
|||
|
|
|
@ -1,4 +1,4 @@
|
|||
分段標題(選填),分段內容(必填,問題答案,最長不超過4096個字元)),問題(選填,單元格內一行一個)
|
||||
分段標題(選填),分段內容(必填,問題答案)),問題(選填,單元格內一行一個)
|
||||
MaxKB產品介紹,"MaxKB 是一款基於 LLM 大語言模型的知識庫問答系統。MaxKB = Max Knowledge Base,旨在成為企業的最強大大腦。
|
||||
開箱即用:支援直接上傳文檔、自動爬取線上文檔,支援文字自動分割、向量化,智慧問答互動體驗好;
|
||||
無縫嵌入:支援零編碼快速嵌入到第三方業務系統;
|
||||
|
|
|
|||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -11,6 +11,8 @@ urlpatterns = [
|
|||
path('dataset/<str:dataset_id>/export', views.Dataset.Export.as_view(), name="export"),
|
||||
path('dataset/<str:dataset_id>/export_zip', views.Dataset.ExportZip.as_view(), name="export_zip"),
|
||||
path('dataset/<str:dataset_id>/re_embedding', views.Dataset.Embedding.as_view(), name="dataset_key"),
|
||||
path('dataset/<str:dataset_id>/generate_related', views.Dataset.GenerateRelated.as_view(),
|
||||
name="dataset_generate_related"),
|
||||
path('dataset/<str:dataset_id>/application', views.Dataset.Application.as_view()),
|
||||
path('dataset/<int:current_page>/<int:page_size>', views.Dataset.Page.as_view(), name="dataset"),
|
||||
path('dataset/<str:dataset_id>/sync_web', views.Dataset.SyncWeb.as_view()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: common.py.py
|
||||
@date:2025/3/25 15:43
|
||||
@desc:
|
||||
"""
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from dataset.models import DataSet, Document
|
||||
|
||||
|
||||
def get_dataset_operation_object(dataset_id: str):
|
||||
dataset_model = QuerySet(model=DataSet).filter(id=dataset_id).first()
|
||||
if dataset_model is not None:
|
||||
return {
|
||||
"name": dataset_model.name,
|
||||
"desc": dataset_model.desc,
|
||||
"type": dataset_model.type,
|
||||
"create_time": dataset_model.create_time,
|
||||
"update_time": dataset_model.update_time
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def get_document_operation_object(document_id: str):
|
||||
document_model = QuerySet(model=Document).filter(id=document_id).first()
|
||||
if document_model is not None:
|
||||
return {
|
||||
"name": document_model.name,
|
||||
"type": document_model.type,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def get_document_operation_object_batch(document_id_list: str):
|
||||
document_model_list = QuerySet(model=Document).filter(id__in=document_id_list)
|
||||
if document_model_list is not None:
|
||||
return {
|
||||
"name": f'[{",".join([document_model.name for document_model in document_model_list])}]',
|
||||
'document_list': [{'name': document_model.name, 'type': document_model.type} for document_model in
|
||||
document_model_list]
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def get_dataset_document_operation_object(dataset_dict: dict, document_dict: dict):
|
||||
return {
|
||||
'name': f'{dataset_dict.get("name", "")}/{document_dict.get("name", "")}',
|
||||
'dataset_name': dataset_dict.get("name", ""),
|
||||
'dataset_desc': dataset_dict.get("desc", ""),
|
||||
'dataset_type': dataset_dict.get("type", ""),
|
||||
'document_name': document_dict.get("name", ""),
|
||||
'document_type': document_dict.get("type", ""),
|
||||
}
|
||||
|
|
@ -13,13 +13,17 @@ from rest_framework.parsers import MultiPartParser
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.views import Request
|
||||
|
||||
import dataset.models
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import PermissionConstants, CompareConstants, Permission, Group, Operate, \
|
||||
ViewPermission, RoleConstants
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.response.result import get_page_request_params, get_page_api_response, get_api_response
|
||||
from common.swagger_api.common_api import CommonApi
|
||||
from dataset.serializers.common_serializers import GenerateRelatedSerializer
|
||||
from dataset.serializers.dataset_serializers import DataSetSerializers
|
||||
from dataset.views.common import get_dataset_operation_object
|
||||
from setting.serializers.provider_serializers import ModelSerializer
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
|
@ -31,8 +35,8 @@ class Dataset(APIView):
|
|||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="同步Web站点知识库",
|
||||
operation_id="同步Web站点知识库",
|
||||
@swagger_auto_schema(operation_summary=_("Synchronize the knowledge base of the website"),
|
||||
operation_id=_("Synchronize the knowledge base of the website"),
|
||||
manual_parameters=DataSetSerializers.SyncWeb.get_request_params_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=[_('Knowledge Base')])
|
||||
|
|
@ -42,6 +46,8 @@ class Dataset(APIView):
|
|||
dynamic_tag=keywords.get('dataset_id'))],
|
||||
compare=CompareConstants.AND), PermissionConstants.DATASET_EDIT,
|
||||
compare=CompareConstants.AND)
|
||||
@log(menu='Knowledge Base', operate="Synchronize the knowledge base of the website",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(DataSetSerializers.SyncWeb(
|
||||
data={'sync_type': request.query_params.get('sync_type'), 'id': dataset_id,
|
||||
|
|
@ -52,14 +58,17 @@ class Dataset(APIView):
|
|||
parser_classes = [MultiPartParser]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="创建QA知识库",
|
||||
operation_id="创建QA知识库",
|
||||
@swagger_auto_schema(operation_summary=_("Create QA knowledge base"),
|
||||
operation_id=_("Create QA knowledge base"),
|
||||
manual_parameters=DataSetSerializers.Create.CreateQASerializers.get_request_params_api(),
|
||||
responses=get_api_response(
|
||||
DataSetSerializers.Create.CreateQASerializers.get_response_body_api()),
|
||||
tags=[_('Knowledge Base')]
|
||||
)
|
||||
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
|
||||
@log(menu='Knowledge Base', operate="Create QA knowledge base",
|
||||
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc'),
|
||||
'file_list': r.FILES.getlist('file')})
|
||||
def post(self, request: Request):
|
||||
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save_qa({
|
||||
'file_list': request.FILES.getlist('file'),
|
||||
|
|
@ -79,6 +88,13 @@ class Dataset(APIView):
|
|||
tags=[_('Knowledge Base')]
|
||||
)
|
||||
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
|
||||
@log(menu='Knowledge Base', operate="Create a web site knowledge base",
|
||||
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc'),
|
||||
'file_list': r.FILES.getlist('file'),
|
||||
'meta': {'source_url': r.data.get('source_url'),
|
||||
'selector': r.data.get('selector'),
|
||||
'embedding_mode_id': r.data.get('embedding_mode_id')}}
|
||||
)
|
||||
def post(self, request: Request):
|
||||
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save_web(request.data))
|
||||
|
||||
|
|
@ -117,6 +133,8 @@ class Dataset(APIView):
|
|||
tags=[_('Knowledge Base')]
|
||||
)
|
||||
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
|
||||
@log(menu='Knowledge Base', operate="Create a knowledge base",
|
||||
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc')})
|
||||
def post(self, request: Request):
|
||||
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save(request.data))
|
||||
|
||||
|
|
@ -150,10 +168,30 @@ class Dataset(APIView):
|
|||
)
|
||||
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('dataset_id')))
|
||||
@log(menu='Knowledge Base', operate="Re-vectorize",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).re_embedding())
|
||||
|
||||
class GenerateRelated(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary=_('Generate related'), operation_id=_('Generate related'),
|
||||
manual_parameters=DataSetSerializers.Operate.get_request_params_api(),
|
||||
request_body=GenerateRelatedSerializer.get_request_body_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=[_('Knowledge Base')]
|
||||
)
|
||||
@log(menu='document', operate="Generate related documents",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id'))
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).generate_related(
|
||||
request.data))
|
||||
|
||||
class Export(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
|
|
@ -164,6 +202,8 @@ class Dataset(APIView):
|
|||
)
|
||||
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('dataset_id')))
|
||||
@log(menu='Knowledge Base', operate="Export knowledge base",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def get(self, request: Request, dataset_id: str):
|
||||
return DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).export_excel()
|
||||
|
||||
|
|
@ -178,6 +218,8 @@ class Dataset(APIView):
|
|||
)
|
||||
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('dataset_id')))
|
||||
@log(menu='Knowledge Base', operate="Export knowledge base containing images",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def get(self, request: Request, dataset_id: str):
|
||||
return DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).export_zip()
|
||||
|
||||
|
|
@ -193,6 +235,8 @@ class Dataset(APIView):
|
|||
dynamic_tag=keywords.get('dataset_id')),
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.DELETE,
|
||||
dynamic_tag=k.get('dataset_id')), compare=CompareConstants.AND)
|
||||
@log(menu='Knowledge Base', operate="Delete knowledge base",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def delete(self, request: Request, dataset_id: str):
|
||||
operate = DataSetSerializers.Operate(data={'id': dataset_id})
|
||||
return result.success(operate.delete())
|
||||
|
|
@ -219,6 +263,8 @@ class Dataset(APIView):
|
|||
)
|
||||
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('dataset_id')))
|
||||
@log(menu='Knowledge Base', operate="Modify knowledge base information",
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).edit(request.data,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
@desc:
|
||||
"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
|
|
@ -15,12 +16,14 @@ from rest_framework.views import Request
|
|||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.util.common import query_params_to_single_dict
|
||||
from dataset.serializers.common_serializers import BatchSerializer
|
||||
from dataset.serializers.document_serializers import DocumentSerializers, DocumentWebInstanceSerializer
|
||||
from dataset.swagger_api.document_api import DocumentApi
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dataset.views.common import get_dataset_document_operation_object, get_dataset_operation_object, \
|
||||
get_document_operation_object_batch, get_document_operation_object
|
||||
|
||||
|
||||
class Template(APIView):
|
||||
|
|
@ -60,6 +63,11 @@ class WebDocument(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Create Web site documents",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
{'name': f'[{",".join([url for url in r.data.get("source_url_list", [])])}]',
|
||||
'document_list': [{'name': url} for url in r.data.get("source_url_list", [])]}))
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_web(request.data, with_valid=True))
|
||||
|
|
@ -78,6 +86,11 @@ class QaDocument(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Import QA and create documentation",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
{'name': f'[{",".join([file.name for file in r.FILES.getlist("file")])}]',
|
||||
'document_list': [{'name': file.name} for file in r.FILES.getlist("file")]}))
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_qa(
|
||||
|
|
@ -98,6 +111,11 @@ class TableDocument(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Import tables and create documents",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
{'name': f'[{",".join([file.name for file in r.FILES.getlist("file")])}]',
|
||||
'document_list': [{'name': file.name} for file in r.FILES.getlist("file")]}))
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_table(
|
||||
|
|
@ -118,6 +136,10 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Create document",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
{'name': r.data.get('name')}))
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save(request.data, with_valid=True))
|
||||
|
|
@ -151,6 +173,10 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Modify document hit processing methods in batches",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('id_list'))))
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_edit_hit_handling(request.data))
|
||||
|
|
@ -170,6 +196,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Create documents in batches",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
{'name': f'[{",".join([document.get("name") for document in r.data])}]',
|
||||
'document_list': r.data})
|
||||
)
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_save(request.data))
|
||||
|
||||
|
|
@ -184,6 +216,11 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Batch sync documents",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('id_list')))
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_sync(request.data))
|
||||
|
||||
|
|
@ -198,6 +235,10 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Delete documents in batches",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('id_list'))))
|
||||
def delete(self, request: Request, dataset_id: str):
|
||||
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_delete(request.data))
|
||||
|
||||
|
|
@ -214,6 +255,11 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Synchronize web site types",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
))
|
||||
def put(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Sync(data={'document_id': document_id, 'dataset_id': dataset_id}).sync(
|
||||
|
|
@ -233,6 +279,11 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Cancel task",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
))
|
||||
def put(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).cancel(
|
||||
|
|
@ -253,6 +304,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Cancel tasks in batches",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('id_list'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_cancel(request.data))
|
||||
|
|
@ -271,6 +328,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Refresh document vector library",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).refresh(
|
||||
|
|
@ -291,6 +354,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Batch refresh document vector library",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('id_list'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_refresh(request.data))
|
||||
|
|
@ -313,6 +382,12 @@ class Document(APIView):
|
|||
dynamic_tag=k.get('target_dataset_id')),
|
||||
compare=CompareConstants.AND
|
||||
)
|
||||
@log(menu='document', operate="Migrate documents in batches",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data)
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, target_dataset_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Migrate(
|
||||
|
|
@ -332,6 +407,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Export document",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def get(self, request: Request, dataset_id: str, document_id: str):
|
||||
return DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).export()
|
||||
|
||||
|
|
@ -346,6 +427,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Export Zip document",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def get(self, request: Request, dataset_id: str, document_id: str):
|
||||
return DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).export_zip()
|
||||
|
||||
|
|
@ -377,6 +464,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Modify document",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).edit(
|
||||
|
|
@ -392,6 +485,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Delete document",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def delete(self, request: Request, dataset_id: str, document_id: str):
|
||||
operate = DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id})
|
||||
operate.is_valid(raise_exception=True)
|
||||
|
|
@ -456,6 +555,12 @@ class Document(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='document', operate="Batch generate related documents",
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object_batch(r.data.get('document_id_list'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str):
|
||||
return result.success(DocumentSerializers.BatchGenerateRelated(data={'dataset_id': dataset_id})
|
||||
.batch_generate_related(request.data))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from rest_framework.views import APIView
|
|||
from rest_framework.views import Request
|
||||
|
||||
from common.auth import TokenAuth
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from dataset.serializers.file_serializers import FileSerializer
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -32,6 +33,7 @@ class FileView(APIView):
|
|||
required=True,
|
||||
description=_('Upload file'))],
|
||||
tags=[_('file')])
|
||||
@log(menu='file', operate='Upload file')
|
||||
def post(self, request: Request):
|
||||
return result.success(FileSerializer(data={'file': request.FILES.get('file')}).upload())
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from rest_framework.views import APIView
|
|||
from rest_framework.views import Request
|
||||
|
||||
from common.auth import TokenAuth
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from dataset.serializers.image_serializers import ImageSerializer
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ from rest_framework.views import Request
|
|||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.util.common import query_params_to_single_dict
|
||||
from dataset.serializers.common_serializers import BatchSerializer
|
||||
from dataset.serializers.paragraph_serializers import ParagraphSerializers
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dataset.views import get_dataset_document_operation_object, get_dataset_operation_object, \
|
||||
get_document_operation_object
|
||||
|
||||
|
||||
class Paragraph(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
|
@ -50,6 +54,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Create Paragraph',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def post(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
ParagraphSerializers.Create(data={'dataset_id': dataset_id, 'document_id': document_id}).save(request.data))
|
||||
|
|
@ -67,6 +77,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Add associated questions',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def post(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
|
||||
return result.success(ParagraphSerializers.Problem(
|
||||
data={"dataset_id": dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id}).save(
|
||||
|
|
@ -99,6 +115,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Disassociation issue',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str, problem_id: str):
|
||||
return result.success(ParagraphSerializers.Association(
|
||||
data={'dataset_id': dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id,
|
||||
|
|
@ -116,6 +138,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Related questions',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str, problem_id: str):
|
||||
return result.success(ParagraphSerializers.Association(
|
||||
data={'dataset_id': dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id,
|
||||
|
|
@ -134,6 +162,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Modify paragraph data',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
|
||||
o = ParagraphSerializers.Operate(
|
||||
data={"paragraph_id": paragraph_id, 'dataset_id': dataset_id, 'document_id': document_id})
|
||||
|
|
@ -164,6 +198,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Delete paragraph',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def delete(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
|
||||
o = ParagraphSerializers.Operate(
|
||||
data={"dataset_id": dataset_id, 'document_id': document_id, "paragraph_id": paragraph_id})
|
||||
|
|
@ -184,6 +224,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Delete paragraphs in batches',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def delete(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(ParagraphSerializers.Batch(
|
||||
data={"dataset_id": dataset_id, 'document_id': document_id}).batch_delete(request.data))
|
||||
|
|
@ -206,6 +252,12 @@ class Paragraph(APIView):
|
|||
dynamic_tag=k.get('target_dataset_id')),
|
||||
compare=CompareConstants.AND
|
||||
)
|
||||
@log(menu='Paragraph', operate='Migrate paragraphs in batches',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, target_dataset_id: str, document_id: str, target_document_id):
|
||||
return result.success(
|
||||
ParagraphSerializers.Migrate(
|
||||
|
|
@ -241,6 +293,12 @@ class Paragraph(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='Paragraph', operate='Batch generate related',
|
||||
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
|
||||
get_dataset_operation_object(keywords.get('dataset_id')),
|
||||
get_document_operation_object(keywords.get('document_id'))
|
||||
)
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(
|
||||
ParagraphSerializers.BatchGenerateRelated(data={'dataset_id': dataset_id, 'document_id': document_id})
|
||||
|
|
|
|||
|
|
@ -13,12 +13,15 @@ from rest_framework.views import Request
|
|||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import Permission, Group, Operate
|
||||
from common.log.log import log
|
||||
from common.response import result
|
||||
from common.util.common import query_params_to_single_dict
|
||||
from dataset.serializers.problem_serializers import ProblemSerializers
|
||||
from dataset.swagger_api.problem_api import ProblemApi
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dataset.views import get_dataset_operation_object
|
||||
|
||||
|
||||
class Problem(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
|
@ -49,6 +52,9 @@ class Problem(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='problem', operate='Create question',
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id'))
|
||||
)
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
ProblemSerializers.Create(
|
||||
|
|
@ -85,6 +91,8 @@ class Problem(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='problem', operate='Batch deletion issues',
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def delete(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
ProblemSerializers.BatchOperate(data={'dataset_id': dataset_id}).delete(request.data))
|
||||
|
|
@ -99,6 +107,8 @@ class Problem(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='problem', operate='Batch associated paragraphs',
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def post(self, request: Request, dataset_id: str):
|
||||
return result.success(
|
||||
ProblemSerializers.BatchOperate(data={'dataset_id': dataset_id}).association(request.data))
|
||||
|
|
@ -115,6 +125,8 @@ class Problem(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='problem', operate='Delete question',
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def delete(self, request: Request, dataset_id: str, problem_id: str):
|
||||
return result.success(ProblemSerializers.Operate(
|
||||
data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id,
|
||||
|
|
@ -130,6 +142,8 @@ class Problem(APIView):
|
|||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
@log(menu='problem', operate='Modify question',
|
||||
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
|
||||
def put(self, request: Request, dataset_id: str, problem_id: str):
|
||||
return result.success(ProblemSerializers.Operate(
|
||||
data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import uuid
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List
|
||||
|
||||
from django.db.models import QuerySet
|
||||
import jieba
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.db.models import QuerySet, Value
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
from common.db.search import generate_sql_by_query_dict
|
||||
|
|
@ -68,7 +70,8 @@ class PGVector(BaseVectorStore):
|
|||
source_id=text_list[index].get('source_id'),
|
||||
source_type=text_list[index].get('source_type'),
|
||||
embedding=embeddings[index],
|
||||
search_vector=to_ts_vector(text_list[index]['text'])) for index in
|
||||
search_vector=SearchVector(Value(to_ts_vector(text_list[index]['text'])))) for
|
||||
index in
|
||||
range(0, len(texts))]
|
||||
if not is_the_task_interrupted():
|
||||
QuerySet(Embedding).bulk_create(embedding_list) if len(embedding_list) > 0 else None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
# Generated by Django 4.2.15 on 2025-03-13 07:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
function_template = '''
|
||||
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-10 06:20:35.945414 +00:00', '2025-03-10 09:19:23.608026 +00:00', 'c75cb48e-fd77-11ef-84d2-5618c4394482', '博查搜索', '从博查搜索任何信息和网页URL', e'def bocha_search(query, apikey):
|
||||
import requests
|
||||
import json
|
||||
url = "https://api.bochaai.com/v1/web-search"
|
||||
payload = json.dumps({
|
||||
"query": query,
|
||||
"Boolean": "true",
|
||||
"count": 8
|
||||
})
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + apikey, #鉴权参数,示例:Bearer xxxxxx,API KEY请先前往博查AI开放平台(https://open.bochaai.com)> API KEY 管理中获取。
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
|
||||
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/bochaai/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
|
||||
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-26 03:36:48.187286 +00:00', '2025-03-11 07:23:46.123972 +00:00', 'e89ad2ae-f3f2-11ef-ad09-0242ac110002', 'Google Search', 'Google Web Search', e'def google_search(query, apikey, cx):
|
||||
import requests
|
||||
import json
|
||||
url = "https://customsearch.googleapis.com/customsearch/v1"
|
||||
params = {
|
||||
"q": query,
|
||||
"key": apikey,
|
||||
"cx": cx,
|
||||
"num": 10, # 每次最多返回10条
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
|
||||
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/google_search/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "cx", "label": "cx", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "cx 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "cx长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
|
||||
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-25 07:44:40.141515 +00:00', '2025-03-11 06:33:53.248495 +00:00', '5e912f00-f34c-11ef-8a9c-5618c4394482', 'LangSearch', e'A Web Search tool supporting natural language search
|
||||
', e'
|
||||
def langsearch(query, apikey):
|
||||
import json
|
||||
import requests
|
||||
|
||||
url = "https://api.langsearch.com/v1/web-search"
|
||||
payload = json.dumps({
|
||||
"query": query,
|
||||
"summary": True,
|
||||
"freshness": "noLimit",
|
||||
"livecrawl": True,
|
||||
"count": 20
|
||||
})
|
||||
headers = {
|
||||
"Authorization": apikey,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
# key从官网申请 https://langsearch.com/
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
|
||||
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/langsearch/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
|
||||
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 08:16:32.626245 +00:00', '2025-03-17 08:16:32.626308 +00:00', '22c21b76-0308-11f0-9694-5618c4394482', 'MySQL 查询', '一个连接MySQL数据库执行SQL查询的工具', e'
|
||||
def query_mysql(host,port, user, password, database, sql):
|
||||
import pymysql
|
||||
import json
|
||||
from pymysql.cursors import DictCursor
|
||||
|
||||
try:
|
||||
# 创建连接
|
||||
db = pymysql.connect(
|
||||
host=host,
|
||||
port=int(port),
|
||||
user=user,
|
||||
password=password,
|
||||
database=database,
|
||||
cursorclass=DictCursor # 使用字典游标
|
||||
)
|
||||
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute(sql)
|
||||
|
||||
# 使用 fetchall() 方法获取所有数据
|
||||
data = cursor.fetchall()
|
||||
|
||||
# 处理 bytes 类型的数据
|
||||
for row in data:
|
||||
for key, value in row.items():
|
||||
if isinstance(value, bytes):
|
||||
row[key] = value.decode("utf-8") # 转换为字符串
|
||||
|
||||
# 将数据序列化为 JSON
|
||||
json_data = json.dumps(data, ensure_ascii=False)
|
||||
return json_data
|
||||
|
||||
# 关闭数据库连接
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error while connecting to MySQL: {e}")
|
||||
raise e', '{"{\\"name\\": \\"sql\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/ui/fx/mysql/icon.png', '[{"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "host", "label": "host", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "host 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "host长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 20, "minlength": 1, "show-word-limit": true}, "field": "port", "label": "port", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "port 为必填属性", "required": true}, {"max": 20, "min": 1, "message": "port长度在 1 到 20 个字符", "trigger": "blur"}]}, "default_value": "3306", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "user", "label": "user", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "user 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "user长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "root", "show_default_value": false}, {"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "password", "label": "password", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "password 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "password长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "database", "label": "database", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "database 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "database长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', null, null);
|
||||
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 07:37:54.620836 +00:00', '2025-03-17 07:37:54.620887 +00:00', 'bd1e8b88-0302-11f0-87bb-5618c4394482', 'PostgreSQL 查询', '一个连接PostgreSQL数据库执行SQL查询的工具', e'
|
||||
def queryPgSQL(database, user, password, host, port, query):
|
||||
import psycopg2
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 自定义 JSON 序列化函数
|
||||
def default_serializer(obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat() # 将 datetime 转换为 ISO 格式字符串
|
||||
raise TypeError(f"Type {type(obj)} not serializable")
|
||||
|
||||
# 数据库连接信息
|
||||
conn_params = {
|
||||
"dbname": database,
|
||||
"user": user,
|
||||
"password": password,
|
||||
"host": host,
|
||||
"port": port
|
||||
}
|
||||
try:
|
||||
# 建立连接
|
||||
conn = psycopg2.connect(**conn_params)
|
||||
print("连接成功!")
|
||||
# 创建游标对象
|
||||
cursor = conn.cursor()
|
||||
# 执行查询语句
|
||||
cursor.execute(query)
|
||||
# 获取查询结果
|
||||
rows = cursor.fetchall()
|
||||
# 处理 bytes 类型的数据
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
result = [dict(zip(columns, row)) for row in rows]
|
||||
# 转换为 JSON 格式
|
||||
json_result = json.dumps(result, default=default_serializer, ensure_ascii=False)
|
||||
return json_result
|
||||
except Exception as e:
|
||||
print(f"发生错误:{e}")
|
||||
raise e
|
||||
finally:
|
||||
# 关闭游标和连接
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if conn:
|
||||
conn.close()', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/ui/fx/postgresql/icon.png', '[{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"host","label":"host","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"host 为必填属性","required":true},{"max":200,"min":1,"message":"host长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false},{"attrs":{"maxlength":20,"minlength":1,"show-word-limit":true},"field":"port","label":"port","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"port 为必填属性","required":true},{"max":20,"min":1,"message":"port长度在 1 到 20 个字符","trigger":"blur"}]},"default_value":"5432","show_default_value":false},{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"user","label":"user","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"user 为必填属性","required":true},{"max":200,"min":1,"message":"user长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"root","show_default_value":false},{"attrs":{"type":"password","maxlength":200,"minlength":1,"show-password":true,"show-word-limit":true},"field":"password","label":"password","required":true,"input_type":"PasswordInput","props_info":{"rules":[{"message":"password 为必填属性","required":true},{"max":200,"min":1,"message":"password长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false},{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"database","label":"database","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"database 为必填属性","required":true},{"max":200,"min":1,"message":"database长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false}]', null, null);
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('function_lib', '0002_functionlib_is_active_functionlib_permission_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='functionlib',
|
||||
name='function_type',
|
||||
field=models.CharField(choices=[('INTERNAL', '内置'), ('PUBLIC', '公开')],
|
||||
default='PUBLIC', max_length=20, verbose_name='函数类型'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='functionlib',
|
||||
name='icon',
|
||||
field=models.CharField(default='/ui/favicon.ico', max_length=256,
|
||||
verbose_name='函数库icon'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='functionlib',
|
||||
name='init_field_list',
|
||||
field=models.JSONField(default=list, verbose_name='启动字段列表'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='functionlib',
|
||||
name='init_params',
|
||||
field=models.CharField(max_length=102400, null=True, verbose_name='初始化参数'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='functionlib',
|
||||
name='template_id',
|
||||
field=models.UUIDField(default=None, null=True, verbose_name='模版id'),
|
||||
),
|
||||
migrations.RunSQL(function_template)
|
||||
]
|
||||
|
|
@ -19,6 +19,10 @@ class PermissionType(models.TextChoices):
|
|||
PUBLIC = "PUBLIC", '公开'
|
||||
PRIVATE = "PRIVATE", "私有"
|
||||
|
||||
class FunctionType(models.TextChoices):
|
||||
INTERNAL = "INTERNAL", '内置'
|
||||
PUBLIC = "PUBLIC", "公开"
|
||||
|
||||
|
||||
class FunctionLib(AppModelMixin):
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
|
||||
|
|
@ -29,9 +33,15 @@ class FunctionLib(AppModelMixin):
|
|||
input_field_list = ArrayField(verbose_name="输入字段列表",
|
||||
base_field=models.JSONField(verbose_name="输入字段", default=dict)
|
||||
, default=list)
|
||||
init_field_list = models.JSONField(verbose_name="启动字段列表", default=list)
|
||||
icon = models.CharField(max_length=256, verbose_name="函数库icon", default="/ui/favicon.ico")
|
||||
is_active = models.BooleanField(default=True)
|
||||
permission_type = models.CharField(max_length=20, verbose_name='权限类型', choices=PermissionType.choices,
|
||||
default=PermissionType.PRIVATE)
|
||||
function_type = models.CharField(max_length=20, verbose_name='函数类型', choices=FunctionType.choices,
|
||||
default=FunctionType.PUBLIC)
|
||||
template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None)
|
||||
init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "function_lib"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/2 14:55
|
||||
@desc:
|
||||
"""
|
||||
|
|
@ -6,27 +6,30 @@
|
|||
@date:2024/8/2 17:35
|
||||
@desc:
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
import uuid
|
||||
from typing import List
|
||||
|
||||
from django.core import validators
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet, Q
|
||||
from django.db.models import QuerySet, Q, OuterRef, Exists
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers, status
|
||||
|
||||
from common.db.search import page_search
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.field.common import UploadedFileField
|
||||
from common.field.common import UploadedFileField, UploadedImageField
|
||||
from common.response import result
|
||||
from common.util.common import restricted_loads
|
||||
from common.util.field_message import ErrMessage
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from function_lib.models.function import FunctionLib
|
||||
from common.util.rsa_util import rsa_long_decrypt, rsa_long_encrypt
|
||||
from dataset.models import File
|
||||
from function_lib.models.function import FunctionLib, PermissionType, FunctionType
|
||||
from smartdoc.const import CONFIG
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
|
@ -35,11 +38,37 @@ class FlibInstance:
|
|||
self.function_lib = function_lib
|
||||
self.version = version
|
||||
|
||||
def encryption(message: str):
|
||||
"""
|
||||
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890
|
||||
:param message:
|
||||
:return:
|
||||
"""
|
||||
if type(message) != str:
|
||||
return message
|
||||
if message == "":
|
||||
return ""
|
||||
max_pre_len = 8
|
||||
max_post_len = 4
|
||||
message_len = len(message)
|
||||
pre_len = int(message_len / 5 * 2)
|
||||
post_len = int(message_len / 5 * 1)
|
||||
pre_str = "".join([message[index] for index in
|
||||
range(0,
|
||||
max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(
|
||||
pre_len))])
|
||||
end_str = "".join(
|
||||
[message[index] for index in
|
||||
range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len),
|
||||
message_len)])
|
||||
content = "***************"
|
||||
return pre_str + content + end_str
|
||||
|
||||
|
||||
class FunctionLibModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FunctionLib
|
||||
fields = ['id', 'name', 'desc', 'code', 'input_field_list', 'permission_type', 'is_active', 'user_id',
|
||||
fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list','init_field_list', 'init_params', 'permission_type', 'is_active', 'user_id', 'template_id',
|
||||
'create_time', 'update_time']
|
||||
|
||||
|
||||
|
|
@ -65,6 +94,8 @@ class DebugField(serializers.Serializer):
|
|||
class DebugInstance(serializers.Serializer):
|
||||
debug_field_list = DebugField(required=True, many=True)
|
||||
input_field_list = FunctionLibInputField(required=True, many=True)
|
||||
init_field_list = serializers.ListField(required=False, default=list)
|
||||
init_params = serializers.JSONField(required=False, default=dict)
|
||||
code = serializers.CharField(required=True, error_messages=ErrMessage.char(_('function content')))
|
||||
|
||||
|
||||
|
|
@ -80,6 +111,8 @@ class EditFunctionLib(serializers.Serializer):
|
|||
|
||||
input_field_list = FunctionLibInputField(required=False, many=True)
|
||||
|
||||
init_field_list = serializers.ListField(required=False, default=list)
|
||||
|
||||
is_active = serializers.BooleanField(required=False, error_messages=ErrMessage.char(_('Is active')))
|
||||
|
||||
|
||||
|
|
@ -93,6 +126,8 @@ class CreateFunctionLib(serializers.Serializer):
|
|||
|
||||
input_field_list = FunctionLibInputField(required=True, many=True)
|
||||
|
||||
init_field_list = serializers.ListField(required=False, default=list)
|
||||
|
||||
permission_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_('permission')), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^PUBLIC|PRIVATE$"),
|
||||
message="权限只支持PUBLIC|PRIVATE", code=500)
|
||||
|
|
@ -111,6 +146,8 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id')))
|
||||
select_user_id = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
function_type = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
|
||||
def get_query_set(self):
|
||||
query_set = QuerySet(FunctionLib).filter(
|
||||
|
|
@ -123,19 +160,33 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
query_set = query_set.filter(is_active=self.data.get('is_active'))
|
||||
if self.data.get('select_user_id') is not None:
|
||||
query_set = query_set.filter(user_id=self.data.get('select_user_id'))
|
||||
if self.data.get('function_type') is not None:
|
||||
query_set = query_set.filter(function_type=self.data.get('function_type'))
|
||||
query_set = query_set.order_by("-create_time")
|
||||
|
||||
return query_set
|
||||
|
||||
def list(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
return [FunctionLibModelSerializer(item).data for item in self.get_query_set()]
|
||||
rs = []
|
||||
for item in self.get_query_set():
|
||||
data = {**FunctionLibModelSerializer(item).data, 'init_params': None}
|
||||
rs.append(data)
|
||||
return rs
|
||||
|
||||
def page(self, current_page: int, page_size: int, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
|
||||
def post_records_handler(row):
|
||||
return {
|
||||
**FunctionLibModelSerializer(row).data,
|
||||
'init_params': None
|
||||
}
|
||||
|
||||
return page_search(current_page, page_size, self.get_query_set(),
|
||||
post_records_handler=lambda row: FunctionLibModelSerializer(row).data)
|
||||
post_records_handler=post_records_handler)
|
||||
|
||||
class Create(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id')))
|
||||
|
|
@ -148,8 +199,9 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
code=instance.get('code'),
|
||||
user_id=self.data.get('user_id'),
|
||||
input_field_list=instance.get('input_field_list'),
|
||||
init_field_list=instance.get('init_field_list'),
|
||||
permission_type=instance.get('permission_type'),
|
||||
is_active=instance.get('is_active', True))
|
||||
is_active=False)
|
||||
function_lib.save()
|
||||
return FunctionLibModelSerializer(function_lib).data
|
||||
|
||||
|
|
@ -163,13 +215,19 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
input_field_list = debug_instance.get('input_field_list')
|
||||
code = debug_instance.get('code')
|
||||
debug_field_list = debug_instance.get('debug_field_list')
|
||||
init_params = debug_instance.get('init_params')
|
||||
params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'))
|
||||
for field in
|
||||
[{'value': self.get_field_value(debug_field_list, field.get('name'), field.get('is_required')),
|
||||
**field} for field in
|
||||
input_field_list]}
|
||||
return function_executor.exec_code(code, params)
|
||||
# 合并初始化参数
|
||||
if init_params is not None:
|
||||
all_params = init_params | params
|
||||
else:
|
||||
all_params = params
|
||||
return function_executor.exec_code(code, all_params)
|
||||
|
||||
@staticmethod
|
||||
def get_field_value(debug_field_list, name, is_required):
|
||||
|
|
@ -217,6 +275,9 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
def delete(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
fun = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
if fun.template_id is None and fun.icon != '/ui/favicon.ico':
|
||||
QuerySet(File).filter(id=fun.icon.split('/')[-1]).delete()
|
||||
QuerySet(FunctionLib).filter(id=self.data.get('id')).delete()
|
||||
return True
|
||||
|
||||
|
|
@ -224,9 +285,25 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
EditFunctionLib(data=instance).is_valid(raise_exception=True)
|
||||
edit_field_list = ['name', 'desc', 'code', 'input_field_list', 'permission_type', 'is_active']
|
||||
edit_field_list = ['name', 'desc', 'code', 'icon', 'input_field_list', 'init_field_list', 'init_params', 'permission_type', 'is_active']
|
||||
edit_dict = {field: instance.get(field) for field in edit_field_list if (
|
||||
field in instance and instance.get(field) is not None)}
|
||||
|
||||
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
if 'init_params' in edit_dict:
|
||||
if edit_dict['init_field_list'] is not None:
|
||||
rm_key = []
|
||||
for key in edit_dict['init_params']:
|
||||
if key not in [field['field'] for field in edit_dict['init_field_list']]:
|
||||
rm_key.append(key)
|
||||
for key in rm_key:
|
||||
edit_dict['init_params'].pop(key)
|
||||
if function_lib.init_params:
|
||||
old_init_params = json.loads(rsa_long_decrypt(function_lib.init_params))
|
||||
for key in edit_dict['init_params']:
|
||||
if key in old_init_params and edit_dict['init_params'][key] == encryption(old_init_params[key]):
|
||||
edit_dict['init_params'][key] = old_init_params[key]
|
||||
edit_dict['init_params'] = rsa_long_encrypt(json.dumps(edit_dict['init_params']))
|
||||
QuerySet(FunctionLib).filter(id=self.data.get('id')).update(**edit_dict)
|
||||
return self.one(False)
|
||||
|
||||
|
|
@ -237,7 +314,15 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
Q(user_id=self.data.get('user_id')) | Q(permission_type='PUBLIC')).exists():
|
||||
raise AppApiException(500, _('Function does not exist'))
|
||||
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
return FunctionLibModelSerializer(function_lib).data
|
||||
if function_lib.init_params:
|
||||
function_lib.init_params = json.loads(rsa_long_decrypt(function_lib.init_params))
|
||||
if function_lib.init_field_list:
|
||||
password_fields = [i["field"] for i in function_lib.init_field_list if i.get("input_type") == "PasswordInput"]
|
||||
if function_lib.init_params:
|
||||
for k in function_lib.init_params:
|
||||
if k in password_fields and function_lib.init_params[k]:
|
||||
function_lib.init_params[k] = encryption(function_lib.init_params[k])
|
||||
return {**FunctionLibModelSerializer(function_lib).data, 'init_params': function_lib.init_params}
|
||||
|
||||
def export(self, with_valid=True):
|
||||
try:
|
||||
|
|
@ -265,7 +350,7 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
user_id = self.data.get('user_id')
|
||||
flib_instance_bytes = self.data.get('file').read()
|
||||
try:
|
||||
flib_instance = pickle.loads(flib_instance_bytes)
|
||||
flib_instance = restricted_loads(flib_instance_bytes)
|
||||
except Exception as e:
|
||||
raise AppApiException(1001, _("Unsupported file format"))
|
||||
function_lib = flib_instance.function_lib
|
||||
|
|
@ -274,7 +359,68 @@ class FunctionLibSerializer(serializers.Serializer):
|
|||
code=function_lib.get('code'),
|
||||
user_id=user_id,
|
||||
input_field_list=function_lib.get('input_field_list'),
|
||||
init_field_list=function_lib.get('init_field_list', []),
|
||||
permission_type='PRIVATE',
|
||||
is_active=function_lib.get('is_active'))
|
||||
is_active=False)
|
||||
function_lib_model.save()
|
||||
return True
|
||||
return True
|
||||
|
||||
class IconOperate(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
|
||||
image = UploadedImageField(required=True, error_messages=ErrMessage.image(_("picture")))
|
||||
|
||||
def edit(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
functionLib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
if functionLib is None:
|
||||
raise AppApiException(500, _('Function does not exist'))
|
||||
# 删除旧的图片
|
||||
if functionLib.icon != '/ui/favicon.ico':
|
||||
QuerySet(File).filter(id=functionLib.icon.split('/')[-1]).delete()
|
||||
if self.data.get('image') is None:
|
||||
functionLib.icon = '/ui/favicon.ico'
|
||||
else:
|
||||
meta = {
|
||||
'debug': False
|
||||
}
|
||||
file_id = uuid.uuid1()
|
||||
file = File(id=file_id, file_name=self.data.get('image').name, meta=meta)
|
||||
file.save(self.data.get('image').read())
|
||||
|
||||
functionLib.icon = f'/api/file/{file_id}'
|
||||
functionLib.save()
|
||||
|
||||
return functionLib.icon
|
||||
|
||||
class InternalFunction(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function name")))
|
||||
|
||||
def add(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
|
||||
internal_function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
if internal_function_lib is None:
|
||||
raise AppApiException(500, _('Function does not exist'))
|
||||
|
||||
function_lib = FunctionLib(
|
||||
id=uuid.uuid1(),
|
||||
name=self.data.get('name'),
|
||||
desc=internal_function_lib.desc,
|
||||
code=internal_function_lib.code,
|
||||
user_id=self.data.get('user_id'),
|
||||
input_field_list=internal_function_lib.input_field_list,
|
||||
init_field_list=internal_function_lib.init_field_list,
|
||||
permission_type=PermissionType.PRIVATE,
|
||||
template_id=internal_function_lib.id,
|
||||
function_type=FunctionType.PUBLIC,
|
||||
icon=internal_function_lib.icon,
|
||||
is_active=False
|
||||
)
|
||||
function_lib.save()
|
||||
|
||||
return FunctionLibModelSerializer(function_lib).data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/2 14:55
|
||||
@desc:
|
||||
"""
|
||||
|
|
@ -195,6 +195,53 @@ class FunctionLibApi(ApiMixin):
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['id', 'name', 'code', 'input_field_list', 'permission_type'],
|
||||
properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_STRING, title="", description=_('ID')),
|
||||
|
||||
'name': openapi.Schema(type=openapi.TYPE_STRING, title=_('function name'),
|
||||
description=_('function name')),
|
||||
'desc': openapi.Schema(type=openapi.TYPE_STRING, title=_('function description'),
|
||||
description=_('function description')),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title=_('function content'),
|
||||
description=_('function content')),
|
||||
'permission_type': openapi.Schema(type=openapi.TYPE_STRING, title=_('permission'),
|
||||
description=_('permission')),
|
||||
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_('Is active'),
|
||||
description=_('Is active')),
|
||||
'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
description=_('Input variable list'),
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=['name', 'is_required', 'source'],
|
||||
properties={
|
||||
'name': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_('variable name'),
|
||||
description=_('variable name')),
|
||||
'is_required': openapi.Schema(
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
title=_('required'),
|
||||
description=_('required')),
|
||||
'type': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_('type'),
|
||||
description=_(
|
||||
'Field type string|int|dict|array|float')
|
||||
),
|
||||
'source': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title=_('source'),
|
||||
description=_(
|
||||
'The source only supports custom|reference')),
|
||||
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
class Export(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
|
|
@ -214,4 +261,4 @@ class FunctionLibApi(ApiMixin):
|
|||
type=openapi.TYPE_FILE,
|
||||
required=True,
|
||||
description=_('Upload image files'))
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ urlpatterns = [
|
|||
path('function_lib/debug', views.FunctionLibView.Debug.as_view()),
|
||||
path('function_lib/<str:id>/export', views.FunctionLibView.Export.as_view()),
|
||||
path('function_lib/import', views.FunctionLibView.Import.as_view()),
|
||||
path('function_lib/<str:id>/edit_icon', views.FunctionLibView.EditIcon.as_view()),
|
||||
path('function_lib/<str:id>/add_internal_fun', views.FunctionLibView.AddInternalFun.as_view()),
|
||||
path('function_lib/pylint', views.PyLintView.as_view()),
|
||||
path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
|
||||
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: common.py
|
||||
@date:2025/3/25 17:27
|
||||
@desc:
|
||||
"""
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from function_lib.models.function import FunctionLib
|
||||
|
||||
|
||||
def get_function_lib_operation_object(function_lib_id):
|
||||
function_lib_model = QuerySet(model=FunctionLib).filter(id=function_lib_id).first()
|
||||
if function_lib_model is not None:
|
||||
return {
|
||||
"name": function_lib_model.name
|
||||
}
|
||||
return {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue