mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d0a5606b2 | |||
| 4ad031e97d | |||
| 0081d0f05f | |||
| 800c25a6b4 | |||
| 9aeb07d830 | |||
| 5590a823c6 | |||
| 3fa570f49b | |||
| 60053e7b02 | |||
| fa1b873280 | |||
| 578f70817e | |||
| 6c6b658ffe | |||
| 9a5ff320f3 | |||
| 48688afa5e | |||
| a2b35098c6 | |||
| 4d5354387b | |||
| c6512e689b | |||
| b7aff4f560 | |||
| 18dfa2900c | |||
| 86b546f657 | |||
| 3fb2bc7613 | |||
| f4cb939317 | |||
| d868c283c4 | |||
| c7dfb0193b | |||
| f7705d6bc9 | |||
| 3ed096fd3f | |||
| 2d1fbefdb5 | |||
| c5a3146a8c | |||
| 1c364e0e5c | |||
| 9906526a91 | |||
| 7e0148c058 | |||
| f86826b7a0 | |||
| 497bc1438a | |||
| d133cc043b | |||
| e56bd770ea | |||
| 07bb2a6fd6 | |||
| 396feadd4b | |||
| f93f485696 | |||
| a813736194 | |||
| 322bafdf2a | |||
| 8257eeb3f2 | |||
| 00810525d6 | |||
| 391b950be6 | |||
| d78f215caa | |||
| 9457d20ef1 | |||
| 648f8e81d1 | |||
| 161c7a231b | |||
| e997b42504 | |||
| 524699da7d | |||
| 765a114be7 | |||
| c86afff447 | |||
| b73fe0cc3c | |||
| 2a614e0e23 | |||
| 50b425cf89 | |||
| 2174c350be | |||
| 7f81fc8f9b | |||
| f090075cb2 | |||
| ec6d942d83 | |||
| 8714754afc | |||
| 43b959fe58 | |||
| 320e8f6553 | |||
| 89d5b2414e | |||
| 91ea559f9e | |||
| 445dce4363 | |||
| 1fce6caf80 | |||
| adb0a93d95 | |||
| 226bdd6e99 | |||
| 5aa9d7787e | |||
| b2524eec49 | |||
| 6a4858a7ee | |||
| 1a623df849 | |||
| bfc07fe4f9 | |||
| 3e702aa4ac | |||
| 2ced25c676 | |||
| 1935c3be1a | |||
| 609cfa7b5f | |||
| ac26d09a59 | |||
| 4bdf3fd48e | |||
| c1d0473f49 | |||
| e5f7733b31 | |||
| 5aec1e3e17 | |||
| 1d6bcf5aa2 | |||
| 1e6d44d6ef | |||
| cec208051f | |||
| 526fcbbfde | |||
| c760f058df | |||
| 8fdfa0f669 | |||
| ceecac69e9 | |||
| e0c0bdeb0a | |||
| cf3106040a | |||
| 791afbba15 | |||
| 8358245f64 | |||
| 396bb4b688 | |||
| 167b4af52b | |||
| bedb05012d | |||
| 6a60e26020 | |||
| 6496055e23 | |||
| dab92ac1e8 | |||
| b9fa00f341 | |||
| e5d3ab0332 | |||
| 4991107822 | |||
| 51ecda0ff5 | |||
| 6850fd69c6 | |||
| e1e5711680 | |||
| 4463128436 | |||
| c8783672d7 | |||
| ce495e4e3e | |||
| fcabdf7745 | |||
| b540d41cdc | |||
| 260d694bbc | |||
| 6329427ad5 | |||
| df223eddf3 | |||
| 85b359556e | |||
| b164116277 | |||
| 8e5efcc47f | |||
| 6eed115723 | |||
| 7d80fc474c | |||
| a20b82092f | |||
| 2a86472b88 | |||
| 190eea7097 | |||
| 2d1c83da59 | |||
| 3f065c75da | |||
| 1bae479b37 | |||
| 5e7c1fb23a | |||
| bae30e5cc4 | |||
| 18f80743eb | |||
| bfaef2cca6 | |||
| cbd7cd7c4d | |||
| a2f9c03a95 | |||
| 2c56d274d8 | |||
| 7742f67481 | |||
| 6af9d4e5f9 | |||
| 51efecf4b5 | |||
| 9dfcae2b5d | |||
| 66172cef3e | |||
| 29f022c91c | |||
| 485bfd6c08 | |||
| f7a73c5149 | |||
| 5d966b1120 | |||
| ce79144e75 | |||
| d8566f0ddf | |||
| e904c134e7 | |||
| 7fc3bb3241 | |||
| 20e63f8ec4 | |||
| 2df15742fc | |||
| 8f815a6c1e | |||
| 8f4bd10b19 | |||
| 511d272d0d | |||
| 7f44cf543a | |||
| 16472eb3ea | |||
| d92acdcf1d | |||
| 2e33ed3ba0 | |||
| 04ff9cda7c | |||
| 5cc9981a4d | |||
| 5845b2b137 | |||
| b3b54680e7 | |||
| a3ab5ba9ac | |||
| c552a02e7f | |||
| a005be7c74 | |||
| 6f7fcdc897 | |||
| 34761fa4ca | |||
| abe9995a7c | |||
| 7f2ee3bbe9 | |||
| a1ffc7fa2c | |||
| 70c6b5a7f9 | |||
| 1b80a693ba | |||
| e46a4d1875 | |||
| 5f4d2dc4fe | |||
| 62202b7eff | |||
| 1518824b0c | |||
| 0a7654c747 | |||
| d6db805885 | |||
| 570ad420a8 | |||
| ae5a877ed4 | |||
| 9945988e44 | |||
| 79b8210498 | |||
| c80d311474 | |||
| 64429578da | |||
| 92a4a095c9 | |||
| 2368d738ab | |||
| 833e3a08cd | |||
| 7a73fec2e5 | |||
| 2f8e0e66ef | |||
| 5b4b252895 | |||
| 9081150c2c | |||
| cb295ec106 | |||
| 4f5210352c | |||
| f98ec9034f | |||
| 4b8ecba32b | |||
| 892166ec24 | |||
| a411330b09 | |||
| 5a8ae4a289 | |||
| 3f16377412 | |||
| d3b37b0b70 | |||
| 01db00b587 | |||
| 25f07e8e29 | |||
| daa65199e8 | |||
| fc867cb959 | |||
| fb694143ee | |||
| a8280d9fd2 | |||
| aea553c3a8 | |||
| 57237634f1 | |||
| 604061c4a5 | |||
| c103dd2746 | |||
| e82e8fde13 | |||
| a44ed9626a | |||
| ff9c11c970 | |||
| 674d342761 | |||
| a246e5644b | |||
| 96f56a3c43 | |||
| 1b2f66fc11 | |||
| ca2de896c7 | |||
| 34abcf7704 | |||
| 4c0b79c4f6 | |||
| e11a74eed5 | |||
| 297b2d0ac9 | |||
| b16f16e19e | |||
| 35598c04ce | |||
| 09d1f7f333 | |||
| 240450ea52 | |||
| 1de3032650 | |||
| 41548bf019 | |||
| b68d349bd6 | |||
| f6bfe4d970 | |||
| cb2ae708f3 | |||
| d7f26786d4 | |||
| b05fab14f7 | |||
| e6da0c7c7b | |||
| ef89e3ebea | |||
| 8ede1c7bf5 | |||
| 6363d58e98 | |||
| c262011393 | |||
| dda1367ab2 | |||
| e4c9cf2264 | |||
| e3b3ec3f79 | |||
| 08d5637770 | |||
| 7bb28ca2bd | |||
| 9251fb39af | |||
| 91dbce30bd | |||
| 949a999478 | |||
| d40041cc82 | |||
| 832c90ac3e | |||
| 7b3099b1a1 | |||
| 4681638974 | |||
| ecf441c830 | |||
| d9c2a128a5 | |||
| 38e3475714 | |||
| 90644246d6 | |||
| 100c60017f | |||
| 51dd6d1f90 | |||
| 521ea6afcb | |||
| dd019e7ba1 | |||
| db1be22a2f | |||
| 139268de6f | |||
| f6ceb43e36 | |||
| d8a43416f5 | |||
| 4a6a2a0f1b | |||
| 9bbef8216d | |||
| 78856703c4 | |||
| 099c37ba95 | |||
| a44f1f735d | |||
| ae6f68e625 | |||
| 5dd19c6a57 | |||
| 5968f148bc | |||
| 4f962d6bff | |||
| ddb8be9219 | |||
| 422c229e52 | |||
| b5d1d2fec4 | |||
| d545633a6c | |||
| af0b4b0828 | |||
| 6c6380d27a | |||
| 2324b88579 | |||
| 2b0dc01a88 | |||
| 01acc3fd5a | |||
| 2484e26cb5 | |||
| 7195742ca5 | |||
| 62cb5f1bac | |||
| e7dd487779 | |||
| e41268efc6 |
86
.github/workflows/tests.yml
vendored
Normal file
86
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- '*.*.*'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.mdx'
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, labeled ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.mdx'
|
||||
|
||||
# https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ragflow_tests:
|
||||
name: ragflow_tests
|
||||
# https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution
|
||||
# https://github.com/orgs/community/discussions/26261
|
||||
if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci') }}
|
||||
runs-on: [ "self-hosted", "debug" ]
|
||||
steps:
|
||||
# https://github.com/hmarr/debug-action
|
||||
#- uses: hmarr/debug-action@v2
|
||||
|
||||
- name: Show PR labels
|
||||
run: |
|
||||
echo "Workflow triggered by ${{ github.event_name }}"
|
||||
if [[ ${{ github.event_name }} == 'pull_request' ]]; then
|
||||
echo "PR labels: ${{ join(github.event.pull_request.labels.*.name, ', ') }}"
|
||||
fi
|
||||
|
||||
- name: Ensure workspace ownership
|
||||
run: echo "chown -R $USER $GITHUB_WORKSPACE" && sudo chown -R $USER $GITHUB_WORKSPACE
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build ragflow:dev-slim
|
||||
run: |
|
||||
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-$HOME}
|
||||
cp -r ${RUNNER_WORKSPACE_PREFIX}/huggingface.co ${RUNNER_WORKSPACE_PREFIX}/nltk_data ${RUNNER_WORKSPACE_PREFIX}/libssl*.deb .
|
||||
sudo docker pull ubuntu:24.04
|
||||
sudo docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
|
||||
- name: Build ragflow:dev
|
||||
run: |
|
||||
sudo docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
|
||||
- name: Start ragflow:dev-slim
|
||||
run: |
|
||||
sudo docker compose -f docker/docker-compose.yml up -d
|
||||
|
||||
- name: Stop ragflow:dev-slim
|
||||
if: always() # always run this step even if previous steps failed
|
||||
run: |
|
||||
sudo docker compose -f docker/docker-compose.yml down -v
|
||||
|
||||
- name: Start ragflow:dev
|
||||
run: |
|
||||
echo "RAGFLOW_IMAGE=infiniflow/ragflow:dev" >> docker/.env
|
||||
sudo docker compose -f docker/docker-compose.yml up -d
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
|
||||
export HOST_ADDRESS=http://host.docker.internal:9380
|
||||
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
|
||||
echo "Waiting for service to be available..."
|
||||
sleep 5
|
||||
done
|
||||
cd sdk/python && poetry install && source .venv/bin/activate && cd test && pytest t_dataset.py t_chat.py t_session.py
|
||||
|
||||
- name: Stop ragflow:dev
|
||||
if: always() # always run this step even if previous steps failed
|
||||
run: |
|
||||
sudo docker compose -f docker/docker-compose.yml down -v
|
||||
@ -1,16 +1,10 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
slug: /contribution_guidelines
|
||||
---
|
||||
|
||||
# Contribution guidelines
|
||||
|
||||
Thanks for wanting to contribute to RAGFlow. This document offers guidlines and major considerations for submitting your contributions.
|
||||
This document offers guidlines and major considerations for submitting your contributions to RAGFlow.
|
||||
|
||||
- To report a bug, file a [GitHub issue](https://github.com/infiniflow/ragflow/issues/new/choose) with us.
|
||||
- For further questions, you can explore existing discussions or initiate a new one in [Discussions](https://github.com/orgs/infiniflow/discussions).
|
||||
|
||||
|
||||
## What you can contribute
|
||||
|
||||
The list below mentions some contributions you can make, but it is not a complete list.
|
||||
@ -42,6 +36,7 @@ The list below mentions some contributions you can make, but it is not a complet
|
||||
- Consider splitting a large PR into multiple smaller, standalone PRs to keep a traceable development history.
|
||||
- Ensure that your PR addresses just one issue, or keep any unrelated changes small.
|
||||
- Add test cases when contributing new features. They demonstrate that your code functions correctly and protect against potential issues from future changes.
|
||||
|
||||
### Describing your PR
|
||||
|
||||
- Ensure that your PR title is concise and clear, providing all the required information.
|
||||
@ -49,4 +44,5 @@ The list below mentions some contributions you can make, but it is not a complet
|
||||
- Include sufficient design details for *breaking changes* or *API changes* in your description.
|
||||
|
||||
### Reviewing & merging a PR
|
||||
- Ensure that your PR passes all Continuous Integration (CI) tests before merging it.
|
||||
|
||||
Ensure that your PR passes all Continuous Integration (CI) tests before merging it.
|
||||
118
Dockerfile
118
Dockerfile
@ -1,23 +1,117 @@
|
||||
FROM infiniflow/ragflow-base:v2.0
|
||||
# base stage
|
||||
FROM ubuntu:24.04 AS base
|
||||
USER root
|
||||
|
||||
ARG ARCH=amd64
|
||||
ENV LIGHTEN=0
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_base_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt-get --no-install-recommends install -y ca-certificates
|
||||
|
||||
# If you download Python modules too slow, you can use a pip mirror site to speed up apt and poetry
|
||||
RUN sed -i 's|http://archive.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
ENV POETRY_PYPI_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_base_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y curl libpython3-dev nginx libglib2.0-0 libglx-mesa0 pkg-config libicu-dev libgdiplus python3-pip python3-poetry \
|
||||
&& pip3 install --user --break-system-packages poetry-plugin-pypi-mirror --index-url https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# https://forum.aspose.com/t/aspose-slides-for-net-no-usable-version-of-libssl-found-with-linux-server/271344/13
|
||||
# aspose-slides on linux/arm64 is unavailable
|
||||
RUN --mount=type=bind,source=libssl1.1_1.1.1f-1ubuntu2_amd64.deb,target=/root/libssl1.1_1.1.1f-1ubuntu2_amd64.deb \
|
||||
if [ "${ARCH}" = "amd64" ]; then \
|
||||
dpkg -i /root/libssl1.1_1.1.1f-1ubuntu2_amd64.deb; \
|
||||
fi
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
# Configure Poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
ENV POETRY_VIRTUALENVS_CREATE=true
|
||||
ENV POETRY_REQUESTS_TIMEOUT=15
|
||||
|
||||
# builder stage
|
||||
FROM base AS builder
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
ADD ./web ./web
|
||||
RUN cd ./web && npm i --force && npm run build
|
||||
RUN --mount=type=cache,id=ragflow_builder_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y nodejs npm cargo && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD ./api ./api
|
||||
ADD ./conf ./conf
|
||||
ADD ./deepdoc ./deepdoc
|
||||
ADD ./rag ./rag
|
||||
ADD ./agent ./agent
|
||||
ADD ./graphrag ./graphrag
|
||||
COPY web web
|
||||
COPY docs docs
|
||||
RUN --mount=type=cache,id=ragflow_builder_npm,target=/root/.npm,sharing=locked \
|
||||
cd web && npm i --force && npm run build
|
||||
|
||||
# install dependencies from poetry.lock file
|
||||
COPY pyproject.toml poetry.toml poetry.lock ./
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_builder_poetry,target=/root/.cache/pypoetry,sharing=locked \
|
||||
if [ "$LIGHTEN" -eq 0 ]; then \
|
||||
poetry install --sync --no-root --with=full; \
|
||||
else \
|
||||
poetry install --sync --no-root; \
|
||||
fi
|
||||
|
||||
# production stage
|
||||
FROM base AS production
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
# Install python packages' dependencies
|
||||
# cv2 requires libGL.so.1
|
||||
RUN --mount=type=cache,id=ragflow_production_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y --no-install-recommends nginx libgl1 vim less && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY web web
|
||||
COPY api api
|
||||
COPY conf conf
|
||||
COPY deepdoc deepdoc
|
||||
COPY rag rag
|
||||
COPY agent agent
|
||||
COPY graphrag graphrag
|
||||
COPY pyproject.toml poetry.toml poetry.lock ./
|
||||
|
||||
# Copy models downloaded via download_deps.py
|
||||
RUN mkdir -p /ragflow/rag/res/deepdoc /root/.ragflow
|
||||
RUN --mount=type=bind,source=huggingface.co,target=/huggingface.co \
|
||||
tar --exclude='.*' -cf - \
|
||||
/huggingface.co/InfiniFlow/text_concat_xgb_v1.0 \
|
||||
/huggingface.co/InfiniFlow/deepdoc \
|
||||
| tar -xf - --strip-components=3 -C /ragflow/rag/res/deepdoc
|
||||
RUN --mount=type=bind,source=huggingface.co,target=/huggingface.co \
|
||||
tar -cf - \
|
||||
/huggingface.co/BAAI/bge-large-zh-v1.5 \
|
||||
/huggingface.co/BAAI/bge-reranker-v2-m3 \
|
||||
/huggingface.co/maidalun1020/bce-embedding-base_v1 \
|
||||
/huggingface.co/maidalun1020/bce-reranker-base_v1 \
|
||||
| tar -xf - --strip-components=2 -C /root/.ragflow
|
||||
|
||||
# Copy nltk data downloaded via download_deps.py
|
||||
COPY nltk_data /root/nltk_data
|
||||
|
||||
# Copy compiled web pages
|
||||
COPY --from=builder /ragflow/web/dist /ragflow/web/dist
|
||||
|
||||
# Copy Python environment and packages
|
||||
ENV VIRTUAL_ENV=/ragflow/.venv
|
||||
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
||||
|
||||
ENV PYTHONPATH=/ragflow/
|
||||
ENV HF_ENDPOINT=https://hf-mirror.com
|
||||
|
||||
ADD docker/entrypoint.sh ./entrypoint.sh
|
||||
ADD docker/.env ./
|
||||
COPY docker/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@ -1,43 +0,0 @@
|
||||
FROM python:3.11
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
COPY requirements_arm.txt /ragflow/requirements.txt
|
||||
|
||||
|
||||
RUN pip install nltk --default-timeout=10000
|
||||
|
||||
RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ --default-timeout=1000 -r requirements.txt &&\
|
||||
python -c "import nltk;nltk.download('punkt');nltk.download('wordnet')"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl gnupg && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --fix-missing nodejs nginx ffmpeg libsm6 libxext6 libgl1
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN pip install graspologic
|
||||
|
||||
ADD ./web ./web
|
||||
RUN cd ./web && npm i --force && npm run build
|
||||
|
||||
ADD ./api ./api
|
||||
ADD ./conf ./conf
|
||||
ADD ./deepdoc ./deepdoc
|
||||
ADD ./rag ./rag
|
||||
ADD ./agent ./agent
|
||||
ADD ./graphrag ./graphrag
|
||||
|
||||
ENV PYTHONPATH=/ragflow/
|
||||
ENV HF_ENDPOINT=https://hf-mirror.com
|
||||
|
||||
ADD docker/entrypoint.sh ./entrypoint.sh
|
||||
ADD docker/.env ./
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@ -1,27 +0,0 @@
|
||||
FROM infiniflow/ragflow-base:v2.0
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
## for cuda > 12.0
|
||||
RUN pip uninstall -y onnxruntime-gpu
|
||||
RUN pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
|
||||
|
||||
ADD ./web ./web
|
||||
RUN cd ./web && npm i --force && npm run build
|
||||
|
||||
ADD ./api ./api
|
||||
ADD ./conf ./conf
|
||||
ADD ./deepdoc ./deepdoc
|
||||
ADD ./rag ./rag
|
||||
ADD ./agent ./agent
|
||||
ADD ./graphrag ./graphrag
|
||||
|
||||
ENV PYTHONPATH=/ragflow/
|
||||
ENV HF_ENDPOINT=https://hf-mirror.com
|
||||
|
||||
ADD docker/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@ -1,56 +0,0 @@
|
||||
FROM ubuntu:22.04
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
RUN apt-get update && apt-get install -y wget curl build-essential libopenmpi-dev
|
||||
|
||||
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
|
||||
bash ~/miniconda.sh -b -p /root/miniconda3 && \
|
||||
rm ~/miniconda.sh && ln -s /root/miniconda3/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
|
||||
echo ". /root/miniconda3/etc/profile.d/conda.sh" >> ~/.bashrc && \
|
||||
echo "conda activate base" >> ~/.bashrc
|
||||
|
||||
ENV PATH /root/miniconda3/bin:$PATH
|
||||
|
||||
RUN conda create -y --name py11 python=3.11
|
||||
|
||||
ENV CONDA_DEFAULT_ENV py11
|
||||
ENV CONDA_PREFIX /root/miniconda3/envs/py11
|
||||
ENV PATH $CONDA_PREFIX/bin:$PATH
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
RUN apt-get install -y nginx
|
||||
|
||||
ADD ./web ./web
|
||||
ADD ./api ./api
|
||||
ADD ./conf ./conf
|
||||
ADD ./deepdoc ./deepdoc
|
||||
ADD ./rag ./rag
|
||||
ADD ./requirements.txt ./requirements.txt
|
||||
ADD ./agent ./agent
|
||||
ADD ./graphrag ./graphrag
|
||||
|
||||
RUN apt install openmpi-bin openmpi-common libopenmpi-dev
|
||||
ENV LD_LIBRARY_PATH /usr/lib/x86_64-linux-gnu/openmpi/lib:$LD_LIBRARY_PATH
|
||||
RUN rm /root/miniconda3/envs/py11/compiler_compat/ld
|
||||
RUN cd ./web && npm i --force && npm run build
|
||||
RUN conda run -n py11 pip install -i https://mirrors.aliyun.com/pypi/simple/ -r ./requirements.txt
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libglib2.0-0 libgl1-mesa-glx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN conda run -n py11 pip install -i https://mirrors.aliyun.com/pypi/simple/ ollama
|
||||
RUN conda run -n py11 python -m nltk.downloader punkt
|
||||
RUN conda run -n py11 python -m nltk.downloader wordnet
|
||||
|
||||
ENV PYTHONPATH=/ragflow/
|
||||
ENV HF_ENDPOINT=https://hf-mirror.com
|
||||
|
||||
ADD docker/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@ -26,6 +26,7 @@ RUN dnf install -y nginx
|
||||
|
||||
ADD ./web ./web
|
||||
ADD ./api ./api
|
||||
ADD ./docs ./docs
|
||||
ADD ./conf ./conf
|
||||
ADD ./deepdoc ./deepdoc
|
||||
ADD ./rag ./rag
|
||||
@ -37,7 +38,7 @@ RUN dnf install -y openmpi openmpi-devel python3-openmpi
|
||||
ENV C_INCLUDE_PATH /usr/include/openmpi-x86_64:$C_INCLUDE_PATH
|
||||
ENV LD_LIBRARY_PATH /usr/lib64/openmpi/lib:$LD_LIBRARY_PATH
|
||||
RUN rm /root/miniconda3/envs/py11/compiler_compat/ld
|
||||
RUN cd ./web && npm i --force && npm run build
|
||||
RUN cd ./web && npm i && npm run build
|
||||
RUN conda run -n py11 pip install $(grep -ivE "mpi4py" ./requirements.txt) # without mpi4py==3.1.5
|
||||
RUN conda run -n py11 pip install redis
|
||||
|
||||
|
||||
109
Dockerfile.slim
Normal file
109
Dockerfile.slim
Normal file
@ -0,0 +1,109 @@
|
||||
# base stage
|
||||
FROM ubuntu:24.04 AS base
|
||||
USER root
|
||||
|
||||
ARG ARCH=amd64
|
||||
ENV LIGHTEN=1
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_base_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt-get --no-install-recommends install -y ca-certificates
|
||||
|
||||
# If you download Python modules too slow, you can use a pip mirror site to speed up apt and poetry
|
||||
RUN sed -i 's|http://archive.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/ubuntu.sources
|
||||
ENV POETRY_PYPI_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_base_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y curl libpython3-dev nginx libglib2.0-0 libglx-mesa0 pkg-config libicu-dev libgdiplus python3-pip python3-poetry \
|
||||
&& pip3 install --user --break-system-packages poetry-plugin-pypi-mirror --index-url https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# https://forum.aspose.com/t/aspose-slides-for-net-no-usable-version-of-libssl-found-with-linux-server/271344/13
|
||||
# aspose-slides on linux/arm64 is unavailable
|
||||
RUN if [ "${ARCH}" = "amd64" ]; then \
|
||||
curl -o libssl1.deb http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb && dpkg -i libssl1.deb && rm -f libssl1.deb; \
|
||||
fi
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
# Configure Poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
ENV POETRY_VIRTUALENVS_CREATE=true
|
||||
ENV POETRY_REQUESTS_TIMEOUT=15
|
||||
|
||||
# builder stage
|
||||
FROM base AS builder
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_builder_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y nodejs npm cargo && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY web web
|
||||
COPY docs docs
|
||||
RUN --mount=type=cache,id=ragflow_builder_npm,target=/root/.npm,sharing=locked \
|
||||
cd web && npm i && npm run build
|
||||
|
||||
# install dependencies from poetry.lock file
|
||||
COPY pyproject.toml poetry.toml poetry.lock ./
|
||||
|
||||
RUN --mount=type=cache,id=ragflow_builder_poetry,target=/root/.cache/pypoetry,sharing=locked \
|
||||
if [ "$LIGHTEN" -eq 0 ]; then \
|
||||
poetry install --sync --no-root --with=full; \
|
||||
else \
|
||||
poetry install --sync --no-root; \
|
||||
fi
|
||||
|
||||
# production stage
|
||||
FROM base AS production
|
||||
USER root
|
||||
|
||||
WORKDIR /ragflow
|
||||
|
||||
# Install python packages' dependencies
|
||||
# cv2 requires libGL.so.1
|
||||
RUN --mount=type=cache,id=ragflow_production_apt,target=/var/cache/apt,sharing=locked \
|
||||
apt update && apt install -y --no-install-recommends nginx libgl1 vim less && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY web web
|
||||
COPY api api
|
||||
COPY conf conf
|
||||
COPY deepdoc deepdoc
|
||||
COPY rag rag
|
||||
COPY agent agent
|
||||
COPY graphrag graphrag
|
||||
COPY pyproject.toml poetry.toml poetry.lock ./
|
||||
|
||||
# Copy models downloaded via download_deps.py
|
||||
RUN mkdir -p /ragflow/rag/res/deepdoc /root/.ragflow
|
||||
RUN --mount=type=bind,source=huggingface.co,target=/huggingface.co \
|
||||
tar --exclude='.*' -cf - \
|
||||
/huggingface.co/InfiniFlow/text_concat_xgb_v1.0 \
|
||||
/huggingface.co/InfiniFlow/deepdoc \
|
||||
| tar -xf - --strip-components=3 -C /ragflow/rag/res/deepdoc
|
||||
|
||||
# Copy nltk data downloaded via download_deps.py
|
||||
COPY nltk_data /root/nltk_data
|
||||
|
||||
# Copy compiled web pages
|
||||
COPY --from=builder /ragflow/web/dist /ragflow/web/dist
|
||||
|
||||
# Copy Python environment and packages
|
||||
ENV VIRTUAL_ENV=/ragflow/.venv
|
||||
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
||||
|
||||
ENV PYTHONPATH=/ragflow/
|
||||
|
||||
COPY docker/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
232
README.md
232
README.md
@ -12,13 +12,18 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=infiniflowai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/infiniflow?logo=X&color=%20%23f5f5f5" alt="follow on X(Twitter)">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.13.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.13.0">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99"></a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.11.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.11.0"></a>
|
||||
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="license">
|
||||
</a>
|
||||
@ -42,8 +47,9 @@
|
||||
- 🔎 [System Architecture](#-system-architecture)
|
||||
- 🎬 [Get Started](#-get-started)
|
||||
- 🔧 [Configurations](#-configurations)
|
||||
- 🛠️ [Build from source](#-build-from-source)
|
||||
- 🛠️ [Launch service from source](#-launch-service-from-source)
|
||||
- 🔧 [Build a docker image without embedding models](#-build-a-docker-image-without-embedding-models)
|
||||
- 🔧 [Build a docker image including embedding models](#-build-a-docker-image-including-embedding-models)
|
||||
- 🔨 [Launch service from source for development](#-launch-service-from-source-for-development)
|
||||
- 📚 [Documentation](#-documentation)
|
||||
- 📜 [Roadmap](#-roadmap)
|
||||
- 🏄 [Community](#-community)
|
||||
@ -53,7 +59,10 @@
|
||||
|
||||
## 💡 What is RAGFlow?
|
||||
|
||||
[RAGFlow](https://ragflow.io/) is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding. It offers a streamlined RAG workflow for businesses of any scale, combining LLM (Large Language Models) to provide truthful question-answering capabilities, backed by well-founded citations from various complex formatted data.
|
||||
[RAGFlow](https://ragflow.io/) is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document
|
||||
understanding. It offers a streamlined RAG workflow for businesses of any scale, combining LLM (Large Language Models)
|
||||
to provide truthful question-answering capabilities, backed by well-founded citations from various complex formatted
|
||||
data.
|
||||
|
||||
## 🎮 Demo
|
||||
|
||||
@ -63,24 +72,28 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
<img src="https://github.com/infiniflow/ragflow/assets/12318111/b083d173-dadc-4ea9-bdeb-180d7df514eb" width="1200"/>
|
||||
</div>
|
||||
|
||||
|
||||
## 🔥 Latest Updates
|
||||
|
||||
- 2024-09-29 Optimizes multi-round conversations.
|
||||
- 2024-09-13 Adds search mode for knowledge base Q&A.
|
||||
- 2024-09-09 Adds a medical consultant agent template.
|
||||
- 2024-08-22 Support text to SQL statements through RAG.
|
||||
- 2024-08-02 Supports GraphRAG inspired by [graphrag](https://github.com/microsoft/graphrag) and mind map.
|
||||
- 2024-07-23 Supports audio file parsing.
|
||||
- 2024-07-08 Supports workflow based on [Graph](./agent/README.md).
|
||||
- 2024-06-27 Supports Markdown and Docx in the Q&A parsing method, extracting images from Docx files, extracting tables from Markdown files.
|
||||
- 2024-05-23 Supports [RAPTOR](https://arxiv.org/html/2401.18059v1) for better text retrieval.
|
||||
|
||||
## 🎉 Stay Tuned
|
||||
|
||||
⭐️ Star our repository to stay up-to-date with exciting new features and improvements! Get instant notifications for new
|
||||
releases! 🌟
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/18c9707e-b8aa-4caf-a154-037089c105ba" width="1200"/>
|
||||
</div>
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
### 🍭 **"Quality in, quality out"**
|
||||
|
||||
- [Deep document understanding](./deepdoc/README.md)-based knowledge extraction from unstructured data with complicated formats.
|
||||
- [Deep document understanding](./deepdoc/README.md)-based knowledge extraction from unstructured data with complicated
|
||||
formats.
|
||||
- Finds "needle in a data haystack" of literally unlimited tokens.
|
||||
|
||||
### 🍱 **Template-based chunking**
|
||||
@ -118,7 +131,8 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
- RAM >= 16 GB
|
||||
- Disk >= 50 GB
|
||||
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
|
||||
> If you have not installed Docker on your local machine (Windows, Mac, or Linux), see [Install Docker Engine](https://docs.docker.com/engine/install/).
|
||||
> If you have not installed Docker on your local machine (Windows, Mac, or Linux),
|
||||
see [Install Docker Engine](https://docs.docker.com/engine/install/).
|
||||
|
||||
### 🚀 Start up the server
|
||||
|
||||
@ -137,7 +151,8 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
> $ sudo sysctl -w vm.max_map_count=262144
|
||||
> ```
|
||||
>
|
||||
> This change will be reset after a system reboot. To ensure your change remains permanent, add or update the `vm.max_map_count` value in **/etc/sysctl.conf** accordingly:
|
||||
> This change will be reset after a system reboot. To ensure your change remains permanent, add or update the
|
||||
`vm.max_map_count` value in **/etc/sysctl.conf** accordingly:
|
||||
>
|
||||
> ```bash
|
||||
> vm.max_map_count=262144
|
||||
@ -151,16 +166,27 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
|
||||
3. Build the pre-built Docker images and start up the server:
|
||||
|
||||
> Running the following commands automatically downloads the *dev* version RAGFlow Docker image. To download and run a specified Docker version, update `RAGFLOW_VERSION` in **docker/.env** to the intended version, for example `RAGFLOW_VERSION=v0.11.0`, before running the following commands.
|
||||
> The command below downloads the dev version Docker image for RAGFlow slim (`dev-slim`). Note that RAGFlow slim
|
||||
Docker images do not include embedding models or Python libraries and hence are approximately 1GB in size.
|
||||
|
||||
```bash
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> - To download a RAGFlow slim Docker image of a specific version, update the `RAGFlow_IMAGE` variable in *
|
||||
*docker/.env** to your desired version. For example, `RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0-slim`. After
|
||||
making this change, rerun the command above to initiate the download.
|
||||
> - To download the dev version of RAGFlow Docker image *including* embedding models and Python libraries, update the
|
||||
`RAGFlow_IMAGE` variable in **docker/.env** to `RAGFLOW_IMAGE=infiniflow/ragflow:dev`. After making this change,
|
||||
rerun the command above to initiate the download.
|
||||
> - To download a specific version of RAGFlow Docker image *including* embedding models and Python libraries, update
|
||||
the `RAGFlow_IMAGE` variable in **docker/.env** to your desired version. For example,
|
||||
`RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0`. After making this change, rerun the command above to initiate the
|
||||
download.
|
||||
|
||||
> The core image is about 9 GB in size and may take a while to load.
|
||||
> **NOTE:** A RAGFlow Docker image that includes embedding models and Python libraries is approximately 9GB in size
|
||||
and may take significantly longer time to load.
|
||||
|
||||
4. Check the server status after having the server up and running:
|
||||
|
||||
@ -171,159 +197,140 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
_The following output confirms a successful launch of the system:_
|
||||
|
||||
```bash
|
||||
____ ______ __
|
||||
/ __ \ ____ _ ____ _ / ____// /____ _ __
|
||||
/ /_/ // __ `// __ `// /_ / // __ \| | /| / /
|
||||
/ _, _// /_/ // /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_| \__,_/ \__, //_/ /_/ \____/ |__/|__/
|
||||
/____/
|
||||
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:9380
|
||||
* Running on http://x.x.x.x:9380
|
||||
INFO:werkzeug:Press CTRL+C to quit
|
||||
```
|
||||
> If you skip this confirmation step and directly log in to RAGFlow, your browser may prompt a `network abnormal` error because, at that moment, your RAGFlow may not be fully initialized.
|
||||
> If you skip this confirmation step and directly log in to RAGFlow, your browser may prompt a `network abnormal`
|
||||
error because, at that moment, your RAGFlow may not be fully initialized.
|
||||
|
||||
5. In your web browser, enter the IP address of your server and log in to RAGFlow.
|
||||
> With the default settings, you only need to enter `http://IP_OF_YOUR_MACHINE` (**sans** port number) as the default HTTP serving port `80` can be omitted when using the default configurations.
|
||||
6. In [service_conf.yaml](./docker/service_conf.yaml), select the desired LLM factory in `user_default_llm` and update the `API_KEY` field with the corresponding API key.
|
||||
> With the default settings, you only need to enter `http://IP_OF_YOUR_MACHINE` (**sans** port number) as the default
|
||||
HTTP serving port `80` can be omitted when using the default configurations.
|
||||
6. In [service_conf.yaml](./docker/service_conf.yaml), select the desired LLM factory in `user_default_llm` and update
|
||||
the `API_KEY` field with the corresponding API key.
|
||||
|
||||
> See [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) for more information.
|
||||
|
||||
_The show is now on!_
|
||||
_The show is on!_
|
||||
|
||||
## 🔧 Configurations
|
||||
|
||||
When it comes to system configurations, you will need to manage the following files:
|
||||
|
||||
- [.env](./docker/.env): Keeps the fundamental setups for the system, such as `SVR_HTTP_PORT`, `MYSQL_PASSWORD`, and `MINIO_PASSWORD`.
|
||||
- [.env](./docker/.env): Keeps the fundamental setups for the system, such as `SVR_HTTP_PORT`, `MYSQL_PASSWORD`, and
|
||||
`MINIO_PASSWORD`.
|
||||
- [service_conf.yaml](./docker/service_conf.yaml): Configures the back-end services.
|
||||
- [docker-compose.yml](./docker/docker-compose.yml): The system relies on [docker-compose.yml](./docker/docker-compose.yml) to start up.
|
||||
- [docker-compose.yml](./docker/docker-compose.yml): The system relies
|
||||
on [docker-compose.yml](./docker/docker-compose.yml) to start up.
|
||||
|
||||
You must ensure that changes to the [.env](./docker/.env) file are in line with what are in the [service_conf.yaml](./docker/service_conf.yaml) file.
|
||||
You must ensure that changes to the [.env](./docker/.env) file are in line with what are in
|
||||
the [service_conf.yaml](./docker/service_conf.yaml) file.
|
||||
|
||||
> The [./docker/README](./docker/README.md) file provides a detailed description of the environment settings and service configurations, and you are REQUIRED to ensure that all environment settings listed in the [./docker/README](./docker/README.md) file are aligned with the corresponding configurations in the [service_conf.yaml](./docker/service_conf.yaml) file.
|
||||
> The [./docker/README](./docker/README.md) file provides a detailed description of the environment settings and service
|
||||
> configurations, and you are REQUIRED to ensure that all environment settings listed in
|
||||
> the [./docker/README](./docker/README.md) file are aligned with the corresponding configurations in
|
||||
> the [service_conf.yaml](./docker/service_conf.yaml) file.
|
||||
|
||||
To update the default HTTP serving port (80), go to [docker-compose.yml](./docker/docker-compose.yml) and change `80:80` to `<YOUR_SERVING_PORT>:80`.
|
||||
To update the default HTTP serving port (80), go to [docker-compose.yml](./docker/docker-compose.yml) and change `80:80`
|
||||
to `<YOUR_SERVING_PORT>:80`.
|
||||
|
||||
Updates to the above configurations require a reboot of all containers to take effect:
|
||||
|
||||
> Updates to all system configurations require a system reboot to take effect:
|
||||
>
|
||||
> ```bash
|
||||
> $ docker-compose up -d
|
||||
> $ docker compose -f docker/docker-compose.yml up -d
|
||||
> ```
|
||||
|
||||
## 🛠️ Build from source
|
||||
## 🔧 Build a Docker image without embedding models
|
||||
|
||||
To build the Docker images from source:
|
||||
This image is approximately 1 GB in size and relies on external LLM and embedding services.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
$ docker build -t infiniflow/ragflow:dev .
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
```
|
||||
|
||||
## 🛠️ Launch service from source
|
||||
## 🔧 Build a Docker image including embedding models
|
||||
|
||||
To launch the service from source:
|
||||
|
||||
1. Clone the repository:
|
||||
This image is approximately 9 GB in size. As it includes embedding models, it relies on external LLM services only.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
```
|
||||
|
||||
2. Create a virtual environment, ensuring that Anaconda or Miniconda is installed:
|
||||
## 🔨 Launch service from source for development
|
||||
|
||||
1. Install Poetry, or skip this step if it is already installed:
|
||||
```bash
|
||||
$ conda create -n ragflow python=3.11.0
|
||||
$ conda activate ragflow
|
||||
$ pip install -r requirements.txt
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
2. Clone the source code and install Python dependencies:
|
||||
```bash
|
||||
# If your CUDA version is higher than 12.0, run the following additional commands:
|
||||
$ pip uninstall -y onnxruntime-gpu
|
||||
$ pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
export POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
~/.local/bin/poetry install --sync --no-root # install RAGFlow dependent python modules
|
||||
```
|
||||
|
||||
3. Copy the entry script and configure environment variables:
|
||||
|
||||
3. Launch the dependent services (MinIO, Elasticsearch, Redis, and MySQL) using Docker Compose:
|
||||
```bash
|
||||
# Get the Python path:
|
||||
$ which python
|
||||
# Get the ragflow project path:
|
||||
$ pwd
|
||||
docker compose -f docker/docker-compose-base.yml up -d
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cp docker/entrypoint.sh .
|
||||
$ vi entrypoint.sh
|
||||
Add the following line to `/etc/hosts` to resolve all hosts specified in **docker/service_conf.yaml** to `127.0.0.1`:
|
||||
```
|
||||
127.0.0.1 es01 mysql minio redis
|
||||
```
|
||||
In **docker/service_conf.yaml**, update mysql port to `5455` and es port to `1200`, as specified in **docker/.env**.
|
||||
|
||||
4. If you cannot access HuggingFace, set the `HF_ENDPOINT` environment variable to use a mirror site:
|
||||
|
||||
```bash
|
||||
# Adjust configurations according to your actual situation (the following two export commands are newly added):
|
||||
# - Assign the result of `which python` to `PY`.
|
||||
# - Assign the result of `pwd` to `PYTHONPATH`.
|
||||
# - Comment out `LD_LIBRARY_PATH`, if it is configured.
|
||||
# - Optional: Add Hugging Face mirror.
|
||||
PY=${PY}
|
||||
export PYTHONPATH=${PYTHONPATH}
|
||||
export HF_ENDPOINT=https://hf-mirror.com
|
||||
```
|
||||
|
||||
4. Launch the third-party services (MinIO, Elasticsearch, Redis, and MySQL):
|
||||
|
||||
5. Launch backend service:
|
||||
```bash
|
||||
$ cd docker
|
||||
$ docker compose -f docker-compose-base.yml up -d
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
bash docker/launch_backend_service.sh
|
||||
```
|
||||
|
||||
5. Check the configuration files, ensuring that:
|
||||
|
||||
- The settings in **docker/.env** match those in **conf/service_conf.yaml**.
|
||||
- The IP addresses and ports for related services in **service_conf.yaml** match the local machine IP and ports exposed by the container.
|
||||
|
||||
6. Launch the RAGFlow backend service:
|
||||
|
||||
6. Install frontend dependencies:
|
||||
```bash
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ bash ./entrypoint.sh
|
||||
cd web
|
||||
npm install --force
|
||||
```
|
||||
7. Configure frontend to update `proxy.target` in **.umirc.ts** to `http://127.0.0.1:9380`:
|
||||
8. Launch frontend service:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
7. Launch the frontend service:
|
||||
_The following output confirms a successful launch of the system:_
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ vim .umirc.ts
|
||||
# Update proxy.target to http://127.0.0.1:9380
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
8. Deploy the frontend service:
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ umi build
|
||||
$ mkdir -p /ragflow/web
|
||||
$ cp -r dist /ragflow/web
|
||||
$ apt install nginx -y
|
||||
$ cp ../docker/nginx/proxy.conf /etc/nginx
|
||||
$ cp ../docker/nginx/nginx.conf /etc/nginx
|
||||
$ cp ../docker/nginx/ragflow.conf /etc/nginx/conf.d
|
||||
$ systemctl start nginx
|
||||
```
|
||||

|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [Quickstart](https://ragflow.io/docs/dev/)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/user-guides)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/guides)
|
||||
- [References](https://ragflow.io/docs/dev/category/references)
|
||||
- [FAQ](https://ragflow.io/docs/dev/faq)
|
||||
|
||||
@ -339,4 +346,5 @@ See the [RAGFlow Roadmap 2024](https://github.com/infiniflow/ragflow/issues/162)
|
||||
|
||||
## 🙌 Contributing
|
||||
|
||||
RAGFlow flourishes via open-source collaboration. In this spirit, we embrace diverse contributions from the community. If you would like to be a part, review our [Contribution Guidelines](./docs/references/CONTRIBUTING.md) first.
|
||||
RAGFlow flourishes via open-source collaboration. In this spirit, we embrace diverse contributions from the community.
|
||||
If you would like to be a part, review our [Contribution Guidelines](./CONTRIBUTING.md) first.
|
||||
|
||||
148
README_ja.md
148
README_ja.md
@ -12,19 +12,24 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=infiniflowai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/infiniflow?logo=X&color=%20%23f5f5f5" alt="follow on X(Twitter)">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.13.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.13.0">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99"></a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.11.0-brightgreen"
|
||||
alt="docker pull infiniflow/ragflow:v0.11.0"></a>
|
||||
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://ragflow.io/docs/dev/">Document</a> |
|
||||
<a href="https://github.com/infiniflow/ragflow/issues/162">Roadmap</a> |
|
||||
@ -48,15 +53,17 @@
|
||||
|
||||
## 🔥 最新情報
|
||||
|
||||
- 2024-09-29 マルチラウンドダイアログを最適化。
|
||||
- 2024-09-13 ナレッジベース Q&A の検索モードを追加しました。
|
||||
- 2024-09-09 エージェントに医療相談テンプレートを追加しました。
|
||||
- 2024-08-22 RAG を介して SQL ステートメントへのテキストをサポートします。
|
||||
- 2024-08-02 [graphrag](https://github.com/microsoft/graphrag) からインスピレーションを得た GraphRAG とマインド マップをサポートします。
|
||||
- 2024-07-23 音声ファイルの解析をサポートしました。
|
||||
- 2024-07-08 [Graph](./agent/README.md) ベースのワークフローをサポート
|
||||
- 2024-06-27 Q&A 解析メソッドで Markdown と Docx をサポートし、Docx ファイルから画像を抽出し、Markdown ファイルからテーブルを抽出します。
|
||||
- 2024-05-23 より良いテキスト検索のために [RAPTOR](https://arxiv.org/html/2401.18059v1) をサポート。
|
||||
|
||||
## 🎉 続きを楽しみに
|
||||
⭐️ リポジトリをスター登録して、エキサイティングな新機能やアップデートを最新の状態に保ちましょう!すべての新しいリリースに関する即時通知を受け取れます! 🌟
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/18c9707e-b8aa-4caf-a154-037089c105ba" width="1200"/>
|
||||
</div>
|
||||
|
||||
## 🌟 主な特徴
|
||||
|
||||
@ -133,15 +140,18 @@
|
||||
|
||||
3. ビルド済みの Docker イメージをビルドし、サーバーを起動する:
|
||||
|
||||
> 以下のコマンドは、RAGFlow slim(`dev-slim`)の開発版Dockerイメージをダウンロードします。RAGFlow slimのDockerイメージには、埋め込みモデルやPythonライブラリが含まれていないため、サイズは約1GBです。
|
||||
|
||||
```bash
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> 上記のコマンドを実行すると、RAGFlowの開発版dockerイメージが自動的にダウンロードされます。 特定のバージョンのDockerイメージをダウンロードして実行したい場合は、docker/.envファイルのRAGFLOW_VERSION変数を見つけて、対応するバージョンに変更してください。 例えば、RAGFLOW_VERSION=v0.11.0として、上記のコマンドを実行してください。
|
||||
> - 特定のバージョンのRAGFlow slim Dockerイメージをダウンロードするには、**docker/.env**内の`RAGFlow_IMAGE`変数を希望のバージョンに更新します。例えば、`RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0`とします。この変更を行った後、上記のコマンドを再実行してダウンロードを開始してください。
|
||||
> - RAGFlowの埋め込みモデルとPythonライブラリを含む開発版Dockerイメージをダウンロードするには、**docker/.env**内の`RAGFlow_IMAGE`変数を`RAGFLOW_IMAGE=infiniflow/ragflow:dev`に更新します。この変更を行った後、上記のコマンドを再実行してダウンロードを開始してください。
|
||||
> - 特定のバージョンのRAGFlow Dockerイメージ(埋め込みモデルとPythonライブラリを含む)をダウンロードするには、**docker/.env**内の`RAGFlow_IMAGE`変数を希望のバージョンに更新します。例えば、`RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0`とします。この変更を行った後、上記のコマンドを再実行してダウンロードを開始してください。
|
||||
|
||||
> コアイメージのサイズは約 9 GB で、ロードに時間がかかる場合があります。
|
||||
> **NOTE:** 埋め込みモデルとPythonライブラリを含むRAGFlow Dockerイメージのサイズは約9GBであり、読み込みにかなりの時間がかかる場合があります。
|
||||
|
||||
4. サーバーを立ち上げた後、サーバーの状態を確認する:
|
||||
|
||||
@ -152,12 +162,11 @@
|
||||
_以下の出力は、システムが正常に起動したことを確認するものです:_
|
||||
|
||||
```bash
|
||||
____ ______ __
|
||||
/ __ \ ____ _ ____ _ / ____// /____ _ __
|
||||
/ /_/ // __ `// __ `// /_ / // __ \| | /| / /
|
||||
/ _, _// /_/ // /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_| \__,_/ \__, //_/ /_/ \____/ |__/|__/
|
||||
/____/
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:9380
|
||||
@ -191,86 +200,91 @@
|
||||
> すべてのシステム設定のアップデートを有効にするには、システムの再起動が必要です:
|
||||
>
|
||||
> ```bash
|
||||
> $ docker-compose up -d
|
||||
> $ docker compose -f docker/docker-compose.yml up -d
|
||||
> ```
|
||||
|
||||
## 🛠️ ソースからビルドする
|
||||
## 🔧 ソースコードでDockerイメージを作成(埋め込みモデルなし)
|
||||
|
||||
ソースからDockerイメージをビルドするには:
|
||||
この Docker イメージのサイズは約 1GB で、外部の大モデルと埋め込みサービスに依存しています。
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
$ docker build -t infiniflow/ragflow:v0.11.0 .
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
```
|
||||
|
||||
## 🛠️ ソースコードからサービスを起動する方法
|
||||
## 🔧 ソースコードをコンパイルしたDockerイメージ(埋め込みモデルを含む)
|
||||
|
||||
ソースコードからサービスを起動する場合は、以下の手順に従ってください:
|
||||
この Docker のサイズは約 9GB で、埋め込みモデルを含むため、外部の大モデルサービスのみが必要です。
|
||||
|
||||
1. リポジトリをクローンします
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
```
|
||||
|
||||
2. 仮想環境を作成します(AnacondaまたはMinicondaがインストールされていることを確認してください)
|
||||
## 🔨 ソースコードからサービスを起動する方法
|
||||
|
||||
1. Poetry をインストールする。すでにインストールされている場合は、このステップをスキップしてください:
|
||||
```bash
|
||||
$ conda create -n ragflow python=3.11.0
|
||||
$ conda activate ragflow
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
CUDAのバージョンが12.0以上の場合、以下の追加コマンドを実行してください:
|
||||
```bash
|
||||
$ pip uninstall -y onnxruntime-gpu
|
||||
$ pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
3. エントリースクリプトをコピーし、環境変数を設定します
|
||||
2. ソースコードをクローンし、Python の依存関係をインストールする:
|
||||
```bash
|
||||
$ cp docker/entrypoint.sh .
|
||||
$ vi entrypoint.sh
|
||||
```
|
||||
以下のコマンドで Python のパスとragflowプロジェクトのパスを取得します:
|
||||
```bash
|
||||
$ which python
|
||||
$ pwd
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
export POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
~/.local/bin/poetry install --sync --no-root # install RAGFlow dependent python modules
|
||||
```
|
||||
|
||||
`which python` の出力を `PY` の値として、`pwd` の出力を `PYTHONPATH` の値として設定します。
|
||||
3. Docker Compose を使用して依存サービス(MinIO、Elasticsearch、Redis、MySQL)を起動する:
|
||||
```bash
|
||||
docker compose -f docker/docker-compose-base.yml up -d
|
||||
```
|
||||
|
||||
`LD_LIBRARY_PATH` が既に設定されている場合は、コメントアウトできます。
|
||||
`/etc/hosts` に以下の行を追加して、**docker/service_conf.yaml** に指定されたすべてのホストを `127.0.0.1` に解決します:
|
||||
```
|
||||
127.0.0.1 es01 mysql minio redis
|
||||
```
|
||||
**docker/service_conf.yaml** で mysql のポートを `5455` に、es のポートを `1200` に更新します(**docker/.env** に指定された通り).
|
||||
|
||||
4. HuggingFace にアクセスできない場合は、`HF_ENDPOINT` 環境変数を設定してミラーサイトを使用してください:
|
||||
|
||||
```bash
|
||||
# 実際の状況に応じて設定を調整してください。以下の二つの export は新たに追加された設定です
|
||||
PY=${PY}
|
||||
export PYTHONPATH=${PYTHONPATH}
|
||||
# オプション:Hugging Face ミラーを追加
|
||||
export HF_ENDPOINT=https://hf-mirror.com
|
||||
```
|
||||
|
||||
4. 基本サービスを起動します
|
||||
5. バックエンドサービスを起動する:
|
||||
```bash
|
||||
$ cd docker
|
||||
$ docker compose -f docker-compose-base.yml up -d
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
bash docker/launch_backend_service.sh
|
||||
```
|
||||
|
||||
5. 設定ファイルを確認します
|
||||
**docker/.env** 内の設定が**conf/service_conf.yaml**内の設定と一致していることを確認してください。**service_conf.yaml**内の関連サービスのIPアドレスとポートは、ローカルマシンのIPアドレスとコンテナが公開するポートに変更する必要があります。
|
||||
|
||||
6. サービスを起動します
|
||||
6. フロントエンドの依存関係をインストールする:
|
||||
```bash
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ bash ./entrypoint.sh
|
||||
cd web
|
||||
npm install --force
|
||||
```
|
||||
7. フロントエンドを設定し、**.umirc.ts** の `proxy.target` を `http://127.0.0.1:9380` に更新します:
|
||||
8. フロントエンドサービスを起動する:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
_以下の画面で、システムが正常に起動したことを示します:_
|
||||
|
||||

|
||||
|
||||
## 📚 ドキュメンテーション
|
||||
|
||||
- [Quickstart](https://ragflow.io/docs/dev/)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/user-guides)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/guides)
|
||||
- [References](https://ragflow.io/docs/dev/category/references)
|
||||
- [FAQ](https://ragflow.io/docs/dev/faq)
|
||||
|
||||
@ -286,4 +300,4 @@ $ bash ./entrypoint.sh
|
||||
|
||||
## 🙌 コントリビュート
|
||||
|
||||
RAGFlow はオープンソースのコラボレーションによって発展してきました。この精神に基づき、私たちはコミュニティからの多様なコントリビュートを受け入れています。 参加を希望される方は、まず [コントリビューションガイド](./docs/references/CONTRIBUTING.md)をご覧ください。
|
||||
RAGFlow はオープンソースのコラボレーションによって発展してきました。この精神に基づき、私たちはコミュニティからの多様なコントリビュートを受け入れています。 参加を希望される方は、まず [コントリビューションガイド](./CONTRIBUTING.md)をご覧ください。
|
||||
|
||||
174
README_ko.md
174
README_ko.md
@ -12,18 +12,24 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=infiniflowai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/infiniflow?logo=X&color=%20%23f5f5f5" alt="follow on X(Twitter)">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.13.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.13.0">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99"></a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.11.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.11.0"></a>
|
||||
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://ragflow.io/docs/dev/">Document</a> |
|
||||
<a href="https://github.com/infiniflow/ragflow/issues/162">Roadmap</a> |
|
||||
@ -49,6 +55,8 @@
|
||||
|
||||
## 🔥 업데이트
|
||||
|
||||
- 2024-09-29 다단계 대화를 최적화합니다.
|
||||
|
||||
- 2024-09-13 지식베이스 Q&A 검색 모드를 추가합니다.
|
||||
|
||||
- 2024-09-09 Agent에 의료상담 템플릿을 추가하였습니다.
|
||||
@ -57,14 +65,12 @@
|
||||
|
||||
- 2024-08-02: [graphrag](https://github.com/microsoft/graphrag)와 마인드맵에서 영감을 받은 GraphRAG를 지원합니다.
|
||||
|
||||
- 2024-07-23: 오디오 파일 분석을 지원합니다.
|
||||
|
||||
- 2024-07-08: [Graph](./agent/README.md)를 기반으로 한 워크플로우를 지원합니다.
|
||||
|
||||
- 2024-06-27 Q&A 구문 분석 방식에서 Markdown 및 Docx를 지원하고, Docx 파일에서 이미지 추출, Markdown 파일에서 테이블 추출을 지원합니다.
|
||||
|
||||
- 2024-05-23: 더 나은 텍스트 검색을 위해 [RAPTOR](https://arxiv.org/html/2401.18059v1)를 지원합니다.
|
||||
|
||||
## 🎉 계속 지켜봐 주세요
|
||||
⭐️우리의 저장소를 즐겨찾기에 등록하여 흥미로운 새로운 기능과 업데이트를 최신 상태로 유지하세요! 모든 새로운 릴리스에 대한 즉시 알림을 받으세요! 🌟
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/18c9707e-b8aa-4caf-a154-037089c105ba" width="1200"/>
|
||||
</div>
|
||||
|
||||
|
||||
## 🌟 주요 기능
|
||||
@ -138,14 +144,18 @@
|
||||
|
||||
3. 미리 빌드된 Docker 이미지를 생성하고 서버를 시작하세요:
|
||||
|
||||
> 다음 명령어를 실행하면 *dev* 버전의 RAGFlow Docker 이미지가 자동으로 다운로드됩니다. 특정 Docker 버전을 다운로드하고 실행하려면, **docker/.env** 파일에서 `RAGFLOW_VERSION`을 원하는 버전으로 업데이트한 후, 예를 들어 `RAGFLOW_VERSION=v0.11.0`로 업데이트 한 뒤, 다음 명령어를 실행하세요.
|
||||
> 아래의 명령은 RAGFlow slim(dev-slim)의 개발 버전 Docker 이미지를 다운로드합니다. RAGFlow slim Docker 이미지에는 임베딩 모델이나 Python 라이브러리가 포함되어 있지 않으므로 크기는 약 1GB입니다.
|
||||
|
||||
```bash
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> 기본 이미지는 약 9GB 크기이며 로드하는 데 시간이 걸릴 수 있습니다.
|
||||
> - 특정 버전의 RAGFlow slim Docker 이미지를 다운로드하려면, **docker/.env**에서 `RAGFlow_IMAGE` 변수를 원하는 버전으로 업데이트하세요. 예를 들어, `RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0-slim`으로 설정합니다. 이 변경을 완료한 후, 위의 명령을 다시 실행하여 다운로드를 시작하세요.
|
||||
> - RAGFlow의 임베딩 모델과 Python 라이브러리를 포함한 개발 버전 Docker 이미지를 다운로드하려면, **docker/.env**에서 `RAGFlow_IMAGE` 변수를 `RAGFLOW_IMAGE=infiniflow/ragflow:dev`로 업데이트하세요. 이 변경을 완료한 후, 위의 명령을 다시 실행하여 다운로드를 시작하세요.
|
||||
> - 특정 버전의 RAGFlow Docker 이미지를 임베딩 모델과 Python 라이브러리를 포함하여 다운로드하려면, **docker/.env**에서 `RAGFlow_IMAGE` 변수를 원하는 버전으로 업데이트하세요. 예를 들어, `RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0` 로 설정합니다. 이 변경을 완료한 후, 위의 명령을 다시 실행하여 다운로드를 시작하세요.
|
||||
|
||||
> **NOTE:** 임베딩 모델과 Python 라이브러리를 포함한 RAGFlow Docker 이미지의 크기는 약 9GB이며, 로드하는 데 상당히 오랜 시간이 걸릴 수 있습니다.
|
||||
|
||||
|
||||
4. 서버가 시작된 후 서버 상태를 확인하세요:
|
||||
@ -157,12 +167,11 @@
|
||||
_다음 출력 결과로 시스템이 성공적으로 시작되었음을 확인합니다:_
|
||||
|
||||
```bash
|
||||
____ ______ __
|
||||
/ __ \ ____ _ ____ _ / ____// /____ _ __
|
||||
/ /_/ // __ `// __ `// /_ / // __ \| | /| / /
|
||||
/ _, _// /_/ // /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_| \__,_/ \__, //_/ /_/ \____/ |__/|__/
|
||||
/____/
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:9380
|
||||
@ -195,118 +204,91 @@
|
||||
> 모든 시스템 구성 업데이트는 적용되기 위해 시스템 재부팅이 필요합니다.
|
||||
>
|
||||
> ```bash
|
||||
> $ docker-compose up -d
|
||||
> $ docker compose -f docker/docker-compose.yml up -d
|
||||
> ```
|
||||
|
||||
## 🛠️ 소스에서 빌드하기
|
||||
## 🔧 소스 코드로 Docker 이미지를 컴파일합니다(임베딩 모델 포함하지 않음)
|
||||
|
||||
Docker 이미지를 소스에서 빌드하려면:
|
||||
이 Docker 이미지의 크기는 약 1GB이며, 외부 대형 모델과 임베딩 서비스에 의존합니다.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
$ docker build -t infiniflow/ragflow:dev .
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
```
|
||||
|
||||
## 🔧 소스 코드로 Docker 이미지를 컴파일합니다(임베딩 모델 포함)
|
||||
|
||||
## 🛠️ 소스에서 서비스 시작하기
|
||||
|
||||
서비스를 소스에서 시작하려면:
|
||||
|
||||
1. 레포지토리를 클론하세요:
|
||||
이 Docker의 크기는 약 9GB이며, 이미 임베딩 모델을 포함하고 있으므로 외부 대형 모델 서비스에만 의존하면 됩니다.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
```
|
||||
|
||||
2. 가상 환경을 생성하고, Anaconda 또는 Miniconda가 설치되어 있는지 확인하세요:
|
||||
## 🔨 소스 코드로 서비스를 시작합니다.
|
||||
|
||||
1. Poetry를 설치하거나 이미 설치된 경우 이 단계를 건너뜁니다:
|
||||
```bash
|
||||
$ conda create -n ragflow python=3.11.0
|
||||
$ conda activate ragflow
|
||||
$ pip install -r requirements.txt
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
2. 소스 코드를 클론하고 Python 의존성을 설치합니다:
|
||||
```bash
|
||||
# CUDA 버전이 12.0보다 높은 경우, 다음 명령어를 추가로 실행하세요:
|
||||
$ pip uninstall -y onnxruntime-gpu
|
||||
$ pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
export POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
~/.local/bin/poetry install --sync --no-root # install RAGFlow dependent python modules
|
||||
```
|
||||
|
||||
3. 진입 스크립트를 복사하고 환경 변수를 설정하세요:
|
||||
3. Docker Compose를 사용하여 의존 서비스(MinIO, Elasticsearch, Redis 및 MySQL)를 시작합니다:
|
||||
```bash
|
||||
# 파이썬 경로를 받아옵니다:
|
||||
$ which python
|
||||
# RAGFlow 프로젝트 경로를 받아옵니다:
|
||||
$ pwd
|
||||
docker compose -f docker/docker-compose-base.yml up -d
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cp docker/entrypoint.sh .
|
||||
$ vi entrypoint.sh
|
||||
`/etc/hosts` 에 다음 줄을 추가하여 **docker/service_conf.yaml** 에 지정된 모든 호스트를 `127.0.0.1` 로 해결합니다:
|
||||
```
|
||||
127.0.0.1 es01 mysql minio redis
|
||||
```
|
||||
**docker/service_conf.yaml** 에서 mysql 포트를 `5455` 로, es 포트를 `1200` 으로 업데이트합니다( **docker/.env** 에 지정된 대로).
|
||||
|
||||
4. HuggingFace에 접근할 수 없는 경우, `HF_ENDPOINT` 환경 변수를 설정하여 미러 사이트를 사용하세요:
|
||||
|
||||
```bash
|
||||
# 실제 상황에 맞게 설정 조정하기 (다음 두 개의 export 명령어는 새로 추가되었습니다):
|
||||
# - `which python`의 결과를 `PY`에 할당합니다.
|
||||
# - `pwd`의 결과를 `PYTHONPATH`에 할당합니다.
|
||||
# - `LD_LIBRARY_PATH`가 설정되어 있는 경우 주석 처리합니다.
|
||||
# - 선택 사항: Hugging Face 미러 추가.
|
||||
PY=${PY}
|
||||
export PYTHONPATH=${PYTHONPATH}
|
||||
export HF_ENDPOINT=https://hf-mirror.com
|
||||
```
|
||||
|
||||
4. 다른 서비스(MinIO, Elasticsearch, Redis, MySQL)를 시작하세요:
|
||||
5. 백엔드 서비스를 시작합니다:
|
||||
```bash
|
||||
$ cd docker
|
||||
$ docker compose -f docker-compose-base.yml up -d
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
bash docker/launch_backend_service.sh
|
||||
```
|
||||
|
||||
5. 설정 파일을 확인하여 다음 사항을 확인하세요:
|
||||
- **docker/.env**의 설정이 **conf/service_conf.yaml**의 설정과 일치하는지 확인합니다.
|
||||
- **service_conf.yaml**의 관련 서비스에 대한 IP 주소와 포트가 로컬 머신의 IP 주소와 컨테이너에서 노출된 포트와 일치하는지 확인합니다.
|
||||
|
||||
|
||||
6. RAGFlow 백엔드 서비스를 시작합니다:
|
||||
|
||||
6. 프론트엔드 의존성을 설치합니다:
|
||||
```bash
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ bash ./entrypoint.sh
|
||||
cd web
|
||||
npm install --force
|
||||
```
|
||||
7. **.umirc.ts** 에서 `proxy.target` 을 `http://127.0.0.1:9380` 으로 업데이트합니다:
|
||||
8. 프론트엔드 서비스를 시작합니다:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
7. 프론트엔드 서비스를 시작합니다:
|
||||
_다음 인터페이스는 시스템이 성공적으로 시작되었음을 나타냅니다:_
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ vim .umirc.ts
|
||||
# proxy.target을 http://127.0.0.1:9380로 업데이트합니다.
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
8. 프론트엔드 서비스를 배포합니다:
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ umi build
|
||||
$ mkdir -p /ragflow/web
|
||||
$ cp -r dist /ragflow/web
|
||||
$ apt install nginx -y
|
||||
$ cp ../docker/nginx/proxy.conf /etc/nginx
|
||||
$ cp ../docker/nginx/nginx.conf /etc/nginx
|
||||
$ cp ../docker/nginx/ragflow.conf /etc/nginx/conf.d
|
||||
$ systemctl start nginx
|
||||
```
|
||||

|
||||
|
||||
## 📚 문서
|
||||
|
||||
- [Quickstart](https://ragflow.io/docs/dev/)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/user-guides)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/guides)
|
||||
- [References](https://ragflow.io/docs/dev/category/references)
|
||||
- [FAQ](https://ragflow.io/docs/dev/faq)
|
||||
|
||||
@ -322,4 +304,4 @@ $ docker compose up -d
|
||||
|
||||
## 🙌 컨트리뷰션
|
||||
|
||||
RAGFlow는 오픈소스 협업을 통해 발전합니다. 이러한 정신을 바탕으로, 우리는 커뮤니티의 다양한 기여를 환영합니다. 참여하고 싶으시다면, 먼저 [가이드라인](./docs/references/CONTRIBUTING.md)을 검토해 주세요.
|
||||
RAGFlow는 오픈소스 협업을 통해 발전합니다. 이러한 정신을 바탕으로, 우리는 커뮤니티의 다양한 기여를 환영합니다. 참여하고 싶으시다면, 먼저 [가이드라인](./CONTRIBUTING.md)을 검토해 주세요.
|
||||
|
||||
179
README_zh.md
179
README_zh.md
@ -12,18 +12,24 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=infiniflowai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/infiniflow?logo=X&color=%20%23f5f5f5" alt="follow on X(Twitter)">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.13.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.13.0">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99"></a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/badge/docker_pull-ragflow:v0.11.0-brightgreen" alt="docker pull infiniflow/ragflow:v0.11.0"></a>
|
||||
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://ragflow.io/docs/dev/">Document</a> |
|
||||
<a href="https://github.com/infiniflow/ragflow/issues/162">Roadmap</a> |
|
||||
@ -47,14 +53,18 @@
|
||||
|
||||
## 🔥 近期更新
|
||||
|
||||
- 2024-09-29 优化多轮对话.
|
||||
- 2024-09-13 增加知识库问答搜索模式。
|
||||
- 2024-09-09 在 Agent 中加入医疗问诊模板。
|
||||
- 2024-08-22 支持用 RAG 技术实现从自然语言到 SQL 语句的转换。
|
||||
- 2024-08-02 支持 GraphRAG 启发于 [graphrag](https://github.com/microsoft/graphrag) 和思维导图。
|
||||
- 2024-07-23 支持解析音频文件。
|
||||
- 2024-07-08 支持 Agentic RAG: 基于 [Graph](./agent/README.md) 的工作流。
|
||||
- 2024-06-27 Q&A 解析方式支持 Markdown 文件和 Docx 文件,支持提取出 Docx 文件中的图片和 Markdown 文件中的表格。
|
||||
- 2024-05-23 实现 [RAPTOR](https://arxiv.org/html/2401.18059v1) 提供更好的文本检索。
|
||||
|
||||
## 🎉 关注项目
|
||||
⭐️点击右上角的 Star 关注RAGFlow,可以获取最新发布的实时通知 !🌟
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/18c9707e-b8aa-4caf-a154-037089c105ba" width="1200"/>
|
||||
</div>
|
||||
|
||||
|
||||
## 🌟 主要功能
|
||||
|
||||
@ -131,15 +141,17 @@
|
||||
|
||||
3. 进入 **docker** 文件夹,利用提前编译好的 Docker 镜像启动服务器:
|
||||
|
||||
> 运行以下命令会自动下载 dev 版的 RAGFlow slim Docker 镜像(`dev-slim`),该镜像并不包含 embedding 模型以及一些 Python 库,因此镜像大小约 1GB。
|
||||
|
||||
```bash
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose -f docker-compose-CN.yml up -d
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> 请注意,运行上述命令会自动下载 RAGFlow 的开发版本 docker 镜像。如果你想下载并运行特定版本的 docker 镜像,请在 docker/.env 文件中找到 RAGFLOW_VERSION 变量,将其改为对应版本。例如 RAGFLOW_VERSION=v0.11.0,然后运行上述命令。
|
||||
|
||||
> 核心镜像文件大约 9 GB,可能需要一定时间拉取。请耐心等待。
|
||||
> - 如果你想下载并运行特定版本的 RAGFlow slim Docker 镜像,请在 **docker/.env** 文件中找到 `RAGFLOW_IMAGE` 变量,将其改为对应版本。例如 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0-slim`,然后再运行上述命令。
|
||||
> - 如果您想安装内置 embedding 模型和 Python 库的 dev 版本的 Docker 镜像,需要将 **docker/.env** 文件中的 `RAGFLOW_IMAGE` 变量修改为: `RAGFLOW_IMAGE=infiniflow/ragflow:dev`。
|
||||
> - 如果您想安装内置 embedding 模型和 Python 库的指定版本的 RAGFlow Docker 镜像,需要将 **docker/.env** 文件中的 `RAGFLOW_IMAGE` 变量修改为: `RAGFLOW_IMAGE=infiniflow/ragflow:v0.13.0`。修改后,再运行上面的命令。
|
||||
> **注意:** 安装内置 embedding 模型和 Python 库的指定版本的 RAGFlow Docker 镜像大小约 9 GB,可能需要更长时间下载,请耐心等待。
|
||||
|
||||
4. 服务器启动成功后再次确认服务器状态:
|
||||
|
||||
@ -150,12 +162,11 @@
|
||||
_出现以下界面提示说明服务器启动成功:_
|
||||
|
||||
```bash
|
||||
____ ______ __
|
||||
/ __ \ ____ _ ____ _ / ____// /____ _ __
|
||||
/ /_/ // __ `// __ `// /_ / // __ \| | /| / /
|
||||
/ _, _// /_/ // /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_| \__,_/ \__, //_/ /_/ \____/ |__/|__/
|
||||
/____/
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:9380
|
||||
@ -178,126 +189,104 @@
|
||||
|
||||
- [.env](./docker/.env):存放一些基本的系统环境变量,比如 `SVR_HTTP_PORT`、`MYSQL_PASSWORD`、`MINIO_PASSWORD` 等。
|
||||
- [service_conf.yaml](./docker/service_conf.yaml):配置各类后台服务。
|
||||
- [docker-compose-CN.yml](./docker/docker-compose-CN.yml): 系统依赖该文件完成启动。
|
||||
- [docker-compose.yml](./docker/docker-compose.yml): 系统依赖该文件完成启动。
|
||||
|
||||
请务必确保 [.env](./docker/.env) 文件中的变量设置与 [service_conf.yaml](./docker/service_conf.yaml) 文件中的配置保持一致!
|
||||
|
||||
如果不能访问镜像站点hub.docker.com或者模型站点huggingface.co,请按照[.env](./docker/.env)注释修改`RAGFLOW_IMAGE`和`HF_ENDPOINT`。
|
||||
|
||||
> [./docker/README](./docker/README.md) 文件提供了环境变量设置和服务配置的详细信息。请**一定要**确保 [./docker/README](./docker/README.md) 文件当中列出来的环境变量的值与 [service_conf.yaml](./docker/service_conf.yaml) 文件当中的系统配置保持一致。
|
||||
|
||||
如需更新默认的 HTTP 服务端口(80), 可以在 [docker-compose-CN.yml](./docker/docker-compose-CN.yml) 文件中将配置 `80:80` 改为 `<YOUR_SERVING_PORT>:80`。
|
||||
如需更新默认的 HTTP 服务端口(80), 可以在 [docker-compose.yml](./docker/docker-compose.yml) 文件中将配置 `80:80` 改为 `<YOUR_SERVING_PORT>:80`。
|
||||
|
||||
> 所有系统配置都需要通过系统重启生效:
|
||||
>
|
||||
> ```bash
|
||||
> $ docker compose -f docker-compose-CN.yml up -d
|
||||
> $ docker compose -f docker-compose.yml up -d
|
||||
> ```
|
||||
|
||||
## 🛠️ 源码编译、安装 Docker 镜像
|
||||
## 🔧 源码编译 Docker 镜像(不含 embedding 模型)
|
||||
|
||||
如需从源码安装 Docker 镜像:
|
||||
本 Docker 镜像大小约 1 GB 左右并且依赖外部的大模型和 embedding 服务。
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
$ docker build -t infiniflow/ragflow:v0.11.0 .
|
||||
$ cd ragflow/docker
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ docker compose up -d
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
```
|
||||
|
||||
## 🛠️ 源码启动服务
|
||||
## 🔧 源码编译 Docker 镜像(包含 embedding 模型)
|
||||
|
||||
如需从源码启动服务,请参考以下步骤:
|
||||
|
||||
1. 克隆仓库
|
||||
本 Docker 大小约 9 GB 左右。由于已包含 embedding 模型,所以只需依赖外部的大模型服务即可。
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
$ cd ragflow/
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
```
|
||||
|
||||
2. 创建虚拟环境(确保已安装 Anaconda 或 Miniconda)
|
||||
## 🔨 以源代码启动服务
|
||||
|
||||
1. 安装 Poetry。如已经安装,可跳过本步骤:
|
||||
```bash
|
||||
$ conda create -n ragflow python=3.11.0
|
||||
$ conda activate ragflow
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
如果 cuda > 12.0,需额外执行以下命令:
|
||||
```bash
|
||||
$ pip uninstall -y onnxruntime-gpu
|
||||
$ pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
3. 拷贝入口脚本并配置环境变量
|
||||
|
||||
2. 下载源代码并安装 Python 依赖:
|
||||
```bash
|
||||
$ cp docker/entrypoint.sh .
|
||||
$ vi entrypoint.sh
|
||||
```
|
||||
使用以下命令获取python路径及ragflow项目路径:
|
||||
```bash
|
||||
$ which python
|
||||
$ pwd
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
export POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
~/.local/bin/poetry install --sync --no-root # install RAGFlow dependent python modules
|
||||
```
|
||||
|
||||
将上述 `which python` 的输出作为 `PY` 的值,将 `pwd` 的输出作为 `PYTHONPATH` 的值。
|
||||
3. 通过 Docker Compose 启动依赖的服务(MinIO, Elasticsearch, Redis, and MySQL):
|
||||
```bash
|
||||
docker compose -f docker/docker-compose-base.yml up -d
|
||||
```
|
||||
|
||||
`LD_LIBRARY_PATH` 如果环境已经配置好,可以注释掉。
|
||||
在 `/etc/hosts` 中添加以下代码,将 **docker/service_conf.yaml** 文件中的所有 host 地址都解析为 `127.0.0.1`:
|
||||
```
|
||||
127.0.0.1 es01 mysql minio redis
|
||||
```
|
||||
在文件 **docker/service_conf.yaml** 中,对照 **docker/.env** 的配置将 mysql 端口更新为 `5455`,es 端口更新为 `1200`。
|
||||
|
||||
4. 如果无法访问 HuggingFace,可以把环境变量 `HF_ENDPOINT` 设成相应的镜像站点:
|
||||
|
||||
```bash
|
||||
# 此处配置需要按照实际情况调整,两个 export 为新增配置
|
||||
PY=${PY}
|
||||
export PYTHONPATH=${PYTHONPATH}
|
||||
# 可选:添加 Hugging Face 镜像
|
||||
export HF_ENDPOINT=https://hf-mirror.com
|
||||
```
|
||||
|
||||
4. 启动基础服务
|
||||
|
||||
5. 启动后端服务:
|
||||
```bash
|
||||
$ cd docker
|
||||
$ docker compose -f docker-compose-base.yml up -d
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
bash docker/launch_backend_service.sh
|
||||
```
|
||||
|
||||
5. 检查配置文件
|
||||
确保**docker/.env**中的配置与**conf/service_conf.yaml**中配置一致, **service_conf.yaml**中相关服务的IP地址与端口应该改成本机IP地址及容器映射出来的端口。
|
||||
|
||||
6. 启动服务
|
||||
|
||||
6. 安装前端依赖:
|
||||
```bash
|
||||
$ chmod +x ./entrypoint.sh
|
||||
$ bash ./entrypoint.sh
|
||||
cd web
|
||||
npm install --force
|
||||
```
|
||||
7. 配置前端,将 **.umirc.ts** 的 `proxy.target` 更新为 `http://127.0.0.1:9380`:
|
||||
8. 启动前端服务:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
7. 启动WebUI服务
|
||||
_以下界面说明系统已经成功启动:_
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ vim .umirc.ts
|
||||
# 修改proxy.target为http://127.0.0.1:9380
|
||||
$ npm run dev
|
||||
```
|
||||

|
||||
|
||||
8. 部署WebUI服务
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ npm install --registry=https://registry.npmmirror.com --force
|
||||
$ umi build
|
||||
$ mkdir -p /ragflow/web
|
||||
$ cp -r dist /ragflow/web
|
||||
$ apt install nginx -y
|
||||
$ cp ../docker/nginx/proxy.conf /etc/nginx
|
||||
$ cp ../docker/nginx/nginx.conf /etc/nginx
|
||||
$ cp ../docker/nginx/ragflow.conf /etc/nginx/conf.d
|
||||
$ systemctl start nginx
|
||||
```
|
||||
## 📚 技术文档
|
||||
|
||||
- [Quickstart](https://ragflow.io/docs/dev/)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/user-guides)
|
||||
- [User guide](https://ragflow.io/docs/dev/category/guides)
|
||||
- [References](https://ragflow.io/docs/dev/category/references)
|
||||
- [FAQ](https://ragflow.io/docs/dev/faq)
|
||||
|
||||
@ -313,7 +302,7 @@ $ systemctl start nginx
|
||||
|
||||
## 🙌 贡献指南
|
||||
|
||||
RAGFlow 只有通过开源协作才能蓬勃发展。秉持这一精神,我们欢迎来自社区的各种贡献。如果您有意参与其中,请查阅我们的 [贡献者指南](./docs/references/CONTRIBUTING.md) 。
|
||||
RAGFlow 只有通过开源协作才能蓬勃发展。秉持这一精神,我们欢迎来自社区的各种贡献。如果您有意参与其中,请查阅我们的 [贡献者指南](./CONTRIBUTING.md) 。
|
||||
|
||||
## 🤝 商务合作
|
||||
|
||||
|
||||
@ -260,9 +260,9 @@ class Canvas(ABC):
|
||||
|
||||
def get_history(self, window_size):
|
||||
convs = []
|
||||
for role, obj in self.history[(window_size + 1) * -1:]:
|
||||
for role, obj in self.history[window_size * -1:]:
|
||||
convs.append({"role": role, "content": (obj if role == "user" else
|
||||
'\n'.join(pd.DataFrame(obj)['content']))})
|
||||
'\n'.join([str(s) for s in pd.DataFrame(obj)['content']]))})
|
||||
return convs
|
||||
|
||||
def add_user_input(self, question):
|
||||
|
||||
@ -9,6 +9,7 @@ from .relevant import Relevant, RelevantParam
|
||||
from .message import Message, MessageParam
|
||||
from .rewrite import RewriteQuestion, RewriteQuestionParam
|
||||
from .keyword import KeywordExtract, KeywordExtractParam
|
||||
from .concentrator import Concentrator, ConcentratorParam
|
||||
from .baidu import Baidu, BaiduParam
|
||||
from .duckduckgo import DuckDuckGo, DuckDuckGoParam
|
||||
from .wikipedia import Wikipedia, WikipediaParam
|
||||
@ -27,6 +28,8 @@ from .wencai import WenCai, WenCaiParam
|
||||
from .jin10 import Jin10, Jin10Param
|
||||
from .tushare import TuShare, TuShareParam
|
||||
from .akshare import AkShare, AkShareParam
|
||||
from .crawler import Crawler, CrawlerParam
|
||||
from .invoke import Invoke, InvokeParam
|
||||
|
||||
|
||||
def component_class(class_name):
|
||||
|
||||
@ -36,7 +36,6 @@ class BaiduFanyiParam(ComponentParamBase):
|
||||
self.domain = 'finance'
|
||||
|
||||
def check(self):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
self.check_empty(self.appid, "BaiduFanyi APPID")
|
||||
self.check_empty(self.secret_key, "BaiduFanyi Secret Key")
|
||||
self.check_valid_value(self.trans_type, "Translate type", ['translate', 'fieldtranslate'])
|
||||
|
||||
@ -444,7 +444,7 @@ class ComponentBase(ABC):
|
||||
|
||||
if DEBUG: print(self.component_name, reversed_cpnts[::-1])
|
||||
for u in reversed_cpnts[::-1]:
|
||||
if self.get_component_name(u) in ["switch"]: continue
|
||||
if self.get_component_name(u) in ["switch", "concentrator"]: continue
|
||||
if self.component_name.lower() == "generate" and self.get_component_name(u) == "retrieval":
|
||||
o = self._canvas.get_component(u)["obj"].output(allow_partial=False)[1]
|
||||
if o is not None:
|
||||
@ -472,7 +472,7 @@ class ComponentBase(ABC):
|
||||
if "content" in df:
|
||||
df = df.drop_duplicates(subset=['content']).reset_index(drop=True)
|
||||
return df
|
||||
return pd.DataFrame()
|
||||
return pd.DataFrame(self._canvas.get_history(3)[-1:])
|
||||
|
||||
def get_stream_input(self):
|
||||
reversed_cpnts = []
|
||||
|
||||
@ -73,7 +73,7 @@ class Categorize(Generate, ABC):
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
input = self.get_input()
|
||||
input = "Question: " + ("; ".join(input["content"]) if "content" in input else "") + "Category: "
|
||||
input = "Question: " + (list(input["content"])[-1] if "content" in input else "") + "\tCategory: "
|
||||
chat_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.CHAT, self._param.llm_id)
|
||||
ans = chat_mdl.chat(self._param.get_prompt(), [{"role": "user", "content": input}],
|
||||
self._param.gen_conf())
|
||||
|
||||
36
agent/component/concentrator.py
Normal file
36
agent/component/concentrator.py
Normal file
@ -0,0 +1,36 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from abc import ABC
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class ConcentratorParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Concentrator component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
|
||||
class Concentrator(ComponentBase, ABC):
|
||||
component_name = "Concentrator"
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
return Concentrator.be_output("")
|
||||
70
agent/component/crawler.py
Normal file
70
agent/component/crawler.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from abc import ABC
|
||||
import asyncio
|
||||
from crawl4ai import AsyncWebCrawler
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class CrawlerParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Crawler component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.proxy = None
|
||||
self.extract_type = "markdown"
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.extract_type, "Type of content from the crawler", ['html', 'markdown', 'content'])
|
||||
|
||||
|
||||
class Crawler(ComponentBase, ABC):
|
||||
component_name = "Crawler"
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
ans = self.get_input()
|
||||
ans = " - ".join(ans["content"]) if "content" in ans else ""
|
||||
if not ans:
|
||||
return Crawler.be_output("")
|
||||
try:
|
||||
result = asyncio.run(self.get_web(ans))
|
||||
|
||||
return Crawler.be_output(result)
|
||||
|
||||
except Exception as e:
|
||||
return Crawler.be_output(f"An unexpected error occurred: {str(e)}")
|
||||
|
||||
async def get_web(self, url):
|
||||
proxy = self._param.proxy if self._param.proxy else None
|
||||
async with AsyncWebCrawler(verbose=True, proxy=proxy) as crawler:
|
||||
result = await crawler.arun(
|
||||
url=url,
|
||||
bypass_cache=True
|
||||
)
|
||||
|
||||
if self._param.extract_type == 'html':
|
||||
return result.cleaned_html
|
||||
elif self._param.extract_type == 'markdown':
|
||||
return result.markdown
|
||||
elif self._param.extract_type == 'content':
|
||||
result.extracted_content
|
||||
return result.markdown
|
||||
|
||||
|
||||
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
from abc import ABC
|
||||
import re
|
||||
import pandas as pd
|
||||
from peewee import MySQLDatabase, PostgresqlDatabase
|
||||
import pymysql
|
||||
import psycopg2
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
@ -44,6 +45,9 @@ class ExeSQLParam(ComponentParamBase):
|
||||
self.check_positive_integer(self.port, "IP Port")
|
||||
self.check_empty(self.password, "Database password")
|
||||
self.check_positive_integer(self.top_n, "Number of records")
|
||||
if self.database == "rag_flow":
|
||||
if self.host == "ragflow-mysql": raise ValueError("The host is not accessible.")
|
||||
if self.password == "infini_rag_flow": raise ValueError("The host is not accessible.")
|
||||
|
||||
|
||||
class ExeSQL(ComponentBase, ABC):
|
||||
@ -66,14 +70,14 @@ class ExeSQL(ComponentBase, ABC):
|
||||
raise Exception("SQL statement not found!")
|
||||
|
||||
if self._param.db_type in ["mysql", "mariadb"]:
|
||||
db = MySQLDatabase(self._param.database, user=self._param.username, host=self._param.host,
|
||||
db = pymysql.connect(db=self._param.database, user=self._param.username, host=self._param.host,
|
||||
port=self._param.port, password=self._param.password)
|
||||
elif self._param.db_type == 'postgresql':
|
||||
db = PostgresqlDatabase(self._param.database, user=self._param.username, host=self._param.host,
|
||||
db = psycopg2.connect(dbname=self._param.database, user=self._param.username, host=self._param.host,
|
||||
port=self._param.port, password=self._param.password)
|
||||
|
||||
try:
|
||||
db.connect()
|
||||
cursor = db.cursor()
|
||||
except Exception as e:
|
||||
raise Exception("Database Connection Failed! \n" + str(e))
|
||||
sql_res = []
|
||||
@ -81,13 +85,13 @@ class ExeSQL(ComponentBase, ABC):
|
||||
if not single_sql:
|
||||
continue
|
||||
try:
|
||||
query = db.execute_sql(single_sql)
|
||||
if query.rowcount == 0:
|
||||
sql_res.append({"content": "\nTotal: " + str(query.rowcount) + "\n No record in the database!"})
|
||||
cursor.execute(single_sql)
|
||||
if cursor.rowcount == 0:
|
||||
sql_res.append({"content": "\nTotal: 0\n No record in the database!"})
|
||||
continue
|
||||
single_res = pd.DataFrame([i for i in query.fetchmany(size=self._param.top_n)])
|
||||
single_res.columns = [i[0] for i in query.description]
|
||||
sql_res.append({"content": "\nTotal: " + str(query.rowcount) + "\n" + single_res.to_markdown()})
|
||||
single_res = pd.DataFrame([i for i in cursor.fetchmany(size=self._param.top_n)])
|
||||
single_res.columns = [i[0] for i in cursor.description]
|
||||
sql_res.append({"content": "\nTotal: " + str(cursor.rowcount) + "\n" + single_res.to_markdown()})
|
||||
except Exception as e:
|
||||
sql_res.append({"content": "**Error**:" + str(e) + "\nError SQL Statement:" + single_sql})
|
||||
pass
|
||||
|
||||
@ -17,6 +17,7 @@ import re
|
||||
from functools import partial
|
||||
import pandas as pd
|
||||
from api.db import LLMType
|
||||
from api.db.services.dialog_service import message_fit_in
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.settings import retrievaler
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
@ -101,18 +102,21 @@ class Generate(ComponentBase):
|
||||
prompt = self._param.prompt
|
||||
|
||||
retrieval_res = self.get_input()
|
||||
input = (" - " + "\n - ".join(retrieval_res["content"])) if "content" in retrieval_res else ""
|
||||
input = (" - "+"\n - ".join([c for c in retrieval_res["content"] if isinstance(c, str)])) if "content" in retrieval_res else ""
|
||||
for para in self._param.parameters:
|
||||
cpn = self._canvas.get_component(para["component_id"])["obj"]
|
||||
if cpn.component_name.lower() == "answer":
|
||||
kwargs[para["key"]] = self._canvas.get_history(1)[0]["content"]
|
||||
continue
|
||||
_, out = cpn.output(allow_partial=False)
|
||||
if "content" not in out.columns:
|
||||
kwargs[para["key"]] = "Nothing"
|
||||
else:
|
||||
kwargs[para["key"]] = " - " + "\n - ".join(out["content"])
|
||||
kwargs[para["key"]] = " - "+"\n - ".join([o if isinstance(o, str) else str(o) for o in out["content"]])
|
||||
|
||||
kwargs["input"] = input
|
||||
for n, v in kwargs.items():
|
||||
prompt = re.sub(r"\{%s\}" % n, re.escape(str(v)), prompt)
|
||||
prompt = re.sub(r"\{%s\}" % re.escape(n), re.escape(str(v)), prompt)
|
||||
|
||||
downstreams = self._canvas.get_component(self._id)["downstream"]
|
||||
if kwargs.get("stream") and len(downstreams) == 1 and self._canvas.get_component(downstreams[0])[
|
||||
@ -122,13 +126,15 @@ class Generate(ComponentBase):
|
||||
if "empty_response" in retrieval_res.columns and not "".join(retrieval_res["content"]):
|
||||
res = {"content": "\n- ".join(retrieval_res["empty_response"]) if "\n- ".join(
|
||||
retrieval_res["empty_response"]) else "Nothing found in knowledgebase!", "reference": []}
|
||||
return Generate.be_output(res)
|
||||
return pd.DataFrame([res])
|
||||
|
||||
msg = self._canvas.get_history(self._param.message_history_window_size)
|
||||
_, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(chat_mdl.max_length * 0.97))
|
||||
ans = chat_mdl.chat(msg[0]["content"], msg[1:], self._param.gen_conf())
|
||||
|
||||
ans = chat_mdl.chat(prompt, self._canvas.get_history(self._param.message_history_window_size),
|
||||
self._param.gen_conf())
|
||||
if self._param.cite and "content_ltks" in retrieval_res.columns and "vector" in retrieval_res.columns:
|
||||
df = self.set_cite(retrieval_res, ans)
|
||||
return pd.DataFrame(df)
|
||||
res = self.set_cite(retrieval_res, ans)
|
||||
return pd.DataFrame([res])
|
||||
|
||||
return Generate.be_output(ans)
|
||||
|
||||
@ -141,9 +147,10 @@ class Generate(ComponentBase):
|
||||
self.set_output(res)
|
||||
return
|
||||
|
||||
msg = self._canvas.get_history(self._param.message_history_window_size)
|
||||
_, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(chat_mdl.max_length * 0.97))
|
||||
answer = ""
|
||||
for ans in chat_mdl.chat_streamly(prompt, self._canvas.get_history(self._param.message_history_window_size),
|
||||
self._param.gen_conf()):
|
||||
for ans in chat_mdl.chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf()):
|
||||
res = {"content": ans, "reference": []}
|
||||
answer = ans
|
||||
yield res
|
||||
|
||||
103
agent/component/invoke.py
Normal file
103
agent/component/invoke.py
Normal file
@ -0,0 +1,103 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import json
|
||||
import re
|
||||
from abc import ABC
|
||||
import requests
|
||||
from deepdoc.parser import HtmlParser
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class InvokeParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Crawler component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.proxy = None
|
||||
self.headers = ""
|
||||
self.method = "get"
|
||||
self.variables = []
|
||||
self.url = ""
|
||||
self.timeout = 60
|
||||
self.clean_html = False
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.method.lower(), "Type of content from the crawler", ['get', 'post', 'put'])
|
||||
self.check_empty(self.url, "End point URL")
|
||||
self.check_positive_integer(self.timeout, "Timeout time in second")
|
||||
self.check_boolean(self.clean_html, "Clean HTML")
|
||||
|
||||
|
||||
class Invoke(ComponentBase, ABC):
|
||||
component_name = "Invoke"
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
args = {}
|
||||
for para in self._param.variables:
|
||||
if para.get("component_id"):
|
||||
cpn = self._canvas.get_component(para["component_id"])["obj"]
|
||||
_, out = cpn.output(allow_partial=False)
|
||||
args[para["key"]] = "\n".join(out["content"])
|
||||
else:
|
||||
args[para["key"]] = "\n".join(para["value"])
|
||||
|
||||
url = self._param.url.strip()
|
||||
if url.find("http") != 0:
|
||||
url = "http://" + url
|
||||
|
||||
method = self._param.method.lower()
|
||||
headers = {}
|
||||
if self._param.headers:
|
||||
headers = json.loads(self._param.headers)
|
||||
proxies = None
|
||||
if re.sub(r"https?:?/?/?", "", self._param.proxy):
|
||||
proxies = {"http": self._param.proxy, "https": self._param.proxy}
|
||||
|
||||
if method == 'get':
|
||||
response = requests.get(url=url,
|
||||
params=args,
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=self._param.timeout)
|
||||
if self._param.clean_html:
|
||||
sections = HtmlParser()(None, response.content)
|
||||
return Invoke.be_output("\n".join(sections))
|
||||
|
||||
return Invoke.be_output(response.text)
|
||||
|
||||
if method == 'put':
|
||||
response = requests.put(url=url,
|
||||
data=args,
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=self._param.timeout)
|
||||
if self._param.clean_html:
|
||||
sections = HtmlParser()(None, response.content)
|
||||
return Invoke.be_output("\n".join(sections))
|
||||
return Invoke.be_output(response.text)
|
||||
|
||||
if method == 'post':
|
||||
response = requests.post(url=url,
|
||||
json=args,
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=self._param.timeout)
|
||||
if self._param.clean_html:
|
||||
sections = HtmlParser()(None, response.content)
|
||||
return Invoke.be_output("\n".join(sections))
|
||||
return Invoke.be_output(response.text)
|
||||
@ -43,22 +43,19 @@ class RetrievalParam(ComponentParamBase):
|
||||
self.check_decimal_float(self.similarity_threshold, "[Retrieval] Similarity threshold")
|
||||
self.check_decimal_float(self.keywords_similarity_weight, "[Retrieval] Keywords similarity weight")
|
||||
self.check_positive_number(self.top_n, "[Retrieval] Top N")
|
||||
self.check_empty(self.kb_ids, "[Retrieval] Knowledge bases")
|
||||
|
||||
|
||||
class Retrieval(ComponentBase, ABC):
|
||||
component_name = "Retrieval"
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
query = []
|
||||
for role, cnt in history[::-1][:self._param.message_history_window_size]:
|
||||
if role != "user":continue
|
||||
query.append(cnt)
|
||||
# query = "\n".join(query)
|
||||
query = query[0]
|
||||
query = self.get_input()
|
||||
query = str(query["content"][0]) if "content" in query else ""
|
||||
|
||||
kbs = KnowledgebaseService.get_by_ids(self._param.kb_ids)
|
||||
if not kbs:
|
||||
raise ValueError("Can't find knowledgebases by {}".format(self._param.kb_ids))
|
||||
return Retrieval.be_output("")
|
||||
|
||||
embd_nms = list(set([kb.embd_id for kb in kbs]))
|
||||
assert len(embd_nms) == 1, "Knowledge bases use different embedding models."
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ class RewriteQuestionParam(GenerateParam):
|
||||
def check(self):
|
||||
super().check()
|
||||
|
||||
def get_prompt(self):
|
||||
def get_prompt(self, conv):
|
||||
self.prompt = """
|
||||
You are an expert at query expansion to generate a paraphrasing of a question.
|
||||
I can't retrieval relevant information from the knowledge base by using user's question directly.
|
||||
@ -42,6 +42,40 @@ class RewriteQuestionParam(GenerateParam):
|
||||
changing the way of expression, translating the original question into another language (English/Chinese), etc.
|
||||
And return 5 versions of question and one is from translation.
|
||||
Just list the question. No other words are needed.
|
||||
"""
|
||||
return f"""
|
||||
Role: A helpful assistant
|
||||
Task: Generate a full user question that would follow the conversation.
|
||||
Requirements & Restrictions:
|
||||
- Text generated MUST be in the same language of the original user's question.
|
||||
- If the user's latest question is completely, don't do anything, just return the original question.
|
||||
- DON'T generate anything except a refined question.
|
||||
|
||||
######################
|
||||
-Examples-
|
||||
######################
|
||||
# Example 1
|
||||
## Conversation
|
||||
USER: What is the name of Donald Trump's father?
|
||||
ASSISTANT: Fred Trump.
|
||||
USER: And his mother?
|
||||
###############
|
||||
Output: What's the name of Donald Trump's mother?
|
||||
------------
|
||||
# Example 2
|
||||
## Conversation
|
||||
USER: What is the name of Donald Trump's father?
|
||||
ASSISTANT: Fred Trump.
|
||||
USER: And his mother?
|
||||
ASSISTANT: Mary Trump.
|
||||
User: What's her full name?
|
||||
###############
|
||||
Output: What's the full name of Donald Trump's mother Mary Trump?
|
||||
######################
|
||||
# Real Data
|
||||
## Conversation
|
||||
{conv}
|
||||
###############
|
||||
"""
|
||||
return self.prompt
|
||||
|
||||
@ -56,15 +90,19 @@ class RewriteQuestion(Generate, ABC):
|
||||
self._loop = 0
|
||||
raise Exception("Sorry! Nothing relevant found.")
|
||||
self._loop += 1
|
||||
q = "Question: "
|
||||
for r, c in self._canvas.history[::-1]:
|
||||
if r == "user":
|
||||
q += c
|
||||
break
|
||||
|
||||
hist = self._canvas.get_history(4)
|
||||
conv = []
|
||||
for m in hist:
|
||||
if m["role"] not in ["user", "assistant"]: continue
|
||||
conv.append("{}: {}".format(m["role"].upper(), m["content"]))
|
||||
conv = "\n".join(conv)
|
||||
|
||||
chat_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.CHAT, self._param.llm_id)
|
||||
ans = chat_mdl.chat(self._param.get_prompt(), [{"role": "user", "content": q}],
|
||||
ans = chat_mdl.chat(self._param.get_prompt(conv), [{"role": "user", "content": "Output: "}],
|
||||
self._param.gen_conf())
|
||||
self._canvas.history.pop()
|
||||
self._canvas.history.append(("user", ans))
|
||||
|
||||
print(ans, ":::::::::::::::::::::::::::::::::")
|
||||
return RewriteQuestion.be_output(ans)
|
||||
|
||||
@ -49,34 +49,15 @@ class Switch(ComponentBase, ABC):
|
||||
|
||||
def _run(self, history, **kwargs):
|
||||
for cond in self._param.conditions:
|
||||
|
||||
if len(cond["items"]) == 1:
|
||||
out = self._canvas.get_component(cond["items"][0]["cpn_id"])["obj"].output()[1]
|
||||
cpn_input = "" if "content" not in out.columns else " ".join(out["content"])
|
||||
if self.process_operator(cpn_input, cond["items"][0]["operator"], cond["items"][0]["value"]):
|
||||
return Switch.be_output(cond["to"])
|
||||
continue
|
||||
|
||||
if cond["logical_operator"] == "and":
|
||||
res = True
|
||||
res = []
|
||||
for item in cond["items"]:
|
||||
out = self._canvas.get_component(item["cpn_id"])["obj"].output()[1]
|
||||
cpn_input = "" if "content" not in out.columns else " ".join(out["content"])
|
||||
if not self.process_operator(cpn_input, item["operator"], item["value"]):
|
||||
res = False
|
||||
break
|
||||
if res:
|
||||
res.append(self.process_operator(cpn_input, item["operator"], item["value"]))
|
||||
if cond["logical_operator"] != "and" and any(res):
|
||||
return Switch.be_output(cond["to"])
|
||||
continue
|
||||
|
||||
res = False
|
||||
for item in cond["items"]:
|
||||
out = self._canvas.get_component(item["cpn_id"])["obj"].output()[1]
|
||||
cpn_input = "" if "content" not in out.columns else " ".join(out["content"])
|
||||
if self.process_operator(cpn_input, item["operator"], item["value"]):
|
||||
res = True
|
||||
break
|
||||
if res:
|
||||
if all(res):
|
||||
return Switch.be_output(cond["to"])
|
||||
|
||||
return Switch.be_output(self._param.end_cpn_id)
|
||||
|
||||
@ -64,6 +64,12 @@ class WenCai(ComponentBase, ABC):
|
||||
continue
|
||||
wencai_res.append({"content": pd.DataFrame.from_dict(item[1], orient='index').to_markdown()})
|
||||
continue
|
||||
if isinstance(item[1], pd.DataFrame):
|
||||
if "image_url" in item[1].columns:
|
||||
continue
|
||||
wencai_res.append({"content": item[1].to_markdown()})
|
||||
continue
|
||||
|
||||
wencai_res.append({"content": item[0] + "\n" + str(item[1])})
|
||||
except Exception as e:
|
||||
return WenCai.be_output("**ERROR**: " + str(e))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -6,71 +6,57 @@
|
||||
"dsl": {
|
||||
"answer": [],
|
||||
"components": {
|
||||
"answer:0": {
|
||||
"downstream": ["generate:0"],
|
||||
"obj": {
|
||||
"component_name": "Answer",
|
||||
"params": {}
|
||||
},
|
||||
"upstream": ["begin", "generate:0"]
|
||||
},
|
||||
"begin": {
|
||||
"downstream": ["answer:0"],
|
||||
"obj": {
|
||||
"component_name": "Begin",
|
||||
"params": {
|
||||
"prologue": "Hi there! Please enter the text you want to translate in format like: 'text you want to translate' => target language. For an example: 您好! => English"
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Answer:ShortPapersShake"
|
||||
],
|
||||
"upstream": []
|
||||
},
|
||||
"generate:0": {
|
||||
"downstream": ["answer:0"],
|
||||
"Answer:ShortPapersShake": {
|
||||
"obj": {
|
||||
"component_name": "Answer",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:HeavyForksTell"
|
||||
],
|
||||
"upstream": [
|
||||
"begin",
|
||||
"Generate:HeavyForksTell"
|
||||
]
|
||||
},
|
||||
"Generate:HeavyForksTell": {
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"llm_id": "deepseek-chat",
|
||||
"prompt": "You are an professional interpreter.\n- Role: an professional interpreter.\n- Input format: content need to be translated => target language. \n- Answer format: => translated content in target language. \n- Examples:\n - user: 您好! => English. assistant: => How are you doing!\n - user: You look good today. => Japanese. assistant: => 今日は調子がいいですね 。\n"
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "You are an professional interpreter.\n- Role: an professional interpreter.\n- Input format: content need to be translated => target language. \n- Answer format: => translated content in target language. \n- Examples:\n - user: 您好! => English. assistant: => How are you doing!\n - user: You look good today. => Japanese. assistant: => 今日は調子がいいですね 。\n",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"upstream": ["answer:0"]
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"edges": [
|
||||
{
|
||||
"id": "c87c7805-8cf0-4cd4-b45b-152031811020",
|
||||
"label": "",
|
||||
"source": "begin",
|
||||
"target": "answer:0"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-answer:0b-generate:0d",
|
||||
"markerEnd": "logo",
|
||||
"source": "answer:0",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "generate:0",
|
||||
"targetHandle": "d",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-generate:0c-answer:0a",
|
||||
"markerEnd": "logo",
|
||||
"source": "generate:0",
|
||||
"sourceHandle": "c",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "answer:0",
|
||||
"targetHandle": "a",
|
||||
"type": "buttonEdge"
|
||||
}
|
||||
"downstream": [
|
||||
"Answer:ShortPapersShake"
|
||||
],
|
||||
"upstream": [
|
||||
"Answer:ShortPapersShake"
|
||||
]
|
||||
}
|
||||
},
|
||||
"embed_id": "",
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
@ -81,21 +67,21 @@
|
||||
"name": "Instruction"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 50,
|
||||
"height": 44,
|
||||
"id": "begin",
|
||||
"position": {
|
||||
"x": -175.31950791077287,
|
||||
"y": 32.340246044613565
|
||||
"x": -227.62119327532662,
|
||||
"y": 204.18864081386155
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -175.31950791077287,
|
||||
"y": 32.340246044613565
|
||||
"x": -227.62119327532662,
|
||||
"y": 204.18864081386155
|
||||
},
|
||||
"selected": true,
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "beginNode",
|
||||
"width": 50
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
@ -104,48 +90,164 @@
|
||||
"name": "Interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "answer:0",
|
||||
"height": 44,
|
||||
"id": "Answer:ShortPapersShake",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 6
|
||||
"x": -2.51245296887717,
|
||||
"y": 206.25402277426554
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 0,
|
||||
"y": 6
|
||||
"x": -2.51245296887717,
|
||||
"y": 206.25402277426554
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"llm_id": "deepseek-chat",
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "You are an professional interpreter.\n- Role: an professional interpreter.\n- Input format: content need to be translated => target language. \n- Answer format: => translated content in target language. \n- Examples:\n - user: 您好! => English. assistant: => How are you doing!\n - user: You look good today. => Japanese. assistant: => 今日は調子がいいですね 。\n",
|
||||
"temperature": 0.5
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "Translate"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 150,
|
||||
"id": "generate:0",
|
||||
"height": 86,
|
||||
"id": "Generate:HeavyForksTell",
|
||||
"position": {
|
||||
"x": 214.89015821545786,
|
||||
"y": 135.10439391733706
|
||||
"x": -1.8557846635797546,
|
||||
"y": 70.16420357406685
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 214.89015821545786,
|
||||
"y": 135.10439391733706
|
||||
"x": -1.8557846635797546,
|
||||
"y": 70.16420357406685
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "logicNode",
|
||||
"width": 150
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "generateNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "The large model translates the user's desired content into the target language, returns the translated language."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Translate"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 180,
|
||||
"id": "Note:VioletNumbersStrive",
|
||||
"position": {
|
||||
"x": 0.8506882512325546,
|
||||
"y": -119.10519445109118
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 0.8506882512325546,
|
||||
"y": -119.10519445109118
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 180,
|
||||
"width": 209
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 209,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Receives the content the user wants to translate and the target language, displays the translation result from the large model."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 157,
|
||||
"id": "Note:WarmDoodlesSwim",
|
||||
"position": {
|
||||
"x": 22.5293807600396,
|
||||
"y": 267.8448268086032
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 22.5293807600396,
|
||||
"y": 267.8448268086032
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 157,
|
||||
"width": 252
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 252,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "reactflow__edge-begin-Answer:ShortPapersShakec",
|
||||
"markerEnd": "logo",
|
||||
"source": "begin",
|
||||
"sourceHandle": null,
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:ShortPapersShake",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:ShortPapersShakeb-Generate:HeavyForksTellb",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:ShortPapersShake",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:HeavyForksTell",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:HeavyForksTellc-Answer:ShortPapersShakec",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:HeavyForksTell",
|
||||
"sourceHandle": "c",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:ShortPapersShake",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
571
agent/templates/investment_advisor.json
Normal file
571
agent/templates/investment_advisor.json
Normal file
File diff suppressed because one or more lines are too long
@ -6,87 +6,109 @@
|
||||
"dsl": {
|
||||
"answer": [],
|
||||
"components": {
|
||||
"Answer:FlatRavensPush": {
|
||||
"begin": {
|
||||
"obj": {
|
||||
"component_name": "Begin",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:BraveSnailsCheer",
|
||||
"Generate:UpsetCarrotsPoke"
|
||||
"Answer:FlatRavensPush"
|
||||
],
|
||||
"upstream": []
|
||||
},
|
||||
"PubMed:TwentyFansShake": {
|
||||
"obj": {
|
||||
"component_name": "PubMed",
|
||||
"params": {
|
||||
"email": "928018077@qq.com",
|
||||
"top_n": 10
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:SolidCrewsStare"
|
||||
],
|
||||
"upstream": [
|
||||
"Generate:FortyBaboonsRule"
|
||||
]
|
||||
},
|
||||
"Answer:FlatRavensPush": {
|
||||
"obj": {
|
||||
"component_name": "Answer",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:QuietMelonsHear",
|
||||
"Generate:FortyBaboonsRule"
|
||||
],
|
||||
"upstream": [
|
||||
"begin",
|
||||
"Generate:WholePansReply"
|
||||
"Generate:SolidCrewsStare"
|
||||
]
|
||||
},
|
||||
"Generate:BraveSnailsCheer": {
|
||||
"Generate:QuietMelonsHear": {
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional medical consulting translation assistant\n\nTask: Translate user questions into Chinese, ensuring accuracy of medical terminology and appropriateness of context.\n\nRequirements:\n- Accurately translate medical terminology to convey the integrity and emotional color of the original message.\n- For unclear or uncertain medical terminology, the original text may be retained to ensure accuracy.\n- Respect the privacy and sensitivity of medical consultations and ensure that sensitive information is not disclosed during the translation process.\n- If the user's question is in Chinese, there is no need to translate, just output the user's question directly\n\nExample:\nOriginal (English): Doctor, I have been suffering from chest pain and shortness of breath for the past few days.\nTranslation (Chinese): 医生,我这几天一直胸痛和气短。\n\nNote:\nOnly the translated content needs to be output, no other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Retrieval:BeigeBagsDress"
|
||||
],
|
||||
"upstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
]
|
||||
},
|
||||
"Generate:FortyBaboonsRule": {
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional Chinese-English medical question translation assistant\n\nTask: Accurately translate users' Chinese medical question content into English, ensuring accuracy of terminology and clarity of expression\n\nRequirements:\n- In-depth understanding of the terminology and disease descriptions in Chinese medical inquiries to ensure correct medical vocabulary is used in the English translation.\n- Maintain the semantic integrity and accuracy of the original text to avoid omitting important information or introducing errors.\n- Pay attention to the differences in expression habits between Chinese and English, and make appropriate adjustments to make the English translation more natural and fluent.\n- Respect the patient's privacy and the principle of medical confidentiality, and do not disclose any sensitive information during the translation process.\n\nExample:\nOriginal sentence: 我最近总是感觉胸闷,有时还会有心悸的感觉。\nTranslated: I've been feeling chest tightness recently, and sometimes I experience palpitations.\n\nNote:\nOnly the translated content should be given, do not output other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"PubMed:TwentyFansShake"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional Chinese-English medical question translation assistant\n\nTask: Accurately translate users' Chinese medical question content into English, ensuring accuracy of terminology and clarity of expression\n\nRequirements:\n- In-depth understanding of the terminology and disease descriptions in Chinese medical inquiries to ensure correct medical vocabulary is used in the English translation.\n- Maintain the semantic integrity and accuracy of the original text to avoid omitting important information or introducing errors.\n- Pay attention to the differences in expression habits between Chinese and English, and make appropriate adjustments to make the English translation more natural and fluent.\n- Respect the patient's privacy and the principle of medical confidentiality, and do not disclose any sensitive information during the translation process.\n\nExample\uff1a\nOriginal sentence: \u6211\u6700\u8fd1\u603b\u662f\u611f\u89c9\u80f8\u95f7\uff0c\u6709\u65f6\u8fd8\u4f1a\u6709\u5fc3\u60b8\u7684\u611f\u89c9\u3002\nTranslated: I've been feeling chest tightness recently, and sometimes I experience palpitations.\n\nNote:\nOnly the translated content should be given, do not output other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
]
|
||||
},
|
||||
"Generate:UpsetCarrotsPoke": {
|
||||
"downstream": [
|
||||
"Retrieval:FastPlumsWish"
|
||||
],
|
||||
"Generate:SolidCrewsStare": {
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional medical consulting translation assistant\n\nTask: Translate user questions into Chinese, ensuring accuracy of medical terminology and appropriateness of context.\n\nRequirements:\n- Accurately translate medical terminology to convey the integrity and emotional color of the original message.\n- For unclear or uncertain medical terminology, the original text may be retained to ensure accuracy.\n- Respect the privacy and sensitivity of medical consultations and ensure that sensitive information is not disclosed during the translation process.\n- If the user's question is in Chinese, there is no need to translate, just output the user's question directly\n\nExample:\nOriginal (English): Doctor, I have been suffering from chest pain and shortness of breath for the past few days.\nTranslation (Chinese): \u533b\u751f\uff0c\u6211\u8fd9\u51e0\u5929\u4e00\u76f4\u80f8\u75db\u548c\u6c14\u77ed\u3002\n\nNote:\nOnly the translated content needs to be output, no other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
]
|
||||
},
|
||||
"Generate:WholePansReply": {
|
||||
"downstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"max_tokens": 1024,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [
|
||||
{
|
||||
"component_id": "PubMed:TwentyFansShake",
|
||||
"id": "2c063fef-5379-44ae-91f6-06e914e5ad2e",
|
||||
"id": "9fe5f82e-7be5-45d2-bc6c-1f9ba7e14b34",
|
||||
"key": "pm_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:FastPlumsWish",
|
||||
"id": "51fb537e-f68d-475f-93b3-d77c85e758a1",
|
||||
"component_id": "Retrieval:BeigeBagsDress",
|
||||
"id": "d2e7b0e2-e222-4776-988c-db239581a083",
|
||||
"key": "kb_input"
|
||||
}
|
||||
],
|
||||
@ -96,30 +118,15 @@
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
],
|
||||
"upstream": [
|
||||
"PubMed:TwentyFansShake",
|
||||
"Retrieval:FastPlumsWish"
|
||||
"Retrieval:BeigeBagsDress"
|
||||
]
|
||||
},
|
||||
"PubMed:TwentyFansShake": {
|
||||
"downstream": [
|
||||
"Generate:WholePansReply"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "PubMed",
|
||||
"params": {
|
||||
"email": "email@example.com",
|
||||
"top_n": 10
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Generate:BraveSnailsCheer"
|
||||
]
|
||||
},
|
||||
"Retrieval:FastPlumsWish": {
|
||||
"downstream": [
|
||||
"Generate:WholePansReply"
|
||||
],
|
||||
"Retrieval:BeigeBagsDress": {
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
@ -129,128 +136,15 @@
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Generate:UpsetCarrotsPoke"
|
||||
]
|
||||
},
|
||||
"begin": {
|
||||
"downstream": [
|
||||
"Answer:FlatRavensPush"
|
||||
"Generate:SolidCrewsStare"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Begin",
|
||||
"params": {}
|
||||
},
|
||||
"upstream": []
|
||||
"upstream": [
|
||||
"Generate:QuietMelonsHear"
|
||||
]
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"edges": [
|
||||
{
|
||||
"id": "reactflow__edge-begin-Answer:FlatRavensPushc",
|
||||
"markerEnd": "logo",
|
||||
"source": "begin",
|
||||
"sourceHandle": null,
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:FlatRavensPush",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-PubMed:TwentyFansShakeb-Generate:WholePansReplyc",
|
||||
"markerEnd": "logo",
|
||||
"source": "PubMed:TwentyFansShake",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:WholePansReply",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Retrieval:FastPlumsWishb-Generate:WholePansReplyc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Retrieval:FastPlumsWish",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:WholePansReply",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:WholePansReplya-Answer:FlatRavensPusha",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:WholePansReply",
|
||||
"sourceHandle": "a",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:FlatRavensPush",
|
||||
"targetHandle": "a",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:FlatRavensPushb-Generate:BraveSnailsCheerc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:FlatRavensPush",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:BraveSnailsCheer",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:BraveSnailsCheerb-PubMed:TwentyFansShakec",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:BraveSnailsCheer",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "PubMed:TwentyFansShake",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:FlatRavensPushd-Generate:UpsetCarrotsPokec",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:FlatRavensPush",
|
||||
"sourceHandle": "d",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:UpsetCarrotsPoke",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:UpsetCarrotsPokeb-Retrieval:FastPlumsWishc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:UpsetCarrotsPoke",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Retrieval:FastPlumsWish",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
@ -258,21 +152,21 @@
|
||||
"name": "opening"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 50,
|
||||
"height": 44,
|
||||
"id": "begin",
|
||||
"position": {
|
||||
"x": -150.51830264174046,
|
||||
"y": 192.36132289534214
|
||||
"x": -599.8361708291377,
|
||||
"y": 161.91688790133628
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -150.51830264174046,
|
||||
"y": 192.36132289534214
|
||||
"x": -599.8361708291377,
|
||||
"y": 161.91688790133628
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "beginNode",
|
||||
"width": 50
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
@ -284,21 +178,21 @@
|
||||
"name": "Search PubMed"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 50,
|
||||
"height": 44,
|
||||
"id": "PubMed:TwentyFansShake",
|
||||
"position": {
|
||||
"x": 411.1209571180216,
|
||||
"y": 293.67922026697573
|
||||
"x": 389.7229173847695,
|
||||
"y": 276.4372267765921
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 411.1209571180216,
|
||||
"y": 293.67922026697573
|
||||
"x": 389.7229173847695,
|
||||
"y": 276.4372267765921
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "ragNode",
|
||||
"width": 50
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
@ -307,49 +201,21 @@
|
||||
"name": "Interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"height": 44,
|
||||
"id": "Answer:FlatRavensPush",
|
||||
"position": {
|
||||
"x": -27.594553801136584,
|
||||
"y": 166.66278050463274
|
||||
"x": -370.881803561134,
|
||||
"y": 161.41373998842477
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -27.594553801136584,
|
||||
"y": 166.66278050463274
|
||||
"x": -370.881803561134,
|
||||
"y": 161.41373998842477
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "Search KB"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "Retrieval:FastPlumsWish",
|
||||
"position": {
|
||||
"x": 389.1925431609217,
|
||||
"y": -53.66130634833843
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 389.1925431609217,
|
||||
"y": -53.66130634833843
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
@ -357,20 +223,100 @@
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 1024,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional medical consulting translation assistant\n\nTask: Translate user questions into Chinese, ensuring accuracy of medical terminology and appropriateness of context.\n\nRequirements:\n- Accurately translate medical terminology to convey the integrity and emotional color of the original message.\n- For unclear or uncertain medical terminology, the original text may be retained to ensure accuracy.\n- Respect the privacy and sensitivity of medical consultations and ensure that sensitive information is not disclosed during the translation process.\n- If the user's question is in Chinese, there is no need to translate, just output the user's question directly\n\nExample:\nOriginal (English): Doctor, I have been suffering from chest pain and shortness of breath for the past few days.\nTranslation (Chinese): 医生,我这几天一直胸痛和气短。\n\nNote:\nOnly the translated content needs to be output, no other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "Translate to Chinese"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 86,
|
||||
"id": "Generate:QuietMelonsHear",
|
||||
"position": {
|
||||
"x": -2.756518132081453,
|
||||
"y": 38.86485966020132
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -2.756518132081453,
|
||||
"y": 38.86485966020132
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "generateNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional Chinese-English medical question translation assistant\n\nTask: Accurately translate users' Chinese medical question content into English, ensuring accuracy of terminology and clarity of expression\n\nRequirements:\n- In-depth understanding of the terminology and disease descriptions in Chinese medical inquiries to ensure correct medical vocabulary is used in the English translation.\n- Maintain the semantic integrity and accuracy of the original text to avoid omitting important information or introducing errors.\n- Pay attention to the differences in expression habits between Chinese and English, and make appropriate adjustments to make the English translation more natural and fluent.\n- Respect the patient's privacy and the principle of medical confidentiality, and do not disclose any sensitive information during the translation process.\n\nExample:\nOriginal sentence: 我最近总是感觉胸闷,有时还会有心悸的感觉。\nTranslated: I've been feeling chest tightness recently, and sometimes I experience palpitations.\n\nNote:\nOnly the translated content should be given, do not output other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "Translate to English"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 86,
|
||||
"id": "Generate:FortyBaboonsRule",
|
||||
"position": {
|
||||
"x": -3.825864707727135,
|
||||
"y": 253.2285157283701
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -3.825864707727135,
|
||||
"y": 253.2285157283701
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "generateNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [
|
||||
{
|
||||
"component_id": "PubMed:TwentyFansShake",
|
||||
"id": "2c063fef-5379-44ae-91f6-06e914e5ad2e",
|
||||
"id": "9fe5f82e-7be5-45d2-bc6c-1f9ba7e14b34",
|
||||
"key": "pm_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:FastPlumsWish",
|
||||
"id": "51fb537e-f68d-475f-93b3-d77c85e758a1",
|
||||
"component_id": "Retrieval:BeigeBagsDress",
|
||||
"id": "d2e7b0e2-e222-4776-988c-db239581a083",
|
||||
"key": "kb_input"
|
||||
}
|
||||
],
|
||||
@ -386,100 +332,336 @@
|
||||
"name": "LLM"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 150,
|
||||
"id": "Generate:WholePansReply",
|
||||
"height": 172,
|
||||
"id": "Generate:SolidCrewsStare",
|
||||
"position": {
|
||||
"x": 632.6457249054133,
|
||||
"y": 243.99641016676225
|
||||
"x": 427.0382682049008,
|
||||
"y": -221.26975391424511
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 632.6457249054133,
|
||||
"y": 243.99641016676225
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional Chinese-English medical question translation assistant\n\nTask: Accurately translate users' Chinese medical question content into English, ensuring accuracy of terminology and clarity of expression\n\nRequirements:\n- In-depth understanding of the terminology and disease descriptions in Chinese medical inquiries to ensure correct medical vocabulary is used in the English translation.\n- Maintain the semantic integrity and accuracy of the original text to avoid omitting important information or introducing errors.\n- Pay attention to the differences in expression habits between Chinese and English, and make appropriate adjustments to make the English translation more natural and fluent.\n- Respect the patient's privacy and the principle of medical confidentiality, and do not disclose any sensitive information during the translation process.\n\nExample\uff1a\nOriginal sentence: \u6211\u6700\u8fd1\u603b\u662f\u611f\u89c9\u80f8\u95f7\uff0c\u6709\u65f6\u8fd8\u4f1a\u6709\u5fc3\u60b8\u7684\u611f\u89c9\u3002\nTranslated: I've been feeling chest tightness recently, and sometimes I experience palpitations.\n\nNote:\nOnly the translated content should be given, do not output other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "Translate to English"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 150,
|
||||
"id": "Generate:BraveSnailsCheer",
|
||||
"position": {
|
||||
"x": 235.27003638545648,
|
||||
"y": 141.22382352447266
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 235.27003638545648,
|
||||
"y": 141.22382352447266
|
||||
"x": 427.0382682049008,
|
||||
"y": -221.26975391424511
|
||||
},
|
||||
"selected": true,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 150
|
||||
"type": "generateNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are a professional medical consulting translation assistant\n\nTask: Translate user questions into Chinese, ensuring accuracy of medical terminology and appropriateness of context.\n\nRequirements:\n- Accurately translate medical terminology to convey the integrity and emotional color of the original message.\n- For unclear or uncertain medical terminology, the original text may be retained to ensure accuracy.\n- Respect the privacy and sensitivity of medical consultations and ensure that sensitive information is not disclosed during the translation process.\n- If the user's question is in Chinese, there is no need to translate, just output the user's question directly\n\nExample:\nOriginal (English): Doctor, I have been suffering from chest pain and shortness of breath for the past few days.\nTranslation (Chinese): \u533b\u751f\uff0c\u6211\u8fd9\u51e0\u5929\u4e00\u76f4\u80f8\u75db\u548c\u6c14\u77ed\u3002\n\nNote:\nOnly the translated content needs to be output, no other irrelevant content!",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "Translate to Chinese"
|
||||
"label": "Retrieval",
|
||||
"name": "Search Q&A"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 150,
|
||||
"id": "Generate:UpsetCarrotsPoke",
|
||||
"height": 44,
|
||||
"id": "Retrieval:BeigeBagsDress",
|
||||
"position": {
|
||||
"x": 174.90602346154253,
|
||||
"y": -74.84373200722371
|
||||
"x": 382.25527986090765,
|
||||
"y": 35.38705653631584
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 174.90602346154253,
|
||||
"y": -74.84373200722371
|
||||
"x": 382.25527986090765,
|
||||
"y": 35.38705653631584
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 150
|
||||
"type": "retrievalNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Receives the user's financial inquiries and displays the large model's response to financial questions."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Interface"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 162,
|
||||
"id": "Note:RedZebrasEnjoy",
|
||||
"position": {
|
||||
"x": -374.13983303471906,
|
||||
"y": 219.54112331790157
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -374.13983303471906,
|
||||
"y": 219.54112331790157
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 162,
|
||||
"width": 200
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Translate user's question to English by LLM."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Translate to English"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 128,
|
||||
"id": "Note:DarkIconsClap",
|
||||
"position": {
|
||||
"x": -0.453362859534991,
|
||||
"y": 357.3687792184929
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -0.453362859534991,
|
||||
"y": 357.3687792184929
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 128,
|
||||
"width": 204
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 204
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Translate user's question to Chinese by LLM."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Translate to Chinese"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 128,
|
||||
"id": "Note:SmallRiversTap",
|
||||
"position": {
|
||||
"x": -5.453362859535048,
|
||||
"y": -105.63122078150693
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -5.453362859535048,
|
||||
"y": -105.63122078150693
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 128,
|
||||
"width": 196
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 196
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "PubMed® comprises more than 37 million citations for biomedical literature from MEDLINE, life science journals, and online books. Citations may include links to full text content from PubMed Central and publisher web sites."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Search PubMed"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 220,
|
||||
"id": "Note:MightyDeerShout",
|
||||
"position": {
|
||||
"x": 718.5466371404648,
|
||||
"y": 275.36877921849293
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 718.5466371404648,
|
||||
"y": 275.36877921849293
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 220,
|
||||
"width": 287
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 287
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "You can download the Q&A dataset at\nhttps://huggingface.co/datasets/InfiniFlow/medical_QA"
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Search Q&A"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 128,
|
||||
"id": "Note:VioletSuitsFlash",
|
||||
"position": {
|
||||
"x": 776.4332169584197,
|
||||
"y": 32.89802610798361
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 776.4332169584197,
|
||||
"y": 32.89802610798361
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 128,
|
||||
"width": 387
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 387
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "A prompt sumerize content from search result from PubMed and Q&A dataset."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: LLM"
|
||||
},
|
||||
"dragHandle": ".note-drag-handle",
|
||||
"dragging": false,
|
||||
"height": 128,
|
||||
"id": "Note:BeigeCoinsBuild",
|
||||
"position": {
|
||||
"x": 756.9053449234701,
|
||||
"y": -212.92342186138177
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 756.9053449234701,
|
||||
"y": -212.92342186138177
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 269
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "reactflow__edge-begin-Answer:FlatRavensPushc",
|
||||
"markerEnd": "logo",
|
||||
"source": "begin",
|
||||
"sourceHandle": null,
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:FlatRavensPush",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:FlatRavensPushb-Generate:QuietMelonsHearc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:FlatRavensPush",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:QuietMelonsHear",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:FlatRavensPushb-Generate:FortyBaboonsRulec",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:FlatRavensPush",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:FortyBaboonsRule",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:FortyBaboonsRuleb-PubMed:TwentyFansShakec",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:FortyBaboonsRule",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "PubMed:TwentyFansShake",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-PubMed:TwentyFansShakeb-Generate:SolidCrewsStareb",
|
||||
"markerEnd": "logo",
|
||||
"source": "PubMed:TwentyFansShake",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:SolidCrewsStare",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Retrieval:BeigeBagsDressb-Generate:SolidCrewsStareb",
|
||||
"markerEnd": "logo",
|
||||
"source": "Retrieval:BeigeBagsDress",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:SolidCrewsStare",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:QuietMelonsHearb-Retrieval:BeigeBagsDressc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:QuietMelonsHear",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Retrieval:BeigeBagsDress",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:SolidCrewsStarec-Answer:FlatRavensPushc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:SolidCrewsStare",
|
||||
"sourceHandle": "c",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:FlatRavensPush",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -6,134 +6,469 @@
|
||||
"dsl": {
|
||||
"answer": [],
|
||||
"components": {
|
||||
"Answer:SocialAdsWonder": {
|
||||
"begin": {
|
||||
"obj": {
|
||||
"component_name": "Begin",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": [
|
||||
"Retrieval:WetNewsHunt",
|
||||
"Retrieval:OpenWingsRepeat",
|
||||
"Retrieval:StrongDrinksShare"
|
||||
"Answer:SocialAdsWonder"
|
||||
],
|
||||
"upstream": []
|
||||
},
|
||||
"Answer:SocialAdsWonder": {
|
||||
"obj": {
|
||||
"component_name": "Answer",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": [
|
||||
"Retrieval:TrueCornersJam",
|
||||
"Retrieval:EasyDryersShop",
|
||||
"Retrieval:LazyChefsWatch"
|
||||
],
|
||||
"upstream": [
|
||||
"begin",
|
||||
"Generate:OliveDotsInvent"
|
||||
"Generate:RareSymbolsGrin"
|
||||
]
|
||||
},
|
||||
"Generate:OliveDotsInvent": {
|
||||
"Retrieval:TrueCornersJam": {
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in DDL!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.02,
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
"Generate:RareSymbolsGrin"
|
||||
],
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"Retrieval:EasyDryersShop": {
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in Q-SQL!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:RareSymbolsGrin"
|
||||
],
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"Retrieval:LazyChefsWatch": {
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in DB-Description!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"downstream": [
|
||||
"Generate:RareSymbolsGrin"
|
||||
],
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"Generate:RareSymbolsGrin": {
|
||||
"obj": {
|
||||
"component_name": "Generate",
|
||||
"params": {
|
||||
"cite": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [
|
||||
{
|
||||
"component_id": "Retrieval:StrongDrinksShare",
|
||||
"id": "36c09e33-bad2-42fe-9a56-f136677bb405",
|
||||
"component_id": "Retrieval:TrueCornersJam",
|
||||
"id": "78644673-9236-4605-8110-59705fc38784",
|
||||
"key": "ddl_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:OpenWingsRepeat",
|
||||
"id": "e4cfe15e-64cd-4351-b49e-0da2f5c8ec34",
|
||||
"component_id": "Retrieval:LazyChefsWatch",
|
||||
"id": "afbf91ce-6f58-4573-b02d-9a4973f124f4",
|
||||
"key": "db_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:WetNewsHunt",
|
||||
"id": "946d8272-fc98-4040-a75f-502df7e4a42e",
|
||||
"component_id": "Retrieval:EasyDryersShop",
|
||||
"id": "ee2b84f4-1cf5-43be-80e6-60bfaea3d834",
|
||||
"key": "sql_input"
|
||||
}
|
||||
],
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "Role: You are an intelligent DB assistant that can help humans generate SQL statements for problems.\n\n##The user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\n\n##You may use the following DDL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {ddl_input}.\n\n##You may use the following documentation as a reference for what tables might be available. Use responses to past questions also to guide you: {db_input}.\n\n##You may use the following SQL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {sql_input}.\n\n##Respond with only SQL code. Do not answer with any explanations -- just the code.",
|
||||
"prompt": "##The user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\n\n##Respond with only SQL code. Do not answer with any explanations -- just the code.\n\n##You may use the following DDL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {ddl_input}.\n\n##You may use the following documentation as a reference for what tables might be available. Use responses to past questions also to guide you: {db_input}.\n\n##You may use the following SQL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {sql_input}.",
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.3
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Retrieval:StrongDrinksShare",
|
||||
"Retrieval:WetNewsHunt",
|
||||
"Retrieval:OpenWingsRepeat"
|
||||
]
|
||||
},
|
||||
"Retrieval:OpenWingsRepeat": {
|
||||
"downstream": [
|
||||
"Generate:OliveDotsInvent"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in DB-Description!",
|
||||
"kb_ids": [
|
||||
"0ab5de985ba911efad9942010a8a0006"
|
||||
],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"Retrieval:StrongDrinksShare": {
|
||||
"downstream": [
|
||||
"Generate:OliveDotsInvent"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in DDL!",
|
||||
"kb_ids": [
|
||||
"b1a6a45e5ba811ef80dc42010a8a0006"
|
||||
],
|
||||
"keywords_similarity_weight": 0.1,
|
||||
"similarity_threshold": 0.02,
|
||||
"top_n": 18
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"Retrieval:WetNewsHunt": {
|
||||
"downstream": [
|
||||
"Generate:OliveDotsInvent"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Retrieval",
|
||||
"params": {
|
||||
"empty_response": "Nothing found in Q-SQL!",
|
||||
"kb_ids": [
|
||||
"31257b925b9f11ef9f0142010a8a0004"
|
||||
],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
}
|
||||
},
|
||||
"upstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
]
|
||||
},
|
||||
"begin": {
|
||||
"downstream": [
|
||||
"Answer:SocialAdsWonder"
|
||||
],
|
||||
"obj": {
|
||||
"component_name": "Begin",
|
||||
"params": {}
|
||||
},
|
||||
"upstream": []
|
||||
"upstream": [
|
||||
"Retrieval:TrueCornersJam",
|
||||
"Retrieval:EasyDryersShop",
|
||||
"Retrieval:LazyChefsWatch"
|
||||
]
|
||||
}
|
||||
},
|
||||
"embed_id": "",
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"label": "Begin",
|
||||
"name": "begin"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 44,
|
||||
"id": "begin",
|
||||
"position": {
|
||||
"x": -520.486587527275,
|
||||
"y": 117.87988995940702
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -520.486587527275,
|
||||
"y": 117.87988995940702
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "beginNode",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {},
|
||||
"label": "Answer",
|
||||
"name": "interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 44,
|
||||
"id": "Answer:SocialAdsWonder",
|
||||
"position": {
|
||||
"x": -284.9289105495367,
|
||||
"y": 119.9282206409824
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -284.9289105495367,
|
||||
"y": 119.9282206409824
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in DDL!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.02,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "DDL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 44,
|
||||
"id": "Retrieval:TrueCornersJam",
|
||||
"position": {
|
||||
"x": 119.61927071085717,
|
||||
"y": -40.184181873335746
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 119.61927071085717,
|
||||
"y": -40.184181873335746
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "retrievalNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in Q-SQL!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "Q->SQL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 44,
|
||||
"id": "Retrieval:EasyDryersShop",
|
||||
"position": {
|
||||
"x": 81.2024576603057,
|
||||
"y": 94.16303322180948
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 81.2024576603057,
|
||||
"y": 94.16303322180948
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "retrievalNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in DB-Description!",
|
||||
"kb_ids": [],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "DB Description"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 44,
|
||||
"id": "Retrieval:LazyChefsWatch",
|
||||
"position": {
|
||||
"x": 51.228157704293324,
|
||||
"y": 252.77721891325103
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 51.228157704293324,
|
||||
"y": 252.77721891325103
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "retrievalNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat@DeepSeek",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameter": "Precise",
|
||||
"parameters": [
|
||||
{
|
||||
"component_id": "Retrieval:TrueCornersJam",
|
||||
"id": "78644673-9236-4605-8110-59705fc38784",
|
||||
"key": "ddl_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:LazyChefsWatch",
|
||||
"id": "afbf91ce-6f58-4573-b02d-9a4973f124f4",
|
||||
"key": "db_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:EasyDryersShop",
|
||||
"id": "ee2b84f4-1cf5-43be-80e6-60bfaea3d834",
|
||||
"key": "sql_input"
|
||||
}
|
||||
],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "##The user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\n\n##Respond with only SQL code. Do not answer with any explanations -- just the code.\n\n##You may use the following DDL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {ddl_input}.\n\n##You may use the following documentation as a reference for what tables might be available. Use responses to past questions also to guide you: {db_input}.\n\n##You may use the following SQL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {sql_input}.",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "LLM"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 212,
|
||||
"id": "Generate:RareSymbolsGrin",
|
||||
"position": {
|
||||
"x": 6.6098512156750076,
|
||||
"y": 405.0105548561454
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 6.6098512156750076,
|
||||
"y": 405.0105548561454
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "generateNode",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Receives a sentence that the user wants to convert into SQL and displays the result of the large model's SQL conversion."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 132,
|
||||
"id": "Note:GentleRabbitsWonder",
|
||||
"position": {
|
||||
"x": -287.3066094433631,
|
||||
"y": -30.808189185380513
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -287.3066094433631,
|
||||
"y": -30.808189185380513
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 132,
|
||||
"width": 324
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 324,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "The large model learns which tables may be available based on the responses from three knowledge bases and converts the user's input into SQL statements."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: LLM"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 147,
|
||||
"id": "Note:SixCitiesJoke",
|
||||
"position": {
|
||||
"x": 5.12121582244032,
|
||||
"y": 637.6539219843564
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 5.12121582244032,
|
||||
"y": 637.6539219843564
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 147,
|
||||
"width": 326
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 326,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Searches for description about meanings of tables and fields."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: DB description"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 128,
|
||||
"id": "Note:FamousCarpetsTaste",
|
||||
"position": {
|
||||
"x": 399.9267065852242,
|
||||
"y": 250.0329701879931
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 399.9267065852242,
|
||||
"y": 250.0329701879931
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 269,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "Searches for samples about question to SQL.\nPlease check this dataset: https://huggingface.co/datasets/InfiniFlow/text2sql"
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: Q->SQL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 130,
|
||||
"id": "Note:PoliteBeesArrive",
|
||||
"position": {
|
||||
"x": 489.0393427986917,
|
||||
"y": 96.58232093146341
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 489.0393427986917,
|
||||
"y": 96.58232093146341
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 130,
|
||||
"width": 451
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 451,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"text": "DDL(Data Definition Language).\n\nSearches for relevent database creation statements.\n\nIt should bind with a KB to which DDL is dumped in.\nYou could use 'General' as parsing method and ';' as delimiter."
|
||||
},
|
||||
"label": "Note",
|
||||
"name": "N: DDL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 258,
|
||||
"id": "Note:SmartWingsDouble",
|
||||
"position": {
|
||||
"x": 404.1930553966363,
|
||||
"y": -208.84980249039137
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 404.1930553966363,
|
||||
"y": -208.84980249039137
|
||||
},
|
||||
"resizing": false,
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"style": {
|
||||
"height": 258,
|
||||
"width": 283
|
||||
},
|
||||
"targetPosition": "left",
|
||||
"type": "noteNode",
|
||||
"width": 283,
|
||||
"dragHandle": ".note-drag-handle"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "reactflow__edge-begin-Answer:SocialAdsWonderc",
|
||||
@ -149,7 +484,7 @@
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:WetNewsHuntc",
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:TrueCornersJamc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:SocialAdsWonder",
|
||||
"sourceHandle": "b",
|
||||
@ -157,12 +492,12 @@
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Retrieval:WetNewsHunt",
|
||||
"target": "Retrieval:TrueCornersJam",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:OpenWingsRepeatc",
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:EasyDryersShopc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:SocialAdsWonder",
|
||||
"sourceHandle": "b",
|
||||
@ -170,12 +505,12 @@
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Retrieval:OpenWingsRepeat",
|
||||
"target": "Retrieval:EasyDryersShop",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:StrongDrinksSharec",
|
||||
"id": "reactflow__edge-Answer:SocialAdsWonderb-Retrieval:LazyChefsWatchc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Answer:SocialAdsWonder",
|
||||
"sourceHandle": "b",
|
||||
@ -183,257 +518,62 @@
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Retrieval:StrongDrinksShare",
|
||||
"target": "Retrieval:LazyChefsWatch",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Retrieval:StrongDrinksShareb-Generate:OliveDotsInventa",
|
||||
"id": "reactflow__edge-Retrieval:TrueCornersJamb-Generate:RareSymbolsGrinb",
|
||||
"markerEnd": "logo",
|
||||
"source": "Retrieval:StrongDrinksShare",
|
||||
"source": "Retrieval:TrueCornersJam",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:OliveDotsInvent",
|
||||
"targetHandle": "a",
|
||||
"target": "Generate:RareSymbolsGrin",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Retrieval:WetNewsHuntb-Generate:OliveDotsInventa",
|
||||
"id": "reactflow__edge-Retrieval:EasyDryersShopb-Generate:RareSymbolsGrinb",
|
||||
"markerEnd": "logo",
|
||||
"source": "Retrieval:WetNewsHunt",
|
||||
"source": "Retrieval:EasyDryersShop",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:OliveDotsInvent",
|
||||
"targetHandle": "a",
|
||||
"target": "Generate:RareSymbolsGrin",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Retrieval:OpenWingsRepeatb-Generate:OliveDotsInventa",
|
||||
"id": "reactflow__edge-Retrieval:LazyChefsWatchb-Generate:RareSymbolsGrinb",
|
||||
"markerEnd": "logo",
|
||||
"source": "Retrieval:OpenWingsRepeat",
|
||||
"source": "Retrieval:LazyChefsWatch",
|
||||
"sourceHandle": "b",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Generate:OliveDotsInvent",
|
||||
"targetHandle": "a",
|
||||
"target": "Generate:RareSymbolsGrin",
|
||||
"targetHandle": "b",
|
||||
"type": "buttonEdge"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-Generate:OliveDotsInventc-Answer:SocialAdsWonderd",
|
||||
"id": "reactflow__edge-Generate:RareSymbolsGrinc-Answer:SocialAdsWonderc",
|
||||
"markerEnd": "logo",
|
||||
"source": "Generate:OliveDotsInvent",
|
||||
"source": "Generate:RareSymbolsGrin",
|
||||
"sourceHandle": "c",
|
||||
"style": {
|
||||
"stroke": "rgb(202 197 245)",
|
||||
"strokeWidth": 2
|
||||
},
|
||||
"target": "Answer:SocialAdsWonder",
|
||||
"targetHandle": "d",
|
||||
"targetHandle": "c",
|
||||
"type": "buttonEdge"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"label": "Begin",
|
||||
"name": "begin"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 50,
|
||||
"id": "begin",
|
||||
"position": {
|
||||
"x": -398.61441136019056,
|
||||
"y": 144.28672563400067
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -398.61441136019056,
|
||||
"y": 144.28672563400067
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "left",
|
||||
"targetPosition": "right",
|
||||
"type": "beginNode",
|
||||
"width": 50
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {},
|
||||
"label": "Answer",
|
||||
"name": "interface"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "Answer:SocialAdsWonder",
|
||||
"position": {
|
||||
"x": -271.9289105495367,
|
||||
"y": 120.9282206409824
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -271.9289105495367,
|
||||
"y": 120.9282206409824
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in Q-SQL!",
|
||||
"kb_ids": [
|
||||
"31257b925b9f11ef9f0142010a8a0004"
|
||||
],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "Q->SQL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "Retrieval:WetNewsHunt",
|
||||
"position": {
|
||||
"x": -44.1852870981603,
|
||||
"y": 130.77161580129524
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -44.1852870981603,
|
||||
"y": 130.77161580129524
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"cite": true,
|
||||
"frequencyPenaltyEnabled": true,
|
||||
"frequency_penalty": 0.7,
|
||||
"llm_id": "deepseek-chat",
|
||||
"maxTokensEnabled": true,
|
||||
"max_tokens": 256,
|
||||
"message_history_window_size": 12,
|
||||
"parameters": [
|
||||
{
|
||||
"component_id": "Retrieval:StrongDrinksShare",
|
||||
"id": "36c09e33-bad2-42fe-9a56-f136677bb405",
|
||||
"key": "ddl_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:OpenWingsRepeat",
|
||||
"id": "e4cfe15e-64cd-4351-b49e-0da2f5c8ec34",
|
||||
"key": "db_input"
|
||||
},
|
||||
{
|
||||
"component_id": "Retrieval:WetNewsHunt",
|
||||
"id": "946d8272-fc98-4040-a75f-502df7e4a42e",
|
||||
"key": "sql_input"
|
||||
}
|
||||
],
|
||||
"presencePenaltyEnabled": true,
|
||||
"presence_penalty": 0.4,
|
||||
"prompt": "##The user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\n\n##Respond with only SQL code. Do not answer with any explanations -- just the code.\n\n##You may use the following DDL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {ddl_input}.\n\n##You may use the following documentation as a reference for what tables might be available. Use responses to past questions also to guide you: {db_input}.\n\n##You may use the following SQL statements as a reference for what tables might be available. Use responses to past questions also to guide you: {sql_input}.",
|
||||
"temperature": 0.1,
|
||||
"temperatureEnabled": true,
|
||||
"topPEnabled": true,
|
||||
"top_p": 0.3
|
||||
},
|
||||
"label": "Generate",
|
||||
"name": "FuzzyGoatsCover"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 150,
|
||||
"id": "Generate:OliveDotsInvent",
|
||||
"position": {
|
||||
"x": 127.63574050151522,
|
||||
"y": -209.30480702441503
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 127.63574050151522,
|
||||
"y": -209.30480702441503
|
||||
},
|
||||
"selected": true,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in DB-Description!",
|
||||
"kb_ids": [
|
||||
"0ab5de985ba911efad9942010a8a0006"
|
||||
],
|
||||
"keywords_similarity_weight": 0.3,
|
||||
"similarity_threshold": 0.2,
|
||||
"top_n": 8
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "DB Description"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "Retrieval:OpenWingsRepeat",
|
||||
"position": {
|
||||
"x": -44.48969718602855,
|
||||
"y": 272.5769102113132
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -44.48969718602855,
|
||||
"y": 272.5769102113132
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"form": {
|
||||
"empty_response": "Nothing found in DDL!",
|
||||
"kb_ids": [
|
||||
"b1a6a45e5ba811ef80dc42010a8a0006"
|
||||
],
|
||||
"keywords_similarity_weight": 0.1,
|
||||
"similarity_threshold": 0.02,
|
||||
"top_n": 18
|
||||
},
|
||||
"label": "Retrieval",
|
||||
"name": "DDL"
|
||||
},
|
||||
"dragging": false,
|
||||
"height": 100,
|
||||
"id": "Retrieval:StrongDrinksShare",
|
||||
"position": {
|
||||
"x": -43.93396035294677,
|
||||
"y": -8.215558854318687
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": -43.93396035294677,
|
||||
"y": -8.215558854318687
|
||||
},
|
||||
"selected": false,
|
||||
"sourcePosition": "right",
|
||||
"targetPosition": "left",
|
||||
"type": "logicNode",
|
||||
"width": 100
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -26,20 +26,48 @@
|
||||
"category_description": {
|
||||
"product_related": {
|
||||
"description": "The question is about the product usage, appearance and how it works.",
|
||||
"examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?"
|
||||
"examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?",
|
||||
"to": "message:0"
|
||||
},
|
||||
"others": {
|
||||
"description": "The question is not about the product usage, appearance and how it works.",
|
||||
"examples": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?"
|
||||
"examples": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?",
|
||||
"to": "message:1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downstream": [],
|
||||
"downstream": ["message:0","message:1"],
|
||||
"upstream": ["answer:0"]
|
||||
},
|
||||
"message:0": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 0!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["categorize:0"]
|
||||
},
|
||||
"message:1": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 1!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["categorize:0"]
|
||||
}
|
||||
},
|
||||
"history": [],
|
||||
"messages": [],
|
||||
"path": [],
|
||||
"reference": [],
|
||||
"answer": []
|
||||
}
|
||||
113
agent/test/dsl_examples/concentrator_message.json
Normal file
113
agent/test/dsl_examples/concentrator_message.json
Normal file
@ -0,0 +1,113 @@
|
||||
{
|
||||
"components": {
|
||||
"begin": {
|
||||
"obj":{
|
||||
"component_name": "Begin",
|
||||
"params": {
|
||||
"prologue": "Hi there!"
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": []
|
||||
},
|
||||
"answer:0": {
|
||||
"obj": {
|
||||
"component_name": "Answer",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": ["categorize:0"],
|
||||
"upstream": ["begin"]
|
||||
},
|
||||
"categorize:0": {
|
||||
"obj": {
|
||||
"component_name": "Categorize",
|
||||
"params": {
|
||||
"llm_id": "deepseek-chat",
|
||||
"category_description": {
|
||||
"product_related": {
|
||||
"description": "The question is about the product usage, appearance and how it works.",
|
||||
"examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?",
|
||||
"to": "concentrator:0"
|
||||
},
|
||||
"others": {
|
||||
"description": "The question is not about the product usage, appearance and how it works.",
|
||||
"examples": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?",
|
||||
"to": "concentrator:1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downstream": ["concentrator:0","concentrator:1"],
|
||||
"upstream": ["answer:0"]
|
||||
},
|
||||
"concentrator:0": {
|
||||
"obj": {
|
||||
"component_name": "Concentrator",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": ["message:0"],
|
||||
"upstream": ["categorize:0"]
|
||||
},
|
||||
"concentrator:1": {
|
||||
"obj": {
|
||||
"component_name": "Concentrator",
|
||||
"params": {}
|
||||
},
|
||||
"downstream": ["message:1_0","message:1_1","message:1_2"],
|
||||
"upstream": ["categorize:0"]
|
||||
},
|
||||
"message:0": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 0_0!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["concentrator:0"]
|
||||
},
|
||||
"message:1_0": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 1_0!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["concentrator:1"]
|
||||
},
|
||||
"message:1_1": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 1_1!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["concentrator:1"]
|
||||
},
|
||||
"message:1_2": {
|
||||
"obj": {
|
||||
"component_name": "Message",
|
||||
"params": {
|
||||
"messages": [
|
||||
"Message 1_2!!!!!!!"
|
||||
]
|
||||
}
|
||||
},
|
||||
"downstream": ["answer:0"],
|
||||
"upstream": ["concentrator:1"]
|
||||
}
|
||||
},
|
||||
"history": [],
|
||||
"messages": [],
|
||||
"path": [],
|
||||
"reference": [],
|
||||
"answer": []
|
||||
}
|
||||
@ -83,7 +83,7 @@ def register_page(page_path):
|
||||
sys.modules[module_name] = page
|
||||
spec.loader.exec_module(page)
|
||||
page_name = getattr(page, 'page_name', page_name)
|
||||
url_prefix = f'/api/{API_VERSION}/{page_name}' if "/sdk/" in path else f'/{API_VERSION}/{page_name}'
|
||||
url_prefix = f'/api/{API_VERSION}' if "/sdk/" in path else f'/{API_VERSION}/{page_name}'
|
||||
|
||||
app.register_blueprint(page.manager, url_prefix=url_prefix)
|
||||
return url_prefix
|
||||
|
||||
@ -22,10 +22,10 @@ from api.db.services.llm_service import TenantLLMService
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from api.db import FileType, LLMType, ParserType, FileSource
|
||||
from api.db.db_models import APIToken, API4Conversation, Task, File
|
||||
from api.db.db_models import APIToken, Task, File
|
||||
from api.db.services import duplicate_name
|
||||
from api.db.services.api_service import APITokenService, API4ConversationService
|
||||
from api.db.services.dialog_service import DialogService, chat
|
||||
from api.db.services.dialog_service import DialogService, chat, keyword_extraction
|
||||
from api.db.services.document_service import DocumentService, doc_upload_and_parse
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
@ -34,23 +34,17 @@ from api.db.services.task_service import queue_tasks, TaskService
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.settings import RetCode, retrievaler
|
||||
from api.utils import get_uuid, current_timestamp, datetime_format
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, get_json_result, validate_request
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, get_json_result, validate_request, \
|
||||
generate_confirmation_token
|
||||
|
||||
from api.utils.file_utils import filename_type, thumbnail
|
||||
from rag.nlp import keyword_extraction
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
|
||||
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService
|
||||
from api.db.services.canvas_service import UserCanvasService
|
||||
from agent.canvas import Canvas
|
||||
from functools import partial
|
||||
|
||||
|
||||
def generate_confirmation_token(tenent_id):
|
||||
serializer = URLSafeTimedSerializer(tenent_id)
|
||||
return "ragflow-" + serializer.dumps(get_uuid(), salt=tenent_id)[2:34]
|
||||
|
||||
|
||||
@manager.route('/new_token', methods=['POST'])
|
||||
@login_required
|
||||
def new_token():
|
||||
@ -454,6 +448,8 @@ def upload():
|
||||
doc["parser_id"] = ParserType.AUDIO.value
|
||||
if re.search(r"\.(ppt|pptx|pages)$", filename):
|
||||
doc["parser_id"] = ParserType.PRESENTATION.value
|
||||
if re.search(r"\.(eml)$", filename):
|
||||
doc["parser_id"] = ParserType.EMAIL.value
|
||||
|
||||
doc_result = DocumentService.insert(doc)
|
||||
FileService.add_file_from_kb(doc, kb_folder["id"], kb.tenant_id)
|
||||
@ -478,7 +474,7 @@ def upload():
|
||||
e, doc = DocumentService.get_by_id(doc["id"])
|
||||
doc = doc.to_dict()
|
||||
doc["tenant_id"] = tenant_id
|
||||
bucket, name = File2DocumentService.get_minio_address(doc_id=doc["id"])
|
||||
bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
|
||||
queue_tasks(doc, bucket, name)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
@ -640,7 +636,7 @@ def document_rm():
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
b, n = File2DocumentService.get_minio_address(doc_id=doc_id)
|
||||
b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
|
||||
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
return get_data_error_result(
|
||||
@ -679,8 +675,79 @@ def completion_faq():
|
||||
|
||||
msg = []
|
||||
msg.append({"role": "user", "content": req["word"]})
|
||||
if not msg[-1].get("id"): msg[-1]["id"] = get_uuid()
|
||||
message_id = msg[-1]["id"]
|
||||
|
||||
def fillin_conv(ans):
|
||||
nonlocal conv, message_id
|
||||
if not conv.reference:
|
||||
conv.reference.append(ans["reference"])
|
||||
else:
|
||||
conv.reference[-1] = ans["reference"]
|
||||
conv.message[-1] = {"role": "assistant", "content": ans["answer"], "id": message_id}
|
||||
ans["id"] = message_id
|
||||
|
||||
try:
|
||||
if conv.source == "agent":
|
||||
conv.message.append(msg[-1])
|
||||
e, cvs = UserCanvasService.get_by_id(conv.dialog_id)
|
||||
if not e:
|
||||
return server_error_response("canvas not found.")
|
||||
|
||||
if not isinstance(cvs.dsl, str):
|
||||
cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False)
|
||||
|
||||
if not conv.reference:
|
||||
conv.reference = []
|
||||
conv.message.append({"role": "assistant", "content": "", "id": message_id})
|
||||
conv.reference.append({"chunks": [], "doc_aggs": []})
|
||||
|
||||
final_ans = {"reference": [], "doc_aggs": []}
|
||||
canvas = Canvas(cvs.dsl, objs[0].tenant_id)
|
||||
|
||||
canvas.messages.append(msg[-1])
|
||||
canvas.add_user_input(msg[-1]["content"])
|
||||
answer = canvas.run(stream=False)
|
||||
|
||||
assert answer is not None, "Nothing. Is it over?"
|
||||
|
||||
data_type_picture = {
|
||||
"type": 3,
|
||||
"url": "base64 content"
|
||||
}
|
||||
data = [
|
||||
{
|
||||
"type": 1,
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
final_ans["content"] = "\n".join(answer["content"]) if "content" in answer else ""
|
||||
canvas.messages.append({"role": "assistant", "content": final_ans["content"], "id": message_id})
|
||||
if final_ans.get("reference"):
|
||||
canvas.reference.append(final_ans["reference"])
|
||||
cvs.dsl = json.loads(str(canvas))
|
||||
|
||||
ans = {"answer": final_ans["content"], "reference": final_ans.get("reference", [])}
|
||||
data[0]["content"] += re.sub(r'##\d\$\$', '', ans["answer"])
|
||||
fillin_conv(ans)
|
||||
API4ConversationService.append_message(conv.id, conv.to_dict())
|
||||
|
||||
chunk_idxs = [int(match[2]) for match in re.findall(r'##\d\$\$', ans["answer"])]
|
||||
for chunk_idx in chunk_idxs[:1]:
|
||||
if ans["reference"]["chunks"][chunk_idx]["img_id"]:
|
||||
try:
|
||||
bkt, nm = ans["reference"]["chunks"][chunk_idx]["img_id"].split("-")
|
||||
response = STORAGE_IMPL.get(bkt, nm)
|
||||
data_type_picture["url"] = base64.b64encode(response).decode('utf-8')
|
||||
data.append(data_type_picture)
|
||||
break
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
response = {"code": 200, "msg": "success", "data": data}
|
||||
return response
|
||||
|
||||
# ******************For dialog******************
|
||||
conv.message.append(msg[-1])
|
||||
e, dia = DialogService.get_by_id(conv.dialog_id)
|
||||
if not e:
|
||||
@ -689,17 +756,9 @@ def completion_faq():
|
||||
|
||||
if not conv.reference:
|
||||
conv.reference = []
|
||||
conv.message.append({"role": "assistant", "content": ""})
|
||||
conv.message.append({"role": "assistant", "content": "", "id": message_id})
|
||||
conv.reference.append({"chunks": [], "doc_aggs": []})
|
||||
|
||||
def fillin_conv(ans):
|
||||
nonlocal conv
|
||||
if not conv.reference:
|
||||
conv.reference.append(ans["reference"])
|
||||
else:
|
||||
conv.reference[-1] = ans["reference"]
|
||||
conv.message[-1] = {"role": "assistant", "content": ans["answer"]}
|
||||
|
||||
data_type_picture = {
|
||||
"type": 3,
|
||||
"url": "base64 content"
|
||||
|
||||
@ -108,6 +108,10 @@ def run():
|
||||
canvas = Canvas(cvs.dsl, current_user.id)
|
||||
if "message" in req:
|
||||
canvas.messages.append({"role": "user", "content": req["message"], "id": message_id})
|
||||
if len([m for m in canvas.messages if m["role"] == "user"]) > 1:
|
||||
#ten = TenantService.get_info_by(current_user.id)[0]
|
||||
#req["message"] = full_question(ten["tenant_id"], ten["llm_id"], canvas.messages)
|
||||
pass
|
||||
canvas.add_user_input(req["message"])
|
||||
answer = canvas.run(stream=stream)
|
||||
print(canvas)
|
||||
|
||||
@ -21,13 +21,14 @@ from flask import request
|
||||
from flask_login import login_required, current_user
|
||||
from elasticsearch_dsl import Q
|
||||
|
||||
from api.db.services.dialog_service import keyword_extraction
|
||||
from rag.app.qa import rmPrefix, beAdoc
|
||||
from rag.nlp import search, rag_tokenizer, keyword_extraction
|
||||
from rag.nlp import search, rag_tokenizer
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from rag.utils import rmSpace
|
||||
from api.db import LLMType, ParserType
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import TenantLLMService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from api.db.services.document_service import DocumentService
|
||||
@ -141,8 +142,7 @@ def set():
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
embd_id = DocumentService.get_embd_id(req["doc_id"])
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
tenant_id, LLMType.EMBEDDING.value, embd_id)
|
||||
embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, embd_id)
|
||||
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
@ -235,8 +235,7 @@ def create():
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
embd_id = DocumentService.get_embd_id(req["doc_id"])
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
tenant_id, LLMType.EMBEDDING.value, embd_id)
|
||||
embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING.value, embd_id)
|
||||
|
||||
v, c = embd_mdl.encode([doc.name, req["content_with_weight"]])
|
||||
v = 0.1 * v[0] + 0.9 * v[1]
|
||||
@ -281,16 +280,14 @@ def retrieval_test():
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Knowledgebase not found!")
|
||||
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
|
||||
embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
|
||||
|
||||
rerank_mdl = None
|
||||
if req.get("rerank_id"):
|
||||
rerank_mdl = TenantLLMService.model_instance(
|
||||
kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
|
||||
rerank_mdl = LLMBundle(kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
|
||||
|
||||
if req.get("keyword", False):
|
||||
chat_mdl = TenantLLMService.model_instance(kb.tenant_id, LLMType.CHAT)
|
||||
chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
|
||||
question += keyword_extraction(chat_mdl, question)
|
||||
|
||||
retr = retrievaler if kb.parser_id != ParserType.KG else kg_retrievaler
|
||||
@ -323,9 +320,28 @@ def knowledge_graph():
|
||||
for id in sres.ids[:2]:
|
||||
ty = sres.field[id]["knowledge_graph_kwd"]
|
||||
try:
|
||||
obj[ty] = json.loads(sres.field[id]["content_with_weight"])
|
||||
content_json = json.loads(sres.field[id]["content_with_weight"])
|
||||
except Exception as e:
|
||||
print(traceback.format_exc(), flush=True)
|
||||
continue
|
||||
|
||||
if ty == 'mind_map':
|
||||
node_dict = {}
|
||||
|
||||
def repeat_deal(content_json, node_dict):
|
||||
if 'id' in content_json:
|
||||
if content_json['id'] in node_dict:
|
||||
node_name = content_json['id']
|
||||
content_json['id'] += f"({node_dict[content_json['id']]})"
|
||||
node_dict[node_name] += 1
|
||||
else:
|
||||
node_dict[content_json['id']] = 1
|
||||
if 'children' in content_json and content_json['children']:
|
||||
for item in content_json['children']:
|
||||
repeat_deal(item, node_dict)
|
||||
|
||||
repeat_deal(content_json, node_dict)
|
||||
|
||||
obj[ty] = content_json
|
||||
|
||||
return get_json_result(data=obj)
|
||||
|
||||
|
||||
@ -26,7 +26,6 @@ from api.db.services.dialog_service import DialogService, ConversationService, c
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMBundle, TenantService, TenantLLMService
|
||||
from api.settings import RetCode, retrievaler
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_json_result
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from graphrag.mind_map_extractor import MindMapExtractor
|
||||
@ -37,7 +36,9 @@ from graphrag.mind_map_extractor import MindMapExtractor
|
||||
def set_conversation():
|
||||
req = request.json
|
||||
conv_id = req.get("conversation_id")
|
||||
if conv_id:
|
||||
is_new = req.get("is_new")
|
||||
del req["is_new"]
|
||||
if not is_new:
|
||||
del req["conversation_id"]
|
||||
try:
|
||||
if not ConversationService.update_by_id(conv_id, req):
|
||||
@ -56,7 +57,7 @@ def set_conversation():
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Dialog not found")
|
||||
conv = {
|
||||
"id": get_uuid(),
|
||||
"id": conv_id,
|
||||
"dialog_id": req["dialog_id"],
|
||||
"name": req.get("name", "New conversation"),
|
||||
"message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}]
|
||||
@ -140,9 +141,6 @@ def list_convsersation():
|
||||
@validate_request("conversation_id", "messages")
|
||||
def completion():
|
||||
req = request.json
|
||||
# req = {"conversation_id": "9aaaca4c11d311efa461fa163e197198", "messages": [
|
||||
# {"role": "user", "content": "上海有吗?"}
|
||||
# ]}
|
||||
msg = []
|
||||
for m in req["messages"]:
|
||||
if m["role"] == "system":
|
||||
@ -185,6 +183,7 @@ def completion():
|
||||
yield "data:" + json.dumps({"retcode": 0, "retmsg": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
ConversationService.update_by_id(conv.id, conv.to_dict())
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield "data:" + json.dumps({"retcode": 500, "retmsg": str(e),
|
||||
"data": {"answer": "**ERROR**: " + str(e), "reference": []}},
|
||||
ensure_ascii=False) + "\n\n"
|
||||
@ -216,7 +215,7 @@ def tts():
|
||||
req = request.json
|
||||
text = req["text"]
|
||||
|
||||
tenants = TenantService.get_by_user_id(current_user.id)
|
||||
tenants = TenantService.get_info_by(current_user.id)
|
||||
if not tenants:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
@ -228,7 +227,8 @@ def tts():
|
||||
|
||||
def stream_audio():
|
||||
try:
|
||||
for chunk in tts_mdl.tts(text):
|
||||
for txt in re.split(r"[,。/《》?;:!\n\r:;]+", text):
|
||||
for chunk in tts_mdl.tts(txt):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
yield ("data:" + json.dumps({"retcode": 500, "retmsg": str(e),
|
||||
|
||||
@ -1,878 +0,0 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import warnings
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
from elasticsearch_dsl import Q
|
||||
from flask import request, send_file
|
||||
from flask_login import login_required, current_user
|
||||
from httpx import HTTPError
|
||||
|
||||
from api.contants import NAME_LENGTH_LIMIT
|
||||
from api.db import FileType, ParserType, FileSource, TaskStatus
|
||||
from api.db import StatusEnum
|
||||
from api.db.db_models import File
|
||||
from api.db.services import duplicate_name
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.settings import RetCode
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import construct_json_result, construct_error_response
|
||||
from api.utils.api_utils import construct_result, validate_request
|
||||
from api.utils.file_utils import filename_type, thumbnail
|
||||
from rag.app import book, laws, manual, naive, one, paper, presentation, qa, resume, table, picture, audio, email
|
||||
from rag.nlp import search
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
|
||||
MAXIMUM_OF_UPLOADING_FILES = 256
|
||||
|
||||
|
||||
# ------------------------------ create a dataset ---------------------------------------
|
||||
|
||||
@manager.route("/", methods=["POST"])
|
||||
@login_required # use login
|
||||
@validate_request("name") # check name key
|
||||
def create_dataset():
|
||||
# Check if Authorization header is present
|
||||
authorization_token = request.headers.get("Authorization")
|
||||
if not authorization_token:
|
||||
return construct_json_result(code=RetCode.AUTHENTICATION_ERROR, message="Authorization header is missing.")
|
||||
|
||||
# TODO: Login or API key
|
||||
# objs = APIToken.query(token=authorization_token)
|
||||
#
|
||||
# # Authorization error
|
||||
# if not objs:
|
||||
# return construct_json_result(code=RetCode.AUTHENTICATION_ERROR, message="Token is invalid.")
|
||||
#
|
||||
# tenant_id = objs[0].tenant_id
|
||||
|
||||
tenant_id = current_user.id
|
||||
request_body = request.json
|
||||
|
||||
# In case that there's no name
|
||||
if "name" not in request_body:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="Expected 'name' field in request body")
|
||||
|
||||
dataset_name = request_body["name"]
|
||||
|
||||
# empty dataset_name
|
||||
if not dataset_name:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="Empty dataset name")
|
||||
|
||||
# In case that there's space in the head or the tail
|
||||
dataset_name = dataset_name.strip()
|
||||
|
||||
# In case that the length of the name exceeds the limit
|
||||
dataset_name_length = len(dataset_name)
|
||||
if dataset_name_length > NAME_LENGTH_LIMIT:
|
||||
return construct_json_result(
|
||||
code=RetCode.DATA_ERROR,
|
||||
message=f"Dataset name: {dataset_name} with length {dataset_name_length} exceeds {NAME_LENGTH_LIMIT}!")
|
||||
|
||||
# In case that there are other fields in the data-binary
|
||||
if len(request_body.keys()) > 1:
|
||||
name_list = []
|
||||
for key_name in request_body.keys():
|
||||
if key_name != "name":
|
||||
name_list.append(key_name)
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"fields: {name_list}, are not allowed in request body.")
|
||||
|
||||
# If there is a duplicate name, it will modify it to make it unique
|
||||
request_body["name"] = duplicate_name(
|
||||
KnowledgebaseService.query,
|
||||
name=dataset_name,
|
||||
tenant_id=tenant_id,
|
||||
status=StatusEnum.VALID.value)
|
||||
try:
|
||||
request_body["id"] = get_uuid()
|
||||
request_body["tenant_id"] = tenant_id
|
||||
request_body["created_by"] = tenant_id
|
||||
exist, t = TenantService.get_by_id(tenant_id)
|
||||
if not exist:
|
||||
return construct_result(code=RetCode.AUTHENTICATION_ERROR, message="Tenant not found.")
|
||||
request_body["embd_id"] = t.embd_id
|
||||
if not KnowledgebaseService.save(**request_body):
|
||||
# failed to create new dataset
|
||||
return construct_result()
|
||||
return construct_json_result(code=RetCode.SUCCESS,
|
||||
data={"dataset_name": request_body["name"], "dataset_id": request_body["id"]})
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# -----------------------------list datasets-------------------------------------------------------
|
||||
|
||||
@manager.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def list_datasets():
|
||||
offset = request.args.get("offset", 0)
|
||||
count = request.args.get("count", -1)
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
desc = request.args.get("desc", True)
|
||||
try:
|
||||
tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
|
||||
datasets = KnowledgebaseService.get_by_tenant_ids_by_offset(
|
||||
[m["tenant_id"] for m in tenants], current_user.id, int(offset), int(count), orderby, desc)
|
||||
return construct_json_result(data=datasets, code=RetCode.SUCCESS, message=f"List datasets successfully!")
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
except HTTPError as http_err:
|
||||
return construct_json_result(http_err)
|
||||
|
||||
|
||||
# ---------------------------------delete a dataset ----------------------------
|
||||
|
||||
@manager.route("/<dataset_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def remove_dataset(dataset_id):
|
||||
try:
|
||||
datasets = KnowledgebaseService.query(created_by=current_user.id, id=dataset_id)
|
||||
|
||||
# according to the id, searching for the dataset
|
||||
if not datasets:
|
||||
return construct_json_result(message=f"The dataset cannot be found for your current account.",
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
|
||||
# Iterating the documents inside the dataset
|
||||
for doc in DocumentService.query(kb_id=dataset_id):
|
||||
if not DocumentService.remove_document(doc, datasets[0].tenant_id):
|
||||
# the process of deleting failed
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message="There was an error during the document removal process. "
|
||||
"Please check the status of the RAGFlow server and try the removal again.")
|
||||
# delete the other files
|
||||
f2d = File2DocumentService.get_by_document_id(doc.id)
|
||||
FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == f2d[0].file_id])
|
||||
File2DocumentService.delete_by_document_id(doc.id)
|
||||
|
||||
# delete the dataset
|
||||
if not KnowledgebaseService.delete_by_id(dataset_id):
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message="There was an error during the dataset removal process. "
|
||||
"Please check the status of the RAGFlow server and try the removal again.")
|
||||
# success
|
||||
return construct_json_result(code=RetCode.SUCCESS, message=f"Remove dataset: {dataset_id} successfully")
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ------------------------------ get details of a dataset ----------------------------------------
|
||||
|
||||
@manager.route("/<dataset_id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_dataset(dataset_id):
|
||||
try:
|
||||
dataset = KnowledgebaseService.get_detail(dataset_id)
|
||||
if not dataset:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="Can't find this dataset!")
|
||||
return construct_json_result(data=dataset, code=RetCode.SUCCESS)
|
||||
except Exception as e:
|
||||
return construct_json_result(e)
|
||||
|
||||
|
||||
# ------------------------------ update a dataset --------------------------------------------
|
||||
|
||||
@manager.route("/<dataset_id>", methods=["PUT"])
|
||||
@login_required
|
||||
def update_dataset(dataset_id):
|
||||
req = request.json
|
||||
try:
|
||||
# the request cannot be empty
|
||||
if not req:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="Please input at least one parameter that "
|
||||
"you want to update!")
|
||||
# check whether the dataset can be found
|
||||
if not KnowledgebaseService.query(created_by=current_user.id, id=dataset_id):
|
||||
return construct_json_result(message=f"Only the owner of knowledgebase is authorized for this operation!",
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
|
||||
exist, dataset = KnowledgebaseService.get_by_id(dataset_id)
|
||||
# check whether there is this dataset
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="This dataset cannot be found!")
|
||||
|
||||
if "name" in req:
|
||||
name = req["name"].strip()
|
||||
# check whether there is duplicate name
|
||||
if name.lower() != dataset.name.lower() \
|
||||
and len(KnowledgebaseService.query(name=name, tenant_id=current_user.id,
|
||||
status=StatusEnum.VALID.value)) > 1:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"The name: {name.lower()} is already used by other "
|
||||
f"datasets. Please choose a different name.")
|
||||
|
||||
dataset_updating_data = {}
|
||||
chunk_num = req.get("chunk_num")
|
||||
# modify the value of 11 parameters
|
||||
|
||||
# 2 parameters: embedding id and chunk method
|
||||
# only if chunk_num is 0, the user can update the embedding id
|
||||
if req.get("embedding_model_id"):
|
||||
if chunk_num == 0:
|
||||
dataset_updating_data["embd_id"] = req["embedding_model_id"]
|
||||
else:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message="You have already parsed the document in this "
|
||||
"dataset, so you cannot change the embedding "
|
||||
"model.")
|
||||
# only if chunk_num is 0, the user can update the chunk_method
|
||||
if "chunk_method" in req:
|
||||
type_value = req["chunk_method"]
|
||||
if is_illegal_value_for_enum(type_value, ParserType):
|
||||
return construct_json_result(message=f"Illegal value {type_value} for 'chunk_method' field.",
|
||||
code=RetCode.DATA_ERROR)
|
||||
if chunk_num != 0:
|
||||
construct_json_result(code=RetCode.DATA_ERROR, message="You have already parsed the document "
|
||||
"in this dataset, so you cannot "
|
||||
"change the chunk method.")
|
||||
dataset_updating_data["parser_id"] = req["template_type"]
|
||||
|
||||
# convert the photo parameter to avatar
|
||||
if req.get("photo"):
|
||||
dataset_updating_data["avatar"] = req["photo"]
|
||||
|
||||
# layout_recognize
|
||||
if "layout_recognize" in req:
|
||||
if "parser_config" not in dataset_updating_data:
|
||||
dataset_updating_data['parser_config'] = {}
|
||||
dataset_updating_data['parser_config']['layout_recognize'] = req['layout_recognize']
|
||||
|
||||
# TODO: updating use_raptor needs to construct a class
|
||||
|
||||
# 6 parameters
|
||||
for key in ["name", "language", "description", "permission", "id", "token_num"]:
|
||||
if key in req:
|
||||
dataset_updating_data[key] = req.get(key)
|
||||
|
||||
# update
|
||||
if not KnowledgebaseService.update_by_id(dataset.id, dataset_updating_data):
|
||||
return construct_json_result(code=RetCode.OPERATING_ERROR, message="Failed to update! "
|
||||
"Please check the status of RAGFlow "
|
||||
"server and try again!")
|
||||
|
||||
exist, dataset = KnowledgebaseService.get_by_id(dataset.id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="Failed to get the dataset "
|
||||
"using the dataset ID.")
|
||||
|
||||
return construct_json_result(data=dataset.to_json(), code=RetCode.SUCCESS)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# --------------------------------content management ----------------------------------------------
|
||||
|
||||
# ----------------------------upload files-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/", methods=["POST"])
|
||||
@login_required
|
||||
def upload_documents(dataset_id):
|
||||
# no files
|
||||
if not request.files:
|
||||
return construct_json_result(
|
||||
message="There is no file!", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# the number of uploading files exceeds the limit
|
||||
file_objs = request.files.getlist("file")
|
||||
num_file_objs = len(file_objs)
|
||||
|
||||
if num_file_objs > MAXIMUM_OF_UPLOADING_FILES:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message=f"You try to upload {num_file_objs} files, "
|
||||
f"which exceeds the maximum number of uploading files: {MAXIMUM_OF_UPLOADING_FILES}")
|
||||
|
||||
# no dataset
|
||||
exist, dataset = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(message="Can't find this dataset", code=RetCode.DATA_ERROR)
|
||||
|
||||
for file_obj in file_objs:
|
||||
file_name = file_obj.filename
|
||||
# no name
|
||||
if not file_name:
|
||||
return construct_json_result(
|
||||
message="There is a file without name!", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# TODO: support the remote files
|
||||
if 'http' in file_name:
|
||||
return construct_json_result(code=RetCode.ARGUMENT_ERROR, message="Remote files have not unsupported.")
|
||||
|
||||
# get the root_folder
|
||||
root_folder = FileService.get_root_folder(current_user.id)
|
||||
# get the id of the root_folder
|
||||
parent_file_id = root_folder["id"] # document id
|
||||
# this is for the new user, create '.knowledgebase' file
|
||||
FileService.init_knowledgebase_docs(parent_file_id, current_user.id)
|
||||
# go inside this folder, get the kb_root_folder
|
||||
kb_root_folder = FileService.get_kb_folder(current_user.id)
|
||||
# link the file management to the kb_folder
|
||||
kb_folder = FileService.new_a_file_from_kb(dataset.tenant_id, dataset.name, kb_root_folder["id"])
|
||||
|
||||
# grab all the errs
|
||||
err = []
|
||||
MAX_FILE_NUM_PER_USER = int(os.environ.get("MAX_FILE_NUM_PER_USER", 0))
|
||||
uploaded_docs_json = []
|
||||
for file in file_objs:
|
||||
try:
|
||||
# TODO: get this value from the database as some tenants have this limit while others don't
|
||||
if MAX_FILE_NUM_PER_USER > 0 and DocumentService.get_doc_count(dataset.tenant_id) >= MAX_FILE_NUM_PER_USER:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message="Exceed the maximum file number of a free user!")
|
||||
# deal with the duplicate name
|
||||
filename = duplicate_name(
|
||||
DocumentService.query,
|
||||
name=file.filename,
|
||||
kb_id=dataset.id)
|
||||
|
||||
# deal with the unsupported type
|
||||
filetype = filename_type(filename)
|
||||
if filetype == FileType.OTHER.value:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message="This type of file has not been supported yet!")
|
||||
|
||||
# upload to the minio
|
||||
location = filename
|
||||
while STORAGE_IMPL.obj_exist(dataset_id, location):
|
||||
location += "_"
|
||||
|
||||
blob = file.read()
|
||||
|
||||
# the content is empty, raising a warning
|
||||
if blob == b'':
|
||||
warnings.warn(f"[WARNING]: The content of the file {filename} is empty.")
|
||||
|
||||
STORAGE_IMPL.put(dataset_id, location, blob)
|
||||
|
||||
doc = {
|
||||
"id": get_uuid(),
|
||||
"kb_id": dataset.id,
|
||||
"parser_id": dataset.parser_id,
|
||||
"parser_config": dataset.parser_config,
|
||||
"created_by": current_user.id,
|
||||
"type": filetype,
|
||||
"name": filename,
|
||||
"location": location,
|
||||
"size": len(blob),
|
||||
"thumbnail": thumbnail(filename, blob)
|
||||
}
|
||||
if doc["type"] == FileType.VISUAL:
|
||||
doc["parser_id"] = ParserType.PICTURE.value
|
||||
if doc["type"] == FileType.AURAL:
|
||||
doc["parser_id"] = ParserType.AUDIO.value
|
||||
if re.search(r"\.(ppt|pptx|pages)$", filename):
|
||||
doc["parser_id"] = ParserType.PRESENTATION.value
|
||||
DocumentService.insert(doc)
|
||||
|
||||
FileService.add_file_from_kb(doc, kb_folder["id"], dataset.tenant_id)
|
||||
uploaded_docs_json.append(doc)
|
||||
except Exception as e:
|
||||
err.append(file.filename + ": " + str(e))
|
||||
|
||||
if err:
|
||||
# return all the errors
|
||||
return construct_json_result(message="\n".join(err), code=RetCode.SERVER_ERROR)
|
||||
# success
|
||||
return construct_json_result(data=uploaded_docs_json, code=RetCode.SUCCESS)
|
||||
|
||||
|
||||
# ----------------------------delete a file-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/<document_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def delete_document(document_id, dataset_id): # string
|
||||
# get the root folder
|
||||
root_folder = FileService.get_root_folder(current_user.id)
|
||||
# parent file's id
|
||||
parent_file_id = root_folder["id"]
|
||||
# consider the new user
|
||||
FileService.init_knowledgebase_docs(parent_file_id, current_user.id)
|
||||
# store all the errors that may have
|
||||
errors = ""
|
||||
try:
|
||||
# whether there is this document
|
||||
exist, doc = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"Document {document_id} not found!", code=RetCode.DATA_ERROR)
|
||||
# whether this doc is authorized by this tenant
|
||||
tenant_id = DocumentService.get_tenant_id(document_id)
|
||||
if not tenant_id:
|
||||
return construct_json_result(
|
||||
message=f"You cannot delete this document {document_id} due to the authorization"
|
||||
f" reason!", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
# get the doc's id and location
|
||||
real_dataset_id, location = File2DocumentService.get_minio_address(doc_id=document_id)
|
||||
|
||||
if real_dataset_id != dataset_id:
|
||||
return construct_json_result(message=f"The document {document_id} is not in the dataset: {dataset_id}, "
|
||||
f"but in the dataset: {real_dataset_id}.", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# there is an issue when removing
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
return construct_json_result(
|
||||
message="There was an error during the document removal process. Please check the status of the "
|
||||
"RAGFlow server and try the removal again.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
# fetch the File2Document record associated with the provided document ID.
|
||||
file_to_doc = File2DocumentService.get_by_document_id(document_id)
|
||||
# delete the associated File record.
|
||||
FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == file_to_doc[0].file_id])
|
||||
# delete the File2Document record itself using the document ID. This removes the
|
||||
# association between the document and the file after the File record has been deleted.
|
||||
File2DocumentService.delete_by_document_id(document_id)
|
||||
|
||||
# delete it from minio
|
||||
STORAGE_IMPL.rm(dataset_id, location)
|
||||
except Exception as e:
|
||||
errors += str(e)
|
||||
if errors:
|
||||
return construct_json_result(data=False, message=errors, code=RetCode.SERVER_ERROR)
|
||||
|
||||
return construct_json_result(data=True, code=RetCode.SUCCESS)
|
||||
|
||||
|
||||
# ----------------------------list files-----------------------------------------------------
|
||||
@manager.route('/<dataset_id>/documents/', methods=['GET'])
|
||||
@login_required
|
||||
def list_documents(dataset_id):
|
||||
if not dataset_id:
|
||||
return construct_json_result(
|
||||
data=False, message="Lack of 'dataset_id'", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# searching keywords
|
||||
keywords = request.args.get("keywords", "")
|
||||
|
||||
offset = request.args.get("offset", 0)
|
||||
count = request.args.get("count", -1)
|
||||
order_by = request.args.get("order_by", "create_time")
|
||||
descend = request.args.get("descend", True)
|
||||
try:
|
||||
docs, total = DocumentService.list_documents_in_dataset(dataset_id, int(offset), int(count), order_by,
|
||||
descend, keywords)
|
||||
|
||||
return construct_json_result(data={"total": total, "docs": docs}, message=RetCode.SUCCESS)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------update: enable rename-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/<document_id>", methods=["PUT"])
|
||||
@login_required
|
||||
def update_document(dataset_id, document_id):
|
||||
req = request.json
|
||||
try:
|
||||
legal_parameters = set()
|
||||
legal_parameters.add("name")
|
||||
legal_parameters.add("enable")
|
||||
legal_parameters.add("template_type")
|
||||
|
||||
for key in req.keys():
|
||||
if key not in legal_parameters:
|
||||
return construct_json_result(code=RetCode.ARGUMENT_ERROR, message=f"{key} is an illegal parameter.")
|
||||
|
||||
# The request body cannot be empty
|
||||
if not req:
|
||||
return construct_json_result(
|
||||
code=RetCode.DATA_ERROR,
|
||||
message="Please input at least one parameter that you want to update!")
|
||||
|
||||
# Check whether there is this dataset
|
||||
exist, dataset = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message=f"This dataset {dataset_id} cannot be found!")
|
||||
|
||||
# The document does not exist
|
||||
exist, document = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"This document {document_id} cannot be found!",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# Deal with the different keys
|
||||
updating_data = {}
|
||||
if "name" in req:
|
||||
new_name = req["name"]
|
||||
updating_data["name"] = new_name
|
||||
# Check whether the new_name is suitable
|
||||
# 1. no name value
|
||||
if not new_name:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR, message="There is no new name.")
|
||||
|
||||
# 2. In case that there's space in the head or the tail
|
||||
new_name = new_name.strip()
|
||||
|
||||
# 3. Check whether the new_name has the same extension of file as before
|
||||
if pathlib.Path(new_name.lower()).suffix != pathlib.Path(
|
||||
document.name.lower()).suffix:
|
||||
return construct_json_result(
|
||||
data=False,
|
||||
message="The extension of file cannot be changed",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# 4. Check whether the new name has already been occupied by other file
|
||||
for d in DocumentService.query(name=new_name, kb_id=document.kb_id):
|
||||
if d.name == new_name:
|
||||
return construct_json_result(
|
||||
message="Duplicated document name in the same dataset.",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
if "enable" in req:
|
||||
enable_value = req["enable"]
|
||||
if is_illegal_value_for_enum(enable_value, StatusEnum):
|
||||
return construct_json_result(message=f"Illegal value {enable_value} for 'enable' field.",
|
||||
code=RetCode.DATA_ERROR)
|
||||
updating_data["status"] = enable_value
|
||||
|
||||
# TODO: Chunk-method - update parameters inside the json object parser_config
|
||||
if "template_type" in req:
|
||||
type_value = req["template_type"]
|
||||
if is_illegal_value_for_enum(type_value, ParserType):
|
||||
return construct_json_result(message=f"Illegal value {type_value} for 'template_type' field.",
|
||||
code=RetCode.DATA_ERROR)
|
||||
updating_data["parser_id"] = req["template_type"]
|
||||
|
||||
# The process of updating
|
||||
if not DocumentService.update_by_id(document_id, updating_data):
|
||||
return construct_json_result(
|
||||
code=RetCode.OPERATING_ERROR,
|
||||
message="Failed to update document in the database! "
|
||||
"Please check the status of RAGFlow server and try again!")
|
||||
|
||||
# name part: file service
|
||||
if "name" in req:
|
||||
# Get file by document id
|
||||
file_information = File2DocumentService.get_by_document_id(document_id)
|
||||
if file_information:
|
||||
exist, file = FileService.get_by_id(file_information[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req["name"]})
|
||||
|
||||
exist, document = DocumentService.get_by_id(document_id)
|
||||
|
||||
# Success
|
||||
return construct_json_result(data=document.to_json(), message="Success", code=RetCode.SUCCESS)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# Helper method to judge whether it's an illegal value
|
||||
def is_illegal_value_for_enum(value, enum_class):
|
||||
return value not in enum_class.__members__.values()
|
||||
|
||||
|
||||
# ----------------------------download a file-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/<document_id>", methods=["GET"])
|
||||
@login_required
|
||||
def download_document(dataset_id, document_id):
|
||||
try:
|
||||
# Check whether there is this dataset
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset '{dataset_id}' cannot be found!")
|
||||
|
||||
# Check whether there is this document
|
||||
exist, document = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"This document '{document_id}' cannot be found!",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
# The process of downloading
|
||||
doc_id, doc_location = File2DocumentService.get_minio_address(doc_id=document_id) # minio address
|
||||
file_stream = STORAGE_IMPL.get(doc_id, doc_location)
|
||||
if not file_stream:
|
||||
return construct_json_result(message="This file is empty.", code=RetCode.DATA_ERROR)
|
||||
|
||||
file = BytesIO(file_stream)
|
||||
|
||||
# Use send_file with a proper filename and MIME type
|
||||
return send_file(
|
||||
file,
|
||||
as_attachment=True,
|
||||
download_name=document.name,
|
||||
mimetype='application/octet-stream' # Set a default MIME type
|
||||
)
|
||||
|
||||
# Error
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------start parsing a document-----------------------------------------------------
|
||||
# helper method for parsing
|
||||
# callback method
|
||||
def doc_parse_callback(doc_id, prog=None, msg=""):
|
||||
cancel = DocumentService.do_cancel(doc_id)
|
||||
if cancel:
|
||||
raise Exception("The parsing process has been cancelled!")
|
||||
|
||||
"""
|
||||
def doc_parse(binary, doc_name, parser_name, tenant_id, doc_id):
|
||||
match parser_name:
|
||||
case "book":
|
||||
book.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "laws":
|
||||
laws.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "manual":
|
||||
manual.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "naive":
|
||||
# It's the mode by default, which is general in the front-end
|
||||
naive.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "one":
|
||||
one.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "paper":
|
||||
paper.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "picture":
|
||||
picture.chunk(doc_name, binary=binary, tenant_id=tenant_id, lang="Chinese",
|
||||
callback=partial(doc_parse_callback, doc_id))
|
||||
case "presentation":
|
||||
presentation.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "qa":
|
||||
qa.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "resume":
|
||||
resume.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "table":
|
||||
table.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "audio":
|
||||
audio.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case "email":
|
||||
email.chunk(doc_name, binary=binary, callback=partial(doc_parse_callback, doc_id))
|
||||
case _:
|
||||
return False
|
||||
|
||||
return True
|
||||
"""
|
||||
|
||||
|
||||
@manager.route("/<dataset_id>/documents/<document_id>/status", methods=["POST"])
|
||||
@login_required
|
||||
def parse_document(dataset_id, document_id):
|
||||
try:
|
||||
# valid dataset
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset '{dataset_id}' cannot be found!")
|
||||
|
||||
return parsing_document_internal(document_id)
|
||||
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------start parsing documents-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/status", methods=["POST"])
|
||||
@login_required
|
||||
def parse_documents(dataset_id):
|
||||
doc_ids = request.json["doc_ids"]
|
||||
try:
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset '{dataset_id}' cannot be found!")
|
||||
# two conditions
|
||||
if not doc_ids:
|
||||
# documents inside the dataset
|
||||
docs, total = DocumentService.list_documents_in_dataset(dataset_id, 0, -1, "create_time",
|
||||
True, "")
|
||||
doc_ids = [doc["id"] for doc in docs]
|
||||
|
||||
message = ""
|
||||
# for loop
|
||||
for id in doc_ids:
|
||||
res = parsing_document_internal(id)
|
||||
res_body = res.json
|
||||
if res_body["code"] == RetCode.SUCCESS:
|
||||
message += res_body["message"]
|
||||
else:
|
||||
return res
|
||||
return construct_json_result(data=True, code=RetCode.SUCCESS, message=message)
|
||||
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# helper method for parsing the document
|
||||
def parsing_document_internal(id):
|
||||
message = ""
|
||||
try:
|
||||
# Check whether there is this document
|
||||
exist, document = DocumentService.get_by_id(id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"This document '{id}' cannot be found!",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
tenant_id = DocumentService.get_tenant_id(id)
|
||||
if not tenant_id:
|
||||
return construct_json_result(message="Tenant not found!", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
info = {"run": "1", "progress": 0}
|
||||
info["progress_msg"] = ""
|
||||
info["chunk_num"] = 0
|
||||
info["token_num"] = 0
|
||||
|
||||
DocumentService.update_by_id(id, info)
|
||||
|
||||
ELASTICSEARCH.deleteByQuery(Q("match", doc_id=id), idxnm=search.index_name(tenant_id))
|
||||
|
||||
_, doc_attributes = DocumentService.get_by_id(id)
|
||||
doc_attributes = doc_attributes.to_dict()
|
||||
doc_id = doc_attributes["id"]
|
||||
|
||||
bucket, doc_name = File2DocumentService.get_minio_address(doc_id=doc_id)
|
||||
binary = STORAGE_IMPL.get(bucket, doc_name)
|
||||
parser_name = doc_attributes["parser_id"]
|
||||
if binary:
|
||||
res = doc_parse(binary, doc_name, parser_name, tenant_id, doc_id)
|
||||
if res is False:
|
||||
message += f"The parser id: {parser_name} of the document {doc_id} is not supported; "
|
||||
else:
|
||||
message += f"Empty data in the document: {doc_name}; "
|
||||
# failed in parsing
|
||||
if doc_attributes["status"] == TaskStatus.FAIL.value:
|
||||
message += f"Failed in parsing the document: {doc_id}; "
|
||||
return construct_json_result(code=RetCode.SUCCESS, message=message)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------stop parsing a doc-----------------------------------------------------
|
||||
@manager.route("<dataset_id>/documents/<document_id>/status", methods=["DELETE"])
|
||||
@login_required
|
||||
def stop_parsing_document(dataset_id, document_id):
|
||||
try:
|
||||
# valid dataset
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset '{dataset_id}' cannot be found!")
|
||||
|
||||
return stop_parsing_document_internal(document_id)
|
||||
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------stop parsing docs-----------------------------------------------------
|
||||
@manager.route("<dataset_id>/documents/status", methods=["DELETE"])
|
||||
@login_required
|
||||
def stop_parsing_documents(dataset_id):
|
||||
doc_ids = request.json["doc_ids"]
|
||||
try:
|
||||
# valid dataset?
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset '{dataset_id}' cannot be found!")
|
||||
if not doc_ids:
|
||||
# documents inside the dataset
|
||||
docs, total = DocumentService.list_documents_in_dataset(dataset_id, 0, -1, "create_time",
|
||||
True, "")
|
||||
doc_ids = [doc["id"] for doc in docs]
|
||||
|
||||
message = ""
|
||||
# for loop
|
||||
for id in doc_ids:
|
||||
res = stop_parsing_document_internal(id)
|
||||
res_body = res.json
|
||||
if res_body["code"] == RetCode.SUCCESS:
|
||||
message += res_body["message"]
|
||||
else:
|
||||
return res
|
||||
return construct_json_result(data=True, code=RetCode.SUCCESS, message=message)
|
||||
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# Helper method
|
||||
def stop_parsing_document_internal(document_id):
|
||||
try:
|
||||
# valid doc?
|
||||
exist, doc = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"This document '{document_id}' cannot be found!",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
doc_attributes = doc.to_dict()
|
||||
|
||||
# only when the status is parsing, we need to stop it
|
||||
if doc_attributes["status"] == TaskStatus.RUNNING.value:
|
||||
tenant_id = DocumentService.get_tenant_id(document_id)
|
||||
if not tenant_id:
|
||||
return construct_json_result(message="Tenant not found!", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
# update successfully?
|
||||
if not DocumentService.update_by_id(document_id, {"status": "2"}): # cancel
|
||||
return construct_json_result(
|
||||
code=RetCode.OPERATING_ERROR,
|
||||
message="There was an error during the stopping parsing the document process. "
|
||||
"Please check the status of the RAGFlow server and try the update again."
|
||||
)
|
||||
|
||||
_, doc_attributes = DocumentService.get_by_id(document_id)
|
||||
doc_attributes = doc_attributes.to_dict()
|
||||
|
||||
# failed in stop parsing
|
||||
if doc_attributes["status"] == TaskStatus.RUNNING.value:
|
||||
return construct_json_result(message=f"Failed in parsing the document: {document_id}; ", code=RetCode.SUCCESS)
|
||||
return construct_json_result(code=RetCode.SUCCESS, message="")
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
# ----------------------------show the status of the file-----------------------------------------------------
|
||||
@manager.route("/<dataset_id>/documents/<document_id>/status", methods=["GET"])
|
||||
@login_required
|
||||
def show_parsing_status(dataset_id, document_id):
|
||||
try:
|
||||
# valid dataset
|
||||
exist, _ = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This dataset: '{dataset_id}' cannot be found!")
|
||||
# valid document
|
||||
exist, _ = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This document: '{document_id}' is not a valid document.")
|
||||
|
||||
_, doc = DocumentService.get_by_id(document_id) # get doc object
|
||||
doc_attributes = doc.to_dict()
|
||||
|
||||
return construct_json_result(
|
||||
data={"progress": doc_attributes["progress"], "status": TaskStatus(doc_attributes["status"]).name},
|
||||
code=RetCode.SUCCESS
|
||||
)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
# ----------------------------list the chunks of the file-----------------------------------------------------
|
||||
|
||||
# -- --------------------------delete the chunk-----------------------------------------------------
|
||||
|
||||
# ----------------------------edit the status of the chunk-----------------------------------------------------
|
||||
|
||||
# ----------------------------insert a new chunk-----------------------------------------------------
|
||||
|
||||
# ----------------------------upload a file-----------------------------------------------------
|
||||
|
||||
# ----------------------------get a specific chunk-----------------------------------------------------
|
||||
|
||||
# ----------------------------retrieval test-----------------------------------------------------
|
||||
@ -13,16 +13,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
#
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from io import BytesIO
|
||||
|
||||
import flask
|
||||
from elasticsearch_dsl import Q
|
||||
@ -30,27 +22,24 @@ from flask import request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from api.db.db_models import Task, File
|
||||
from api.db.services.dialog_service import DialogService, ConversationService
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.db.services.task_service import TaskService, queue_tasks
|
||||
from api.db.services.user_service import TenantService, UserTenantService
|
||||
from graphrag.mind_map_extractor import MindMapExtractor
|
||||
from rag.app import naive
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from rag.nlp import search
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from api.db.services import duplicate_name
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from api.utils import get_uuid
|
||||
from api.db import FileType, TaskStatus, ParserType, FileSource, LLMType
|
||||
from api.db import FileType, TaskStatus, ParserType, FileSource
|
||||
from api.db.services.document_service import DocumentService, doc_upload_and_parse
|
||||
from api.settings import RetCode, stat_logger
|
||||
from api.settings import RetCode
|
||||
from api.utils.api_utils import get_json_result
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
from api.utils.file_utils import filename_type, thumbnail, get_project_base_directory
|
||||
from api.utils.file_utils import filename_type, thumbnail
|
||||
from api.utils.web_utils import html2pdf, is_valid_url
|
||||
from api.contants import IMG_BASE64_PREFIX
|
||||
|
||||
|
||||
@manager.route('/upload', methods=['POST'])
|
||||
@ -139,6 +128,8 @@ def web_crawl():
|
||||
doc["parser_id"] = ParserType.AUDIO.value
|
||||
if re.search(r"\.(ppt|pptx|pages)$", filename):
|
||||
doc["parser_id"] = ParserType.PRESENTATION.value
|
||||
if re.search(r"\.(eml)$", filename):
|
||||
doc["parser_id"] = ParserType.EMAIL.value
|
||||
DocumentService.insert(doc)
|
||||
FileService.add_file_from_kb(doc, kb_folder["id"], kb.tenant_id)
|
||||
except Exception as e:
|
||||
@ -207,15 +198,28 @@ def list_docs():
|
||||
try:
|
||||
docs, tol = DocumentService.get_by_kb_id(
|
||||
kb_id, page_number, items_per_page, orderby, desc, keywords)
|
||||
|
||||
for doc_item in docs:
|
||||
if doc_item['thumbnail'] and not doc_item['thumbnail'].startswith(IMG_BASE64_PREFIX):
|
||||
doc_item['thumbnail'] = f"/v1/document/image/{kb_id}-{doc_item['thumbnail']}"
|
||||
|
||||
return get_json_result(data={"total": tol, "docs": docs})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/infos', methods=['POST'])
|
||||
@login_required
|
||||
def docinfos():
|
||||
req = request.json
|
||||
doc_ids = req["doc_ids"]
|
||||
for doc_id in doc_ids:
|
||||
if not DocumentService.accessible(doc_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
docs = DocumentService.get_by_ids(doc_ids)
|
||||
return get_json_result(data=list(docs.dicts()))
|
||||
|
||||
@ -230,6 +234,11 @@ def thumbnails():
|
||||
|
||||
try:
|
||||
docs = DocumentService.get_thumbnails(doc_ids)
|
||||
|
||||
for doc_item in docs:
|
||||
if doc_item['thumbnail'] and not doc_item['thumbnail'].startswith(IMG_BASE64_PREFIX):
|
||||
doc_item['thumbnail'] = f"/v1/document/image/{doc_item['kb_id']}-{doc_item['thumbnail']}"
|
||||
|
||||
return get_json_result(data={d["id"]: d["thumbnail"] for d in docs})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
@ -241,11 +250,17 @@ def thumbnails():
|
||||
def change_status():
|
||||
req = request.json
|
||||
if str(req["status"]) not in ["0", "1"]:
|
||||
get_json_result(
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='"Status" must be either 0 or 1!',
|
||||
retcode=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
if not DocumentService.accessible(req["doc_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
@ -284,6 +299,15 @@ def rm():
|
||||
req = request.json
|
||||
doc_ids = req["doc_id"]
|
||||
if isinstance(doc_ids, str): doc_ids = [doc_ids]
|
||||
|
||||
for doc_id in doc_ids:
|
||||
if not DocumentService.accessible4deletion(doc_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
|
||||
root_folder = FileService.get_root_folder(current_user.id)
|
||||
pf_id = root_folder["id"]
|
||||
FileService.init_knowledgebase_docs(pf_id, current_user.id)
|
||||
@ -297,7 +321,7 @@ def rm():
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
b, n = File2DocumentService.get_minio_address(doc_id=doc_id)
|
||||
b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
|
||||
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
return get_data_error_result(
|
||||
@ -322,6 +346,13 @@ def rm():
|
||||
@validate_request("doc_ids", "run")
|
||||
def run():
|
||||
req = request.json
|
||||
for doc_id in req["doc_ids"]:
|
||||
if not DocumentService.accessible(doc_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
for id in req["doc_ids"]:
|
||||
info = {"run": str(req["run"]), "progress": 0}
|
||||
@ -342,7 +373,7 @@ def run():
|
||||
e, doc = DocumentService.get_by_id(id)
|
||||
doc = doc.to_dict()
|
||||
doc["tenant_id"] = tenant_id
|
||||
bucket, name = File2DocumentService.get_minio_address(doc_id=doc["id"])
|
||||
bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
|
||||
queue_tasks(doc, bucket, name)
|
||||
|
||||
return get_json_result(data=True)
|
||||
@ -355,6 +386,12 @@ def run():
|
||||
@validate_request("doc_id", "name")
|
||||
def rename():
|
||||
req = request.json
|
||||
if not DocumentService.accessible(req["doc_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
@ -393,7 +430,7 @@ def get(doc_id):
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
|
||||
b, n = File2DocumentService.get_minio_address(doc_id=doc_id)
|
||||
b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
|
||||
response = flask.make_response(STORAGE_IMPL.get(b, n))
|
||||
|
||||
ext = re.search(r"\.([^.]+)$", doc.name)
|
||||
@ -415,6 +452,13 @@ def get(doc_id):
|
||||
@validate_request("doc_id", "parser_id")
|
||||
def change_parser():
|
||||
req = request.json
|
||||
|
||||
if not DocumentService.accessible(req["doc_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
@ -426,8 +470,9 @@ def change_parser():
|
||||
else:
|
||||
return get_json_result(data=True)
|
||||
|
||||
if doc.type == FileType.VISUAL or re.search(
|
||||
r"\.(ppt|pptx|pages)$", doc.name):
|
||||
if ((doc.type == FileType.VISUAL and req["parser_id"] != "picture")
|
||||
or (re.search(
|
||||
r"\.(ppt|pptx|pages)$", doc.name) and req["parser_id"] != "presentation")):
|
||||
return get_data_error_result(retmsg="Not supported yet!")
|
||||
|
||||
e = DocumentService.update_by_id(doc.id,
|
||||
|
||||
@ -77,7 +77,7 @@ def convert():
|
||||
doc = DocumentService.insert({
|
||||
"id": get_uuid(),
|
||||
"kb_id": kb.id,
|
||||
"parser_id": kb.parser_id,
|
||||
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
|
||||
"parser_config": kb.parser_config,
|
||||
"created_by": current_user.id,
|
||||
"type": file.type,
|
||||
|
||||
@ -332,7 +332,7 @@ def get(file_id):
|
||||
e, file = FileService.get_by_id(file_id)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
b, n = File2DocumentService.get_minio_address(file_id=file_id)
|
||||
b, n = File2DocumentService.get_storage_address(file_id=file_id)
|
||||
response = flask.make_response(STORAGE_IMPL.get(b, n))
|
||||
ext = re.search(r"\.([^.]+)$", file.name)
|
||||
if ext:
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from elasticsearch_dsl import Q
|
||||
from flask import request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
@ -23,14 +22,12 @@ from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.user_service import TenantService, UserTenantService
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from api.utils import get_uuid, get_format_time
|
||||
from api.db import StatusEnum, UserTenantRole, FileSource
|
||||
from api.utils import get_uuid
|
||||
from api.db import StatusEnum, FileSource
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.db_models import Knowledgebase, File
|
||||
from api.settings import stat_logger, RetCode
|
||||
from api.db.db_models import File
|
||||
from api.settings import RetCode
|
||||
from api.utils.api_utils import get_json_result
|
||||
from rag.nlp import search
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
|
||||
|
||||
@manager.route('/create', methods=['post'])
|
||||
@ -65,6 +62,12 @@ def create():
|
||||
def update():
|
||||
req = request.json
|
||||
req["name"] = req["name"].strip()
|
||||
if not KnowledgebaseService.accessible4deletion(req["kb_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
if not KnowledgebaseService.query(
|
||||
created_by=current_user.id, id=req["kb_id"]):
|
||||
@ -139,6 +142,12 @@ def list_kbs():
|
||||
@validate_request("kb_id")
|
||||
def rm():
|
||||
req = request.json
|
||||
if not KnowledgebaseService.accessible4deletion(req["kb_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg='No authorization.',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
kbs = KnowledgebaseService.query(
|
||||
created_by=current_user.id, id=req["kb_id"])
|
||||
|
||||
@ -13,9 +13,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import json
|
||||
|
||||
from flask import request
|
||||
from flask_login import login_required, current_user
|
||||
from api.db.services.llm_service import LLMFactoriesService, TenantLLMService, LLMService
|
||||
from api.settings import LIGHTEN
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from api.db import StatusEnum, LLMType
|
||||
from api.db.db_models import TenantLLM
|
||||
@ -55,7 +58,7 @@ def set_api_key():
|
||||
chat_passed, embd_passed, rerank_passed = False, False, False
|
||||
factory = req["llm_factory"]
|
||||
msg = ""
|
||||
for llm in LLMService.query(fid=factory)[:3]:
|
||||
for llm in LLMService.query(fid=factory):
|
||||
if not embd_passed and llm.model_type == LLMType.EMBEDDING.value:
|
||||
mdl = EmbeddingModel[factory](
|
||||
req["api_key"], llm.llm_name, base_url=req.get("base_url"))
|
||||
@ -74,10 +77,10 @@ def set_api_key():
|
||||
{"temperature": 0.9,'max_tokens':50})
|
||||
if m.find("**ERROR**") >=0:
|
||||
raise Exception(m)
|
||||
chat_passed = True
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({llm.llm_name}) using this api key." + str(
|
||||
e)
|
||||
chat_passed = True
|
||||
elif not rerank_passed and llm.model_type == LLMType.RERANK:
|
||||
mdl = RerankModel[factory](
|
||||
req["api_key"], llm.llm_name, base_url=req.get("base_url"))
|
||||
@ -85,32 +88,39 @@ def set_api_key():
|
||||
arr, tc = mdl.similarity("What's the weather?", ["Is it sunny today?"])
|
||||
if len(arr) == 0 or tc == 0:
|
||||
raise Exception("Fail")
|
||||
rerank_passed = True
|
||||
print(f'passed model rerank{llm.llm_name}',flush=True)
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({llm.llm_name}) using this api key." + str(
|
||||
e)
|
||||
rerank_passed = True
|
||||
if any([embd_passed, chat_passed, rerank_passed]):
|
||||
msg = ''
|
||||
break
|
||||
|
||||
if msg:
|
||||
return get_data_error_result(retmsg=msg)
|
||||
|
||||
llm = {
|
||||
llm_config = {
|
||||
"api_key": req["api_key"],
|
||||
"api_base": req.get("base_url", "")
|
||||
}
|
||||
for n in ["model_type", "llm_name"]:
|
||||
if n in req:
|
||||
llm[n] = req[n]
|
||||
llm_config[n] = req[n]
|
||||
|
||||
if not TenantLLMService.filter_update(
|
||||
[TenantLLM.tenant_id == current_user.id, TenantLLM.llm_factory == factory], llm):
|
||||
for llm in LLMService.query(fid=factory):
|
||||
if not TenantLLMService.filter_update(
|
||||
[TenantLLM.tenant_id == current_user.id,
|
||||
TenantLLM.llm_factory == factory,
|
||||
TenantLLM.llm_name == llm.llm_name],
|
||||
llm_config):
|
||||
TenantLLMService.save(
|
||||
tenant_id=current_user.id,
|
||||
llm_factory=factory,
|
||||
llm_name=llm.llm_name,
|
||||
model_type=llm.model_type,
|
||||
api_key=req["api_key"],
|
||||
api_base=req.get("base_url", "")
|
||||
api_key=llm_config["api_key"],
|
||||
api_base=llm_config["api_base"]
|
||||
)
|
||||
|
||||
return get_json_result(data=True)
|
||||
@ -123,53 +133,64 @@ def add_llm():
|
||||
req = request.json
|
||||
factory = req["llm_factory"]
|
||||
|
||||
def apikey_json(keys):
|
||||
nonlocal req
|
||||
return json.dumps({k: req.get(k, "") for k in keys})
|
||||
|
||||
if factory == "VolcEngine":
|
||||
# For VolcEngine, due to its special authentication method
|
||||
# Assemble ark_api_key endpoint_id into api_key
|
||||
llm_name = req["llm_name"]
|
||||
api_key = '{' + f'"ark_api_key": "{req.get("ark_api_key", "")}", ' \
|
||||
f'"ep_id": "{req.get("endpoint_id", "")}", ' + '}'
|
||||
api_key = apikey_json(["ark_api_key", "endpoint_id"])
|
||||
|
||||
elif factory == "Tencent Hunyuan":
|
||||
api_key = '{' + f'"hunyuan_sid": "{req.get("hunyuan_sid", "")}", ' \
|
||||
f'"hunyuan_sk": "{req.get("hunyuan_sk", "")}"' + '}'
|
||||
req["api_key"] = api_key
|
||||
req["api_key"] = apikey_json(["hunyuan_sid", "hunyuan_sk"])
|
||||
return set_api_key()
|
||||
|
||||
elif factory == "Tencent Cloud":
|
||||
api_key = '{' + f'"tencent_cloud_sid": "{req.get("tencent_cloud_sid", "")}", ' \
|
||||
f'"tencent_cloud_sk": "{req.get("tencent_cloud_sk", "")}"' + '}'
|
||||
req["api_key"] = api_key
|
||||
req["api_key"] = apikey_json(["tencent_cloud_sid", "tencent_cloud_sk"])
|
||||
|
||||
elif factory == "Bedrock":
|
||||
# For Bedrock, due to its special authentication method
|
||||
# Assemble bedrock_ak, bedrock_sk, bedrock_region
|
||||
llm_name = req["llm_name"]
|
||||
api_key = '{' + f'"bedrock_ak": "{req.get("bedrock_ak", "")}", ' \
|
||||
f'"bedrock_sk": "{req.get("bedrock_sk", "")}", ' \
|
||||
f'"bedrock_region": "{req.get("bedrock_region", "")}", ' + '}'
|
||||
api_key = apikey_json(["bedrock_ak", "bedrock_sk", "bedrock_region"])
|
||||
|
||||
elif factory == "LocalAI":
|
||||
llm_name = req["llm_name"]+"___LocalAI"
|
||||
api_key = "xxxxxxxxxxxxxxx"
|
||||
|
||||
elif factory == "HuggingFace":
|
||||
llm_name = req["llm_name"]+"___HuggingFace"
|
||||
api_key = "xxxxxxxxxxxxxxx"
|
||||
|
||||
elif factory == "OpenAI-API-Compatible":
|
||||
llm_name = req["llm_name"]+"___OpenAI-API"
|
||||
api_key = req.get("api_key","xxxxxxxxxxxxxxx")
|
||||
|
||||
elif factory =="XunFei Spark":
|
||||
llm_name = req["llm_name"]
|
||||
if req["model_type"] == "chat":
|
||||
api_key = req.get("spark_api_password", "xxxxxxxxxxxxxxx")
|
||||
elif req["model_type"] == "tts":
|
||||
api_key = apikey_json(["spark_app_id", "spark_api_secret","spark_api_key"])
|
||||
|
||||
elif factory == "BaiduYiyan":
|
||||
llm_name = req["llm_name"]
|
||||
api_key = '{' + f'"yiyan_ak": "{req.get("yiyan_ak", "")}", ' \
|
||||
f'"yiyan_sk": "{req.get("yiyan_sk", "")}"' + '}'
|
||||
api_key = apikey_json(["yiyan_ak", "yiyan_sk"])
|
||||
|
||||
elif factory == "Fish Audio":
|
||||
llm_name = req["llm_name"]
|
||||
api_key = '{' + f'"fish_audio_ak": "{req.get("fish_audio_ak", "")}", ' \
|
||||
f'"fish_audio_refid": "{req.get("fish_audio_refid", "59cb5986671546eaa6ca8ae6f29f6d22")}"' + '}'
|
||||
api_key = apikey_json(["fish_audio_ak", "fish_audio_refid"])
|
||||
|
||||
elif factory == "Google Cloud":
|
||||
llm_name = req["llm_name"]
|
||||
api_key = (
|
||||
"{" + f'"google_project_id": "{req.get("google_project_id", "")}", '
|
||||
f'"google_region": "{req.get("google_region", "")}", '
|
||||
f'"google_service_account_key": "{req.get("google_service_account_key", "")}"'
|
||||
+ "}"
|
||||
)
|
||||
api_key = apikey_json(["google_project_id", "google_region", "google_service_account_key"])
|
||||
|
||||
elif factory == "Azure-OpenAI":
|
||||
llm_name = req["llm_name"]
|
||||
api_key = apikey_json(["api_key", "api_version"])
|
||||
|
||||
else:
|
||||
llm_name = req["llm_name"]
|
||||
api_key = req.get("api_key", "xxxxxxxxxxxxxxx")
|
||||
@ -276,6 +297,16 @@ def delete_llm():
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/delete_factory', methods=['POST'])
|
||||
@login_required
|
||||
@validate_request("llm_factory")
|
||||
def delete_factory():
|
||||
req = request.json
|
||||
TenantLLMService.filter_delete(
|
||||
[TenantLLM.tenant_id == current_user.id, TenantLLM.llm_factory == req["llm_factory"]])
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/my_llms', methods=['GET'])
|
||||
@login_required
|
||||
def my_llms():
|
||||
@ -300,20 +331,22 @@ def my_llms():
|
||||
@manager.route('/list', methods=['GET'])
|
||||
@login_required
|
||||
def list_app():
|
||||
self_deploied = ["Youdao","FastEmbed", "BAAI", "Ollama", "Xinference", "LocalAI", "LM-Studio"]
|
||||
weighted = ["Youdao","FastEmbed", "BAAI"] if LIGHTEN != 0 else []
|
||||
model_type = request.args.get("model_type")
|
||||
try:
|
||||
objs = TenantLLMService.query(tenant_id=current_user.id)
|
||||
facts = set([o.to_dict()["llm_factory"] for o in objs if o.api_key])
|
||||
llms = LLMService.get_all()
|
||||
llms = [m.to_dict()
|
||||
for m in llms if m.status == StatusEnum.VALID.value]
|
||||
for m in llms if m.status == StatusEnum.VALID.value and m.fid not in weighted]
|
||||
for m in llms:
|
||||
m["available"] = m["fid"] in facts or m["llm_name"].lower() == "flag-embedding" or m["fid"] in ["Youdao","FastEmbed", "BAAI"]
|
||||
m["available"] = m["fid"] in facts or m["llm_name"].lower() == "flag-embedding" or m["fid"] in self_deploied
|
||||
|
||||
llm_set = set([m["llm_name"] for m in llms])
|
||||
llm_set = set([m["llm_name"]+"@"+m["fid"] for m in llms])
|
||||
for o in objs:
|
||||
if not o.api_key:continue
|
||||
if o.llm_name in llm_set:continue
|
||||
if o.llm_name+"@"+o.llm_factory in llm_set:continue
|
||||
llms.append({"llm_name": o.llm_name, "model_type": o.model_type, "fid": o.llm_factory, "available": True})
|
||||
|
||||
res = {}
|
||||
|
||||
@ -1,304 +0,0 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from flask import request
|
||||
|
||||
from api.db import StatusEnum
|
||||
from api.db.db_models import TenantLLM
|
||||
from api.db.services.dialog_service import DialogService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMService, TenantLLMService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.settings import RetCode
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_data_error_result, token_required
|
||||
from api.utils.api_utils import get_json_result
|
||||
|
||||
|
||||
@manager.route('/save', methods=['POST'])
|
||||
@token_required
|
||||
def save(tenant_id):
|
||||
req = request.json
|
||||
# dataset
|
||||
if req.get("knowledgebases") == []:
|
||||
return get_data_error_result(retmsg="knowledgebases can not be empty list")
|
||||
kb_list = []
|
||||
if req.get("knowledgebases"):
|
||||
for kb in req.get("knowledgebases"):
|
||||
if not kb["id"]:
|
||||
return get_data_error_result(retmsg="knowledgebase needs id")
|
||||
if not KnowledgebaseService.query(id=kb["id"], tenant_id=tenant_id):
|
||||
return get_data_error_result(retmsg="you do not own the knowledgebase")
|
||||
# if not DocumentService.query(kb_id=kb["id"]):
|
||||
# return get_data_error_result(retmsg="There is a invalid knowledgebase")
|
||||
kb_list.append(kb["id"])
|
||||
req["kb_ids"] = kb_list
|
||||
# llm
|
||||
llm = req.get("llm")
|
||||
if llm:
|
||||
if "model_name" in llm:
|
||||
req["llm_id"] = llm.pop("model_name")
|
||||
req["llm_setting"] = req.pop("llm")
|
||||
e, tenant = TenantService.get_by_id(tenant_id)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
# prompt
|
||||
prompt = req.get("prompt")
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
if prompt:
|
||||
for new_key, old_key in key_mapping.items():
|
||||
if old_key in prompt:
|
||||
prompt[new_key] = prompt.pop(old_key)
|
||||
for key in key_list:
|
||||
if key in prompt:
|
||||
req[key] = prompt.pop(key)
|
||||
req["prompt_config"] = req.pop("prompt")
|
||||
# create
|
||||
if "id" not in req:
|
||||
# dataset
|
||||
if not kb_list:
|
||||
return get_data_error_result(retmsg="knowledgebases are required!")
|
||||
# init
|
||||
req["id"] = get_uuid()
|
||||
req["description"] = req.get("description", "A helpful Assistant")
|
||||
req["icon"] = req.get("avatar", "")
|
||||
req["top_n"] = req.get("top_n", 6)
|
||||
req["top_k"] = req.get("top_k", 1024)
|
||||
req["rerank_id"] = req.get("rerank_id", "")
|
||||
if req.get("llm_id"):
|
||||
if not TenantLLMService.query(llm_name=req["llm_id"]):
|
||||
return get_data_error_result(retmsg="the model_name does not exist.")
|
||||
else:
|
||||
req["llm_id"] = tenant.llm_id
|
||||
if not req.get("name"):
|
||||
return get_data_error_result(retmsg="name is required.")
|
||||
if DialogService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(retmsg="Duplicated assistant name in creating dataset.")
|
||||
# tenant_id
|
||||
if req.get("tenant_id"):
|
||||
return get_data_error_result(retmsg="tenant_id must not be provided.")
|
||||
req["tenant_id"] = tenant_id
|
||||
# prompt more parameter
|
||||
default_prompt = {
|
||||
"system": """你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。
|
||||
以下是知识库:
|
||||
{knowledge}
|
||||
以上是知识库。""",
|
||||
"prologue": "您好,我是您的助手小樱,长得可爱又善良,can I help you?",
|
||||
"parameters": [
|
||||
{"key": "knowledge", "optional": False}
|
||||
],
|
||||
"empty_response": "Sorry! 知识库中未找到相关内容!"
|
||||
}
|
||||
key_list_2 = ["system", "prologue", "parameters", "empty_response"]
|
||||
if "prompt_config" not in req:
|
||||
req['prompt_config'] = {}
|
||||
for key in key_list_2:
|
||||
temp = req['prompt_config'].get(key)
|
||||
if not temp:
|
||||
req['prompt_config'][key] = default_prompt[key]
|
||||
for p in req['prompt_config']["parameters"]:
|
||||
if p["optional"]:
|
||||
continue
|
||||
if req['prompt_config']["system"].find("{%s}" % p["key"]) < 0:
|
||||
return get_data_error_result(
|
||||
retmsg="Parameter '{}' is not used".format(p["key"]))
|
||||
# save
|
||||
if not DialogService.save(**req):
|
||||
return get_data_error_result(retmsg="Fail to new an assistant!")
|
||||
# response
|
||||
e, res = DialogService.get_by_id(req["id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Fail to new an assistant!")
|
||||
res = res.to_json()
|
||||
renamed_dict = {}
|
||||
for key, value in res["prompt_config"].items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_dict[new_key] = value
|
||||
res["prompt"] = renamed_dict
|
||||
del res["prompt_config"]
|
||||
new_dict = {"similarity_threshold": res["similarity_threshold"],
|
||||
"keywords_similarity_weight": res["vector_similarity_weight"],
|
||||
"top_n": res["top_n"],
|
||||
"rerank_model": res['rerank_id']}
|
||||
res["prompt"].update(new_dict)
|
||||
for key in key_list:
|
||||
del res[key]
|
||||
res["llm"] = res.pop("llm_setting")
|
||||
res["llm"]["model_name"] = res.pop("llm_id")
|
||||
del res["kb_ids"]
|
||||
res["knowledgebases"] = req["knowledgebases"]
|
||||
res["avatar"] = res.pop("icon")
|
||||
return get_json_result(data=res)
|
||||
else:
|
||||
# authorization
|
||||
if not DialogService.query(tenant_id=tenant_id, id=req["id"], status=StatusEnum.VALID.value):
|
||||
return get_json_result(data=False, retmsg='You do not own the assistant', retcode=RetCode.OPERATING_ERROR)
|
||||
# prompt
|
||||
if not req["id"]:
|
||||
return get_data_error_result(retmsg="id can not be empty")
|
||||
e, res = DialogService.get_by_id(req["id"])
|
||||
res = res.to_json()
|
||||
if "llm_id" in req:
|
||||
if not TenantLLMService.query(llm_name=req["llm_id"]):
|
||||
return get_data_error_result(retmsg="the model_name does not exist.")
|
||||
if "name" in req:
|
||||
if not req.get("name"):
|
||||
return get_data_error_result(retmsg="name is not empty.")
|
||||
if req["name"].lower() != res["name"].lower() \
|
||||
and len(
|
||||
DialogService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value)) > 0:
|
||||
return get_data_error_result(retmsg="Duplicated assistant name in updating dataset.")
|
||||
if "prompt_config" in req:
|
||||
res["prompt_config"].update(req["prompt_config"])
|
||||
for p in res["prompt_config"]["parameters"]:
|
||||
if p["optional"]:
|
||||
continue
|
||||
if res["prompt_config"]["system"].find("{%s}" % p["key"]) < 0:
|
||||
return get_data_error_result(retmsg="Parameter '{}' is not used".format(p["key"]))
|
||||
if "llm_setting" in req:
|
||||
res["llm_setting"].update(req["llm_setting"])
|
||||
req["prompt_config"] = res["prompt_config"]
|
||||
req["llm_setting"] = res["llm_setting"]
|
||||
# avatar
|
||||
if "avatar" in req:
|
||||
req["icon"] = req.pop("avatar")
|
||||
assistant_id = req.pop("id")
|
||||
if "knowledgebases" in req:
|
||||
req.pop("knowledgebases")
|
||||
if not DialogService.update_by_id(assistant_id, req):
|
||||
return get_data_error_result(retmsg="Assistant not found!")
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/delete', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete(tenant_id):
|
||||
req = request.args
|
||||
if "id" not in req:
|
||||
return get_data_error_result(retmsg="id is required")
|
||||
id = req['id']
|
||||
if not DialogService.query(tenant_id=tenant_id, id=id, status=StatusEnum.VALID.value):
|
||||
return get_json_result(data=False, retmsg='you do not own the assistant.', retcode=RetCode.OPERATING_ERROR)
|
||||
|
||||
temp_dict = {"status": StatusEnum.INVALID.value}
|
||||
DialogService.update_by_id(req["id"], temp_dict)
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/get', methods=['GET'])
|
||||
@token_required
|
||||
def get(tenant_id):
|
||||
req = request.args
|
||||
if "id" in req:
|
||||
id = req["id"]
|
||||
ass = DialogService.query(tenant_id=tenant_id, id=id, status=StatusEnum.VALID.value)
|
||||
if not ass:
|
||||
return get_json_result(data=False, retmsg='You do not own the assistant.', retcode=RetCode.OPERATING_ERROR)
|
||||
if "name" in req:
|
||||
name = req["name"]
|
||||
if ass[0].name != name:
|
||||
return get_json_result(data=False, retmsg='name does not match id.', retcode=RetCode.OPERATING_ERROR)
|
||||
res = ass[0].to_json()
|
||||
else:
|
||||
if "name" in req:
|
||||
name = req["name"]
|
||||
ass = DialogService.query(name=name, tenant_id=tenant_id, status=StatusEnum.VALID.value)
|
||||
if not ass:
|
||||
return get_json_result(data=False, retmsg='You do not own the assistant.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
res = ass[0].to_json()
|
||||
else:
|
||||
return get_data_error_result(retmsg="At least one of `id` or `name` must be provided.")
|
||||
renamed_dict = {}
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
for key, value in res["prompt_config"].items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_dict[new_key] = value
|
||||
res["prompt"] = renamed_dict
|
||||
del res["prompt_config"]
|
||||
new_dict = {"similarity_threshold": res["similarity_threshold"],
|
||||
"keywords_similarity_weight": res["vector_similarity_weight"],
|
||||
"top_n": res["top_n"],
|
||||
"rerank_model": res['rerank_id']}
|
||||
res["prompt"].update(new_dict)
|
||||
for key in key_list:
|
||||
del res[key]
|
||||
res["llm"] = res.pop("llm_setting")
|
||||
res["llm"]["model_name"] = res.pop("llm_id")
|
||||
kb_list = []
|
||||
for kb_id in res["kb_ids"]:
|
||||
kb = KnowledgebaseService.query(id=kb_id)
|
||||
kb_list.append(kb[0].to_json())
|
||||
del res["kb_ids"]
|
||||
res["knowledgebases"] = kb_list
|
||||
res["avatar"] = res.pop("icon")
|
||||
return get_json_result(data=res)
|
||||
|
||||
|
||||
@manager.route('/list', methods=['GET'])
|
||||
@token_required
|
||||
def list_assistants(tenant_id):
|
||||
assts = DialogService.query(
|
||||
tenant_id=tenant_id,
|
||||
status=StatusEnum.VALID.value,
|
||||
reverse=True,
|
||||
order_by=DialogService.model.create_time)
|
||||
assts = [d.to_dict() for d in assts]
|
||||
list_assts = []
|
||||
renamed_dict = {}
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
for res in assts:
|
||||
for key, value in res["prompt_config"].items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_dict[new_key] = value
|
||||
res["prompt"] = renamed_dict
|
||||
del res["prompt_config"]
|
||||
new_dict = {"similarity_threshold": res["similarity_threshold"],
|
||||
"keywords_similarity_weight": res["vector_similarity_weight"],
|
||||
"top_n": res["top_n"],
|
||||
"rerank_model": res['rerank_id']}
|
||||
res["prompt"].update(new_dict)
|
||||
for key in key_list:
|
||||
del res[key]
|
||||
res["llm"] = res.pop("llm_setting")
|
||||
res["llm"]["model_name"] = res.pop("llm_id")
|
||||
kb_list = []
|
||||
for kb_id in res["kb_ids"]:
|
||||
kb = KnowledgebaseService.query(id=kb_id)
|
||||
kb_list.append(kb[0].to_json())
|
||||
del res["kb_ids"]
|
||||
res["knowledgebases"] = kb_list
|
||||
res["avatar"] = res.pop("icon")
|
||||
list_assts.append(res)
|
||||
return get_json_result(data=list_assts)
|
||||
311
api/apps/sdk/chat.py
Normal file
311
api/apps/sdk/chat.py
Normal file
@ -0,0 +1,311 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from flask import request
|
||||
from api.settings import RetCode
|
||||
from api.db import StatusEnum
|
||||
from api.db.services.dialog_service import DialogService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import TenantLLMService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_error_data_result, token_required
|
||||
from api.utils.api_utils import get_result
|
||||
|
||||
|
||||
|
||||
@manager.route('/chats', methods=['POST'])
|
||||
@token_required
|
||||
def create(tenant_id):
|
||||
req=request.json
|
||||
ids= req.get("dataset_ids")
|
||||
if not ids:
|
||||
return get_error_data_result(retmsg="`dataset_ids` is required")
|
||||
for kb_id in ids:
|
||||
kbs = KnowledgebaseService.query(id=kb_id,tenant_id=tenant_id)
|
||||
if not kbs:
|
||||
return get_error_data_result(f"You don't own the dataset {kb_id}")
|
||||
kb=kbs[0]
|
||||
if kb.chunk_num == 0:
|
||||
return get_error_data_result(f"The dataset {kb_id} doesn't own parsed file")
|
||||
kbs = KnowledgebaseService.get_by_ids(ids)
|
||||
embd_count = list(set([kb.embd_id for kb in kbs]))
|
||||
if len(embd_count) != 1:
|
||||
return get_result(retmsg='Datasets use different embedding models."',retcode=RetCode.AUTHENTICATION_ERROR)
|
||||
req["kb_ids"] = ids
|
||||
# llm
|
||||
llm = req.get("llm")
|
||||
if llm:
|
||||
if "model_name" in llm:
|
||||
req["llm_id"] = llm.pop("model_name")
|
||||
if not TenantLLMService.query(tenant_id=tenant_id,llm_name=req["llm_id"],model_type="chat"):
|
||||
return get_error_data_result(f"`model_name` {req.get('llm_id')} doesn't exist")
|
||||
req["llm_setting"] = req.pop("llm")
|
||||
e, tenant = TenantService.get_by_id(tenant_id)
|
||||
if not e:
|
||||
return get_error_data_result(retmsg="Tenant not found!")
|
||||
# prompt
|
||||
prompt = req.get("prompt")
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
if prompt:
|
||||
for new_key, old_key in key_mapping.items():
|
||||
if old_key in prompt:
|
||||
prompt[new_key] = prompt.pop(old_key)
|
||||
for key in key_list:
|
||||
if key in prompt:
|
||||
req[key] = prompt.pop(key)
|
||||
req["prompt_config"] = req.pop("prompt")
|
||||
# init
|
||||
req["id"] = get_uuid()
|
||||
req["description"] = req.get("description", "A helpful Assistant")
|
||||
req["icon"] = req.get("avatar", "")
|
||||
req["top_n"] = req.get("top_n", 6)
|
||||
req["top_k"] = req.get("top_k", 1024)
|
||||
req["rerank_id"] = req.get("rerank_id", "")
|
||||
if req.get("rerank_id"):
|
||||
if not TenantLLMService.query(tenant_id=tenant_id,llm_name=req.get("rerank_id"),model_type="rerank"):
|
||||
return get_error_data_result(f"`rerank_model` {req.get('rerank_id')} doesn't exist")
|
||||
if not req.get("llm_id"):
|
||||
req["llm_id"] = tenant.llm_id
|
||||
if not req.get("name"):
|
||||
return get_error_data_result(retmsg="`name` is required.")
|
||||
if DialogService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg="Duplicated chat name in creating chat.")
|
||||
# tenant_id
|
||||
if req.get("tenant_id"):
|
||||
return get_error_data_result(retmsg="`tenant_id` must not be provided.")
|
||||
req["tenant_id"] = tenant_id
|
||||
# prompt more parameter
|
||||
default_prompt = {
|
||||
"system": """You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history.
|
||||
Here is the knowledge base:
|
||||
{knowledge}
|
||||
The above is the knowledge base.""",
|
||||
"prologue": "Hi! I'm your assistant, what can I do for you?",
|
||||
"parameters": [
|
||||
{"key": "knowledge", "optional": False}
|
||||
],
|
||||
"empty_response": "Sorry! No relevant content was found in the knowledge base!"
|
||||
}
|
||||
key_list_2 = ["system", "prologue", "parameters", "empty_response"]
|
||||
if "prompt_config" not in req:
|
||||
req['prompt_config'] = {}
|
||||
for key in key_list_2:
|
||||
temp = req['prompt_config'].get(key)
|
||||
if not temp:
|
||||
req['prompt_config'][key] = default_prompt[key]
|
||||
for p in req['prompt_config']["parameters"]:
|
||||
if p["optional"]:
|
||||
continue
|
||||
if req['prompt_config']["system"].find("{%s}" % p["key"]) < 0:
|
||||
return get_error_data_result(
|
||||
retmsg="Parameter '{}' is not used".format(p["key"]))
|
||||
# save
|
||||
if not DialogService.save(**req):
|
||||
return get_error_data_result(retmsg="Fail to new a chat!")
|
||||
# response
|
||||
e, res = DialogService.get_by_id(req["id"])
|
||||
if not e:
|
||||
return get_error_data_result(retmsg="Fail to new a chat!")
|
||||
res = res.to_json()
|
||||
renamed_dict = {}
|
||||
for key, value in res["prompt_config"].items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_dict[new_key] = value
|
||||
res["prompt"] = renamed_dict
|
||||
del res["prompt_config"]
|
||||
new_dict = {"similarity_threshold": res["similarity_threshold"],
|
||||
"keywords_similarity_weight": res["vector_similarity_weight"],
|
||||
"top_n": res["top_n"],
|
||||
"rerank_model": res['rerank_id']}
|
||||
res["prompt"].update(new_dict)
|
||||
for key in key_list:
|
||||
del res[key]
|
||||
res["llm"] = res.pop("llm_setting")
|
||||
res["llm"]["model_name"] = res.pop("llm_id")
|
||||
del res["kb_ids"]
|
||||
res["dataset_ids"] = req["dataset_ids"]
|
||||
res["avatar"] = res.pop("icon")
|
||||
return get_result(data=res)
|
||||
|
||||
@manager.route('/chats/<chat_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def update(tenant_id,chat_id):
|
||||
if not DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg='You do not own the chat')
|
||||
req =request.json
|
||||
ids = req.get("dataset_ids")
|
||||
if "show_quotation" in req:
|
||||
req["do_refer"]=req.pop("show_quotation")
|
||||
if "dataset_ids" in req:
|
||||
if not ids:
|
||||
return get_error_data_result("`datasets` can't be empty")
|
||||
if ids:
|
||||
for kb_id in ids:
|
||||
kbs = KnowledgebaseService.query(id=kb_id, tenant_id=tenant_id)
|
||||
if not kbs:
|
||||
return get_error_data_result(f"You don't own the dataset {kb_id}")
|
||||
kb = kbs[0]
|
||||
if kb.chunk_num == 0:
|
||||
return get_error_data_result(f"The dataset {kb_id} doesn't own parsed file")
|
||||
kbs = KnowledgebaseService.get_by_ids(ids)
|
||||
embd_count=list(set([kb.embd_id for kb in kbs]))
|
||||
if len(embd_count) != 1 :
|
||||
return get_result(
|
||||
retmsg='Datasets use different embedding models."',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR)
|
||||
req["kb_ids"] = ids
|
||||
llm = req.get("llm")
|
||||
if llm:
|
||||
if "model_name" in llm:
|
||||
req["llm_id"] = llm.pop("model_name")
|
||||
if not TenantLLMService.query(tenant_id=tenant_id,llm_name=req["llm_id"],model_type="chat"):
|
||||
return get_error_data_result(f"`model_name` {req.get('llm_id')} doesn't exist")
|
||||
req["llm_setting"] = req.pop("llm")
|
||||
e, tenant = TenantService.get_by_id(tenant_id)
|
||||
if not e:
|
||||
return get_error_data_result(retmsg="Tenant not found!")
|
||||
if req.get("rerank_model"):
|
||||
if not TenantLLMService.query(tenant_id=tenant_id,llm_name=req.get("rerank_model"),model_type="rerank"):
|
||||
return get_error_data_result(f"`rerank_model` {req.get('rerank_model')} doesn't exist")
|
||||
# prompt
|
||||
prompt = req.get("prompt")
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
if prompt:
|
||||
for new_key, old_key in key_mapping.items():
|
||||
if old_key in prompt:
|
||||
prompt[new_key] = prompt.pop(old_key)
|
||||
for key in key_list:
|
||||
if key in prompt:
|
||||
req[key] = prompt.pop(key)
|
||||
req["prompt_config"] = req.pop("prompt")
|
||||
e, res = DialogService.get_by_id(chat_id)
|
||||
res = res.to_json()
|
||||
if "name" in req:
|
||||
if not req.get("name"):
|
||||
return get_error_data_result(retmsg="`name` is not empty.")
|
||||
if req["name"].lower() != res["name"].lower() \
|
||||
and len(
|
||||
DialogService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value)) > 0:
|
||||
return get_error_data_result(retmsg="Duplicated chat name in updating dataset.")
|
||||
if "prompt_config" in req:
|
||||
res["prompt_config"].update(req["prompt_config"])
|
||||
for p in res["prompt_config"]["parameters"]:
|
||||
if p["optional"]:
|
||||
continue
|
||||
if res["prompt_config"]["system"].find("{%s}" % p["key"]) < 0:
|
||||
return get_error_data_result(retmsg="Parameter '{}' is not used".format(p["key"]))
|
||||
if "llm_setting" in req:
|
||||
res["llm_setting"].update(req["llm_setting"])
|
||||
req["prompt_config"] = res["prompt_config"]
|
||||
req["llm_setting"] = res["llm_setting"]
|
||||
# avatar
|
||||
if "avatar" in req:
|
||||
req["icon"] = req.pop("avatar")
|
||||
if "dataset_ids" in req:
|
||||
req.pop("dataset_ids")
|
||||
if not DialogService.update_by_id(chat_id, req):
|
||||
return get_error_data_result(retmsg="Chat not found!")
|
||||
return get_result()
|
||||
|
||||
|
||||
@manager.route('/chats', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete(tenant_id):
|
||||
req = request.json
|
||||
if not req:
|
||||
ids=None
|
||||
else:
|
||||
ids=req.get("ids")
|
||||
if not ids:
|
||||
id_list = []
|
||||
dias=DialogService.query(tenant_id=tenant_id,status=StatusEnum.VALID.value)
|
||||
for dia in dias:
|
||||
id_list.append(dia.id)
|
||||
else:
|
||||
id_list=ids
|
||||
for id in id_list:
|
||||
if not DialogService.query(tenant_id=tenant_id, id=id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg=f"You don't own the chat {id}")
|
||||
temp_dict = {"status": StatusEnum.INVALID.value}
|
||||
DialogService.update_by_id(id, temp_dict)
|
||||
return get_result()
|
||||
|
||||
@manager.route('/chats', methods=['GET'])
|
||||
@token_required
|
||||
def list_chat(tenant_id):
|
||||
id = request.args.get("id")
|
||||
name = request.args.get("name")
|
||||
chat = DialogService.query(id=id,name=name,status=StatusEnum.VALID.value)
|
||||
if not chat:
|
||||
return get_error_data_result(retmsg="The chat doesn't exist")
|
||||
page_number = int(request.args.get("page", 1))
|
||||
items_per_page = int(request.args.get("page_size", 1024))
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
if request.args.get("desc") == "False" or request.args.get("desc") == "false":
|
||||
desc = False
|
||||
else:
|
||||
desc = True
|
||||
chats = DialogService.get_list(tenant_id,page_number,items_per_page,orderby,desc,id,name)
|
||||
if not chats:
|
||||
return get_result(data=[])
|
||||
list_assts = []
|
||||
renamed_dict = {}
|
||||
key_mapping = {"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
"quote": "show_quote",
|
||||
"system": "prompt",
|
||||
"rerank_id": "rerank_model",
|
||||
"vector_similarity_weight": "keywords_similarity_weight",
|
||||
"do_refer":"show_quotation"}
|
||||
key_list = ["similarity_threshold", "vector_similarity_weight", "top_n", "rerank_id"]
|
||||
for res in chats:
|
||||
for key, value in res["prompt_config"].items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_dict[new_key] = value
|
||||
res["prompt"] = renamed_dict
|
||||
del res["prompt_config"]
|
||||
new_dict = {"similarity_threshold": res["similarity_threshold"],
|
||||
"keywords_similarity_weight": res["vector_similarity_weight"],
|
||||
"top_n": res["top_n"],
|
||||
"rerank_model": res['rerank_id']}
|
||||
res["prompt"].update(new_dict)
|
||||
for key in key_list:
|
||||
del res[key]
|
||||
res["llm"] = res.pop("llm_setting")
|
||||
res["llm"]["model_name"] = res.pop("llm_id")
|
||||
kb_list = []
|
||||
for kb_id in res["kb_ids"]:
|
||||
kb = KnowledgebaseService.query(id=kb_id)
|
||||
if not kb :
|
||||
return get_error_data_result(retmsg=f"Don't exist the kb {kb_id}")
|
||||
kb_list.append(kb[0].to_json())
|
||||
del res["kb_ids"]
|
||||
res["datasets"] = kb_list
|
||||
res["avatar"] = res.pop("icon")
|
||||
list_assts.append(res)
|
||||
return get_result(data=list_assts)
|
||||
@ -15,159 +15,213 @@
|
||||
#
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.db import StatusEnum, FileSource
|
||||
from api.db.db_models import File
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import TenantLLMService,LLMService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.settings import RetCode
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_json_result, token_required, get_data_error_result
|
||||
from api.utils.api_utils import get_result, token_required, get_error_data_result, valid,get_parser_config
|
||||
|
||||
|
||||
@manager.route('/save', methods=['POST'])
|
||||
@manager.route('/datasets', methods=['POST'])
|
||||
@token_required
|
||||
def save(tenant_id):
|
||||
def create(tenant_id):
|
||||
req = request.json
|
||||
e, t = TenantService.get_by_id(tenant_id)
|
||||
if "id" not in req:
|
||||
if "tenant_id" in req or "embedding_model" in req:
|
||||
return get_data_error_result(
|
||||
retmsg="Tenant_id or embedding_model must not be provided")
|
||||
permission = req.get("permission")
|
||||
language = req.get("language")
|
||||
chunk_method = req.get("chunk_method")
|
||||
parser_config = req.get("parser_config")
|
||||
valid_permission = ["me", "team"]
|
||||
valid_language =["Chinese", "English"]
|
||||
valid_chunk_method = ["naive","manual","qa","table","paper","book","laws","presentation","picture","one","knowledge_graph","email"]
|
||||
check_validation=valid(permission,valid_permission,language,valid_language,chunk_method,valid_chunk_method)
|
||||
if check_validation:
|
||||
return check_validation
|
||||
req["parser_config"]=get_parser_config(chunk_method,parser_config)
|
||||
if "tenant_id" in req:
|
||||
return get_error_data_result(
|
||||
retmsg="`tenant_id` must not be provided")
|
||||
if "chunk_count" in req or "document_count" in req:
|
||||
return get_error_data_result(retmsg="`chunk_count` or `document_count` must not be provided")
|
||||
if "name" not in req:
|
||||
return get_data_error_result(
|
||||
retmsg="Name is not empty!")
|
||||
return get_error_data_result(
|
||||
retmsg="`name` is not empty!")
|
||||
req['id'] = get_uuid()
|
||||
req["name"] = req["name"].strip()
|
||||
if req["name"] == "":
|
||||
return get_data_error_result(
|
||||
retmsg="Name is not empty string!")
|
||||
return get_error_data_result(
|
||||
retmsg="`name` is not empty string!")
|
||||
if KnowledgebaseService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(
|
||||
retmsg="Duplicated knowledgebase name in creating dataset.")
|
||||
return get_error_data_result(
|
||||
retmsg="Duplicated dataset name in creating dataset.")
|
||||
req["tenant_id"] = req['created_by'] = tenant_id
|
||||
if not req.get("embedding_model"):
|
||||
req['embedding_model'] = t.embd_id
|
||||
else:
|
||||
valid_embedding_models=["BAAI/bge-large-zh-v1.5","BAAI/bge-base-en-v1.5","BAAI/bge-large-en-v1.5","BAAI/bge-small-en-v1.5",
|
||||
"BAAI/bge-small-zh-v1.5","jinaai/jina-embeddings-v2-base-en","jinaai/jina-embeddings-v2-small-en",
|
||||
"nomic-ai/nomic-embed-text-v1.5","sentence-transformers/all-MiniLM-L6-v2","text-embedding-v2",
|
||||
"text-embedding-v3","maidalun1020/bce-embedding-base_v1"]
|
||||
embd_model=LLMService.query(llm_name=req["embedding_model"],model_type="embedding")
|
||||
if not embd_model:
|
||||
return get_error_data_result(f"`embedding_model` {req.get('embedding_model')} doesn't exist")
|
||||
if embd_model:
|
||||
if req["embedding_model"] not in valid_embedding_models and not TenantLLMService.query(tenant_id=tenant_id,model_type="embedding", llm_name=req.get("embedding_model")):
|
||||
return get_error_data_result(f"`embedding_model` {req.get('embedding_model')} doesn't exist")
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"doc_num": "document_count",
|
||||
"parser_id": "parse_method",
|
||||
"parser_id": "chunk_method",
|
||||
"embd_id": "embedding_model"
|
||||
}
|
||||
mapped_keys = {new_key: req[old_key] for new_key, old_key in key_mapping.items() if old_key in req}
|
||||
req.update(mapped_keys)
|
||||
if not KnowledgebaseService.save(**req):
|
||||
return get_data_error_result(retmsg="Create dataset error.(Database error)")
|
||||
return get_error_data_result(retmsg="Create dataset error.(Database error)")
|
||||
renamed_data = {}
|
||||
e, k = KnowledgebaseService.get_by_id(req["id"])
|
||||
for key, value in k.to_dict().items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_data[new_key] = value
|
||||
return get_json_result(data=renamed_data)
|
||||
else:
|
||||
invalid_keys = {"embd_id", "chunk_num", "doc_num", "parser_id"}
|
||||
if any(key in req for key in invalid_keys):
|
||||
return get_data_error_result(retmsg="The input parameters are invalid.")
|
||||
return get_result(data=renamed_data)
|
||||
|
||||
@manager.route('/datasets', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete(tenant_id):
|
||||
req = request.json
|
||||
if not req:
|
||||
ids=None
|
||||
else:
|
||||
ids=req.get("ids")
|
||||
if not ids:
|
||||
id_list = []
|
||||
kbs=KnowledgebaseService.query(tenant_id=tenant_id)
|
||||
for kb in kbs:
|
||||
id_list.append(kb.id)
|
||||
else:
|
||||
id_list=ids
|
||||
for id in id_list:
|
||||
kbs = KnowledgebaseService.query(id=id, tenant_id=tenant_id)
|
||||
if not kbs:
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {id}")
|
||||
for doc in DocumentService.query(kb_id=id):
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
return get_error_data_result(
|
||||
retmsg="Remove document error.(Database error)")
|
||||
f2d = File2DocumentService.get_by_document_id(doc.id)
|
||||
FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == f2d[0].file_id])
|
||||
File2DocumentService.delete_by_document_id(doc.id)
|
||||
if not KnowledgebaseService.delete_by_id(id):
|
||||
return get_error_data_result(
|
||||
retmsg="Delete dataset error.(Database error)")
|
||||
return get_result(retcode=RetCode.SUCCESS)
|
||||
|
||||
@manager.route('/datasets/<dataset_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def update(tenant_id,dataset_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id,tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg="You don't own the dataset")
|
||||
req = request.json
|
||||
e, t = TenantService.get_by_id(tenant_id)
|
||||
invalid_keys = {"id", "embd_id", "chunk_num", "doc_num", "parser_id"}
|
||||
if any(key in req for key in invalid_keys):
|
||||
return get_error_data_result(retmsg="The input parameters are invalid.")
|
||||
permission = req.get("permission")
|
||||
language = req.get("language")
|
||||
chunk_method = req.get("chunk_method")
|
||||
parser_config = req.get("parser_config")
|
||||
valid_permission = ["me", "team"]
|
||||
valid_language = ["Chinese", "English"]
|
||||
valid_chunk_method = ["naive", "manual", "qa", "table", "paper", "book", "laws", "presentation", "picture", "one",
|
||||
"knowledge_graph", "email"]
|
||||
check_validation = valid(permission, valid_permission, language, valid_language, chunk_method, valid_chunk_method)
|
||||
if check_validation:
|
||||
return check_validation
|
||||
if "tenant_id" in req:
|
||||
if req["tenant_id"] != tenant_id:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change tenant_id.")
|
||||
|
||||
if "embedding_model" in req:
|
||||
if req["embedding_model"] != t.embd_id:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change embedding_model.")
|
||||
req.pop("embedding_model")
|
||||
|
||||
if not KnowledgebaseService.query(
|
||||
created_by=tenant_id, id=req["id"]):
|
||||
return get_json_result(
|
||||
data=False, retmsg='You do not own the dataset.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
|
||||
if not req["id"]:
|
||||
return get_data_error_result(
|
||||
retmsg="id can not be empty.")
|
||||
e, kb = KnowledgebaseService.get_by_id(req["id"])
|
||||
|
||||
return get_error_data_result(
|
||||
retmsg="Can't change `tenant_id`.")
|
||||
e, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if "parser_config" in req:
|
||||
temp_dict=kb.parser_config
|
||||
temp_dict.update(req["parser_config"])
|
||||
req["parser_config"] = temp_dict
|
||||
if "chunk_count" in req:
|
||||
if req["chunk_count"] != kb.chunk_num:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change chunk_count.")
|
||||
return get_error_data_result(
|
||||
retmsg="Can't change `chunk_count`.")
|
||||
req.pop("chunk_count")
|
||||
|
||||
if "document_count" in req:
|
||||
if req['document_count'] != kb.doc_num:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change document_count.")
|
||||
return get_error_data_result(
|
||||
retmsg="Can't change `document_count`.")
|
||||
req.pop("document_count")
|
||||
|
||||
if "parse_method" in req:
|
||||
if kb.chunk_num != 0 and req['parse_method'] != kb.parser_id:
|
||||
return get_data_error_result(
|
||||
retmsg="If chunk count is not 0, parse method is not changable.")
|
||||
req['parser_id'] = req.pop('parse_method')
|
||||
if "chunk_method" in req:
|
||||
if kb.chunk_num != 0 and req['chunk_method'] != kb.parser_id:
|
||||
return get_error_data_result(
|
||||
retmsg="If `chunk_count` is not 0, `chunk_method` is not changeable.")
|
||||
req['parser_id'] = req.pop('chunk_method')
|
||||
if req['parser_id'] != kb.parser_id:
|
||||
if not req.get("parser_config"):
|
||||
req["parser_config"] = get_parser_config(chunk_method, parser_config)
|
||||
if "embedding_model" in req:
|
||||
if kb.chunk_num != 0 and req['embedding_model'] != kb.embd_id:
|
||||
return get_error_data_result(
|
||||
retmsg="If `chunk_count` is not 0, `embedding_model` is not changeable.")
|
||||
if not req.get("embedding_model"):
|
||||
return get_error_data_result("`embedding_model` can't be empty")
|
||||
valid_embedding_models=["BAAI/bge-large-zh-v1.5","BAAI/bge-base-en-v1.5","BAAI/bge-large-en-v1.5","BAAI/bge-small-en-v1.5",
|
||||
"BAAI/bge-small-zh-v1.5","jinaai/jina-embeddings-v2-base-en","jinaai/jina-embeddings-v2-small-en",
|
||||
"nomic-ai/nomic-embed-text-v1.5","sentence-transformers/all-MiniLM-L6-v2","text-embedding-v2",
|
||||
"text-embedding-v3","maidalun1020/bce-embedding-base_v1"]
|
||||
embd_model=LLMService.query(llm_name=req["embedding_model"],model_type="embedding")
|
||||
if not embd_model:
|
||||
return get_error_data_result(f"`embedding_model` {req.get('embedding_model')} doesn't exist")
|
||||
if embd_model:
|
||||
if req["embedding_model"] not in valid_embedding_models and not TenantLLMService.query(tenant_id=tenant_id,model_type="embedding", llm_name=req.get("embedding_model")):
|
||||
return get_error_data_result(f"`embedding_model` {req.get('embedding_model')} doesn't exist")
|
||||
req['embd_id'] = req.pop('embedding_model')
|
||||
if "name" in req:
|
||||
req["name"] = req["name"].strip()
|
||||
if req["name"].lower() != kb.name.lower() \
|
||||
and len(KnowledgebaseService.query(name=req["name"], tenant_id=tenant_id,
|
||||
status=StatusEnum.VALID.value)) > 0:
|
||||
return get_data_error_result(
|
||||
retmsg="Duplicated knowledgebase name in updating dataset.")
|
||||
|
||||
del req["id"]
|
||||
return get_error_data_result(
|
||||
retmsg="Duplicated dataset name in updating dataset.")
|
||||
if not KnowledgebaseService.update_by_id(kb.id, req):
|
||||
return get_data_error_result(retmsg="Update dataset error.(Database error)")
|
||||
return get_json_result(data=True)
|
||||
return get_error_data_result(retmsg="Update dataset error.(Database error)")
|
||||
return get_result(retcode=RetCode.SUCCESS)
|
||||
|
||||
|
||||
@manager.route('/delete', methods=['DELETE'])
|
||||
@manager.route('/datasets', methods=['GET'])
|
||||
@token_required
|
||||
def delete(tenant_id):
|
||||
req = request.args
|
||||
if "id" not in req:
|
||||
return get_data_error_result(
|
||||
retmsg="id is required")
|
||||
kbs = KnowledgebaseService.query(
|
||||
created_by=tenant_id, id=req["id"])
|
||||
def list(tenant_id):
|
||||
id = request.args.get("id")
|
||||
name = request.args.get("name")
|
||||
kbs = KnowledgebaseService.query(id=id,name=name,status=1)
|
||||
if not kbs:
|
||||
return get_json_result(
|
||||
data=False, retmsg='You do not own the dataset',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
|
||||
for doc in DocumentService.query(kb_id=req["id"]):
|
||||
if not DocumentService.remove_document(doc, kbs[0].tenant_id):
|
||||
return get_data_error_result(
|
||||
retmsg="Remove document error.(Database error)")
|
||||
f2d = File2DocumentService.get_by_document_id(doc.id)
|
||||
FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == f2d[0].file_id])
|
||||
File2DocumentService.delete_by_document_id(doc.id)
|
||||
|
||||
if not KnowledgebaseService.delete_by_id(req["id"]):
|
||||
return get_data_error_result(
|
||||
retmsg="Delete dataset error.(Database serror)")
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/list', methods=['GET'])
|
||||
@token_required
|
||||
def list_datasets(tenant_id):
|
||||
return get_error_data_result(retmsg="The dataset doesn't exist")
|
||||
page_number = int(request.args.get("page", 1))
|
||||
items_per_page = int(request.args.get("page_size", 1024))
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
desc = bool(request.args.get("desc", True))
|
||||
if request.args.get("desc") == "False" or request.args.get("desc") == "false" :
|
||||
desc = False
|
||||
else:
|
||||
desc = True
|
||||
tenants = TenantService.get_joined_tenants_by_user_id(tenant_id)
|
||||
kbs = KnowledgebaseService.get_by_tenant_ids(
|
||||
[m["tenant_id"] for m in tenants], tenant_id, page_number, items_per_page, orderby, desc)
|
||||
kbs = KnowledgebaseService.get_list(
|
||||
[m["tenant_id"] for m in tenants], tenant_id, page_number, items_per_page, orderby, desc, id, name)
|
||||
renamed_list = []
|
||||
for kb in kbs:
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"doc_num": "document_count",
|
||||
"parser_id": "parse_method",
|
||||
"parser_id": "chunk_method",
|
||||
"embd_id": "embedding_model"
|
||||
}
|
||||
renamed_data = {}
|
||||
@ -175,50 +229,4 @@ def list_datasets(tenant_id):
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_data[new_key] = value
|
||||
renamed_list.append(renamed_data)
|
||||
return get_json_result(data=renamed_list)
|
||||
|
||||
|
||||
@manager.route('/detail', methods=['GET'])
|
||||
@token_required
|
||||
def detail(tenant_id):
|
||||
req = request.args
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"doc_num": "document_count",
|
||||
"parser_id": "parse_method",
|
||||
"embd_id": "embedding_model"
|
||||
}
|
||||
renamed_data = {}
|
||||
if "id" in req:
|
||||
id = req["id"]
|
||||
kb = KnowledgebaseService.query(created_by=tenant_id, id=req["id"])
|
||||
if not kb:
|
||||
return get_json_result(
|
||||
data=False, retmsg='You do not own the dataset.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
if "name" in req:
|
||||
name = req["name"]
|
||||
if kb[0].name != name:
|
||||
return get_json_result(
|
||||
data=False, retmsg='You do not own the dataset.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
e, k = KnowledgebaseService.get_by_id(id)
|
||||
for key, value in k.to_dict().items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_data[new_key] = value
|
||||
return get_json_result(data=renamed_data)
|
||||
else:
|
||||
if "name" in req:
|
||||
name = req["name"]
|
||||
e, k = KnowledgebaseService.get_by_name(kb_name=name, tenant_id=tenant_id)
|
||||
if not e:
|
||||
return get_json_result(
|
||||
data=False, retmsg='You do not own the dataset.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
for key, value in k.to_dict().items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_data[new_key] = value
|
||||
return get_json_result(data=renamed_data)
|
||||
else:
|
||||
return get_data_error_result(
|
||||
retmsg="At least one of `id` or `name` must be provided.")
|
||||
return get_result(data=renamed_list)
|
||||
|
||||
77
api/apps/sdk/dify_retrieval.py
Normal file
77
api/apps/sdk/dify_retrieval.py
Normal file
@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from flask import request, jsonify
|
||||
|
||||
from api.db import LLMType, ParserType
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.settings import retrievaler, kg_retrievaler, RetCode
|
||||
from api.utils.api_utils import validate_request, build_error_result, apikey_required
|
||||
|
||||
|
||||
@manager.route('/dify/retrieval', methods=['POST'])
|
||||
@apikey_required
|
||||
@validate_request("knowledge_id", "query")
|
||||
def retrieval(tenant_id):
|
||||
req = request.json
|
||||
question = req["query"]
|
||||
kb_id = req["knowledge_id"]
|
||||
retrieval_setting = req.get("retrieval_setting", {})
|
||||
similarity_threshold = float(retrieval_setting.get("score_threshold", 0.0))
|
||||
top = int(retrieval_setting.get("top_k", 1024))
|
||||
|
||||
try:
|
||||
|
||||
e, kb = KnowledgebaseService.get_by_id(kb_id)
|
||||
if not e:
|
||||
return build_error_result(error_msg="Knowledgebase not found!", retcode=RetCode.NOT_FOUND)
|
||||
|
||||
if kb.tenant_id != tenant_id:
|
||||
return build_error_result(error_msg="Knowledgebase not found!", retcode=RetCode.NOT_FOUND)
|
||||
|
||||
embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
|
||||
|
||||
retr = retrievaler if kb.parser_id != ParserType.KG else kg_retrievaler
|
||||
ranks = retr.retrieval(
|
||||
question,
|
||||
embd_mdl,
|
||||
kb.tenant_id,
|
||||
[kb_id],
|
||||
page=1,
|
||||
page_size=top,
|
||||
similarity_threshold=similarity_threshold,
|
||||
vector_similarity_weight=0.3,
|
||||
top=top
|
||||
)
|
||||
records = []
|
||||
for c in ranks["chunks"]:
|
||||
if "vector" in c:
|
||||
del c["vector"]
|
||||
records.append({
|
||||
"content": c["content_ltks"],
|
||||
"score": c["similarity"],
|
||||
"title": c["docnm_kwd"],
|
||||
"metadata": {}
|
||||
})
|
||||
|
||||
return jsonify({"records": records})
|
||||
except Exception as e:
|
||||
if str(e).find("not_found") > 0:
|
||||
return build_error_result(
|
||||
error_msg=f'No chunk found! Check the chunk status please!',
|
||||
retcode=RetCode.NOT_FOUND
|
||||
)
|
||||
return build_error_result(error_msg=str(e), retcode=RetCode.SERVER_ERROR)
|
||||
@ -1,45 +1,37 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import pathlib
|
||||
import re
|
||||
import datetime
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from flask import request
|
||||
from flask_login import login_required, current_user
|
||||
from elasticsearch_dsl import Q
|
||||
|
||||
from api.db.services.dialog_service import keyword_extraction
|
||||
from rag.app.qa import rmPrefix, beAdoc
|
||||
from rag.nlp import search, rag_tokenizer, keyword_extraction
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from rag.utils import rmSpace
|
||||
from rag.nlp import rag_tokenizer
|
||||
from api.db import LLMType, ParserType
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import TenantLLMService
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.settings import RetCode, retrievaler, kg_retrievaler
|
||||
from api.utils.api_utils import get_json_result
|
||||
from api.settings import kg_retrievaler
|
||||
import hashlib
|
||||
import re
|
||||
from api.utils.api_utils import get_json_result, token_required, get_data_error_result
|
||||
|
||||
from api.db.db_models import Task, File
|
||||
|
||||
from api.utils.api_utils import token_required
|
||||
from api.db.db_models import Task
|
||||
from api.db.services.task_service import TaskService, queue_tasks
|
||||
from api.db.services.user_service import TenantService, UserTenantService
|
||||
|
||||
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
|
||||
|
||||
from api.utils.api_utils import get_json_result
|
||||
|
||||
from functools import partial
|
||||
from api.utils.api_utils import server_error_response
|
||||
from api.utils.api_utils import get_result, get_error_data_result
|
||||
from io import BytesIO
|
||||
|
||||
from elasticsearch_dsl import Q
|
||||
from flask import request, send_file
|
||||
from flask_login import login_required
|
||||
|
||||
from api.db import FileSource, TaskStatus, FileType
|
||||
from api.db.db_models import File
|
||||
from api.db.services.document_service import DocumentService
|
||||
@ -47,303 +39,233 @@ from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.settings import RetCode, retrievaler
|
||||
from api.utils.api_utils import construct_json_result, construct_error_response
|
||||
from rag.app import book, laws, manual, naive, one, paper, presentation, qa, resume, table, picture, audio, email
|
||||
from api.utils.api_utils import construct_json_result,get_parser_config
|
||||
from rag.nlp import search
|
||||
from rag.utils import rmSpace
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
import os
|
||||
|
||||
MAXIMUM_OF_UPLOADING_FILES = 256
|
||||
|
||||
MAXIMUM_OF_UPLOADING_FILES = 256
|
||||
|
||||
|
||||
@manager.route('/dataset/<dataset_id>/documents/upload', methods=['POST'])
|
||||
@manager.route('/datasets/<dataset_id>/documents', methods=['POST'])
|
||||
@token_required
|
||||
def upload(dataset_id, tenant_id):
|
||||
if 'file' not in request.files:
|
||||
return get_json_result(
|
||||
data=False, retmsg='No file part!', retcode=RetCode.ARGUMENT_ERROR)
|
||||
return get_error_data_result(
|
||||
retmsg='No file part!', retcode=RetCode.ARGUMENT_ERROR)
|
||||
file_objs = request.files.getlist('file')
|
||||
for file_obj in file_objs:
|
||||
if file_obj.filename == '':
|
||||
return get_json_result(
|
||||
data=False, retmsg='No file selected!', retcode=RetCode.ARGUMENT_ERROR)
|
||||
return get_result(
|
||||
retmsg='No file selected!', retcode=RetCode.ARGUMENT_ERROR)
|
||||
# total size
|
||||
total_size = 0
|
||||
for file_obj in file_objs:
|
||||
file_obj.seek(0, os.SEEK_END)
|
||||
total_size += file_obj.tell()
|
||||
file_obj.seek(0)
|
||||
MAX_TOTAL_FILE_SIZE=10*1024*1024
|
||||
if total_size > MAX_TOTAL_FILE_SIZE:
|
||||
return get_result(
|
||||
retmsg=f'Total file size exceeds 10MB limit! ({total_size / (1024 * 1024):.2f} MB)',
|
||||
retcode=RetCode.ARGUMENT_ERROR)
|
||||
e, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not e:
|
||||
raise LookupError(f"Can't find the knowledgebase with ID {dataset_id}!")
|
||||
err, _ = FileService.upload_document(kb, file_objs, tenant_id)
|
||||
raise LookupError(f"Can't find the dataset with ID {dataset_id}!")
|
||||
err, files= FileService.upload_document(kb, file_objs, tenant_id)
|
||||
if err:
|
||||
return get_json_result(
|
||||
data=False, retmsg="\n".join(err), retcode=RetCode.SERVER_ERROR)
|
||||
return get_json_result(data=True)
|
||||
return get_result(
|
||||
retmsg="\n".join(err), retcode=RetCode.SERVER_ERROR)
|
||||
# rename key's name
|
||||
renamed_doc_list = []
|
||||
for file in files:
|
||||
doc = file[0]
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method"
|
||||
}
|
||||
renamed_doc = {}
|
||||
for key, value in doc.items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_doc[new_key] = value
|
||||
renamed_doc["run"] = "UNSTART"
|
||||
renamed_doc_list.append(renamed_doc)
|
||||
return get_result(data=renamed_doc_list)
|
||||
|
||||
|
||||
@manager.route('/infos', methods=['GET'])
|
||||
@manager.route('/datasets/<dataset_id>/documents/<document_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def docinfos(tenant_id):
|
||||
req = request.args
|
||||
if "id" in req:
|
||||
doc_id = req["id"]
|
||||
e, doc = DocumentService.get_by_id(doc_id)
|
||||
return get_json_result(data=doc.to_json())
|
||||
if "name" in req:
|
||||
doc_name = req["name"]
|
||||
doc_id = DocumentService.get_doc_id_by_doc_name(doc_name)
|
||||
e, doc = DocumentService.get_by_id(doc_id)
|
||||
return get_json_result(data=doc.to_json())
|
||||
|
||||
|
||||
@manager.route('/save', methods=['POST'])
|
||||
@token_required
|
||||
def save_doc(tenant_id):
|
||||
def update_doc(tenant_id, dataset_id, document_id):
|
||||
req = request.json
|
||||
#get doc by id or name
|
||||
doc_id = None
|
||||
if "id" in req:
|
||||
doc_id = req["id"]
|
||||
elif "name" in req:
|
||||
doc_name = req["name"]
|
||||
doc_id = DocumentService.get_doc_id_by_doc_name(doc_name)
|
||||
if not doc_id:
|
||||
return get_json_result(retcode=400, retmsg="Document ID or name is required")
|
||||
e, doc = DocumentService.get_by_id(doc_id)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
#other value can't be changed
|
||||
if "chunk_num" in req:
|
||||
if req["chunk_num"] != doc.chunk_num:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change chunk_count.")
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg="You don't own the dataset.")
|
||||
doc = DocumentService.query(kb_id=dataset_id, id=document_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg="The dataset doesn't own the document.")
|
||||
doc = doc[0]
|
||||
if "chunk_count" in req:
|
||||
if req["chunk_count"] != doc.chunk_num:
|
||||
return get_error_data_result(retmsg="Can't change `chunk_count`.")
|
||||
if "token_count" in req:
|
||||
if req["token_count"] != doc.token_num:
|
||||
return get_error_data_result(retmsg="Can't change `token_count`.")
|
||||
if "progress" in req:
|
||||
if req['progress'] != doc.progress:
|
||||
return get_data_error_result(
|
||||
retmsg="Can't change progress.")
|
||||
#change name or parse_method
|
||||
return get_error_data_result(retmsg="Can't change `progress`.")
|
||||
|
||||
if "name" in req and req["name"] != doc.name:
|
||||
try:
|
||||
if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
|
||||
doc.name.lower()).suffix:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg="The extension of file can't be changed",
|
||||
retcode=RetCode.ARGUMENT_ERROR)
|
||||
if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(doc.name.lower()).suffix:
|
||||
return get_result(retmsg="The extension of file can't be changed", retcode=RetCode.ARGUMENT_ERROR)
|
||||
for d in DocumentService.query(name=req["name"], kb_id=doc.kb_id):
|
||||
if d.name == req["name"]:
|
||||
return get_data_error_result(
|
||||
retmsg="Duplicated document name in the same knowledgebase.")
|
||||
|
||||
return get_error_data_result(
|
||||
retmsg="Duplicated document name in the same dataset.")
|
||||
if not DocumentService.update_by_id(
|
||||
doc_id, {"name": req["name"]}):
|
||||
return get_data_error_result(
|
||||
document_id, {"name": req["name"]}):
|
||||
return get_error_data_result(
|
||||
retmsg="Database error (Document rename)!")
|
||||
|
||||
informs = File2DocumentService.get_by_document_id(doc_id)
|
||||
informs = File2DocumentService.get_by_document_id(document_id)
|
||||
if informs:
|
||||
e, file = FileService.get_by_id(informs[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req["name"]})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
if "parser_id" in req:
|
||||
try:
|
||||
if doc.parser_id.lower() == req["parser_id"].lower():
|
||||
if "parser_config" in req:
|
||||
if req["parser_config"] == doc.parser_config:
|
||||
return get_json_result(data=True)
|
||||
else:
|
||||
return get_json_result(data=True)
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
if "chunk_method" in req:
|
||||
valid_chunk_method = {"naive","manual","qa","table","paper","book","laws","presentation","picture","one","knowledge_graph","email"}
|
||||
if req.get("chunk_method") not in valid_chunk_method:
|
||||
return get_error_data_result(f"`chunk_method` {req['chunk_method']} doesn't exist")
|
||||
if doc.parser_id.lower() == req["chunk_method"].lower():
|
||||
return get_result()
|
||||
|
||||
if doc.type == FileType.VISUAL or re.search(
|
||||
r"\.(ppt|pptx|pages)$", doc.name):
|
||||
return get_data_error_result(retmsg="Not supported yet!")
|
||||
return get_error_data_result(retmsg="Not supported yet!")
|
||||
|
||||
e = DocumentService.update_by_id(doc.id,
|
||||
{"parser_id": req["parser_id"], "progress": 0, "progress_msg": "",
|
||||
{"parser_id": req["chunk_method"], "progress": 0, "progress_msg": "",
|
||||
"run": TaskStatus.UNSTART.value})
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
if "parser_config" in req:
|
||||
return get_error_data_result(retmsg="Document not found!")
|
||||
req["parser_config"] = get_parser_config(req["chunk_method"], req.get("parser_config"))
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
if doc.token_num > 0:
|
||||
e = DocumentService.increment_chunk_num(doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1,
|
||||
doc.process_duation * -1)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
ELASTICSEARCH.deleteByQuery(
|
||||
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
|
||||
@manager.route('/change_parser', methods=['POST'])
|
||||
@token_required
|
||||
def change_parser(tenant_id):
|
||||
req = request.json
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
if doc.parser_id.lower() == req["parser_id"].lower():
|
||||
if "parser_config" in req:
|
||||
if req["parser_config"] == doc.parser_config:
|
||||
return get_json_result(data=True)
|
||||
else:
|
||||
return get_json_result(data=True)
|
||||
|
||||
if doc.type == FileType.VISUAL or re.search(
|
||||
r"\.(ppt|pptx|pages)$", doc.name):
|
||||
return get_data_error_result(retmsg="Not supported yet!")
|
||||
|
||||
e = DocumentService.update_by_id(doc.id,
|
||||
{"parser_id": req["parser_id"], "progress": 0, "progress_msg": "",
|
||||
"run": TaskStatus.UNSTART.value})
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
if "parser_config" in req:
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
if doc.token_num > 0:
|
||||
e = DocumentService.increment_chunk_num(doc.id, doc.kb_id, doc.token_num * -1, doc.chunk_num * -1,
|
||||
doc.process_duation * -1)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
return get_error_data_result(retmsg="Document not found!")
|
||||
ELASTICSEARCH.deleteByQuery(
|
||||
Q("match", doc_id=doc.id), idxnm=search.index_name(tenant_id))
|
||||
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
@manager.route('/rename', methods=['POST'])
|
||||
@login_required
|
||||
@validate_request("doc_id", "name")
|
||||
def rename():
|
||||
req = request.json
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
|
||||
doc.name.lower()).suffix:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
retmsg="The extension of file can't be changed",
|
||||
retcode=RetCode.ARGUMENT_ERROR)
|
||||
for d in DocumentService.query(name=req["name"], kb_id=doc.kb_id):
|
||||
if d.name == req["name"]:
|
||||
return get_data_error_result(
|
||||
retmsg="Duplicated document name in the same knowledgebase.")
|
||||
|
||||
if not DocumentService.update_by_id(
|
||||
req["doc_id"], {"name": req["name"]}):
|
||||
return get_data_error_result(
|
||||
retmsg="Database error (Document rename)!")
|
||||
|
||||
informs = File2DocumentService.get_by_document_id(req["doc_id"])
|
||||
if informs:
|
||||
e, file = FileService.get_by_id(informs[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req["name"]})
|
||||
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
return get_result()
|
||||
|
||||
|
||||
@manager.route("/<document_id>", methods=["GET"])
|
||||
@manager.route('/datasets/<dataset_id>/documents/<document_id>', methods=['GET'])
|
||||
@token_required
|
||||
def download_document(dataset_id, document_id):
|
||||
try:
|
||||
# Check whether there is this document
|
||||
exist, document = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(message=f"This document '{document_id}' cannot be found!",
|
||||
code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
def download(tenant_id, dataset_id, document_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f'You do not own the dataset {dataset_id}.')
|
||||
doc = DocumentService.query(kb_id=dataset_id, id=document_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f'The dataset not own the document {document_id}.')
|
||||
# The process of downloading
|
||||
doc_id, doc_location = File2DocumentService.get_minio_address(doc_id=document_id) # minio address
|
||||
doc_id, doc_location = File2DocumentService.get_storage_address(doc_id=document_id) # minio address
|
||||
file_stream = STORAGE_IMPL.get(doc_id, doc_location)
|
||||
if not file_stream:
|
||||
return construct_json_result(message="This file is empty.", code=RetCode.DATA_ERROR)
|
||||
|
||||
file = BytesIO(file_stream)
|
||||
|
||||
# Use send_file with a proper filename and MIME type
|
||||
return send_file(
|
||||
file,
|
||||
as_attachment=True,
|
||||
download_name=document.name,
|
||||
download_name=doc[0].name,
|
||||
mimetype='application/octet-stream' # Set a default MIME type
|
||||
)
|
||||
|
||||
# Error
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/dataset/<dataset_id>/documents', methods=['GET'])
|
||||
@manager.route('/datasets/<dataset_id>/documents', methods=['GET'])
|
||||
@token_required
|
||||
def list_docs(dataset_id, tenant_id):
|
||||
kb_id = request.args.get("kb_id")
|
||||
if not kb_id:
|
||||
return get_json_result(
|
||||
data=False, retmsg='Lack of "KB ID"', retcode=RetCode.ARGUMENT_ERROR)
|
||||
tenants = UserTenantService.query(user_id=tenant_id)
|
||||
for tenant in tenants:
|
||||
if KnowledgebaseService.query(
|
||||
tenant_id=tenant.tenant_id, id=kb_id):
|
||||
break
|
||||
else:
|
||||
return get_json_result(
|
||||
data=False, retmsg=f'Only owner of knowledgebase authorized for this operation.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}. ")
|
||||
id = request.args.get("id")
|
||||
if not DocumentService.query(id=id,kb_id=dataset_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the document {id}.")
|
||||
offset = int(request.args.get("offset", 1))
|
||||
keywords = request.args.get("keywords","")
|
||||
|
||||
page_number = int(request.args.get("page", 1))
|
||||
items_per_page = int(request.args.get("page_size", 15))
|
||||
limit = int(request.args.get("limit", 1024))
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
desc = request.args.get("desc", True)
|
||||
try:
|
||||
docs, tol = DocumentService.get_by_kb_id(
|
||||
kb_id, page_number, items_per_page, orderby, desc, keywords)
|
||||
return get_json_result(data={"total": tol, "docs": docs})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
if request.args.get("desc") == "False":
|
||||
desc = False
|
||||
else:
|
||||
desc = True
|
||||
docs, tol = DocumentService.get_list(dataset_id, offset, limit, orderby, desc, keywords, id)
|
||||
|
||||
# rename key's name
|
||||
renamed_doc_list = []
|
||||
for doc in docs:
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method"
|
||||
}
|
||||
run_mapping = {
|
||||
"0" :"UNSTART",
|
||||
"1":"RUNNING",
|
||||
"2":"CANCEL",
|
||||
"3":"DONE",
|
||||
"4":"FAIL"
|
||||
}
|
||||
renamed_doc = {}
|
||||
for key, value in doc.items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_doc[new_key] = value
|
||||
if key =="run":
|
||||
renamed_doc["run"]=run_mapping.get(value)
|
||||
renamed_doc_list.append(renamed_doc)
|
||||
return get_result(data={"total": tol, "docs": renamed_doc_list})
|
||||
|
||||
|
||||
@manager.route('/delete', methods=['DELETE'])
|
||||
@manager.route('/datasets/<dataset_id>/documents', methods=['DELETE'])
|
||||
@token_required
|
||||
def rm(tenant_id):
|
||||
req = request.args
|
||||
if "doc_id" not in req:
|
||||
return get_data_error_result(
|
||||
retmsg="doc_id is required")
|
||||
doc_ids = req["doc_id"]
|
||||
if isinstance(doc_ids, str): doc_ids = [doc_ids]
|
||||
def delete(tenant_id,dataset_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}. ")
|
||||
req = request.json
|
||||
if not req:
|
||||
doc_ids=None
|
||||
else:
|
||||
doc_ids=req.get("ids")
|
||||
if not doc_ids:
|
||||
doc_list = []
|
||||
docs=DocumentService.query(kb_id=dataset_id)
|
||||
for doc in docs:
|
||||
doc_list.append(doc.id)
|
||||
else:
|
||||
doc_list=doc_ids
|
||||
root_folder = FileService.get_root_folder(tenant_id)
|
||||
pf_id = root_folder["id"]
|
||||
FileService.init_knowledgebase_docs(pf_id, tenant_id)
|
||||
errors = ""
|
||||
for doc_id in doc_ids:
|
||||
for doc_id in doc_list:
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(doc_id)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
return get_error_data_result(retmsg="Document not found!")
|
||||
tenant_id = DocumentService.get_tenant_id(doc_id)
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
return get_error_data_result(retmsg="Tenant not found!")
|
||||
|
||||
b, n = File2DocumentService.get_minio_address(doc_id=doc_id)
|
||||
b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
|
||||
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
return get_data_error_result(
|
||||
return get_error_data_result(
|
||||
retmsg="Database error (Document removal)!")
|
||||
|
||||
f2d = File2DocumentService.get_by_document_id(doc_id)
|
||||
@ -355,87 +277,100 @@ def rm(tenant_id):
|
||||
errors += str(e)
|
||||
|
||||
if errors:
|
||||
return get_json_result(data=False, retmsg=errors, retcode=RetCode.SERVER_ERROR)
|
||||
return get_result(retmsg=errors, retcode=RetCode.SERVER_ERROR)
|
||||
|
||||
return get_json_result(data=True, retmsg="success")
|
||||
return get_result()
|
||||
|
||||
@manager.route("/<document_id>/status", methods=["GET"])
|
||||
|
||||
@manager.route('/datasets/<dataset_id>/chunks', methods=['POST'])
|
||||
@token_required
|
||||
def show_parsing_status(tenant_id, document_id):
|
||||
try:
|
||||
# valid document
|
||||
exist, _ = DocumentService.get_by_id(document_id)
|
||||
if not exist:
|
||||
return construct_json_result(code=RetCode.DATA_ERROR,
|
||||
message=f"This document: '{document_id}' is not a valid document.")
|
||||
|
||||
_, doc = DocumentService.get_by_id(document_id) # get doc object
|
||||
doc_attributes = doc.to_dict()
|
||||
|
||||
return construct_json_result(
|
||||
data={"progress": doc_attributes["progress"], "status": TaskStatus(doc_attributes["status"]).name},
|
||||
code=RetCode.SUCCESS
|
||||
)
|
||||
except Exception as e:
|
||||
return construct_error_response(e)
|
||||
|
||||
|
||||
|
||||
@manager.route('/run', methods=['POST'])
|
||||
@token_required
|
||||
def run(tenant_id):
|
||||
def parse(tenant_id,dataset_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
req = request.json
|
||||
try:
|
||||
for id in req["doc_ids"]:
|
||||
info = {"run": str(req["run"]), "progress": 0}
|
||||
if str(req["run"]) == TaskStatus.RUNNING.value:
|
||||
if not req.get("document_ids"):
|
||||
return get_error_data_result("`document_ids` is required")
|
||||
for id in req["document_ids"]:
|
||||
doc = DocumentService.query(id=id,kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {id}.")
|
||||
info = {"run": "1", "progress": 0}
|
||||
info["progress_msg"] = ""
|
||||
info["chunk_num"] = 0
|
||||
info["token_num"] = 0
|
||||
DocumentService.update_by_id(id, info)
|
||||
# if str(req["run"]) == TaskStatus.CANCEL.value:
|
||||
tenant_id = DocumentService.get_tenant_id(id)
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
ELASTICSEARCH.deleteByQuery(
|
||||
Q("match", doc_id=id), idxnm=search.index_name(tenant_id))
|
||||
|
||||
if str(req["run"]) == TaskStatus.RUNNING.value:
|
||||
TaskService.filter_delete([Task.doc_id == id])
|
||||
e, doc = DocumentService.get_by_id(id)
|
||||
doc = doc.to_dict()
|
||||
doc["tenant_id"] = tenant_id
|
||||
bucket, name = File2DocumentService.get_minio_address(doc_id=doc["id"])
|
||||
bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
|
||||
queue_tasks(doc, bucket, name)
|
||||
return get_result()
|
||||
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/chunk/list', methods=['POST'])
|
||||
@manager.route('/datasets/<dataset_id>/chunks', methods=['DELETE'])
|
||||
@token_required
|
||||
@validate_request("doc_id")
|
||||
def list_chunk(tenant_id):
|
||||
def stop_parsing(tenant_id,dataset_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
req = request.json
|
||||
doc_id = req["doc_id"]
|
||||
page = int(req.get("page", 1))
|
||||
size = int(req.get("size", 30))
|
||||
if not req.get("document_ids"):
|
||||
return get_error_data_result("`document_ids` is required")
|
||||
for id in req["document_ids"]:
|
||||
doc = DocumentService.query(id=id, kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {id}.")
|
||||
if doc[0].progress == 100.0 or doc[0].progress == 0.0:
|
||||
return get_error_data_result("Can't stop parsing document with progress at 0 or 100")
|
||||
info = {"run": "2", "progress": 0,"chunk_num":0}
|
||||
DocumentService.update_by_id(id, info)
|
||||
ELASTICSEARCH.deleteByQuery(
|
||||
Q("match", doc_id=id), idxnm=search.index_name(tenant_id))
|
||||
return get_result()
|
||||
|
||||
|
||||
@manager.route('/datasets/<dataset_id>/documents/<document_id>/chunks', methods=['GET'])
|
||||
@token_required
|
||||
def list_chunks(tenant_id,dataset_id,document_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
doc=DocumentService.query(id=document_id, kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {document_id}.")
|
||||
doc=doc[0]
|
||||
req = request.args
|
||||
doc_id = document_id
|
||||
page = int(req.get("offset", 1))
|
||||
size = int(req.get("limit", 30))
|
||||
question = req.get("keywords", "")
|
||||
try:
|
||||
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
e, doc = DocumentService.get_by_id(doc_id)
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
query = {
|
||||
"doc_ids": [doc_id], "page": page, "size": size, "question": question, "sort": True
|
||||
}
|
||||
if "available_int" in req:
|
||||
query["available_int"] = int(req["available_int"])
|
||||
sres = retrievaler.search(query, search.index_name(tenant_id), highlight=True)
|
||||
res = {"total": sres.total, "chunks": [], "doc": doc.to_dict()}
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method"
|
||||
}
|
||||
run_mapping = {
|
||||
"0": "UNSTART",
|
||||
"1": "RUNNING",
|
||||
"2": "CANCEL",
|
||||
"3": "DONE",
|
||||
"4": "FAIL"
|
||||
}
|
||||
doc=doc.to_dict()
|
||||
renamed_doc = {}
|
||||
for key, value in doc.items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_doc[new_key] = value
|
||||
if key == "run":
|
||||
renamed_doc["run"] = run_mapping.get(str(value))
|
||||
res = {"total": sres.total, "chunks": [], "doc": renamed_doc}
|
||||
origin_chunks = []
|
||||
sign = 0
|
||||
for id in sres.ids:
|
||||
d = {
|
||||
"chunk_id": id,
|
||||
@ -455,75 +390,272 @@ def list_chunk(tenant_id):
|
||||
poss.append([float(d["positions"][i]), float(d["positions"][i + 1]), float(d["positions"][i + 2]),
|
||||
float(d["positions"][i + 3]), float(d["positions"][i + 4])])
|
||||
d["positions"] = poss
|
||||
res["chunks"].append(d)
|
||||
return get_json_result(data=res)
|
||||
except Exception as e:
|
||||
if str(e).find("not_found") > 0:
|
||||
return get_json_result(data=False, retmsg=f'No chunk found!',
|
||||
retcode=RetCode.DATA_ERROR)
|
||||
return server_error_response(e)
|
||||
|
||||
origin_chunks.append(d)
|
||||
if req.get("id"):
|
||||
if req.get("id") == id:
|
||||
origin_chunks.clear()
|
||||
origin_chunks.append(d)
|
||||
sign = 1
|
||||
break
|
||||
if req.get("id"):
|
||||
if sign == 0:
|
||||
return get_error_data_result(f"Can't find this chunk {req.get('id')}")
|
||||
for chunk in origin_chunks:
|
||||
key_mapping = {
|
||||
"chunk_id": "id",
|
||||
"content_with_weight": "content",
|
||||
"doc_id": "document_id",
|
||||
"important_kwd": "important_keywords",
|
||||
"img_id": "image_id",
|
||||
"available_int":"available"
|
||||
}
|
||||
renamed_chunk = {}
|
||||
for key, value in chunk.items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_chunk[new_key] = value
|
||||
if renamed_chunk["available"] == "0":
|
||||
renamed_chunk["available"] = False
|
||||
if renamed_chunk["available"] == "1":
|
||||
renamed_chunk["available"] = True
|
||||
res["chunks"].append(renamed_chunk)
|
||||
return get_result(data=res)
|
||||
|
||||
|
||||
@manager.route('/chunk/create', methods=['POST'])
|
||||
|
||||
@manager.route('/datasets/<dataset_id>/documents/<document_id>/chunks', methods=['POST'])
|
||||
@token_required
|
||||
@validate_request("doc_id", "content_with_weight")
|
||||
def create(tenant_id):
|
||||
def add_chunk(tenant_id,dataset_id,document_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
doc = DocumentService.query(id=document_id, kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {document_id}.")
|
||||
doc = doc[0]
|
||||
req = request.json
|
||||
if not req.get("content"):
|
||||
return get_error_data_result(retmsg="`content` is required")
|
||||
if "important_keywords" in req:
|
||||
if type(req["important_keywords"]) != list:
|
||||
return get_error_data_result("`important_keywords` is required to be a list")
|
||||
md5 = hashlib.md5()
|
||||
md5.update((req["content_with_weight"] + req["doc_id"]).encode("utf-8"))
|
||||
chunck_id = md5.hexdigest()
|
||||
d = {"id": chunck_id, "content_ltks": rag_tokenizer.tokenize(req["content_with_weight"]),
|
||||
"content_with_weight": req["content_with_weight"]}
|
||||
md5.update((req["content"] + document_id).encode("utf-8"))
|
||||
|
||||
chunk_id = md5.hexdigest()
|
||||
d = {"id": chunk_id, "content_ltks": rag_tokenizer.tokenize(req["content"]),
|
||||
"content_with_weight": req["content"]}
|
||||
d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])
|
||||
d["important_kwd"] = req.get("important_kwd", [])
|
||||
d["important_tks"] = rag_tokenizer.tokenize(" ".join(req.get("important_kwd", [])))
|
||||
d["important_kwd"] = req.get("important_keywords", [])
|
||||
d["important_tks"] = rag_tokenizer.tokenize(" ".join(req.get("important_keywords", [])))
|
||||
d["create_time"] = str(datetime.datetime.now()).replace("T", " ")[:19]
|
||||
d["create_timestamp_flt"] = datetime.datetime.now().timestamp()
|
||||
|
||||
try:
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
d["kb_id"] = [doc.kb_id]
|
||||
d["docnm_kwd"] = doc.name
|
||||
d["doc_id"] = doc.id
|
||||
|
||||
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
|
||||
if not tenant_id:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
embd_id = DocumentService.get_embd_id(req["doc_id"])
|
||||
embd_id = DocumentService.get_embd_id(document_id)
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
tenant_id, LLMType.EMBEDDING.value, embd_id)
|
||||
|
||||
v, c = embd_mdl.encode([doc.name, req["content_with_weight"]])
|
||||
print(embd_mdl,flush=True)
|
||||
v, c = embd_mdl.encode([doc.name, req["content"]])
|
||||
v = 0.1 * v[0] + 0.9 * v[1]
|
||||
d["q_%d_vec" % len(v)] = v.tolist()
|
||||
ELASTICSEARCH.upsert([d], search.index_name(tenant_id))
|
||||
|
||||
DocumentService.increment_chunk_num(
|
||||
doc.id, doc.kb_id, c, 1, 0)
|
||||
return get_json_result(data={"chunk": d})
|
||||
# return get_json_result(data={"chunk_id": chunck_id})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
d["chunk_id"] = chunk_id
|
||||
# rename keys
|
||||
key_mapping = {
|
||||
"chunk_id": "id",
|
||||
"content_with_weight": "content",
|
||||
"doc_id": "document_id",
|
||||
"important_kwd": "important_keywords",
|
||||
"kb_id": "dataset_id",
|
||||
"create_timestamp_flt": "create_timestamp",
|
||||
"create_time": "create_time",
|
||||
"document_keyword": "document"
|
||||
}
|
||||
renamed_chunk = {}
|
||||
for key, value in d.items():
|
||||
if key in key_mapping:
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_chunk[new_key] = value
|
||||
return get_result(data={"chunk": renamed_chunk})
|
||||
# return get_result(data={"chunk_id": chunk_id})
|
||||
|
||||
|
||||
@manager.route('/chunk/rm', methods=['POST'])
|
||||
@manager.route('datasets/<dataset_id>/documents/<document_id>/chunks', methods=['DELETE'])
|
||||
@token_required
|
||||
@validate_request("chunk_ids", "doc_id")
|
||||
def rm_chunk():
|
||||
def rm_chunk(tenant_id,dataset_id,document_id):
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
doc = DocumentService.query(id=document_id, kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {document_id}.")
|
||||
doc = doc[0]
|
||||
req = request.json
|
||||
try:
|
||||
query = {
|
||||
"doc_ids": [doc.id], "page": 1, "size": 1024, "question": "", "sort": True}
|
||||
sres = retrievaler.search(query, search.index_name(tenant_id), highlight=True)
|
||||
if not req:
|
||||
chunk_ids=None
|
||||
else:
|
||||
chunk_ids=req.get("chunk_ids")
|
||||
if not chunk_ids:
|
||||
chunk_list=sres.ids
|
||||
else:
|
||||
chunk_list=chunk_ids
|
||||
for chunk_id in chunk_list:
|
||||
if chunk_id not in sres.ids:
|
||||
return get_error_data_result(f"Chunk {chunk_id} not found")
|
||||
if not ELASTICSEARCH.deleteByQuery(
|
||||
Q("ids", values=req["chunk_ids"]), search.index_name(current_user.id)):
|
||||
return get_data_error_result(retmsg="Index updating failure")
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Document not found!")
|
||||
deleted_chunk_ids = req["chunk_ids"]
|
||||
Q("ids", values=chunk_list), search.index_name(tenant_id)):
|
||||
return get_error_data_result(retmsg="Index updating failure")
|
||||
deleted_chunk_ids = chunk_list
|
||||
chunk_number = len(deleted_chunk_ids)
|
||||
DocumentService.decrement_chunk_num(doc.id, doc.kb_id, 1, chunk_number, 0)
|
||||
return get_json_result(data=True)
|
||||
return get_result()
|
||||
|
||||
|
||||
|
||||
@manager.route('/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def update_chunk(tenant_id,dataset_id,document_id,chunk_id):
|
||||
try:
|
||||
res = ELASTICSEARCH.get(
|
||||
chunk_id, search.index_name(
|
||||
tenant_id))
|
||||
except Exception as e:
|
||||
return get_error_data_result(f"Can't find this chunk {chunk_id}")
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(retmsg=f"You don't own the dataset {dataset_id}.")
|
||||
doc = DocumentService.query(id=document_id, kb_id=dataset_id)
|
||||
if not doc:
|
||||
return get_error_data_result(retmsg=f"You don't own the document {document_id}.")
|
||||
doc = doc[0]
|
||||
query = {
|
||||
"doc_ids": [document_id], "page": 1, "size": 1024, "question": "", "sort": True
|
||||
}
|
||||
sres = retrievaler.search(query, search.index_name(tenant_id), highlight=True)
|
||||
if chunk_id not in sres.ids:
|
||||
return get_error_data_result(f"You don't own the chunk {chunk_id}")
|
||||
req = request.json
|
||||
content=res["_source"].get("content_with_weight")
|
||||
d = {
|
||||
"id": chunk_id,
|
||||
"content_with_weight": req.get("content",content)}
|
||||
d["content_ltks"] = rag_tokenizer.tokenize(d["content_with_weight"])
|
||||
d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])
|
||||
if "important_keywords" in req:
|
||||
if not isinstance(req["important_keywords"],list):
|
||||
return get_error_data_result("`important_keywords` should be a list")
|
||||
d["important_kwd"] = req.get("important_keywords")
|
||||
d["important_tks"] = rag_tokenizer.tokenize(" ".join(req["important_keywords"]))
|
||||
if "available" in req:
|
||||
d["available_int"] = int(req["available"])
|
||||
embd_id = DocumentService.get_embd_id(document_id)
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
tenant_id, LLMType.EMBEDDING.value, embd_id)
|
||||
if doc.parser_id == ParserType.QA:
|
||||
arr = [
|
||||
t for t in re.split(
|
||||
r"[\n\t]",
|
||||
d["content_with_weight"]) if len(t) > 1]
|
||||
if len(arr) != 2:
|
||||
return get_error_data_result(
|
||||
retmsg="Q&A must be separated by TAB/ENTER key.")
|
||||
q, a = rmPrefix(arr[0]), rmPrefix(arr[1])
|
||||
d = beAdoc(d, arr[0], arr[1], not any(
|
||||
[rag_tokenizer.is_chinese(t) for t in q + a]))
|
||||
|
||||
v, c = embd_mdl.encode([doc.name, d["content_with_weight"]])
|
||||
v = 0.1 * v[0] + 0.9 * v[1] if doc.parser_id != ParserType.QA else v[1]
|
||||
d["q_%d_vec" % len(v)] = v.tolist()
|
||||
ELASTICSEARCH.upsert([d], search.index_name(tenant_id))
|
||||
return get_result()
|
||||
|
||||
|
||||
|
||||
@manager.route('/retrieval', methods=['POST'])
|
||||
@token_required
|
||||
def retrieval_test(tenant_id):
|
||||
req = request.json
|
||||
if not req.get("dataset_ids"):
|
||||
return get_error_data_result("`datasets` is required.")
|
||||
kb_ids = req["dataset_ids"]
|
||||
if not isinstance(kb_ids,list):
|
||||
return get_error_data_result("`datasets` should be a list")
|
||||
kbs = KnowledgebaseService.get_by_ids(kb_ids)
|
||||
for id in kb_ids:
|
||||
if not KnowledgebaseService.query(id=id,tenant_id=tenant_id):
|
||||
return get_error_data_result(f"You don't own the dataset {id}.")
|
||||
embd_nms = list(set([kb.embd_id for kb in kbs]))
|
||||
if len(embd_nms) != 1:
|
||||
return get_result(
|
||||
retmsg='Datasets use different embedding models."',
|
||||
retcode=RetCode.AUTHENTICATION_ERROR)
|
||||
if "question" not in req:
|
||||
return get_error_data_result("`question` is required.")
|
||||
page = int(req.get("offset", 1))
|
||||
size = int(req.get("limit", 1024))
|
||||
question = req["question"]
|
||||
doc_ids = req.get("document_ids", [])
|
||||
if not isinstance(doc_ids,list):
|
||||
return get_error_data_result("`documents` should be a list")
|
||||
doc_ids_list=KnowledgebaseService.list_documents_by_ids(kb_ids)
|
||||
for doc_id in doc_ids:
|
||||
if doc_id not in doc_ids_list:
|
||||
return get_error_data_result(f"The datasets don't own the document {doc_id}")
|
||||
similarity_threshold = float(req.get("similarity_threshold", 0.2))
|
||||
vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
|
||||
top = int(req.get("top_k", 1024))
|
||||
if req.get("highlight")=="False" or req.get("highlight")=="false":
|
||||
highlight = False
|
||||
else:
|
||||
highlight = True
|
||||
try:
|
||||
e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
|
||||
if not e:
|
||||
return get_error_data_result(retmsg="Dataset not found!")
|
||||
embd_mdl = TenantLLMService.model_instance(
|
||||
kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
|
||||
|
||||
rerank_mdl = None
|
||||
if req.get("rerank_id"):
|
||||
rerank_mdl = TenantLLMService.model_instance(
|
||||
kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
|
||||
|
||||
if req.get("keyword", False):
|
||||
chat_mdl = TenantLLMService.model_instance(kb.tenant_id, LLMType.CHAT)
|
||||
question += keyword_extraction(chat_mdl, question)
|
||||
|
||||
retr = retrievaler if kb.parser_id != ParserType.KG else kg_retrievaler
|
||||
ranks = retr.retrieval(question, embd_mdl, kb.tenant_id, kb_ids, page, size,
|
||||
similarity_threshold, vector_similarity_weight, top,
|
||||
doc_ids, rerank_mdl=rerank_mdl, highlight=highlight)
|
||||
for c in ranks["chunks"]:
|
||||
if "vector" in c:
|
||||
del c["vector"]
|
||||
|
||||
##rename keys
|
||||
renamed_chunks = []
|
||||
for chunk in ranks["chunks"]:
|
||||
key_mapping = {
|
||||
"chunk_id": "id",
|
||||
"content_with_weight": "content",
|
||||
"doc_id": "document_id",
|
||||
"important_kwd": "important_keywords",
|
||||
"docnm_kwd": "document_keyword"
|
||||
}
|
||||
rename_chunk = {}
|
||||
for key, value in chunk.items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
rename_chunk[new_key] = value
|
||||
renamed_chunks.append(rename_chunk)
|
||||
ranks["chunks"] = renamed_chunks
|
||||
return get_result(data=ranks)
|
||||
except Exception as e:
|
||||
if str(e).find("not_found") > 0:
|
||||
return get_result(retmsg=f'No chunk found! Check the chunk status please!',
|
||||
retcode=RetCode.DATA_ERROR)
|
||||
return server_error_response(e)
|
||||
@ -20,47 +20,18 @@ from flask import request, Response
|
||||
|
||||
from api.db import StatusEnum
|
||||
from api.db.services.dialog_service import DialogService, ConversationService, chat
|
||||
from api.settings import RetCode
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_data_error_result
|
||||
from api.utils.api_utils import get_json_result, token_required
|
||||
from api.utils.api_utils import get_error_data_result
|
||||
from api.utils.api_utils import get_result, token_required
|
||||
|
||||
|
||||
@manager.route('/save', methods=['POST'])
|
||||
@manager.route('/chats/<chat_id>/sessions', methods=['POST'])
|
||||
@token_required
|
||||
def set_conversation(tenant_id):
|
||||
def create(tenant_id,chat_id):
|
||||
req = request.json
|
||||
conv_id = req.get("id")
|
||||
if "assistant_id" in req:
|
||||
req["dialog_id"] = req.pop("assistant_id")
|
||||
if "id" in req:
|
||||
del req["id"]
|
||||
conv = ConversationService.query(id=conv_id)
|
||||
if not conv:
|
||||
return get_data_error_result(retmsg="Session does not exist")
|
||||
if not DialogService.query(id=conv[0].dialog_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(retmsg="You do not own the session")
|
||||
if req.get("dialog_id"):
|
||||
req["dialog_id"] = chat_id
|
||||
dia = DialogService.query(tenant_id=tenant_id, id=req["dialog_id"], status=StatusEnum.VALID.value)
|
||||
if not dia:
|
||||
return get_data_error_result(retmsg="You do not own the assistant")
|
||||
if "dialog_id" in req and not req.get("dialog_id"):
|
||||
return get_data_error_result(retmsg="assistant_id can not be empty.")
|
||||
if "message" in req:
|
||||
return get_data_error_result(retmsg="message can not be change")
|
||||
if "reference" in req:
|
||||
return get_data_error_result(retmsg="reference can not be change")
|
||||
if "name" in req and not req.get("name"):
|
||||
return get_data_error_result(retmsg="name can not be empty.")
|
||||
if not ConversationService.update_by_id(conv_id, req):
|
||||
return get_data_error_result(retmsg="Session updates error")
|
||||
return get_json_result(data=True)
|
||||
|
||||
if not req.get("dialog_id"):
|
||||
return get_data_error_result(retmsg="assistant_id is required.")
|
||||
dia = DialogService.query(tenant_id=tenant_id, id=req["dialog_id"], status=StatusEnum.VALID.value)
|
||||
if not dia:
|
||||
return get_data_error_result(retmsg="You do not own the assistant")
|
||||
return get_error_data_result(retmsg="You do not own the assistant")
|
||||
conv = {
|
||||
"id": get_uuid(),
|
||||
"dialog_id": req["dialog_id"],
|
||||
@ -68,33 +39,65 @@ def set_conversation(tenant_id):
|
||||
"message": [{"role": "assistant", "content": "Hi! I am your assistant,can I help you?"}]
|
||||
}
|
||||
if not conv.get("name"):
|
||||
return get_data_error_result(retmsg="name can not be empty.")
|
||||
return get_error_data_result(retmsg="`name` can not be empty.")
|
||||
ConversationService.save(**conv)
|
||||
e, conv = ConversationService.get_by_id(conv["id"])
|
||||
if not e:
|
||||
return get_data_error_result(retmsg="Fail to new session!")
|
||||
return get_error_data_result(retmsg="Fail to create a session!")
|
||||
conv = conv.to_dict()
|
||||
conv['messages'] = conv.pop("message")
|
||||
conv["assistant_id"] = conv.pop("dialog_id")
|
||||
conv["chat_id"] = conv.pop("dialog_id")
|
||||
del conv["reference"]
|
||||
return get_json_result(data=conv)
|
||||
return get_result(data=conv)
|
||||
|
||||
|
||||
@manager.route('/completion', methods=['POST'])
|
||||
@manager.route('/chats/<chat_id>/sessions/<session_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def completion(tenant_id):
|
||||
def update(tenant_id,chat_id,session_id):
|
||||
req = request.json
|
||||
# req = {"conversation_id": "9aaaca4c11d311efa461fa163e197198", "messages": [
|
||||
# {"role": "user", "content": "上海有吗?"}
|
||||
# ]}
|
||||
if "id" not in req:
|
||||
return get_data_error_result(retmsg="id is required")
|
||||
conv = ConversationService.query(id=req["id"])
|
||||
req["dialog_id"] = chat_id
|
||||
conv_id = session_id
|
||||
conv = ConversationService.query(id=conv_id,dialog_id=chat_id)
|
||||
if not conv:
|
||||
return get_data_error_result(retmsg="Session does not exist")
|
||||
return get_error_data_result(retmsg="Session does not exist")
|
||||
if not DialogService.query(id=chat_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg="You do not own the session")
|
||||
if "message" in req or "messages" in req:
|
||||
return get_error_data_result(retmsg="`message` can not be change")
|
||||
if "reference" in req:
|
||||
return get_error_data_result(retmsg="`reference` can not be change")
|
||||
if "name" in req and not req.get("name"):
|
||||
return get_error_data_result(retmsg="`name` can not be empty.")
|
||||
if not ConversationService.update_by_id(conv_id, req):
|
||||
return get_error_data_result(retmsg="Session updates error")
|
||||
return get_result()
|
||||
|
||||
|
||||
@manager.route('/chats/<chat_id>/completions', methods=['POST'])
|
||||
@token_required
|
||||
def completion(tenant_id,chat_id):
|
||||
req = request.json
|
||||
if not req.get("session_id"):
|
||||
conv = {
|
||||
"id": get_uuid(),
|
||||
"dialog_id": chat_id,
|
||||
"name": req.get("name", "New session"),
|
||||
"message": [{"role": "assistant", "content": "Hi! I am your assistant,can I help you?"}]
|
||||
}
|
||||
if not conv.get("name"):
|
||||
return get_error_data_result(retmsg="`name` can not be empty.")
|
||||
ConversationService.save(**conv)
|
||||
e, conv = ConversationService.get_by_id(conv["id"])
|
||||
session_id=conv.id
|
||||
else:
|
||||
session_id = req.get("session_id")
|
||||
if not req.get("question"):
|
||||
return get_error_data_result(retmsg="Please input your question.")
|
||||
conv = ConversationService.query(id=session_id,dialog_id=chat_id)
|
||||
if not conv:
|
||||
return get_error_data_result(retmsg="Session does not exist")
|
||||
conv = conv[0]
|
||||
if not DialogService.query(id=conv.dialog_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(retmsg="You do not own the session")
|
||||
if not DialogService.query(id=chat_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg="You do not own the chat")
|
||||
msg = []
|
||||
question = {
|
||||
"content": req.get("question"),
|
||||
@ -108,7 +111,6 @@ def completion(tenant_id):
|
||||
msg.append(m)
|
||||
message_id = msg[-1].get("id")
|
||||
e, dia = DialogService.get_by_id(conv.dialog_id)
|
||||
del req["id"]
|
||||
|
||||
if not conv.reference:
|
||||
conv.reference = []
|
||||
@ -124,19 +126,20 @@ def completion(tenant_id):
|
||||
conv.message[-1] = {"role": "assistant", "content": ans["answer"],
|
||||
"id": message_id, "prompt": ans.get("prompt", "")}
|
||||
ans["id"] = message_id
|
||||
ans["session_id"]=session_id
|
||||
|
||||
def stream():
|
||||
nonlocal dia, msg, req, conv
|
||||
try:
|
||||
for ans in chat(dia, msg, **req):
|
||||
fillin_conv(ans)
|
||||
yield "data:" + json.dumps({"retcode": 0, "retmsg": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 0, "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
ConversationService.update_by_id(conv.id, conv.to_dict())
|
||||
except Exception as e:
|
||||
yield "data:" + json.dumps({"retcode": 500, "retmsg": str(e),
|
||||
yield "data:" + json.dumps({"code": 500, "message": str(e),
|
||||
"data": {"answer": "**ERROR**: " + str(e),"reference": []}},
|
||||
ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"retcode": 0, "retmsg": "", "data": True}, ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 0, "data": True}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
if req.get("stream", True):
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
@ -153,70 +156,32 @@ def completion(tenant_id):
|
||||
fillin_conv(ans)
|
||||
ConversationService.update_by_id(conv.id, conv.to_dict())
|
||||
break
|
||||
return get_json_result(data=answer)
|
||||
return get_result(data=answer)
|
||||
|
||||
|
||||
@manager.route('/get', methods=['GET'])
|
||||
@manager.route('/chats/<chat_id>/sessions', methods=['GET'])
|
||||
@token_required
|
||||
def get(tenant_id):
|
||||
req = request.args
|
||||
if "id" not in req:
|
||||
return get_data_error_result(retmsg="id is required")
|
||||
conv_id = req["id"]
|
||||
conv = ConversationService.query(id=conv_id)
|
||||
if not conv:
|
||||
return get_data_error_result(retmsg="Session does not exist")
|
||||
if not DialogService.query(id=conv[0].dialog_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(retmsg="You do not own the session")
|
||||
conv = conv[0].to_dict()
|
||||
conv['messages'] = conv.pop("message")
|
||||
conv["assistant_id"] = conv.pop("dialog_id")
|
||||
if conv["reference"]:
|
||||
messages = conv["messages"]
|
||||
message_num = 0
|
||||
chunk_num = 0
|
||||
while message_num < len(messages):
|
||||
if message_num != 0 and messages[message_num]["role"] != "user":
|
||||
chunk_list = []
|
||||
if "chunks" in conv["reference"][chunk_num]:
|
||||
chunks = conv["reference"][chunk_num]["chunks"]
|
||||
for chunk in chunks:
|
||||
new_chunk = {
|
||||
"id": chunk["chunk_id"],
|
||||
"content": chunk["content_with_weight"],
|
||||
"document_id": chunk["doc_id"],
|
||||
"document_name": chunk["docnm_kwd"],
|
||||
"knowledgebase_id": chunk["kb_id"],
|
||||
"image_id": chunk["img_id"],
|
||||
"similarity": chunk["similarity"],
|
||||
"vector_similarity": chunk["vector_similarity"],
|
||||
"term_similarity": chunk["term_similarity"],
|
||||
"positions": chunk["positions"],
|
||||
}
|
||||
chunk_list.append(new_chunk)
|
||||
chunk_num += 1
|
||||
messages[message_num]["reference"] = chunk_list
|
||||
message_num += 1
|
||||
del conv["reference"]
|
||||
return get_json_result(data=conv)
|
||||
|
||||
|
||||
@manager.route('/list', methods=["GET"])
|
||||
@token_required
|
||||
def list(tenant_id):
|
||||
assistant_id = request.args["assistant_id"]
|
||||
if not DialogService.query(tenant_id=tenant_id, id=assistant_id, status=StatusEnum.VALID.value):
|
||||
return get_json_result(
|
||||
data=False, retmsg=f'Only owner of the assistant is authorized for this operation.',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
convs = ConversationService.query(
|
||||
dialog_id=assistant_id,
|
||||
order_by=ConversationService.model.create_time,
|
||||
reverse=True)
|
||||
convs = [d.to_dict() for d in convs]
|
||||
def list(chat_id,tenant_id):
|
||||
if not DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg=f"You don't own the assistant {chat_id}.")
|
||||
id = request.args.get("id")
|
||||
name = request.args.get("name")
|
||||
page_number = int(request.args.get("page", 1))
|
||||
items_per_page = int(request.args.get("page_size", 1024))
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
if request.args.get("desc") == "False" or request.args.get("desc") == "false":
|
||||
desc = False
|
||||
else:
|
||||
desc = True
|
||||
convs = ConversationService.get_list(chat_id,page_number,items_per_page,orderby,desc,id,name)
|
||||
if not convs:
|
||||
return get_result(data=[])
|
||||
for conv in convs:
|
||||
conv['messages'] = conv.pop("message")
|
||||
conv["assistant_id"] = conv.pop("dialog_id")
|
||||
infos = conv["messages"]
|
||||
for info in infos:
|
||||
if "prompt" in info:
|
||||
info.pop("prompt")
|
||||
conv["chat"] = conv.pop("dialog_id")
|
||||
if conv["reference"]:
|
||||
messages = conv["messages"]
|
||||
message_num = 0
|
||||
@ -232,7 +197,7 @@ def list(tenant_id):
|
||||
"content": chunk["content_with_weight"],
|
||||
"document_id": chunk["doc_id"],
|
||||
"document_name": chunk["docnm_kwd"],
|
||||
"knowledgebase_id": chunk["kb_id"],
|
||||
"dataset_id": chunk["kb_id"],
|
||||
"image_id": chunk["img_id"],
|
||||
"similarity": chunk["similarity"],
|
||||
"vector_similarity": chunk["vector_similarity"],
|
||||
@ -244,20 +209,29 @@ def list(tenant_id):
|
||||
messages[message_num]["reference"] = chunk_list
|
||||
message_num += 1
|
||||
del conv["reference"]
|
||||
return get_json_result(data=convs)
|
||||
return get_result(data=convs)
|
||||
|
||||
|
||||
@manager.route('/delete', methods=["DELETE"])
|
||||
@manager.route('/chats/<chat_id>/sessions', methods=["DELETE"])
|
||||
@token_required
|
||||
def delete(tenant_id):
|
||||
id = request.args.get("id")
|
||||
if not id:
|
||||
return get_data_error_result(retmsg="`id` is required in deleting operation")
|
||||
conv = ConversationService.query(id=id)
|
||||
def delete(tenant_id,chat_id):
|
||||
if not DialogService.query(id=chat_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_error_data_result(retmsg="You don't own the chat")
|
||||
req = request.json
|
||||
convs = ConversationService.query(dialog_id=chat_id)
|
||||
if not req:
|
||||
ids = None
|
||||
else:
|
||||
ids=req.get("ids")
|
||||
|
||||
if not ids:
|
||||
conv_list = []
|
||||
for conv in convs:
|
||||
conv_list.append(conv.id)
|
||||
else:
|
||||
conv_list=ids
|
||||
for id in conv_list:
|
||||
conv = ConversationService.query(id=id,dialog_id=chat_id)
|
||||
if not conv:
|
||||
return get_data_error_result(retmsg="Session doesn't exist")
|
||||
conv = conv[0]
|
||||
if not DialogService.query(id=conv.dialog_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
|
||||
return get_data_error_result(retmsg="You don't own the session")
|
||||
return get_error_data_result(retmsg="The chat doesn't own the session")
|
||||
ConversationService.delete_by_id(id)
|
||||
return get_json_result(data=True)
|
||||
return get_result()
|
||||
|
||||
@ -14,15 +14,21 @@
|
||||
# limitations under the License
|
||||
#
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import login_required
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from api.db.db_models import APIToken
|
||||
from api.db.services.api_service import APITokenService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.utils.api_utils import get_json_result
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.settings import DATABASE_TYPE
|
||||
from api.utils import current_timestamp, datetime_format
|
||||
from api.utils.api_utils import get_json_result, get_data_error_result, server_error_response, \
|
||||
generate_confirmation_token, request, validate_request
|
||||
from api.versions import get_rag_version
|
||||
from rag.settings import SVR_QUEUE_NAME
|
||||
from rag.utils.es_conn import ELASTICSEARCH
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
from rag.utils.storage_factory import STORAGE_IMPL, STORAGE_IMPL_TYPE
|
||||
from timeit import default_timer as timer
|
||||
|
||||
from rag.utils.redis_conn import REDIS_CONN
|
||||
@ -48,16 +54,16 @@ def status():
|
||||
st = timer()
|
||||
try:
|
||||
STORAGE_IMPL.health()
|
||||
res["minio"] = {"status": "green", "elapsed": "{:.1f}".format((timer() - st)*1000.)}
|
||||
res["storage"] = {"storage": STORAGE_IMPL_TYPE.lower(), "status": "green", "elapsed": "{:.1f}".format((timer() - st)*1000.)}
|
||||
except Exception as e:
|
||||
res["minio"] = {"status": "red", "elapsed": "{:.1f}".format((timer() - st)*1000.), "error": str(e)}
|
||||
res["storage"] = {"storage": STORAGE_IMPL_TYPE.lower(), "status": "red", "elapsed": "{:.1f}".format((timer() - st)*1000.), "error": str(e)}
|
||||
|
||||
st = timer()
|
||||
try:
|
||||
KnowledgebaseService.get_by_id("x")
|
||||
res["mysql"] = {"status": "green", "elapsed": "{:.1f}".format((timer() - st)*1000.)}
|
||||
res["database"] = {"database": DATABASE_TYPE.lower(), "status": "green", "elapsed": "{:.1f}".format((timer() - st)*1000.)}
|
||||
except Exception as e:
|
||||
res["mysql"] = {"status": "red", "elapsed": "{:.1f}".format((timer() - st)*1000.), "error": str(e)}
|
||||
res["database"] = {"database": DATABASE_TYPE.lower(), "status": "red", "elapsed": "{:.1f}".format((timer() - st)*1000.), "error": str(e)}
|
||||
|
||||
st = timer()
|
||||
try:
|
||||
@ -87,3 +93,49 @@ def status():
|
||||
res["task_executor"] = {"status": "red", "error": str(e)}
|
||||
|
||||
return get_json_result(data=res)
|
||||
|
||||
|
||||
@manager.route('/new_token', methods=['POST'])
|
||||
@login_required
|
||||
def new_token():
|
||||
try:
|
||||
tenants = UserTenantService.query(user_id=current_user.id)
|
||||
if not tenants:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
tenant_id = tenants[0].tenant_id
|
||||
obj = {"tenant_id": tenant_id, "token": generate_confirmation_token(tenant_id),
|
||||
"create_time": current_timestamp(),
|
||||
"create_date": datetime_format(datetime.now()),
|
||||
"update_time": None,
|
||||
"update_date": None
|
||||
}
|
||||
|
||||
if not APITokenService.save(**obj):
|
||||
return get_data_error_result(retmsg="Fail to new a dialog!")
|
||||
|
||||
return get_json_result(data=obj)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/token_list', methods=['GET'])
|
||||
@login_required
|
||||
def token_list():
|
||||
try:
|
||||
tenants = UserTenantService.query(user_id=current_user.id)
|
||||
if not tenants:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
|
||||
objs = APITokenService.query(tenant_id=tenants[0].tenant_id)
|
||||
return get_json_result(data=[o.to_dict() for o in objs])
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/token/<token>', methods=['DELETE'])
|
||||
@login_required
|
||||
def rm(token):
|
||||
APITokenService.filter_delete(
|
||||
[APIToken.tenant_id == current_user.id, APIToken.token == token])
|
||||
return get_json_result(data=True)
|
||||
@ -15,25 +15,14 @@
|
||||
#
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user, login_required
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from api.db import UserTenantRole, StatusEnum
|
||||
from api.db.db_models import UserTenant
|
||||
from api.db.services.user_service import TenantService, UserTenantService
|
||||
from api.settings import RetCode
|
||||
from api.db.services.user_service import UserTenantService, UserService
|
||||
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_json_result, validate_request, server_error_response
|
||||
|
||||
|
||||
@manager.route("/list", methods=["GET"])
|
||||
@login_required
|
||||
def tenant_list():
|
||||
try:
|
||||
tenants = TenantService.get_by_user_id(current_user.id)
|
||||
return get_json_result(data=tenants)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
from api.utils import get_uuid, delta_seconds
|
||||
from api.utils.api_utils import get_json_result, validate_request, server_error_response, get_data_error_result
|
||||
|
||||
|
||||
@manager.route("/<tenant_id>/user/list", methods=["GET"])
|
||||
@ -41,6 +30,8 @@ def tenant_list():
|
||||
def user_list(tenant_id):
|
||||
try:
|
||||
users = UserTenantService.get_by_tenant_id(tenant_id)
|
||||
for u in users:
|
||||
u["delta_seconds"] = delta_seconds(str(u["update_date"]))
|
||||
return get_json_result(data=users)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
@ -48,30 +39,32 @@ def user_list(tenant_id):
|
||||
|
||||
@manager.route('/<tenant_id>/user', methods=['POST'])
|
||||
@login_required
|
||||
@validate_request("user_id")
|
||||
@validate_request("email")
|
||||
def create(tenant_id):
|
||||
user_id = request.json.get("user_id")
|
||||
if not user_id:
|
||||
return get_json_result(
|
||||
data=False, retmsg='Lack of "USER ID"', retcode=RetCode.ARGUMENT_ERROR)
|
||||
req = request.json
|
||||
usrs = UserService.query(email=req["email"])
|
||||
if not usrs:
|
||||
return get_data_error_result(retmsg="User not found.")
|
||||
|
||||
try:
|
||||
user_id = usrs[0].id
|
||||
user_tenants = UserTenantService.query(user_id=user_id, tenant_id=tenant_id)
|
||||
if user_tenants:
|
||||
uuid = user_tenants[0].id
|
||||
return get_json_result(data={"id": uuid})
|
||||
if user_tenants[0].status == UserTenantRole.NORMAL.value:
|
||||
return get_data_error_result(retmsg="This user is in the team already.")
|
||||
return get_data_error_result(retmsg="Invitation notification is sent.")
|
||||
|
||||
uuid = get_uuid()
|
||||
UserTenantService.save(
|
||||
id = uuid,
|
||||
id=get_uuid(),
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
role = UserTenantRole.NORMAL.value,
|
||||
invited_by=current_user.id,
|
||||
role=UserTenantRole.INVITE,
|
||||
status=StatusEnum.VALID.value)
|
||||
|
||||
return get_json_result(data={"id": uuid})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
usr = usrs[0].to_dict()
|
||||
usr = {k: v for k, v in usr.items() if k in ["id", "avatar", "email", "nickname"]}
|
||||
|
||||
return get_json_result(data=usr)
|
||||
|
||||
|
||||
@manager.route('/<tenant_id>/user/<user_id>', methods=['DELETE'])
|
||||
@ -83,3 +76,24 @@ def rm(tenant_id, user_id):
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/list", methods=["GET"])
|
||||
@login_required
|
||||
def tenant_list():
|
||||
try:
|
||||
users = UserTenantService.get_tenants_by_user_id(current_user.id)
|
||||
for u in users:
|
||||
u["delta_seconds"] = delta_seconds(str(u["update_date"]))
|
||||
return get_json_result(data=users)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/agree/<tenant_id>", methods=["PUT"])
|
||||
@login_required
|
||||
def agree(tenant_id):
|
||||
try:
|
||||
UserTenantService.filter_update([UserTenant.tenant_id == tenant_id, UserTenant.user_id == current_user.id], {"role": UserTenantRole.NORMAL})
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
@ -23,7 +23,7 @@ from flask_login import login_required, current_user, login_user, logout_user
|
||||
|
||||
from api.db.db_models import TenantLLM
|
||||
from api.db.services.llm_service import TenantLLMService, LLMService
|
||||
from api.utils.api_utils import server_error_response, validate_request
|
||||
from api.utils.api_utils import server_error_response, validate_request, get_data_error_result
|
||||
from api.utils import get_uuid, get_format_time, decrypt, download_img, current_timestamp, datetime_format
|
||||
from api.db import UserTenantRole, LLMType, FileType
|
||||
from api.settings import RetCode, GITHUB_OAUTH, FEISHU_OAUTH, CHAT_MDL, EMBEDDING_MDL, ASR_MDL, IMAGE2TEXT_MDL, PARSERS, \
|
||||
@ -260,7 +260,8 @@ def setting_user():
|
||||
update_dict["password"] = generate_password_hash(decrypt(new_password))
|
||||
|
||||
for k in request_data.keys():
|
||||
if k in ["password", "new_password"]:
|
||||
if k in ["password", "new_password", "email", "status", "is_superuser", "login_channel", "is_anonymous",
|
||||
"is_active", "is_authenticated", "last_login_time"]:
|
||||
continue
|
||||
update_dict[k] = request_data[k]
|
||||
|
||||
@ -354,7 +355,7 @@ def user_add():
|
||||
email_address = req["email"]
|
||||
|
||||
# Validate the email address
|
||||
if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,4}$", email_address):
|
||||
if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,5}$", email_address):
|
||||
return get_json_result(data=False,
|
||||
retmsg=f'Invalid email address: {email_address}!',
|
||||
retcode=RetCode.OPERATING_ERROR)
|
||||
@ -402,8 +403,10 @@ def user_add():
|
||||
@login_required
|
||||
def tenant_info():
|
||||
try:
|
||||
tenants = TenantService.get_by_user_id(current_user.id)[0]
|
||||
return get_json_result(data=tenants)
|
||||
tenants = TenantService.get_info_by(current_user.id)
|
||||
if not tenants:
|
||||
return get_data_error_result(retmsg="Tenant not found!")
|
||||
return get_json_result(data=tenants[0])
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@ -14,3 +14,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
NAME_LENGTH_LIMIT = 2 ** 10
|
||||
|
||||
IMG_BASE64_PREFIX = 'data:image/png;base64,'
|
||||
@ -27,6 +27,7 @@ class UserTenantRole(StrEnum):
|
||||
OWNER = 'owner'
|
||||
ADMIN = 'admin'
|
||||
NORMAL = 'normal'
|
||||
INVITE = 'invite'
|
||||
|
||||
|
||||
class TenantPermission(StrEnum):
|
||||
|
||||
@ -879,8 +879,8 @@ class Dialog(DataBaseModel):
|
||||
default="simple",
|
||||
help_text="simple|advanced",
|
||||
index=True)
|
||||
prompt_config = JSONField(null=False, default={"system": "", "prologue": "您好,我是您的助手小樱,长得可爱又善良,can I help you?",
|
||||
"parameters": [], "empty_response": "Sorry! 知识库中未找到相关内容!"})
|
||||
prompt_config = JSONField(null=False, default={"system": "", "prologue": "Hi! I'm your assistant, what can I do for you?",
|
||||
"parameters": [], "empty_response": "Sorry! No relevant content was found in the knowledge base!"})
|
||||
|
||||
similarity_threshold = FloatField(default=0.2)
|
||||
vector_similarity_weight = FloatField(default=0.3)
|
||||
@ -1052,4 +1052,11 @@ def migrate_db():
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
migrate(
|
||||
migrator.alter_column_type('api_token', 'dialog_id',
|
||||
CharField(max_length=32, null=True, index=True))
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
@ -31,10 +32,15 @@ from api.settings import CHAT_MDL, EMBEDDING_MDL, ASR_MDL, IMAGE2TEXT_MDL, PARSE
|
||||
from api.utils.file_utils import get_project_base_directory
|
||||
|
||||
|
||||
def encode_to_base64(input_string):
|
||||
base64_encoded = base64.b64encode(input_string.encode('utf-8'))
|
||||
return base64_encoded.decode('utf-8')
|
||||
|
||||
|
||||
def init_superuser():
|
||||
user_info = {
|
||||
"id": uuid.uuid1().hex,
|
||||
"password": "admin",
|
||||
"password": encode_to_base64("admin"),
|
||||
"nickname": "admin",
|
||||
"is_superuser": True,
|
||||
"email": "admin@ragflow.io",
|
||||
@ -126,7 +132,7 @@ def init_llm_factory():
|
||||
TenantService.filter_update([1 == 1], {
|
||||
"parser_ids": "naive:General,qa:Q&A,resume:Resume,manual:Manual,table:Table,paper:Paper,book:Book,laws:Laws,presentation:Presentation,picture:Picture,one:One,audio:Audio,knowledge_graph:Knowledge Graph,email:Email"})
|
||||
## insert openai two embedding models to the current openai user.
|
||||
print("Start to insert 2 OpenAI embedding models...")
|
||||
# print("Start to insert 2 OpenAI embedding models...")
|
||||
tenant_ids = set([row["tenant_id"] for row in TenantLLMService.get_openai_models()])
|
||||
for tid in tenant_ids:
|
||||
for row in TenantLLMService.query(llm_factory="OpenAI", tenant_id=tid):
|
||||
@ -172,8 +178,8 @@ def init_web_data():
|
||||
start_time = time.time()
|
||||
|
||||
init_llm_factory()
|
||||
if not UserService.get_all().count():
|
||||
init_superuser()
|
||||
#if not UserService.get_all().count():
|
||||
# init_superuser()
|
||||
|
||||
add_graph_templates()
|
||||
print("init web data success:{}".format(time.time() - start_time))
|
||||
|
||||
@ -19,14 +19,15 @@ import json
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from timeit import default_timer as timer
|
||||
from api.db import LLMType, ParserType
|
||||
from api.db.db_models import Dialog, Conversation
|
||||
|
||||
|
||||
from api.db import LLMType, ParserType,StatusEnum
|
||||
from api.db.db_models import Dialog, Conversation,DB
|
||||
from api.db.services.common_service import CommonService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMService, TenantLLMService, LLMBundle
|
||||
from api.settings import chat_logger, retrievaler, kg_retrievaler
|
||||
from rag.app.resume import forbidden_select_fields4resume
|
||||
from rag.nlp import keyword_extraction
|
||||
from rag.nlp.search import index_name
|
||||
from rag.utils import rmSpace, num_tokens_from_string, encoder
|
||||
from api.utils.file_utils import get_project_base_directory
|
||||
@ -35,10 +36,49 @@ from api.utils.file_utils import get_project_base_directory
|
||||
class DialogService(CommonService):
|
||||
model = Dialog
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_list(cls, tenant_id,
|
||||
page_number, items_per_page, orderby, desc, id , name):
|
||||
chats = cls.model.select()
|
||||
if id:
|
||||
chats = chats.where(cls.model.id == id)
|
||||
if name:
|
||||
chats = chats.where(cls.model.name == name)
|
||||
chats = chats.where(
|
||||
(cls.model.tenant_id == tenant_id)
|
||||
& (cls.model.status == StatusEnum.VALID.value)
|
||||
)
|
||||
if desc:
|
||||
chats = chats.order_by(cls.model.getter_by(orderby).desc())
|
||||
else:
|
||||
chats = chats.order_by(cls.model.getter_by(orderby).asc())
|
||||
|
||||
chats = chats.paginate(page_number, items_per_page)
|
||||
|
||||
return list(chats.dicts())
|
||||
|
||||
|
||||
class ConversationService(CommonService):
|
||||
model = Conversation
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_list(cls,dialog_id,page_number, items_per_page, orderby, desc, id , name):
|
||||
sessions = cls.model.select().where(cls.model.dialog_id ==dialog_id)
|
||||
if id:
|
||||
sessions = sessions.where(cls.model.id == id)
|
||||
if name:
|
||||
sessions = sessions.where(cls.model.name == name)
|
||||
if desc:
|
||||
sessions = sessions.order_by(cls.model.getter_by(orderby).desc())
|
||||
else:
|
||||
sessions = sessions.order_by(cls.model.getter_by(orderby).asc())
|
||||
|
||||
sessions = sessions.paginate(page_number, items_per_page)
|
||||
|
||||
return list(sessions.dicts())
|
||||
|
||||
|
||||
def message_fit_in(msg, max_length=4000):
|
||||
def count():
|
||||
@ -78,6 +118,7 @@ def message_fit_in(msg, max_length=4000):
|
||||
|
||||
|
||||
def llm_id2llm_type(llm_id):
|
||||
llm_id = llm_id.split("@")[0]
|
||||
fnm = os.path.join(get_project_base_directory(), "conf")
|
||||
llm_factories = json.load(open(os.path.join(fnm, "llm_factories.json"), "r"))
|
||||
for llm_factory in llm_factories["factory_llm_infos"]:
|
||||
@ -89,9 +130,15 @@ def llm_id2llm_type(llm_id):
|
||||
def chat(dialog, messages, stream=True, **kwargs):
|
||||
assert messages[-1]["role"] == "user", "The last content of this conversation is not from user."
|
||||
st = timer()
|
||||
llm = LLMService.query(llm_name=dialog.llm_id)
|
||||
tmp = dialog.llm_id.split("@")
|
||||
fid = None
|
||||
llm_id = tmp[0]
|
||||
if len(tmp)>1: fid = tmp[1]
|
||||
|
||||
llm = LLMService.query(llm_name=llm_id) if not fid else LLMService.query(llm_name=llm_id, fid=fid)
|
||||
if not llm:
|
||||
llm = TenantLLMService.query(tenant_id=dialog.tenant_id, llm_name=dialog.llm_id)
|
||||
llm = TenantLLMService.query(tenant_id=dialog.tenant_id, llm_name=llm_id) if not fid else \
|
||||
TenantLLMService.query(tenant_id=dialog.tenant_id, llm_name=llm_id, llm_factory=fid)
|
||||
if not llm:
|
||||
raise LookupError("LLM(%s) not found" % dialog.llm_id)
|
||||
max_tokens = 8192
|
||||
@ -142,6 +189,11 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
prompt_config["system"] = prompt_config["system"].replace(
|
||||
"{%s}" % p["key"], " ")
|
||||
|
||||
if len(questions) > 1 and prompt_config.get("refine_multiturn"):
|
||||
questions = [full_question(dialog.tenant_id, dialog.llm_id, messages)]
|
||||
else:
|
||||
questions = questions[-1:]
|
||||
|
||||
rerank_mdl = None
|
||||
if dialog.rerank_id:
|
||||
rerank_mdl = LLMBundle(dialog.tenant_id, LLMType.RERANK, dialog.rerank_id)
|
||||
@ -153,7 +205,9 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
else:
|
||||
if prompt_config.get("keyword", False):
|
||||
questions[-1] += keyword_extraction(chat_mdl, questions[-1])
|
||||
kbinfos = retr.retrieval(" ".join(questions), embd_mdl, dialog.tenant_id, dialog.kb_ids, 1, dialog.top_n,
|
||||
|
||||
tenant_ids = list(set([kb.tenant_id for kb in kbs]))
|
||||
kbinfos = retr.retrieval(" ".join(questions), embd_mdl, tenant_ids, dialog.kb_ids, 1, dialog.top_n,
|
||||
dialog.similarity_threshold,
|
||||
dialog.vector_similarity_weight,
|
||||
doc_ids=attachments,
|
||||
@ -168,7 +222,7 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
yield {"answer": empty_res, "reference": kbinfos, "audio_binary": tts(tts_mdl, empty_res)}
|
||||
return {"answer": prompt_config["empty_response"], "reference": kbinfos}
|
||||
|
||||
kwargs["knowledge"] = "\n------\n".join(knowledges)
|
||||
kwargs["knowledge"] = "\n\n------\n\n".join(knowledges)
|
||||
gen_conf = dialog.llm_setting
|
||||
|
||||
msg = [{"role": "system", "content": prompt_config["system"].format(**kwargs)}]
|
||||
@ -177,6 +231,7 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
used_token_count, msg = message_fit_in(msg, int(max_tokens * 0.97))
|
||||
assert len(msg) >= 2, f"message_fit_in has bug: {msg}"
|
||||
prompt = msg[0]["content"]
|
||||
prompt += "\n\n### Query:\n%s" % " ".join(questions)
|
||||
|
||||
if "max_tokens" in gen_conf:
|
||||
gen_conf["max_tokens"] = min(
|
||||
@ -209,7 +264,7 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
if answer.lower().find("invalid key") >= 0 or answer.lower().find("invalid api") >= 0:
|
||||
answer += " Please set LLM API-Key in 'User Setting -> Model Providers -> API-Key'"
|
||||
done_tm = timer()
|
||||
prompt += "\n### Elapsed\n - Retrieval: %.1f ms\n - LLM: %.1f ms"%((retrieval_tm-st)*1000, (done_tm-st)*1000)
|
||||
prompt += "\n\n### Elapsed\n - Retrieval: %.1f ms\n - LLM: %.1f ms"%((retrieval_tm-st)*1000, (done_tm-st)*1000)
|
||||
return {"answer": answer, "reference": refs, "prompt": prompt}
|
||||
|
||||
if stream:
|
||||
@ -403,6 +458,110 @@ def rewrite(tenant_id, llm_id, question):
|
||||
return ans
|
||||
|
||||
|
||||
def keyword_extraction(chat_mdl, content, topn=3):
|
||||
prompt = f"""
|
||||
Role: You're a text analyzer.
|
||||
Task: extract the most important keywords/phrases of a given piece of text content.
|
||||
Requirements:
|
||||
- Summarize the text content, and give top {topn} important keywords/phrases.
|
||||
- The keywords MUST be in language of the given piece of text content.
|
||||
- The keywords are delimited by ENGLISH COMMA.
|
||||
- Keywords ONLY in output.
|
||||
|
||||
### Text Content
|
||||
{content}
|
||||
|
||||
"""
|
||||
msg = [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": "Output: "}
|
||||
]
|
||||
_, msg = message_fit_in(msg, chat_mdl.max_length)
|
||||
kwd = chat_mdl.chat(prompt, msg[1:], {"temperature": 0.2})
|
||||
if isinstance(kwd, tuple): kwd = kwd[0]
|
||||
if kwd.find("**ERROR**") >=0: return ""
|
||||
return kwd
|
||||
|
||||
|
||||
def question_proposal(chat_mdl, content, topn=3):
|
||||
prompt = f"""
|
||||
Role: You're a text analyzer.
|
||||
Task: propose {topn} questions about a given piece of text content.
|
||||
Requirements:
|
||||
- Understand and summarize the text content, and propose top {topn} important questions.
|
||||
- The questions SHOULD NOT have overlapping meanings.
|
||||
- The questions SHOULD cover the main content of the text as much as possible.
|
||||
- The questions MUST be in language of the given piece of text content.
|
||||
- One question per line.
|
||||
- Question ONLY in output.
|
||||
|
||||
### Text Content
|
||||
{content}
|
||||
|
||||
"""
|
||||
msg = [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": "Output: "}
|
||||
]
|
||||
_, msg = message_fit_in(msg, chat_mdl.max_length)
|
||||
kwd = chat_mdl.chat(prompt, msg[1:], {"temperature": 0.2})
|
||||
if isinstance(kwd, tuple): kwd = kwd[0]
|
||||
if kwd.find("**ERROR**") >= 0: return ""
|
||||
return kwd
|
||||
|
||||
|
||||
def full_question(tenant_id, llm_id, messages):
|
||||
if llm_id2llm_type(llm_id) == "image2text":
|
||||
chat_mdl = LLMBundle(tenant_id, LLMType.IMAGE2TEXT, llm_id)
|
||||
else:
|
||||
chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, llm_id)
|
||||
conv = []
|
||||
for m in messages:
|
||||
if m["role"] not in ["user", "assistant"]: continue
|
||||
conv.append("{}: {}".format(m["role"].upper(), m["content"]))
|
||||
conv = "\n".join(conv)
|
||||
prompt = f"""
|
||||
Role: A helpful assistant
|
||||
Task: Generate a full user question that would follow the conversation.
|
||||
Requirements & Restrictions:
|
||||
- Text generated MUST be in the same language of the original user's question.
|
||||
- If the user's latest question is completely, don't do anything, just return the original question.
|
||||
- DON'T generate anything except a refined question.
|
||||
|
||||
######################
|
||||
-Examples-
|
||||
######################
|
||||
|
||||
# Example 1
|
||||
## Conversation
|
||||
USER: What is the name of Donald Trump's father?
|
||||
ASSISTANT: Fred Trump.
|
||||
USER: And his mother?
|
||||
###############
|
||||
Output: What's the name of Donald Trump's mother?
|
||||
|
||||
------------
|
||||
# Example 2
|
||||
## Conversation
|
||||
USER: What is the name of Donald Trump's father?
|
||||
ASSISTANT: Fred Trump.
|
||||
USER: And his mother?
|
||||
ASSISTANT: Mary Trump.
|
||||
User: What's her full name?
|
||||
###############
|
||||
Output: What's the full name of Donald Trump's mother Mary Trump?
|
||||
|
||||
######################
|
||||
|
||||
# Real Data
|
||||
## Conversation
|
||||
{conv}
|
||||
###############
|
||||
"""
|
||||
ans = chat_mdl.chat(prompt, [{"role": "user", "content": "Output: "}], {"temperature": 0.2})
|
||||
return ans if ans.find("**ERROR**") < 0 else messages[-1]["content"]
|
||||
|
||||
|
||||
def tts(tts_mdl, text):
|
||||
if not tts_mdl or not text: return
|
||||
bin = b""
|
||||
|
||||
@ -38,7 +38,7 @@ from rag.utils.storage_factory import STORAGE_IMPL
|
||||
from rag.nlp import search, rag_tokenizer
|
||||
|
||||
from api.db import FileType, TaskStatus, ParserType, LLMType
|
||||
from api.db.db_models import DB, Knowledgebase, Tenant, Task
|
||||
from api.db.db_models import DB, Knowledgebase, Tenant, Task, UserTenant
|
||||
from api.db.db_models import Document
|
||||
from api.db.services.common_service import CommonService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
@ -49,6 +49,28 @@ from rag.utils.redis_conn import REDIS_CONN
|
||||
class DocumentService(CommonService):
|
||||
model = Document
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_list(cls, kb_id, page_number, items_per_page,
|
||||
orderby, desc, keywords, id):
|
||||
docs =cls.model.select().where(cls.model.kb_id==kb_id)
|
||||
if id:
|
||||
docs = docs.where(
|
||||
cls.model.id== id )
|
||||
if keywords:
|
||||
docs = docs.where(
|
||||
fn.LOWER(cls.model.name).contains(keywords.lower())
|
||||
)
|
||||
if desc:
|
||||
docs = docs.order_by(cls.model.getter_by(orderby).desc())
|
||||
else:
|
||||
docs = docs.order_by(cls.model.getter_by(orderby).asc())
|
||||
|
||||
docs = docs.paginate(page_number, items_per_page)
|
||||
count = docs.count()
|
||||
return list(docs.dicts()), count
|
||||
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_by_kb_id(cls, kb_id, page_number, items_per_page,
|
||||
@ -241,6 +263,33 @@ class DocumentService(CommonService):
|
||||
return
|
||||
return docs[0]["tenant_id"]
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible(cls, doc_id, user_id):
|
||||
docs = cls.model.select(
|
||||
cls.model.id).join(
|
||||
Knowledgebase, on=(
|
||||
Knowledgebase.id == cls.model.kb_id)
|
||||
).join(UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
|
||||
).where(cls.model.id == doc_id, UserTenant.user_id == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible4deletion(cls, doc_id, user_id):
|
||||
docs = cls.model.select(
|
||||
cls.model.id).join(
|
||||
Knowledgebase, on=(
|
||||
Knowledgebase.id == cls.model.kb_id)
|
||||
).where(cls.model.id == doc_id, Knowledgebase.created_by == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_embd_id(cls, doc_id):
|
||||
@ -268,7 +317,7 @@ class DocumentService(CommonService):
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_thumbnails(cls, docids):
|
||||
fields = [cls.model.id, cls.model.thumbnail]
|
||||
fields = [cls.model.id, cls.model.kb_id, cls.model.thumbnail]
|
||||
return list(cls.model.select(
|
||||
*fields).where(cls.model.id.in_(docids)).dicts())
|
||||
|
||||
@ -339,7 +388,7 @@ class DocumentService(CommonService):
|
||||
elif finished:
|
||||
if d["parser_config"].get("raptor", {}).get("use_raptor") and d["progress_msg"].lower().find(" raptor")<0:
|
||||
queue_raptor_tasks(d)
|
||||
prg *= 0.98
|
||||
prg = 0.98 * len(tsks)/(len(tsks)+1)
|
||||
msg.append("------ RAPTOR -------")
|
||||
else:
|
||||
status = TaskStatus.DONE.value
|
||||
@ -356,6 +405,7 @@ class DocumentService(CommonService):
|
||||
info["progress_msg"] = msg
|
||||
cls.update_by_id(d["id"], info)
|
||||
except Exception as e:
|
||||
if str(e).find("'0'") < 0:
|
||||
stat_logger.error("fetch task exception:" + str(e))
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -69,7 +69,7 @@ class File2DocumentService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_minio_address(cls, doc_id=None, file_id=None):
|
||||
def get_storage_address(cls, doc_id=None, file_id=None):
|
||||
if doc_id:
|
||||
f2d = cls.get_by_document_id(doc_id)
|
||||
else:
|
||||
|
||||
@ -26,7 +26,7 @@ from api.db.services.common_service import CommonService
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.utils import get_uuid
|
||||
from api.utils.file_utils import filename_type, thumbnail
|
||||
from api.utils.file_utils import filename_type, thumbnail_img
|
||||
from rag.utils.storage_factory import STORAGE_IMPL
|
||||
|
||||
|
||||
@ -354,26 +354,27 @@ class FileService(CommonService):
|
||||
location += "_"
|
||||
blob = file.read()
|
||||
STORAGE_IMPL.put(kb.id, location, blob)
|
||||
|
||||
doc_id = get_uuid()
|
||||
|
||||
img = thumbnail_img(filename, blob)
|
||||
thumbnail_location = ''
|
||||
if img is not None:
|
||||
thumbnail_location = f'thumbnail_{doc_id}.png'
|
||||
STORAGE_IMPL.put(kb.id, thumbnail_location, img)
|
||||
|
||||
doc = {
|
||||
"id": get_uuid(),
|
||||
"id": doc_id,
|
||||
"kb_id": kb.id,
|
||||
"parser_id": kb.parser_id,
|
||||
"parser_id": self.get_parser(filetype, filename, kb.parser_id),
|
||||
"parser_config": kb.parser_config,
|
||||
"created_by": user_id,
|
||||
"type": filetype,
|
||||
"name": filename,
|
||||
"location": location,
|
||||
"size": len(blob),
|
||||
"thumbnail": thumbnail(filename, blob)
|
||||
"thumbnail": thumbnail_location
|
||||
}
|
||||
if doc["type"] == FileType.VISUAL:
|
||||
doc["parser_id"] = ParserType.PICTURE.value
|
||||
if doc["type"] == FileType.AURAL:
|
||||
doc["parser_id"] = ParserType.AUDIO.value
|
||||
if re.search(r"\.(ppt|pptx|pages)$", filename):
|
||||
doc["parser_id"] = ParserType.PRESENTATION.value
|
||||
if re.search(r"\.(eml)$", filename):
|
||||
doc["parser_id"] = ParserType.EMAIL.value
|
||||
DocumentService.insert(doc)
|
||||
|
||||
FileService.add_file_from_kb(doc, kb_folder["id"], kb.tenant_id)
|
||||
@ -382,3 +383,15 @@ class FileService(CommonService):
|
||||
err.append(file.filename + ": " + str(e))
|
||||
|
||||
return err, files
|
||||
|
||||
@staticmethod
|
||||
def get_parser(doc_type, filename, default):
|
||||
if doc_type == FileType.VISUAL:
|
||||
return ParserType.PICTURE.value
|
||||
if doc_type == FileType.AURAL:
|
||||
return ParserType.AUDIO.value
|
||||
if re.search(r"\.(ppt|pptx|pages)$", filename):
|
||||
return ParserType.PRESENTATION.value
|
||||
if re.search(r"\.(eml)$", filename):
|
||||
return ParserType.EMAIL.value
|
||||
return default
|
||||
@ -14,18 +14,44 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
from api.db import StatusEnum, TenantPermission
|
||||
from api.db.db_models import Knowledgebase, DB, Tenant
|
||||
from api.db.db_models import Knowledgebase, DB, Tenant, User, UserTenant,Document
|
||||
from api.db.services.common_service import CommonService
|
||||
|
||||
|
||||
class KnowledgebaseService(CommonService):
|
||||
model = Knowledgebase
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def list_documents_by_ids(cls,kb_ids):
|
||||
doc_ids=cls.model.select(Document.id.alias("document_id")).join(Document,on=(cls.model.id == Document.kb_id)).where(
|
||||
cls.model.id.in_(kb_ids)
|
||||
)
|
||||
doc_ids =list(doc_ids.dicts())
|
||||
doc_ids = [doc["document_id"] for doc in doc_ids]
|
||||
return doc_ids
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
|
||||
page_number, items_per_page, orderby, desc):
|
||||
kbs = cls.model.select().where(
|
||||
fields = [
|
||||
cls.model.id,
|
||||
cls.model.avatar,
|
||||
cls.model.name,
|
||||
cls.model.language,
|
||||
cls.model.description,
|
||||
cls.model.permission,
|
||||
cls.model.doc_num,
|
||||
cls.model.token_num,
|
||||
cls.model.chunk_num,
|
||||
cls.model.parser_id,
|
||||
cls.model.embd_id,
|
||||
User.nickname,
|
||||
User.avatar.alias('tenant_avatar'),
|
||||
cls.model.update_time
|
||||
]
|
||||
kbs = cls.model.select(*fields).join(User, on=(cls.model.tenant_id == User.id)).where(
|
||||
((cls.model.tenant_id.in_(joined_tenant_ids) & (cls.model.permission ==
|
||||
TenantPermission.TEAM.value)) | (
|
||||
cls.model.tenant_id == user_id))
|
||||
@ -142,3 +168,49 @@ class KnowledgebaseService(CommonService):
|
||||
@DB.connection_context()
|
||||
def get_all_ids(cls):
|
||||
return [m["id"] for m in cls.model.select(cls.model.id).dicts()]
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_list(cls, joined_tenant_ids, user_id,
|
||||
page_number, items_per_page, orderby, desc, id, name):
|
||||
kbs = cls.model.select()
|
||||
if id:
|
||||
kbs = kbs.where(cls.model.id == id)
|
||||
if name:
|
||||
kbs = kbs.where(cls.model.name == name)
|
||||
kbs = kbs.where(
|
||||
((cls.model.tenant_id.in_(joined_tenant_ids) & (cls.model.permission ==
|
||||
TenantPermission.TEAM.value)) | (
|
||||
cls.model.tenant_id == user_id))
|
||||
& (cls.model.status == StatusEnum.VALID.value)
|
||||
)
|
||||
if desc:
|
||||
kbs = kbs.order_by(cls.model.getter_by(orderby).desc())
|
||||
else:
|
||||
kbs = kbs.order_by(cls.model.getter_by(orderby).asc())
|
||||
|
||||
kbs = kbs.paginate(page_number, items_per_page)
|
||||
|
||||
return list(kbs.dicts())
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible(cls, kb_id, user_id):
|
||||
docs = cls.model.select(
|
||||
cls.model.id).join(UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
|
||||
).where(cls.model.id == kb_id, UserTenant.user_id == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible4deletion(cls, kb_id, user_id):
|
||||
docs = cls.model.select(
|
||||
cls.model.id).where(cls.model.id == kb_id, cls.model.created_by == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ from api.db.services.user_service import TenantService
|
||||
from api.settings import database_logger
|
||||
from rag.llm import EmbeddingModel, CvModel, ChatModel, RerankModel, Seq2txtModel, TTSModel
|
||||
from api.db import LLMType
|
||||
from api.db.db_models import DB, UserTenant
|
||||
from api.db.db_models import DB
|
||||
from api.db.db_models import LLMFactories, LLM, TenantLLM
|
||||
from api.db.services.common_service import CommonService
|
||||
|
||||
@ -36,7 +36,11 @@ class TenantLLMService(CommonService):
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_api_key(cls, tenant_id, model_name):
|
||||
arr = model_name.split("@")
|
||||
if len(arr) < 2:
|
||||
objs = cls.query(tenant_id=tenant_id, llm_name=model_name)
|
||||
else:
|
||||
objs = cls.query(tenant_id=tenant_id, llm_name=arr[0], llm_factory=arr[1])
|
||||
if not objs:
|
||||
return
|
||||
return objs[0]
|
||||
@ -81,14 +85,17 @@ class TenantLLMService(CommonService):
|
||||
assert False, "LLM type error"
|
||||
|
||||
model_config = cls.get_api_key(tenant_id, mdlnm)
|
||||
tmp = mdlnm.split("@")
|
||||
fid = None if len(tmp) < 2 else tmp[1]
|
||||
mdlnm = tmp[0]
|
||||
if model_config: model_config = model_config.to_dict()
|
||||
if not model_config:
|
||||
if llm_type in [LLMType.EMBEDDING, LLMType.RERANK]:
|
||||
llm = LLMService.query(llm_name=llm_name if llm_name else mdlnm)
|
||||
llm = LLMService.query(llm_name=mdlnm) if not fid else LLMService.query(llm_name=mdlnm, fid=fid)
|
||||
if llm and llm[0].fid in ["Youdao", "FastEmbed", "BAAI"]:
|
||||
model_config = {"llm_factory": llm[0].fid, "api_key":"", "llm_name": llm_name if llm_name else mdlnm, "api_base": ""}
|
||||
model_config = {"llm_factory": llm[0].fid, "api_key":"", "llm_name": mdlnm, "api_base": ""}
|
||||
if not model_config:
|
||||
if llm_name == "flag-embedding":
|
||||
if mdlnm == "flag-embedding":
|
||||
model_config = {"llm_factory": "Tongyi-Qianwen", "api_key": "",
|
||||
"llm_name": llm_name, "api_base": ""}
|
||||
else:
|
||||
@ -126,7 +133,8 @@ class TenantLLMService(CommonService):
|
||||
if model_config["llm_factory"] not in Seq2txtModel:
|
||||
return
|
||||
return Seq2txtModel[model_config["llm_factory"]](
|
||||
model_config["api_key"], model_config["llm_name"], lang,
|
||||
key=model_config["api_key"], model_name=model_config["llm_name"],
|
||||
lang=lang,
|
||||
base_url=model_config["api_base"]
|
||||
)
|
||||
if llm_type == LLMType.TTS:
|
||||
@ -160,11 +168,13 @@ class TenantLLMService(CommonService):
|
||||
else:
|
||||
assert False, "LLM type error"
|
||||
|
||||
llm_name = mdlnm.split("@")[0] if "@" in mdlnm else mdlnm
|
||||
|
||||
num = 0
|
||||
try:
|
||||
for u in cls.query(tenant_id = tenant_id, llm_name=mdlnm):
|
||||
for u in cls.query(tenant_id=tenant_id, llm_name=llm_name):
|
||||
num += cls.model.update(used_tokens=u.used_tokens + used_tokens)\
|
||||
.where(cls.model.tenant_id == tenant_id, cls.model.llm_name == mdlnm)\
|
||||
.where(cls.model.tenant_id == tenant_id, cls.model.llm_name == llm_name)\
|
||||
.execute()
|
||||
except Exception as e:
|
||||
pass
|
||||
@ -188,7 +198,7 @@ class LLMBundle(object):
|
||||
self.llm_name = llm_name
|
||||
self.mdl = TenantLLMService.model_instance(
|
||||
tenant_id, llm_type, llm_name, lang=lang)
|
||||
assert self.mdl, "Can't find mole for {}/{}/{}".format(
|
||||
assert self.mdl, "Can't find model for {}/{}/{}".format(
|
||||
tenant_id, llm_type, llm_name)
|
||||
self.max_length = 8192
|
||||
for lm in LLMService.query(llm_name=llm_name):
|
||||
@ -200,7 +210,7 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/EMBEDDING".format(self.tenant_id))
|
||||
"Can't update token usage for {}/EMBEDDING used_tokens: {}".format(self.tenant_id, used_tokens))
|
||||
return emd, used_tokens
|
||||
|
||||
def encode_queries(self, query: str):
|
||||
@ -208,7 +218,7 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/EMBEDDING".format(self.tenant_id))
|
||||
"Can't update token usage for {}/EMBEDDING used_tokens: {}".format(self.tenant_id, used_tokens))
|
||||
return emd, used_tokens
|
||||
|
||||
def similarity(self, query: str, texts: list):
|
||||
@ -216,7 +226,7 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/RERANK".format(self.tenant_id))
|
||||
"Can't update token usage for {}/RERANK used_tokens: {}".format(self.tenant_id, used_tokens))
|
||||
return sim, used_tokens
|
||||
|
||||
def describe(self, image, max_tokens=300):
|
||||
@ -224,7 +234,7 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/IMAGE2TEXT".format(self.tenant_id))
|
||||
"Can't update token usage for {}/IMAGE2TEXT used_tokens: {}".format(self.tenant_id, used_tokens))
|
||||
return txt
|
||||
|
||||
def transcription(self, audio):
|
||||
@ -232,7 +242,7 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/SEQUENCE2TXT".format(self.tenant_id))
|
||||
"Can't update token usage for {}/SEQUENCE2TXT used_tokens: {}".format(self.tenant_id, used_tokens))
|
||||
return txt
|
||||
|
||||
def tts(self, text):
|
||||
@ -245,13 +255,12 @@ class LLMBundle(object):
|
||||
return
|
||||
yield chunk
|
||||
|
||||
|
||||
def chat(self, system, history, gen_conf):
|
||||
txt, used_tokens = self.mdl.chat(system, history, gen_conf)
|
||||
if not TenantLLMService.increase_usage(
|
||||
if isinstance(txt, int) and not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens, self.llm_name):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/CHAT".format(self.tenant_id))
|
||||
"Can't update token usage for {}/CHAT llm_name: {}, used_tokens: {}".format(self.tenant_id, self.llm_name, used_tokens))
|
||||
return txt
|
||||
|
||||
def chat_streamly(self, system, history, gen_conf):
|
||||
@ -260,6 +269,6 @@ class LLMBundle(object):
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, txt, self.llm_name):
|
||||
database_logger.error(
|
||||
"Can't update token usage for {}/CHAT".format(self.tenant_id))
|
||||
"Can't update token usage for {}/CHAT llm_name: {}, content: {}".format(self.tenant_id, self.llm_name, txt))
|
||||
return
|
||||
yield txt
|
||||
|
||||
@ -133,9 +133,8 @@ class TaskService(CommonService):
|
||||
cls.model.id == id).execute()
|
||||
|
||||
|
||||
def queue_tasks(doc, bucket, name):
|
||||
def queue_tasks(doc: dict, bucket: str, name: str):
|
||||
def new_task():
|
||||
nonlocal doc
|
||||
return {
|
||||
"id": get_uuid(),
|
||||
"doc_id": doc["id"]
|
||||
@ -149,15 +148,9 @@ def queue_tasks(doc, bucket, name):
|
||||
page_size = doc["parser_config"].get("task_page_size", 12)
|
||||
if doc["parser_id"] == "paper":
|
||||
page_size = doc["parser_config"].get("task_page_size", 22)
|
||||
if doc["parser_id"] == "one":
|
||||
page_size = 1000000000
|
||||
if doc["parser_id"] == "knowledge_graph":
|
||||
page_size = 1000000000
|
||||
if not do_layout:
|
||||
page_size = 1000000000
|
||||
page_ranges = doc["parser_config"].get("pages")
|
||||
if not page_ranges:
|
||||
page_ranges = [(1, 100000)]
|
||||
if doc["parser_id"] in ["one", "knowledge_graph"] or not do_layout:
|
||||
page_size = 10 ** 9
|
||||
page_ranges = doc["parser_config"].get("pages") or [(1, 10 ** 5)]
|
||||
for s, e in page_ranges:
|
||||
s -= 1
|
||||
s = max(0, s)
|
||||
@ -170,8 +163,7 @@ def queue_tasks(doc, bucket, name):
|
||||
|
||||
elif doc["parser_id"] == "table":
|
||||
file_bin = STORAGE_IMPL.get(bucket, name)
|
||||
rn = RAGFlowExcelParser.row_number(
|
||||
doc["name"], file_bin)
|
||||
rn = RAGFlowExcelParser.row_number(doc["name"], file_bin)
|
||||
for i in range(0, rn, 3000):
|
||||
task = new_task()
|
||||
task["from_page"] = i
|
||||
|
||||
@ -87,7 +87,7 @@ class TenantService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_by_user_id(cls, user_id):
|
||||
def get_info_by(cls, user_id):
|
||||
fields = [
|
||||
cls.model.id.alias("tenant_id"),
|
||||
cls.model.name,
|
||||
@ -100,7 +100,7 @@ class TenantService(CommonService):
|
||||
cls.model.parser_ids,
|
||||
UserTenant.role]
|
||||
return list(cls.model.select(*fields)
|
||||
.join(UserTenant, on=((cls.model.id == UserTenant.tenant_id) & (UserTenant.user_id == user_id) & (UserTenant.status == StatusEnum.VALID.value)))
|
||||
.join(UserTenant, on=((cls.model.id == UserTenant.tenant_id) & (UserTenant.user_id == user_id) & (UserTenant.status == StatusEnum.VALID.value) & (UserTenant.role == UserTenantRole.OWNER)))
|
||||
.where(cls.model.status == StatusEnum.VALID.value).dicts())
|
||||
|
||||
@classmethod
|
||||
@ -115,7 +115,7 @@ class TenantService(CommonService):
|
||||
cls.model.img2txt_id,
|
||||
UserTenant.role]
|
||||
return list(cls.model.select(*fields)
|
||||
.join(UserTenant, on=((cls.model.id == UserTenant.tenant_id) & (UserTenant.user_id == user_id) & (UserTenant.status == StatusEnum.VALID.value) & (UserTenant.role == UserTenantRole.NORMAL.value)))
|
||||
.join(UserTenant, on=((cls.model.id == UserTenant.tenant_id) & (UserTenant.user_id == user_id) & (UserTenant.status == StatusEnum.VALID.value) & (UserTenant.role == UserTenantRole.NORMAL)))
|
||||
.where(cls.model.status == StatusEnum.VALID.value).dicts())
|
||||
|
||||
@classmethod
|
||||
@ -143,9 +143,8 @@ class UserTenantService(CommonService):
|
||||
def get_by_tenant_id(cls, tenant_id):
|
||||
fields = [
|
||||
cls.model.user_id,
|
||||
cls.model.tenant_id,
|
||||
cls.model.role,
|
||||
cls.model.status,
|
||||
cls.model.role,
|
||||
User.nickname,
|
||||
User.email,
|
||||
User.avatar,
|
||||
@ -153,8 +152,24 @@ class UserTenantService(CommonService):
|
||||
User.is_active,
|
||||
User.is_anonymous,
|
||||
User.status,
|
||||
User.update_date,
|
||||
User.is_superuser]
|
||||
return list(cls.model.select(*fields)
|
||||
.join(User, on=((cls.model.user_id == User.id) & (cls.model.status == StatusEnum.VALID.value)))
|
||||
.join(User, on=((cls.model.user_id == User.id) & (cls.model.status == StatusEnum.VALID.value) & (cls.model.role != UserTenantRole.OWNER)))
|
||||
.where(cls.model.tenant_id == tenant_id)
|
||||
.dicts())
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_tenants_by_user_id(cls, user_id):
|
||||
fields = [
|
||||
cls.model.tenant_id,
|
||||
cls.model.role,
|
||||
User.nickname,
|
||||
User.email,
|
||||
User.avatar,
|
||||
User.update_date
|
||||
]
|
||||
return list(cls.model.select(*fields)
|
||||
.join(User, on=((cls.model.tenant_id == User.id) & (UserTenant.user_id == user_id) & (UserTenant.status == StatusEnum.VALID.value)))
|
||||
.where(cls.model.status == StatusEnum.VALID.value).dicts())
|
||||
|
||||
@ -38,7 +38,7 @@ from api.versions import get_versions
|
||||
|
||||
def update_progress():
|
||||
while True:
|
||||
time.sleep(1)
|
||||
time.sleep(3)
|
||||
try:
|
||||
DocumentService.update_progress()
|
||||
except Exception as e:
|
||||
@ -46,13 +46,12 @@ def update_progress():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("""
|
||||
____ ______ __
|
||||
/ __ \ ____ _ ____ _ / ____// /____ _ __
|
||||
/ /_/ // __ `// __ `// /_ / // __ \| | /| / /
|
||||
/ _, _// /_/ // /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_| \__,_/ \__, //_/ /_/ \____/ |__/|__/
|
||||
/____/
|
||||
print(r"""
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
""", flush=True)
|
||||
stat_logger.info(
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
import os
|
||||
from datetime import date
|
||||
from enum import IntEnum, Enum
|
||||
from api.utils.file_utils import get_project_base_directory
|
||||
from api.utils.log_utils import LoggerFactory, getLogger
|
||||
@ -42,6 +43,7 @@ RAG_FLOW_SERVICE_NAME = "ragflow"
|
||||
SERVER_MODULE = "rag_flow_server.py"
|
||||
TEMP_DIRECTORY = os.path.join(get_project_base_directory(), "temp")
|
||||
RAG_FLOW_CONF_PATH = os.path.join(get_project_base_directory(), "conf")
|
||||
LIGHTEN = int(os.environ.get('LIGHTEN', "0"))
|
||||
|
||||
SUBPROCESS_STD_LOG_NAME = "std.log"
|
||||
|
||||
@ -57,6 +59,11 @@ REQUEST_MAX_WAIT_SEC = 300
|
||||
|
||||
USE_REGISTRY = get_base_config("use_registry")
|
||||
|
||||
LLM = get_base_config("user_default_llm", {})
|
||||
LLM_FACTORY = LLM.get("factory", "Tongyi-Qianwen")
|
||||
LLM_BASE_URL = LLM.get("base_url")
|
||||
|
||||
if not LIGHTEN:
|
||||
default_llm = {
|
||||
"Tongyi-Qianwen": {
|
||||
"chat_model": "qwen-plus",
|
||||
@ -71,10 +78,10 @@ default_llm = {
|
||||
"asr_model": "whisper-1",
|
||||
},
|
||||
"Azure-OpenAI": {
|
||||
"chat_model": "azure-gpt-35-turbo",
|
||||
"embedding_model": "azure-text-embedding-ada-002",
|
||||
"image2text_model": "azure-gpt-4-vision-preview",
|
||||
"asr_model": "azure-whisper-1",
|
||||
"chat_model": "gpt-35-turbo",
|
||||
"embedding_model": "text-embedding-ada-002",
|
||||
"image2text_model": "gpt-4-vision-preview",
|
||||
"asr_model": "whisper-1",
|
||||
},
|
||||
"ZHIPU-AI": {
|
||||
"chat_model": "glm-3-turbo",
|
||||
@ -114,20 +121,14 @@ default_llm = {
|
||||
"rerank_model": "BAAI/bge-reranker-v2-m3",
|
||||
}
|
||||
}
|
||||
LLM = get_base_config("user_default_llm", {})
|
||||
LLM_FACTORY = LLM.get("factory", "Tongyi-Qianwen")
|
||||
LLM_BASE_URL = LLM.get("base_url")
|
||||
|
||||
if LLM_FACTORY not in default_llm:
|
||||
print(
|
||||
"\33[91m【ERROR】\33[0m:",
|
||||
f"LLM factory {LLM_FACTORY} has not supported yet, switch to 'Tongyi-Qianwen/QWen' automatically, and please check the API_KEY in service_conf.yaml.")
|
||||
LLM_FACTORY = "Tongyi-Qianwen"
|
||||
CHAT_MDL = default_llm[LLM_FACTORY]["chat_model"]
|
||||
EMBEDDING_MDL = default_llm["BAAI"]["embedding_model"]
|
||||
RERANK_MDL = default_llm["BAAI"]["rerank_model"]
|
||||
ASR_MDL = default_llm[LLM_FACTORY]["asr_model"]
|
||||
IMAGE2TEXT_MDL = default_llm[LLM_FACTORY]["image2text_model"]
|
||||
else:
|
||||
CHAT_MDL = EMBEDDING_MDL = RERANK_MDL = ASR_MDL = IMAGE2TEXT_MDL = ""
|
||||
|
||||
API_KEY = LLM.get("api_key", "")
|
||||
PARSERS = LLM.get(
|
||||
@ -143,9 +144,8 @@ HTTP_PORT = get_base_config(RAG_FLOW_SERVICE_NAME, {}).get("http_port")
|
||||
|
||||
SECRET_KEY = get_base_config(
|
||||
RAG_FLOW_SERVICE_NAME,
|
||||
{}).get(
|
||||
"secret_key",
|
||||
"infiniflow")
|
||||
{}).get("secret_key", str(date.today()))
|
||||
|
||||
TOKEN_EXPIRE_IN = get_base_config(
|
||||
RAG_FLOW_SERVICE_NAME, {}).get(
|
||||
"token_expires_in", 3600)
|
||||
@ -250,3 +250,5 @@ class RetCode(IntEnum, CustomEnum):
|
||||
AUTHENTICATION_ERROR = 109
|
||||
UNAUTHORIZED = 401
|
||||
SERVER_ERROR = 500
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
|
||||
@ -344,3 +344,8 @@ def download_img(url):
|
||||
return "data:" + \
|
||||
response.headers.get('Content-Type', 'image/jpg') + ";" + \
|
||||
"base64," + base64.b64encode(response.content).decode("utf-8")
|
||||
|
||||
|
||||
def delta_seconds(date_string: str):
|
||||
dt = datetime.datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S")
|
||||
return (datetime.datetime.now() - dt).total_seconds()
|
||||
|
||||
@ -29,6 +29,7 @@ from flask import (
|
||||
Response, jsonify, send_file, make_response,
|
||||
request as flask_request,
|
||||
)
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
|
||||
from api.db.db_models import APIToken
|
||||
@ -37,7 +38,7 @@ from api.settings import (
|
||||
stat_logger, CLIENT_AUTHENTICATION, HTTP_APP_KEY, SECRET_KEY
|
||||
)
|
||||
from api.settings import RetCode
|
||||
from api.utils import CustomJSONEncoder
|
||||
from api.utils import CustomJSONEncoder, get_uuid
|
||||
from api.utils import json_dumps
|
||||
|
||||
requests.models.complexjson.dumps = functools.partial(
|
||||
@ -96,26 +97,6 @@ def get_exponential_backoff_interval(retries, full_jitter=False):
|
||||
return max(0, countdown)
|
||||
|
||||
|
||||
def get_json_result(retcode=RetCode.SUCCESS, retmsg='success',
|
||||
data=None, job_id=None, meta=None):
|
||||
result_dict = {
|
||||
"retcode": retcode,
|
||||
"retmsg": retmsg,
|
||||
# "retmsg": re.sub(r"rag", "seceum", retmsg, flags=re.IGNORECASE),
|
||||
"data": data,
|
||||
"jobId": job_id,
|
||||
"meta": meta,
|
||||
}
|
||||
|
||||
response = {}
|
||||
for key, value in result_dict.items():
|
||||
if value is None and key != "retcode":
|
||||
continue
|
||||
else:
|
||||
response[key] = value
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def get_data_error_result(retcode=RetCode.DATA_ERROR,
|
||||
retmsg='Sorry! Data missing!'):
|
||||
import re
|
||||
@ -219,6 +200,27 @@ def get_json_result(retcode=RetCode.SUCCESS, retmsg='success', data=None):
|
||||
response = {"retcode": retcode, "retmsg": retmsg, "data": data}
|
||||
return jsonify(response)
|
||||
|
||||
def apikey_required(func):
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = flask_request.headers.get('Authorization').split()[1]
|
||||
objs = APIToken.query(token=token)
|
||||
if not objs:
|
||||
return build_error_result(
|
||||
error_msg='API-KEY is invalid!', retcode=RetCode.FORBIDDEN
|
||||
)
|
||||
kwargs['tenant_id'] = objs[0].tenant_id
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def build_error_result(retcode=RetCode.FORBIDDEN, error_msg='success'):
|
||||
response = {"error_code": retcode, "error_msg": error_msg}
|
||||
response = jsonify(response)
|
||||
response.status_code = retcode
|
||||
return response
|
||||
|
||||
|
||||
def construct_response(retcode=RetCode.SUCCESS,
|
||||
retmsg='success', data=None, auth=None):
|
||||
@ -288,3 +290,72 @@ def token_required(func):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def get_result(retcode=RetCode.SUCCESS, retmsg='error', data=None):
|
||||
if retcode == 0:
|
||||
if data is not None:
|
||||
response = {"code": retcode, "data": data}
|
||||
else:
|
||||
response = {"code": retcode}
|
||||
else:
|
||||
response = {"code": retcode, "message": retmsg}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def get_error_data_result(retmsg='Sorry! Data missing!', retcode=RetCode.DATA_ERROR,
|
||||
):
|
||||
import re
|
||||
result_dict = {
|
||||
"code": retcode,
|
||||
"message": re.sub(
|
||||
r"rag",
|
||||
"seceum",
|
||||
retmsg,
|
||||
flags=re.IGNORECASE)}
|
||||
response = {}
|
||||
for key, value in result_dict.items():
|
||||
if value is None and key != "code":
|
||||
continue
|
||||
else:
|
||||
response[key] = value
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def generate_confirmation_token(tenent_id):
|
||||
serializer = URLSafeTimedSerializer(tenent_id)
|
||||
return "ragflow-" + serializer.dumps(get_uuid(), salt=tenent_id)[2:34]
|
||||
|
||||
|
||||
def valid(permission,valid_permission,language,valid_language,chunk_method,valid_chunk_method):
|
||||
if valid_parameter(permission,valid_permission):
|
||||
return valid_parameter(permission,valid_permission)
|
||||
if valid_parameter(language,valid_language):
|
||||
return valid_parameter(language,valid_language)
|
||||
if valid_parameter(chunk_method,valid_chunk_method):
|
||||
return valid_parameter(chunk_method,valid_chunk_method)
|
||||
|
||||
def valid_parameter(parameter,valid_values):
|
||||
if parameter and parameter not in valid_values:
|
||||
return get_error_data_result(f"'{parameter}' is not in {valid_values}")
|
||||
|
||||
def get_parser_config(chunk_method,parser_config):
|
||||
if parser_config:
|
||||
return parser_config
|
||||
if not chunk_method:
|
||||
chunk_method = "naive"
|
||||
key_mapping={"naive":{"chunk_token_num": 128, "delimiter": "\\n!?;。;!?", "html4excel": False,"layout_recognize": True, "raptor": {"use_raptor": False}},
|
||||
"qa":{"raptor":{"use_raptor":False}},
|
||||
"resume":None,
|
||||
"manual":{"raptor":{"use_raptor":False}},
|
||||
"table":None,
|
||||
"paper":{"raptor":{"use_raptor":False}},
|
||||
"book":{"raptor":{"use_raptor":False}},
|
||||
"laws":{"raptor":{"use_raptor":False}},
|
||||
"presentation":{"raptor":{"use_raptor":False}},
|
||||
"one":None,
|
||||
"knowledge_graph":{"chunk_token_num":8192,"delimiter":"\\n!?;。;!?","entity_types":["organization","person","location","event","time"]},
|
||||
"email":None,
|
||||
"picture":None}
|
||||
parser_config=key_mapping[chunk_method]
|
||||
return parser_config
|
||||
@ -25,6 +25,7 @@ from cachetools import LRUCache, cached
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from api.db import FileType
|
||||
from api.contants import IMG_BASE64_PREFIX
|
||||
|
||||
PROJECT_BASE = os.getenv("RAG_PROJECT_BASE") or os.getenv("RAG_DEPLOY_BASE")
|
||||
RAG_BASE = os.getenv("RAG_BASE")
|
||||
@ -168,23 +169,20 @@ def filename_type(filename):
|
||||
|
||||
return FileType.OTHER.value
|
||||
|
||||
|
||||
def thumbnail(filename, blob):
|
||||
def thumbnail_img(filename, blob):
|
||||
filename = filename.lower()
|
||||
if re.match(r".*\.pdf$", filename):
|
||||
pdf = pdfplumber.open(BytesIO(blob))
|
||||
buffered = BytesIO()
|
||||
pdf.pages[0].to_image(resolution=32).annotated.save(buffered, format="png")
|
||||
return "data:image/png;base64," + \
|
||||
base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
return buffered.getvalue()
|
||||
|
||||
if re.match(r".*\.(jpg|jpeg|png|tif|gif|icon|ico|webp)$", filename):
|
||||
image = Image.open(BytesIO(blob))
|
||||
image.thumbnail((30, 30))
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format="png")
|
||||
return "data:image/png;base64," + \
|
||||
base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
return buffered.getvalue()
|
||||
|
||||
if re.match(r".*\.(ppt|pptx)$", filename):
|
||||
import aspose.slides as slides
|
||||
@ -194,10 +192,19 @@ def thumbnail(filename, blob):
|
||||
buffered = BytesIO()
|
||||
presentation.slides[0].get_thumbnail(0.03, 0.03).save(
|
||||
buffered, drawing.imaging.ImageFormat.png)
|
||||
return "data:image/png;base64," + \
|
||||
base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
return buffered.getvalue()
|
||||
except Exception as e:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def thumbnail(filename, blob):
|
||||
img = thumbnail_img(filename, blob)
|
||||
if img is not None:
|
||||
return IMG_BASE64_PREFIX + \
|
||||
base64.b64encode(img).decode("utf-8")
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def traversal_files(base):
|
||||
|
||||
@ -13,10 +13,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import os
|
||||
import dotenv
|
||||
import typing
|
||||
from api.utils.file_utils import get_project_base_directory
|
||||
|
||||
|
||||
def get_versions() -> typing.Mapping[str, typing.Any]:
|
||||
@ -25,4 +23,4 @@ def get_versions() -> typing.Mapping[str, typing.Any]:
|
||||
|
||||
|
||||
def get_rag_version() -> typing.Optional[str]:
|
||||
return get_versions().get("RAGFLOW_VERSION", "dev")
|
||||
return get_versions().get("RAGFLOW_IMAGE", "infiniflow/ragflow:dev").split(":")[-1]
|
||||
@ -77,15 +77,27 @@
|
||||
"tags": "LLM,CHAT,IMAGE2TEXT",
|
||||
"max_tokens": 765,
|
||||
"model_type": "image2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "tts-1",
|
||||
"tags": "TTS",
|
||||
"max_tokens": 2048,
|
||||
"model_type": "tts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tongyi-Qianwen",
|
||||
"logo": "",
|
||||
"tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
|
||||
"tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,SPEECH2TEXT,MODERATION",
|
||||
"status": "1",
|
||||
"llm": [
|
||||
{
|
||||
"llm_name": "qwen-long",
|
||||
"tags": "LLM,CHAT,10000K",
|
||||
"max_tokens": 1000000,
|
||||
"model_type": "chat"
|
||||
},
|
||||
{
|
||||
"llm_name": "qwen-turbo",
|
||||
"tags": "LLM,CHAT,8K",
|
||||
@ -133,6 +145,12 @@
|
||||
"tags": "LLM,CHAT,IMAGE2TEXT",
|
||||
"max_tokens": 765,
|
||||
"model_type": "image2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "gte-rerank",
|
||||
"tags": "RE-RANK,4k",
|
||||
"max_tokens": 4000,
|
||||
"model_type": "rerank"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -184,6 +202,12 @@
|
||||
"max_tokens": 2000,
|
||||
"model_type": "image2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "glm-4-9b",
|
||||
"tags": "LLM,CHAT,",
|
||||
"max_tokens": 8192,
|
||||
"model_type": "chat"
|
||||
},
|
||||
{
|
||||
"llm_name": "embedding-2",
|
||||
"tags": "TEXT EMBEDDING",
|
||||
@ -242,6 +266,12 @@
|
||||
"tags": "LLM,CHAT",
|
||||
"max_tokens": 128000,
|
||||
"model_type": "chat"
|
||||
},
|
||||
{
|
||||
"llm_name": "moonshot-v1-auto",
|
||||
"tags": "LLM,CHAT,",
|
||||
"max_tokens": 128000,
|
||||
"model_type": "chat"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -613,13 +643,13 @@
|
||||
"model_type": "chat,image2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "gpt-35-turbo",
|
||||
"llm_name": "gpt-3.5-turbo",
|
||||
"tags": "LLM,CHAT,4K",
|
||||
"max_tokens": 4096,
|
||||
"model_type": "chat"
|
||||
},
|
||||
{
|
||||
"llm_name": "gpt-35-turbo-16k",
|
||||
"llm_name": "gpt-3.5-turbo-16k",
|
||||
"tags": "LLM,CHAT,16k",
|
||||
"max_tokens": 16385,
|
||||
"model_type": "chat"
|
||||
@ -2091,6 +2121,12 @@
|
||||
"tags": "LLM,IMAGE2TEXT",
|
||||
"status": "1",
|
||||
"llm": [
|
||||
{
|
||||
"llm_name": "yi-lightning",
|
||||
"tags": "LLM,CHAT,16k",
|
||||
"max_tokens": 16384,
|
||||
"model_type": "chat"
|
||||
},
|
||||
{
|
||||
"llm_name": "yi-large",
|
||||
"tags": "LLM,CHAT,32k",
|
||||
@ -2338,6 +2374,13 @@
|
||||
"tags": "LLM",
|
||||
"status": "1",
|
||||
"llm": []
|
||||
},
|
||||
{
|
||||
"name": "HuggingFace",
|
||||
"logo": "",
|
||||
"tags": "TEXT EMBEDDING",
|
||||
"status": "1",
|
||||
"llm": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
ragflow:
|
||||
host: 0.0.0.0
|
||||
http_port: 9380
|
||||
mysql:
|
||||
name: 'rag_flow'
|
||||
user: 'root'
|
||||
password: 'infini_rag_flow'
|
||||
host: 'mysql'
|
||||
port: 3306
|
||||
max_connections: 100
|
||||
stale_timeout: 30
|
||||
postgres:
|
||||
name: 'rag_flow'
|
||||
user: 'rag_flow'
|
||||
password: 'infini_rag_flow'
|
||||
host: 'postgres'
|
||||
port: 5432
|
||||
max_connections: 100
|
||||
stale_timeout: 30
|
||||
minio:
|
||||
user: 'rag_flow'
|
||||
password: 'infini_rag_flow'
|
||||
host: 'minio:9000'
|
||||
azure:
|
||||
auth_type: 'sas'
|
||||
container_url: 'container_url'
|
||||
sas_token: 'sas_token'
|
||||
#azure:
|
||||
# auth_type: 'spn'
|
||||
# account_url: 'account_url'
|
||||
# client_id: 'client_id'
|
||||
# secret: 'secret'
|
||||
# tenant_id: 'tenant_id'
|
||||
# container_name: 'container_name'
|
||||
s3:
|
||||
endpoint: 'endpoint'
|
||||
access_key: 'access_key'
|
||||
secret_key: 'secret_key'
|
||||
region: 'region'
|
||||
es:
|
||||
hosts: 'http://es01:9200'
|
||||
username: 'elastic'
|
||||
password: 'infini_rag_flow'
|
||||
redis:
|
||||
db: 1
|
||||
password: 'infini_rag_flow'
|
||||
host: 'redis:6379'
|
||||
user_default_llm:
|
||||
factory: 'Tongyi-Qianwen'
|
||||
api_key: 'sk-xxxxxxxxxxxxx'
|
||||
base_url: ''
|
||||
oauth:
|
||||
github:
|
||||
client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
url: https://github.com/login/oauth/access_token
|
||||
feishu:
|
||||
app_id: cli_xxxxxxxxxxxxxxxxxxx
|
||||
app_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
app_access_token_url: https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal
|
||||
user_access_token_url: https://open.feishu.cn/open-apis/authen/v1/oidc/access_token
|
||||
grant_type: 'authorization_code'
|
||||
authentication:
|
||||
client:
|
||||
switch: false
|
||||
http_app_key:
|
||||
http_secret_key:
|
||||
site:
|
||||
switch: false
|
||||
permission:
|
||||
switch: false
|
||||
component: false
|
||||
dataset: false
|
||||
1
conf/service_conf.yaml
Symbolic link
1
conf/service_conf.yaml
Symbolic link
@ -0,0 +1 @@
|
||||
../docker/service_conf.yaml
|
||||
@ -16,11 +16,13 @@ import readability
|
||||
import html_text
|
||||
import chardet
|
||||
|
||||
|
||||
def get_encoding(file):
|
||||
with open(file,'rb') as f:
|
||||
tmp = chardet.detect(f.read())
|
||||
return tmp['encoding']
|
||||
|
||||
|
||||
class RAGFlowHtmlParser:
|
||||
def __call__(self, fnm, binary=None):
|
||||
txt = ""
|
||||
|
||||
@ -16,7 +16,6 @@ import random
|
||||
|
||||
import xgboost as xgb
|
||||
from io import BytesIO
|
||||
import torch
|
||||
import re
|
||||
import pdfplumber
|
||||
import logging
|
||||
@ -25,6 +24,7 @@ import numpy as np
|
||||
from timeit import default_timer as timer
|
||||
from pypdf import PdfReader as pdf2_read
|
||||
|
||||
from api.settings import LIGHTEN
|
||||
from api.utils.file_utils import get_project_base_directory
|
||||
from deepdoc.vision import OCR, Recognizer, LayoutRecognizer, TableStructureRecognizer
|
||||
from rag.nlp import rag_tokenizer
|
||||
@ -44,8 +44,13 @@ class RAGFlowPdfParser:
|
||||
self.tbl_det = TableStructureRecognizer()
|
||||
|
||||
self.updown_cnt_mdl = xgb.Booster()
|
||||
if not LIGHTEN:
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
self.updown_cnt_mdl.set_param({"device": "cuda"})
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
try:
|
||||
model_dir = os.path.join(
|
||||
get_project_base_directory(),
|
||||
@ -486,7 +491,7 @@ class RAGFlowPdfParser:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not down["text"].strip():
|
||||
if not down["text"].strip() or not up["text"].strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
@ -952,6 +957,8 @@ class RAGFlowPdfParser:
|
||||
fnm, str) else pdfplumber.open(BytesIO(fnm))
|
||||
self.page_images = [p.to_image(resolution=72 * zoomin).annotated for i, p in
|
||||
enumerate(self.pdf.pages[page_from:page_to])]
|
||||
self.page_images_x2 = [p.to_image(resolution=72 * zoomin * 2).annotated for i, p in
|
||||
enumerate(self.pdf.pages[page_from:page_to])]
|
||||
self.page_chars = [[{**c, 'top': c['top'], 'bottom': c['bottom']} for c in page.dedupe_chars().chars if self._has_color(c)] for page in
|
||||
self.pdf.pages[page_from:page_to]]
|
||||
self.total_page = len(self.pdf.pages)
|
||||
@ -987,7 +994,7 @@ class RAGFlowPdfParser:
|
||||
self.is_english = False
|
||||
|
||||
st = timer()
|
||||
for i, img in enumerate(self.page_images):
|
||||
for i, img in enumerate(self.page_images_x2):
|
||||
chars = self.page_chars[i] if not self.is_english else []
|
||||
self.mean_height.append(
|
||||
np.median(sorted([c["height"] for c in chars])) if chars else 0
|
||||
@ -995,7 +1002,7 @@ class RAGFlowPdfParser:
|
||||
self.mean_width.append(
|
||||
np.median(sorted([c["width"] for c in chars])) if chars else 8
|
||||
)
|
||||
self.page_cum_height.append(img.size[1] / zoomin)
|
||||
self.page_cum_height.append(img.size[1] / zoomin/2)
|
||||
j = 0
|
||||
while j + 1 < len(chars):
|
||||
if chars[j]["text"] and chars[j + 1]["text"] \
|
||||
@ -1005,7 +1012,7 @@ class RAGFlowPdfParser:
|
||||
chars[j]["text"] += " "
|
||||
j += 1
|
||||
|
||||
self.__ocr(i + 1, img, chars, zoomin)
|
||||
self.__ocr(i + 1, img, chars, zoomin*2)
|
||||
if callback and i % 6 == 5:
|
||||
callback(prog=(i + 1) * 0.6 / len(self.page_images), msg="")
|
||||
# print("OCR:", timer()-st)
|
||||
|
||||
@ -10,28 +10,18 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from deepdoc.parser.utils import get_text
|
||||
from rag.nlp import num_tokens_from_string
|
||||
|
||||
from rag.nlp import find_codec,num_tokens_from_string
|
||||
import re
|
||||
|
||||
class RAGFlowTxtParser:
|
||||
def __call__(self, fnm, binary=None, chunk_token_num=128, delimiter="\n!?;。;!?"):
|
||||
txt = ""
|
||||
if binary:
|
||||
encoding = find_codec(binary)
|
||||
txt = binary.decode(encoding, errors="ignore")
|
||||
else:
|
||||
with open(fnm, "r") as f:
|
||||
while True:
|
||||
l = f.readline()
|
||||
if not l:
|
||||
break
|
||||
txt += l
|
||||
txt = get_text(fnm, binary)
|
||||
return self.parser_txt(txt, chunk_token_num, delimiter)
|
||||
|
||||
@classmethod
|
||||
def parser_txt(cls, txt, chunk_token_num=128, delimiter="\n!?;。;!?"):
|
||||
if type(txt) != str:
|
||||
if not isinstance(txt, str):
|
||||
raise TypeError("txt type should be str!")
|
||||
cks = [""]
|
||||
tk_nums = [0]
|
||||
|
||||
29
deepdoc/parser/utils.py
Normal file
29
deepdoc/parser/utils.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from rag.nlp import find_codec
|
||||
|
||||
|
||||
def get_text(fnm: str, binary=None) -> str:
|
||||
txt = ""
|
||||
if binary:
|
||||
encoding = find_codec(binary)
|
||||
txt = binary.decode(encoding, errors="ignore")
|
||||
else:
|
||||
with open(fnm, "r") as f:
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
txt += line
|
||||
return txt
|
||||
@ -102,7 +102,7 @@ class StandardizeImage(object):
|
||||
|
||||
|
||||
class NormalizeImage(object):
|
||||
""" normalize image such as substract mean, divide std
|
||||
""" normalize image such as subtract mean, divide std
|
||||
"""
|
||||
|
||||
def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs):
|
||||
|
||||
43
docker/.env
43
docker/.env
@ -1,7 +1,6 @@
|
||||
# Version of Elastic products
|
||||
STACK_VERSION=8.11.3
|
||||
|
||||
|
||||
# Port to expose Elasticsearch HTTP API to the host
|
||||
ES_PORT=1200
|
||||
|
||||
@ -13,11 +12,10 @@ KIBANA_PORT=6601
|
||||
KIBANA_USER=rag_flow
|
||||
KIBANA_PASSWORD=infini_rag_flow
|
||||
|
||||
# Increase or decrease based on the available host memory (in bytes)
|
||||
# Update according to the available host memory (in bytes)
|
||||
|
||||
MEM_LIMIT=8073741824
|
||||
|
||||
|
||||
MYSQL_PASSWORD=infini_rag_flow
|
||||
MYSQL_PORT=5455
|
||||
|
||||
@ -33,14 +31,45 @@ REDIS_PASSWORD=infini_rag_flow
|
||||
|
||||
SVR_HTTP_PORT=9380
|
||||
|
||||
RAGFLOW_VERSION=dev
|
||||
# the Docker image for the slim version
|
||||
RAGFLOW_IMAGE=infiniflow/ragflow:dev-slim
|
||||
|
||||
# If you cannot download the RAGFlow Docker image, try uncommenting either of the following hub.docker.com mirrors:
|
||||
# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:dev-slim
|
||||
# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:dev-slim
|
||||
|
||||
# To download the RAGFlow Docker image with embedding models, modify the line above as follows:
|
||||
# RAGFLOW_IMAGE=infiniflow/ragflow:dev
|
||||
|
||||
# This Docker image includes the following four models:
|
||||
# - BAAI/bge-large-zh-v1.5
|
||||
# - BAAI/bge-reranker-v2-m3
|
||||
# - maidalun1020/bce-embedding-base_v1
|
||||
# - maidalun1020/bce-reranker-base_v1
|
||||
|
||||
# And the following models will be downloaded if you select them in the RAGFlow UI.
|
||||
# - BAAI/bge-base-en-v1.5
|
||||
# - BAAI/bge-large-en-v1.5
|
||||
# - BAAI/bge-small-en-v1.5
|
||||
# - BAAI/bge-small-zh-v1.5
|
||||
# - jinaai/jina-embeddings-v2-base-en
|
||||
# - jinaai/jina-embeddings-v2-small-en
|
||||
# - nomic-ai/nomic-embed-text-v1.5
|
||||
# - sentence-transformers/all-MiniLM-L6-v2
|
||||
|
||||
# If you cannot download the RAGFlow Docker image, try uncommenting either of the following hub.docker.com mirrors:
|
||||
# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:dev
|
||||
# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:dev
|
||||
|
||||
TIMEZONE='Asia/Shanghai'
|
||||
|
||||
# If you cannot download the RAGFlow Docker image, try uncommenting the following huggingface.co mirror:
|
||||
# HF_ENDPOINT=https://hf-mirror.com
|
||||
|
||||
######## OS setup for ES ###########
|
||||
# sysctl vm.max_map_count
|
||||
# sudo sysctl -w vm.max_map_count=262144
|
||||
# However, this change is not persistent and will be reset after a system reboot.
|
||||
# To make the change permanent, you need to update the /etc/sysctl.conf file.
|
||||
# Add or update the following line in the file:
|
||||
# Note that this change is not permanent and will be reset after a system reboot.
|
||||
# To make your change permanent, update /etc/sysctl.conf by:
|
||||
# Adding or modifying the following line:
|
||||
# vm.max_map_count=262144
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
include:
|
||||
- path: ./docker-compose-base.yml
|
||||
env_file: ./.env
|
||||
|
||||
services:
|
||||
ragflow:
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
es01:
|
||||
condition: service_healthy
|
||||
image: swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:${RAGFLOW_VERSION}
|
||||
container_name: ragflow-server
|
||||
ports:
|
||||
- ${SVR_HTTP_PORT}:9380
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./service_conf.yaml:/ragflow/conf/service_conf.yaml
|
||||
- ./ragflow-logs:/ragflow/logs
|
||||
- ./nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf
|
||||
- ./nginx/proxy.conf:/etc/nginx/proxy.conf
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- HF_ENDPOINT=https://hf-mirror.com
|
||||
- MACOS=${MACOS}
|
||||
networks:
|
||||
- ragflow
|
||||
restart: always
|
||||
@ -30,7 +30,8 @@ services:
|
||||
restart: always
|
||||
|
||||
mysql:
|
||||
image: mysql:5.7.18
|
||||
# mysql:5.7 linux/arm64 image is unavailable.
|
||||
image: mysql:8.0.39
|
||||
container_name: ragflow-mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
include:
|
||||
- path: ./docker-compose-base.yml
|
||||
env_file: ./.env
|
||||
|
||||
services:
|
||||
ragflow:
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
es01:
|
||||
condition: service_healthy
|
||||
image: swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:${RAGFLOW_VERSION}
|
||||
container_name: ragflow-server
|
||||
ports:
|
||||
- ${SVR_HTTP_PORT}:9380
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./service_conf.yaml:/ragflow/conf/service_conf.yaml
|
||||
- ./ragflow-logs:/ragflow/logs
|
||||
- ./nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf
|
||||
- ./nginx/proxy.conf:/etc/nginx/proxy.conf
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- HF_ENDPOINT=https://hf-mirror.com
|
||||
- MACOS=${MACOS}
|
||||
networks:
|
||||
- ragflow
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
@ -9,7 +9,7 @@ services:
|
||||
condition: service_healthy
|
||||
es01:
|
||||
condition: service_healthy
|
||||
image: infiniflow/ragflow:${RAGFLOW_VERSION}
|
||||
image: ${RAGFLOW_IMAGE}
|
||||
container_name: ragflow-server
|
||||
ports:
|
||||
- ${SVR_HTTP_PORT}:9380
|
||||
|
||||
@ -9,12 +9,13 @@ services:
|
||||
condition: service_healthy
|
||||
es01:
|
||||
condition: service_healthy
|
||||
image: infiniflow/ragflow:${RAGFLOW_VERSION}
|
||||
image: ${RAGFLOW_IMAGE}
|
||||
container_name: ragflow-server
|
||||
ports:
|
||||
- ${SVR_HTTP_PORT}:9380
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ./service_conf.yaml:/ragflow/conf/service_conf.yaml
|
||||
- ./ragflow-logs:/ragflow/logs
|
||||
@ -23,8 +24,10 @@ services:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- HF_ENDPOINT=https://huggingface.co
|
||||
- HF_ENDPOINT=${HF_ENDPOINT}
|
||||
- MACOS=${MACOS}
|
||||
networks:
|
||||
- ragflow
|
||||
restart: always
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
0
docker/entrypoint.sh
Normal file → Executable file
0
docker/entrypoint.sh
Normal file → Executable file
28
docker/launch_backend_service.sh
Normal file
28
docker/launch_backend_service.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# unset http proxy which maybe set by docker daemon
|
||||
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
|
||||
|
||||
export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
|
||||
|
||||
PY=python3
|
||||
if [[ -z "$WS" || $WS -lt 1 ]]; then
|
||||
WS=1
|
||||
fi
|
||||
|
||||
function task_exe(){
|
||||
while [ 1 -eq 1 ];do
|
||||
$PY rag/svr/task_executor.py $1;
|
||||
done
|
||||
}
|
||||
|
||||
for ((i=0;i<WS;i++))
|
||||
do
|
||||
task_exe $i &
|
||||
done
|
||||
|
||||
while [ 1 -eq 1 ];do
|
||||
$PY api/ragflow_server.py
|
||||
done
|
||||
|
||||
wait;
|
||||
@ -10,7 +10,7 @@ server {
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
location /v1 {
|
||||
location ~ ^/(v1|api) {
|
||||
proxy_pass http://ragflow:9380;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
@ -21,23 +21,54 @@ redis:
|
||||
db: 1
|
||||
password: 'infini_rag_flow'
|
||||
host: 'redis:6379'
|
||||
user_default_llm:
|
||||
factory: 'Tongyi-Qianwen'
|
||||
api_key: 'sk-xxxxxxxxxxxxx'
|
||||
base_url: ''
|
||||
oauth:
|
||||
github:
|
||||
client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
url: https://github.com/login/oauth/access_token
|
||||
authentication:
|
||||
client:
|
||||
switch: false
|
||||
http_app_key:
|
||||
http_secret_key:
|
||||
site:
|
||||
switch: false
|
||||
permission:
|
||||
switch: false
|
||||
component: false
|
||||
dataset: false
|
||||
|
||||
# postgres:
|
||||
# name: 'rag_flow'
|
||||
# user: 'rag_flow'
|
||||
# password: 'infini_rag_flow'
|
||||
# host: 'postgres'
|
||||
# port: 5432
|
||||
# max_connections: 100
|
||||
# stale_timeout: 30
|
||||
# s3:
|
||||
# endpoint: 'endpoint'
|
||||
# access_key: 'access_key'
|
||||
# secret_key: 'secret_key'
|
||||
# region: 'region'
|
||||
# azure:
|
||||
# auth_type: 'sas'
|
||||
# container_url: 'container_url'
|
||||
# sas_token: 'sas_token'
|
||||
# azure:
|
||||
# auth_type: 'spn'
|
||||
# account_url: 'account_url'
|
||||
# client_id: 'client_id'
|
||||
# secret: 'secret'
|
||||
# tenant_id: 'tenant_id'
|
||||
# container_name: 'container_name'
|
||||
# user_default_llm:
|
||||
# factory: 'Tongyi-Qianwen'
|
||||
# api_key: 'sk-xxxxxxxxxxxxx'
|
||||
# base_url: ''
|
||||
# oauth:
|
||||
# github:
|
||||
# client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# url: https://github.com/login/oauth/access_token
|
||||
# feishu:
|
||||
# app_id: cli_xxxxxxxxxxxxxxxxxxx
|
||||
# app_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# app_access_token_url: https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal
|
||||
# user_access_token_url: https://open.feishu.cn/open-apis/authen/v1/oidc/access_token
|
||||
# grant_type: 'authorization_code'
|
||||
# authentication:
|
||||
# client:
|
||||
# switch: false
|
||||
# http_app_key:
|
||||
# http_secret_key:
|
||||
# site:
|
||||
# switch: false
|
||||
# permission:
|
||||
# switch: false
|
||||
# component: false
|
||||
# dataset: false
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"label": "User Guides",
|
||||
"label": "Guides",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "RAGFlow User Guides"
|
||||
"description": "Guides for RAGFlow users and developers."
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,9 +53,9 @@ Please review the flowing description of the RAG-specific components before you
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| **Retrieval** | A component that retrieves information from specified knowledge bases and returns 'Empty response' if no information is found. Ensure the correct knowledge bases are selected. |
|
||||
| **Generate** | A component that prompts the LLM to generate responses. You must ensure the prompt is set correctly. |
|
||||
| **Answer** | A component that serves as the interface between human and the bot, receiving user inputs and displaying the agent's responses. |
|
||||
| **Interact** | A component that serves as the interface between human and the bot, receiving user inputs and displaying the agent's responses. |
|
||||
| **Categorize** | A component that uses the LLM to classify user inputs into predefined categories. Ensure you specify the name, description, and examples for each category, along with the corresponding next component. |
|
||||
| **Message** | A component that sends out a static message. If multiple messages are supplied, it randomly selects one to send. Ensure its downstream is **Answer**, the interface component. |
|
||||
| **Message** | A component that sends out a static message. If multiple messages are supplied, it randomly selects one to send. Ensure its downstream is **Interact**, the interface component. |
|
||||
| **Relevant** | A component that uses the LLM to assess whether the upstream output is relevant to the user's latest query. Ensure you specify the next component for each judge result. |
|
||||
| **Rewrite** | A component that refines a user query if it fails to retrieve relevant information from the knowledge base. It repeats this process until the predefined looping upper limit is reached. Ensure its upstream is **Relevant** and downstream is **Retrieval**. |
|
||||
| **Keyword** | A component that retrieves top N search results from wikipedia.org. Ensure the TopN value is set properly before use. |
|
||||
@ -63,8 +63,8 @@ Please review the flowing description of the RAG-specific components before you
|
||||
:::caution NOTE
|
||||
|
||||
- Ensure **Rewrite**'s upstream component is **Relevant** and downstream component is **Retrieval**.
|
||||
- Ensure the downstream component of **Message** is **Answer**.
|
||||
- The downstream component of **Begin** is always **Answer**.
|
||||
- Ensure the downstream component of **Message** is **Interact**.
|
||||
- The downstream component of **Begin** is always **Interact**.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ To create a general-purpose chatbot agent using our template:
|
||||
3. On the **agent template** page, hover over the card on **General-purpose chatbot** and click **Use this template**.
|
||||
*You are now directed to the **no-code workflow editor** page.*
|
||||
|
||||

|
||||

|
||||
|
||||
:::tip NOTE
|
||||
RAGFlow's no-code editor spares you the trouble of coding, making agent development effortless.
|
||||
@ -40,10 +40,9 @@ Here’s a breakdown of each component and its role and requirements in the chat
|
||||
- Function: Sets the opening greeting for the user.
|
||||
- Purpose: Establishes a welcoming atmosphere and prepares the user for interaction.
|
||||
|
||||
- **Answer**
|
||||
- **Interact**
|
||||
- Function: Serves as the interface between human and the bot.
|
||||
- Role: Acts as the downstream component of **Begin**.
|
||||
- Note: Though named "Answer", it does not engage with the LLM.
|
||||
|
||||
- **Retrieval**
|
||||
- Function: Retrieves information from specified knowledge base(s).
|
||||
@ -78,7 +77,7 @@ Here’s a breakdown of each component and its role and requirements in the chat
|
||||
|
||||
4. Click **Relevant** to review or change its settings:
|
||||
*You may retain the current settings, but feel free to experiment with changes to understand how the agent operates.*
|
||||

|
||||

|
||||
|
||||
5. Click **Rewrite** to select a different model for query rewriting or update the maximum loop times for query rewriting:
|
||||

|
||||
|
||||
@ -58,7 +58,7 @@ You can also change the chunk template for a particular file on the **Datasets**
|
||||
|
||||
### Select embedding model
|
||||
|
||||
An embedding model builds vector index on file chunks. Once you have chosen an embedding model and used it to parse a file, you are no longer allowed to change it. To switch to a different embedding model, you *must* deletes all completed file chunks in the knowledge base. The obvious reason is that we must *ensure* that all files in a specific knowledge base are parsed using the *same* embedding model (ensure that they are compared in the same embedding space).
|
||||
An embedding model builds vector index on file chunks. Once you have chosen an embedding model and used it to parse a file, you are no longer allowed to change it. To switch to a different embedding model, you *must* delete all completed file chunks in the knowledge base. The obvious reason is that we must *ensure* that all files in a specific knowledge base are parsed using the *same* embedding model (ensure that they are compared in the same embedding space).
|
||||
|
||||
The following embedding models can be deployed locally:
|
||||
|
||||
@ -128,7 +128,7 @@ RAGFlow uses multiple recall of both full-text search and vector search in its c
|
||||
|
||||
## Search for knowledge base
|
||||
|
||||
As of RAGFlow v0.11.0, the search feature is still in a rudimentary form, supporting only knowledge base search by name.
|
||||
As of RAGFlow v0.13.0, the search feature is still in a rudimentary form, supporting only knowledge base search by name.
|
||||
|
||||

|
||||
|
||||
|
||||
8
docs/guides/develop/_category_.json
Normal file
8
docs/guides/develop/_category_.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Develop",
|
||||
"position": 10,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Guides for Hardcore Developers"
|
||||
}
|
||||
}
|
||||
18
docs/guides/develop/acquire_ragflow_api_key.md
Normal file
18
docs/guides/develop/acquire_ragflow_api_key.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
slug: /acquire_ragflow_api_key
|
||||
---
|
||||
|
||||
# Acquire a RAGFlow API key
|
||||
|
||||
A key is required for the RAGFlow server to authenticate your requests via HTTP or a Python API. This documents provides instructions on obtaining a RAGFlow API key.
|
||||
|
||||
1. Click your avatar on the top right corner of the RAGFlow UI to access the configuration page.
|
||||
2. Click **API** to switch to the **API** page.
|
||||
3. Obtain a RAGFlow API key:
|
||||
|
||||

|
||||
|
||||
:::tip NOTE
|
||||
See the [RAGFlow HTTP API reference](../../references/http_api_reference.md) or the [RAGFlow Python API reference](../../references/python_api_reference.md) for a complete reference of RAGFlow's HTTP or Python APIs.
|
||||
:::
|
||||
64
docs/guides/develop/build_docker_image.mdx
Normal file
64
docs/guides/develop/build_docker_image.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
slug: /build_docker_image
|
||||
---
|
||||
|
||||
# Build a RAGFlow Docker Image
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
A guide explaining how to build a RAGFlow Docker image from its source code. By following this guide, you'll be able to create a local Docker image that can be used for development, debugging, or testing purposes.
|
||||
|
||||
## Target Audience
|
||||
|
||||
- Developers who have added new features or modified the existing code and require a Docker image to view and debug their changes.
|
||||
- Testers looking to explore the latest features of RAGFlow in a Docker image.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- CPU ≥ 4 cores
|
||||
- RAM ≥ 16 GB
|
||||
- Disk ≥ 50 GB
|
||||
- Docker ≥ 24.0.0 & Docker Compose ≥ v2.26.1
|
||||
|
||||
:::tip NOTE
|
||||
If you have not installed Docker on your local machine (Windows, Mac, or Linux), see the [Install Docker Engine](https://docs.docker.com/engine/install/) guide.
|
||||
:::
|
||||
|
||||
## Build a Docker image
|
||||
|
||||
<Tabs
|
||||
defaultValue="without"
|
||||
values={[
|
||||
{label: 'Build a Docker image without embedding models', value: 'without'},
|
||||
{label: 'Build a Docker image including embedding models', value: 'including'}
|
||||
]}>
|
||||
<TabItem value="without">
|
||||
|
||||
This image is approximately 1 GB in size and relies on external LLM and embedding services.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile.slim -t infiniflow/ragflow:dev-slim .
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="including">
|
||||
|
||||
## Build a Docker image including embedding models
|
||||
|
||||
This image is approximately 9 GB in size. As it includes embedding models, it relies on external LLM services only.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
pip3 install huggingface-hub nltk
|
||||
python3 download_deps.py
|
||||
docker build -f Dockerfile -t infiniflow/ragflow:dev .
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user