mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 18:32:48 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
499fc90f2f | ||
|
|
56a7b7b524 | ||
|
|
28ed136f2d | ||
|
|
16f490567e | ||
|
|
a5ca97ac06 | ||
|
|
601d19d7ad | ||
|
|
5e7879c582 | ||
|
|
0ce3fc5cfa |
|
|
@ -1,50 +1,61 @@
|
|||
name: 'Bug Report'
|
||||
description: 'Report an Bug'
|
||||
title: "[Bug] "
|
||||
assignees: zyyfit
|
||||
name: BUG 提交
|
||||
description: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[BUG]"
|
||||
labels: "类型: 缺陷"
|
||||
assignees: baixin513
|
||||
body:
|
||||
- type: markdown
|
||||
id: contacts_title
|
||||
attributes:
|
||||
value: "## Contact Information"
|
||||
value: "## 联系方式"
|
||||
- type: input
|
||||
id: contacts
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "Contact Information"
|
||||
description: "The ways to quickly contact you: WeChat group number and nickname, email, etc."
|
||||
label: "联系方式"
|
||||
description: "可以快速联系到您的方式:交流群号及昵称、邮箱等"
|
||||
- type: markdown
|
||||
id: environment
|
||||
attributes:
|
||||
value: "## Environment Information"
|
||||
value: "## 环境信息"
|
||||
- type: input
|
||||
id: version
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "MaxKB Version"
|
||||
description: "Log in to the MaxKB Web Console and check the current version on the `About` page in the top right corner."
|
||||
label: "MaxKB 版本"
|
||||
description: "登录 MaxKB Web 控制台,在右上角关于页面查看当前版本。"
|
||||
- type: markdown
|
||||
id: details
|
||||
attributes:
|
||||
value: "## Detailed information"
|
||||
value: "## 详细信息"
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: "Problem Description"
|
||||
description: "Briefly describe the issue you’ve encountered."
|
||||
label: "问题描述"
|
||||
description: "简要描述您碰到的问题"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-happened
|
||||
attributes:
|
||||
label: "Steps to Reproduce"
|
||||
description: "How can this issue be reproduced."
|
||||
label: "重现步骤"
|
||||
description: "如果操作可以重现该问题"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expect
|
||||
attributes:
|
||||
label: "The expected correct result"
|
||||
label: "期待的正确结果"
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: "Related log output"
|
||||
description: "Please paste any relevant log output here. It will automatically be formatted as code, so no backticks are necessary."
|
||||
label: "相关日志输出"
|
||||
description: "请复制并粘贴任何相关的日志输出。 这将自动格式化为代码,因此无需反引号。"
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: "Additional Information"
|
||||
description: "If you have any additional information to provide, you can include it here (screenshots, videos, etc., are welcome)."
|
||||
label: "附加信息"
|
||||
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/1Panel-dev/MaxKB/discussions
|
||||
about: Raise questions about the installation, deployment, use and other aspects of the project.
|
||||
- name: 对 MaxKB 项目有其他问题
|
||||
url: https://bbs.fit2cloud.com/c/mk/11
|
||||
about: 如果你对 MaxKB 有其他想要提问的,我们欢迎到我们的官方社区进行提问。
|
||||
|
|
@ -1,29 +1,36 @@
|
|||
name: 'Feature Request'
|
||||
description: 'Suggest an idea'
|
||||
title: '[Feature] '
|
||||
name: 需求建议
|
||||
description: 提出针对本项目的想法和建议
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: baixin513
|
||||
body:
|
||||
- type: markdown
|
||||
id: environment
|
||||
attributes:
|
||||
value: "## Environment Information"
|
||||
value: "## 环境信息"
|
||||
- type: input
|
||||
id: version
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "MaxKB Version"
|
||||
description: "Log in to the MaxKB Web Console and check the current version on the `About` page in the top right corner."
|
||||
label: "MaxKB 版本"
|
||||
description: "登录 MaxKB Web 控制台,在右上角关于页面查看当前版本。"
|
||||
- type: markdown
|
||||
id: details
|
||||
attributes:
|
||||
value: "## Detailed information"
|
||||
value: "## 详细信息"
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "Please describe your needs or suggestions for improvements"
|
||||
label: "请描述您的需求或者改进建议"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Please describe the solution you suggest"
|
||||
label: "请描述你建议的实现方案"
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: "Additional Information"
|
||||
description: "If you have any additional information to provide, you can include it here (screenshots, videos, etc., are welcome)."
|
||||
label: "附加信息"
|
||||
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
timezone: "Asia/Shanghai"
|
||||
day: "friday"
|
||||
target-branch: "v2"
|
||||
groups:
|
||||
python-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
# ignore:
|
||||
# - dependency-name: "pymupdf"
|
||||
# versions: ["*"]
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ on:
|
|||
- linux/amd64,linux/arm64
|
||||
jobs:
|
||||
build-and-push-python-pg-to-ghcr:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: df -h
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
run: |
|
||||
DOCKER_IMAGE=ghcr.io/1panel-dev/maxkb-python-pg
|
||||
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
|
||||
TAG_NAME=python3.11-pg15.8
|
||||
TAG_NAME=python3.11-pg15.6
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
echo ::set-output name=version::${TAG_NAME}
|
||||
|
|
@ -50,9 +50,6 @@ jobs:
|
|||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
# Until https://github.com/tonistiigi/binfmt/issues/215
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-and-push-vector-model-to-ghcr:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: df -h
|
||||
|
|
@ -55,9 +55,6 @@ jobs:
|
|||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
# Until https://github.com/tonistiigi/binfmt/issues/215
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
|
|
|
|||
|
|
@ -1,19 +1,12 @@
|
|||
name: build-and-push
|
||||
|
||||
run-name: 构建镜像并推送仓库 ${{ github.event.inputs.dockerImageTag }} (${{ github.event.inputs.registry }})
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dockerImageTag:
|
||||
description: 'Image Tag'
|
||||
default: 'v1.10.7-dev'
|
||||
description: 'Docker Image Tag'
|
||||
default: 'v1.1.0-dev'
|
||||
required: true
|
||||
dockerImageTagWithLatest:
|
||||
description: '是否发布latest tag(正式发版时选择,测试版本切勿选择)'
|
||||
default: false
|
||||
required: true
|
||||
type: boolean
|
||||
architecture:
|
||||
description: 'Architecture'
|
||||
required: true
|
||||
|
|
@ -26,11 +19,11 @@ on:
|
|||
registry:
|
||||
description: 'Push To Registry'
|
||||
required: true
|
||||
default: 'fit2cloud-registry'
|
||||
default: 'dockerhub'
|
||||
type: choice
|
||||
options:
|
||||
- fit2cloud-registry
|
||||
- dockerhub
|
||||
- fit2cloud-registry
|
||||
- dockerhub, fit2cloud-registry
|
||||
|
||||
jobs:
|
||||
|
|
@ -59,17 +52,16 @@ jobs:
|
|||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
DOCKER_IMAGE=${{ secrets.FIT2CLOUD_REGISTRY_HOST }}/maxkb/maxkb
|
||||
DOCKER_IMAGE=registry-hkproxy.fit2cloud.com/maxkb/maxkb
|
||||
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
|
||||
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
|
||||
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
|
||||
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
|
||||
else
|
||||
if [[ ${TAG_NAME} == *dev* ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
|
||||
else
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=${GITHUB_SHA::8} --no-cache \
|
||||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
|
@ -84,12 +76,11 @@ jobs:
|
|||
- name: Login to FIT2CLOUD Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.FIT2CLOUD_REGISTRY_HOST }}
|
||||
registry: registry-hkproxy.fit2cloud.com
|
||||
username: ${{ secrets.FIT2CLOUD_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }}
|
||||
- name: Docker Buildx (build-and-push)
|
||||
run: |
|
||||
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
|
||||
|
||||
build-and-push-to-dockerhub:
|
||||
|
|
@ -119,15 +110,14 @@ jobs:
|
|||
run: |
|
||||
DOCKER_IMAGE=1panel/maxkb
|
||||
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
|
||||
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
|
||||
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
|
||||
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
|
||||
else
|
||||
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
|
||||
if [[ ${TAG_NAME} == *dev* ]]; then
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
|
||||
else
|
||||
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=${GITHUB_SHA::8} --no-cache \
|
||||
${DOCKER_IMAGE_TAGS} .
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
|
@ -146,5 +136,4 @@ jobs:
|
|||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Docker Buildx (build-and-push)
|
||||
run: |
|
||||
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'pr@**'
|
||||
- 'repr@**'
|
||||
|
||||
name: 针对特定分支名自动创建 PR
|
||||
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: 自动创建 PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create pull request
|
||||
uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
name: Issue Translator
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
BOT_GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
name: LLM Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
llm-code-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fit2cloud/LLM-CodeReview-Action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
|
||||
LANGUAGE: English
|
||||
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
MODEL: qwen2.5-coder-3b-instruct
|
||||
PROMPT: "Please check the following code for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
|
||||
top_p: 1
|
||||
temperature: 1
|
||||
# max_tokens: 10000
|
||||
MAX_PATCH_LENGTH: 10000
|
||||
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
|
||||
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
name: Typos Check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
run:
|
||||
|
|
|
|||
|
|
@ -178,10 +178,6 @@ ui/node_modules
|
|||
ui/dist
|
||||
apps/static
|
||||
models/
|
||||
apps/xpack
|
||||
!apps/**/models/
|
||||
data
|
||||
.dev
|
||||
poetry.lock
|
||||
apps/setting/models_provider/impl/*/icon/
|
||||
tmp/
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[files]
|
||||
extend-exclude = [
|
||||
'apps/setting/models_provider/impl/*/icon/*'
|
||||
]
|
||||
141
README.md
141
README.md
|
|
@ -1,126 +1,83 @@
|
|||
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
|
||||
<h3 align="center">Open-source platform for building enterprise-grade agents</h3>
|
||||
<h3 align="center">强大易用的企业级智能体平台</h3>
|
||||
<h3 align="center">基于 LLM 大语言模型的知识库问答系统</h3>
|
||||
<p align="center"><a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
|
||||
<a href="https://app.codacy.com/gh/1Panel-dev/maxkb?utm_source=github.com&utm_medium=referral&utm_content=1Panel-dev/maxkb&utm_campaign=Badge_Grade_Dashboard"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb/releases/latest"><img src="https://img.shields.io/github/v/release/1Panel-dev/maxkb" alt="Latest release"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a><br/>
|
||||
[<a href="/README_CN.md">中文(简体)</a>] | [<a href="/README.md">English</a>]
|
||||
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
|
||||
</p>
|
||||
<hr/>
|
||||
|
||||
MaxKB = Max Knowledge Brain, it is an open-source platform for building enterprise-grade agents. MaxKB integrates Retrieval-Augmented Generation (RAG) pipelines, supports robust workflows, and provides advanced MCP tool-use capabilities. MaxKB is widely applied in scenarios such as intelligent customer service, corporate internal knowledge bases, academic research, and education.
|
||||
MaxKB = Max Knowledge Base,是一款基于 LLM 大语言模型的开源知识库问答系统,旨在成为企业的最强大脑。
|
||||
|
||||
- **RAG Pipeline**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization. This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience.
|
||||
- **Agentic Workflow**: Equipped with a powerful workflow engine, function library and MCP tool-use, enabling the orchestration of AI processes to meet the needs of complex business scenarios.
|
||||
- **Seamless Integration**: Facilitates zero-coding rapid integration into third-party business systems, quickly equipping existing systems with intelligent Q&A capabilities to enhance user satisfaction.
|
||||
- **Model-Agnostic**: Supports various large models, including private models (such as DeepSeek, Llama, Qwen, etc.) and public models (like OpenAI, Claude, Gemini, etc.).
|
||||
- **Multi Modal**: Native support for input and output text, image, audio and video.
|
||||
- **开箱即用**:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化、RAG(检索增强生成),智能问答交互体验好;
|
||||
- **模型中立**:支持对接各种大语言模型,包括本地私有大模型(Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 智谱 AI / 百度千帆 / Kimi / DeepSeek 等)和国外公共大模型(OpenAI / Azure OpenAI / Gemini 等);
|
||||
- **灵活编排**:内置强大的工作流引擎,支持编排 AI 工作过程,满足复杂业务场景下的需求;
|
||||
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度。
|
||||
|
||||
## Quick start
|
||||
## 快速开始
|
||||
|
||||
Execute the script below to start a MaxKB container using Docker:
|
||||
```
|
||||
docker run -d --name=maxkb -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data cr2.fit2cloud.com/1panel/maxkb
|
||||
|
||||
```bash
|
||||
docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data -v ~/.python-packages:/opt/maxkb/app/sandbox/python-packages 1panel/maxkb
|
||||
# 用户名: admin
|
||||
# 密码: MaxKB@123..
|
||||
```
|
||||
|
||||
Access MaxKB web interface at `http://your_server_ip:8080` with default admin credentials:
|
||||
你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB + Ollama + Llama 3,30 分钟内即可上线基于本地大模型的知识库问答系统,并嵌入到第三方业务系统中。
|
||||
|
||||
- username: admin
|
||||
- password: MaxKB@123..
|
||||
如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署。
|
||||
|
||||
中国用户如遇到 Docker 镜像 Pull 失败问题,请参照该 [离线安装文档](https://maxkb.cn/docs/installation/offline_installtion/) 进行安装。
|
||||
你也可以在线体验:[DataEase 小助手](https://dataease.io/docs/v2/),它是基于 MaxKB 搭建的智能问答系统,已经嵌入到 DataEase 产品及在线文档中。
|
||||
|
||||
## Screenshots
|
||||
如果你需要搭建技术博客或者知识库,推荐使用 [Halo 开源建站工具](https://github.com/halo-dev/halo/),你可以体验下飞致云官方的 [技术博客](https://blog.fit2cloud.com/) 和 [知识库](https://kb.fit2cloud.com) 案例。
|
||||
|
||||
如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。
|
||||
|
||||
- [使用手册](https://github.com/1Panel-dev/MaxKB/wiki/1-%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2)
|
||||
- [演示视频](https://www.bilibili.com/video/BV1BE421M7YM/)
|
||||
- [论坛求助](https://bbs.fit2cloud.com/c/mk/11)
|
||||
- 技术交流群
|
||||
|
||||
<image height="150px" width="150px" src="https://github.com/1Panel-dev/MaxKB/assets/52996290/a083d214-02be-4178-a1db-4f428124153a"/>
|
||||
|
||||
|
||||
|
||||
## UI 展示
|
||||
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/overview.png" alt="MaxKB Demo1" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-models.png" alt="MaxKB Demo2" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/2b893a25-ae46-48da-b6a1-61d23015565e" alt="MaxKB Demo1" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/3e50e7ff-cdc4-4a37-b430-d84975f11d4e" alt="MaxKB Demo2" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-knowledge.png" alt="MaxKB Demo3" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-function.png" alt="MaxKB Demo4" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/dfdcc03f-ef36-4f75-bb82-797c0f9da1ad" alt="MaxKB Demo3" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/52996290/f8e36cad-b6d5-44bb-a9ab-8fa8e289377a" alt="MaxKB Demo4" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Technical stack
|
||||
## 技术栈
|
||||
|
||||
- Frontend:[Vue.js](https://vuejs.org/)
|
||||
- Backend:[Python / Django](https://www.djangoproject.com/)
|
||||
- LLM Framework:[LangChain](https://www.langchain.com/)
|
||||
- Database:[PostgreSQL + pgvector](https://www.postgresql.org/)
|
||||
- 前端:[Vue.js](https://cn.vuejs.org/)
|
||||
- 后端:[Python / Django](https://www.djangoproject.com/)
|
||||
- LangChain:[LangChain](https://www.langchain.com/)
|
||||
- 向量数据库:[PostgreSQL / pgvector](https://www.postgresql.org/)
|
||||
- 大模型:各种本地私有或者公共大模型
|
||||
|
||||
## 飞致云的其他明星项目
|
||||
|
||||
## Feature Comparison
|
||||
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th align="center">Feature</th>
|
||||
<th align="center">LangChain</th>
|
||||
<th align="center">Dify.AI</th>
|
||||
<th align="center">Flowise</th>
|
||||
<th align="center">MaxKB <br>(Built upon LangChain)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Supported LLMs</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">RAG Engine</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Agent</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Workflow</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Observability</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">SSO/Access control</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅ (Pro)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">On-premise Deployment</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#1Panel-dev/MaxKB&Date)
|
||||
- [1Panel](https://github.com/1panel-dev/1panel/) - 现代化、开源的 Linux 服务器运维管理面板
|
||||
- [JumpServer](https://github.com/jumpserver/jumpserver/) - 广受欢迎的开源堡垒机
|
||||
- [DataEase](https://github.com/dataease/dataease/) - 人人可用的开源数据可视化分析工具
|
||||
- [MeterSphere](https://github.com/metersphere/metersphere/) - 现代化、开源的测试管理及接口测试工具
|
||||
- [Halo](https://github.com/halo-dev/halo/) - 强大易用的开源建站工具
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
<https://www.gnu.org/licenses/gpl-3.0.html>
|
||||
|
|
|
|||
89
README_CN.md
89
README_CN.md
|
|
@ -1,89 +0,0 @@
|
|||
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
|
||||
<h3 align="center">强大易用的企业级智能体平台</h3>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: auto;" /></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="README_EN.md"><img src="https://img.shields.io/badge/English_README-blue" alt="English README"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb/releases/latest"><img src="https://img.shields.io/github/v/release/1Panel-dev/maxkb" alt="Latest release"></a>
|
||||
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?style=flat-square" alt="Stars"></a>
|
||||
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
|
||||
<a href="https://gitee.com/fit2cloud-feizhiyun/MaxKB"><img src="https://gitee.com/fit2cloud-feizhiyun/MaxKB/badge/star.svg?theme=gvp" alt="Gitee Stars"></a>
|
||||
<a href="https://gitcode.com/feizhiyun/MaxKB"><img src="https://gitcode.com/feizhiyun/MaxKB/star/badge.svg" alt="GitCode Stars"></a>
|
||||
</p>
|
||||
<hr/>
|
||||
|
||||
MaxKB = Max Knowledge Brain,是一款强大易用的企业级智能体平台,支持 RAG 检索增强生成、工作流编排、MCP 工具调用能力。MaxKB 支持对接各种主流大语言模型,广泛应用于智能客服、企业内部知识库问答、员工助手、学术研究与教育等场景。
|
||||
|
||||
- **RAG 检索增强生成**:高效搭建本地 AI 知识库,支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化,有效减少大模型幻觉,提升问答效果;
|
||||
- **灵活编排**:内置强大的工作流引擎、函数库和 MCP 工具调用能力,支持编排 AI 工作过程,满足复杂业务场景下的需求;
|
||||
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度;
|
||||
- **模型中立**:支持对接各种大模型,包括本地私有大模型(DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等)。
|
||||
|
||||
MaxKB 三分钟视频介绍:https://www.bilibili.com/video/BV18JypYeEkj/
|
||||
|
||||
## 快速开始
|
||||
|
||||
```
|
||||
# Linux 机器
|
||||
docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data -v ~/.python-packages:/opt/maxkb/app/sandbox/python-packages registry.fit2cloud.com/maxkb/maxkb
|
||||
|
||||
# Windows 机器
|
||||
docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/var/lib/postgresql/data -v C:/python-packages:/opt/maxkb/app/sandbox/python-packages registry.fit2cloud.com/maxkb/maxkb
|
||||
|
||||
# 用户名: admin
|
||||
# 密码: MaxKB@123..
|
||||
```
|
||||
|
||||
- 你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB;
|
||||
- 如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署;
|
||||
- MaxKB 产品版本分为社区版和专业版,详情请参见:[MaxKB 产品版本对比](https://maxkb.cn/pricing.html);
|
||||
- 如果您需要向团队介绍 MaxKB,可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202503.pdf)。
|
||||
|
||||
如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。
|
||||
|
||||
- [案例展示](USE-CASES.md)
|
||||
- [使用手册](https://maxkb.cn/docs/)
|
||||
- [论坛求助](https://bbs.fit2cloud.com/c/mk/11)
|
||||
- 技术交流群
|
||||
|
||||
<image height="150px" width="150px" src="https://github.com/1Panel-dev/MaxKB/assets/52996290/a083d214-02be-4178-a1db-4f428124153a"/>
|
||||
|
||||
## UI 展示
|
||||
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/52996290/d87395fa-a8d7-401c-82bf-c6e475d10ae9" alt="MaxKB Demo1" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/52996290/47c35ee4-3a3b-4bd4-9f4f-ee20788b2b9a" alt="MaxKB Demo2" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/9a1043cb-fa62-4f71-b9a3-0b46fa59a70e" alt="MaxKB Demo3" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/3407ce9a-779c-4eb4-858e-9441a2ddc664" alt="MaxKB Demo4" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:[Vue.js](https://cn.vuejs.org/)
|
||||
- 后端:[Python / Django](https://www.djangoproject.com/)
|
||||
- LangChain:[LangChain](https://www.langchain.com/)
|
||||
- 向量数据库:[PostgreSQL / pgvector](https://www.postgresql.org/)
|
||||
|
||||
## 飞致云的其他明星项目
|
||||
|
||||
- [1Panel](https://github.com/1panel-dev/1panel/) - 现代化、开源的 Linux 服务器运维管理面板
|
||||
- [JumpServer](https://github.com/jumpserver/jumpserver/) - 广受欢迎的开源堡垒机
|
||||
- [DataEase](https://github.com/dataease/dataease/) - 人人可用的开源数据可视化分析工具
|
||||
- [MeterSphere](https://github.com/metersphere/metersphere/) - 新一代的开源持续测试工具
|
||||
- [Halo](https://github.com/halo-dev/halo/) - 强大易用的开源建站工具
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2025 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
<https://www.gnu.org/licenses/gpl-3.0.html>
|
||||
|
||||
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.
|
||||
39
USE-CASES.md
39
USE-CASES.md
|
|
@ -1,39 +0,0 @@
|
|||
<h3 align="center">MaxKB 应用案例,持续更新中...</h3>
|
||||
|
||||
------------------------------
|
||||
|
||||
- [MaxKB 应用案例:中国农业大学-小鹉哥](https://mp.weixin.qq.com/s/4g_gySMBQZCJ9OZ-yBkmvw)
|
||||
- [MaxKB 应用案例:东北财经大学-小银杏](https://mp.weixin.qq.com/s/3BoxkY7EMomMmmvFYxvDIA)
|
||||
- [MaxKB 应用案例:中铁水务](https://mp.weixin.qq.com/s/voNAddbK2CJOrJJs1ewZ8g)
|
||||
- [MaxKB 应用案例:解放军总医院](https://mp.weixin.qq.com/s/ETrZC-vrA4Aap0eF-15EeQ)
|
||||
- [MaxKB 应用案例:无锡市数据局](https://mp.weixin.qq.com/s/enfUFLevvL_La74PQ0kIXw)
|
||||
- [MaxKB 应用案例:中核西仪研究院-西仪睿答](https://mp.weixin.qq.com/s/CbKr4mev8qahKLAtV6Dxdg)
|
||||
- [MaxKB 应用案例:南京中医药大学](https://mp.weixin.qq.com/s/WUmAKYbZjp3272HIecpRFA)
|
||||
- [MaxKB 应用案例:西北电力设计院-AI数字助理Memex](https://mp.weixin.qq.com/s/ezHFdB7C7AVL9MTtDwYGSA)
|
||||
- [MaxKB 应用案例:西安国际医院中心医院-国医小助](https://mp.weixin.qq.com/s/DSOUvwrQrxbqQxKBilTCFQ)
|
||||
- [MaxKB 应用案例:华莱士智能AI客服助手上线啦!](https://www.bilibili.com/video/BV1hQtVeXEBL)
|
||||
- [MaxKB 应用案例:把医疗行业知识转化为知识库问答助手!](https://www.bilibili.com/video/BV157wme9EgB)
|
||||
- [MaxKB 应用案例:会展AI智能客服体验](https://www.bilibili.com/video/BV1J7BqY6EKA)
|
||||
- [MaxKB 应用案例:孩子要上幼儿园了,AI 智能助手择校好帮手](https://www.bilibili.com/video/BV1wKrhYvEer)
|
||||
- [MaxKB 应用案例:产品使用指南AI助手,新手小白也能轻松搞定!](https://www.bilibili.com/video/BV1Yz6gYtEqX)
|
||||
- [MaxKB 应用案例:生物医药AI客服智能体验!](https://www.bilibili.com/video/BV13JzvYsE3e)
|
||||
- [MaxKB 应用案例:高校行政管理AI小助手](https://www.bilibili.com/video/BV1yvBMYvEdy)
|
||||
- [MaxKB 应用案例:岳阳市人民医院-OA小助手](https://mp.weixin.qq.com/s/O94Qo3UH-MiUtDdWCVg8sQ)
|
||||
- [MaxKB 应用案例:常熟市第一人民医院](https://mp.weixin.qq.com/s/s5XXGTR3_MUo41NbJ8WzZQ)
|
||||
- [MaxKB 应用案例:华北水利水电大学](https://mp.weixin.qq.com/s/PoOFAcMCr9qJdvSj8c08qg)
|
||||
- [MaxKB 应用案例:唐山海事局-“小海”AI语音助手](https://news.qq.com/rain/a/20250223A030BE00)
|
||||
- [MaxKB 应用案例:湖南汉寿政务](http://hsds.hsdj.gov.cn:19999/ui/chat/a2c976736739aadc)
|
||||
- [MaxKB 应用案例:广州市妇女儿童医疗中心-AI医疗数据分类分级小助手](https://mp.weixin.qq.com/s/YHUMkUOAaUomBV8bswpK3g)
|
||||
- [MaxKB 应用案例:苏州热工研究院有限公司-维修大纲评估质量自查AI小助手](https://mp.weixin.qq.com/s/Ts5FQdnv7Tu9Jp7bvofCVA)
|
||||
- [MaxKB 应用案例:国核自仪系统工程有限公司-NuCON AI帮](https://mp.weixin.qq.com/s/HNPc7u5xVfGLJr8IQz3vjQ)
|
||||
- [MaxKB 应用案例:深圳通开启Deep Seek智能应用新篇章](https://mp.weixin.qq.com/s/SILN0GSescH9LyeQqYP0VQ)
|
||||
- [MaxKB 应用案例:南通智慧出行领跑长三角!首款接入DeepSeek的"畅行南通"APP上线AI新场景](https://mp.weixin.qq.com/s/WEC9UQ6msY0VS8LhTZh-Ew)
|
||||
- [MaxKB 应用案例:中船动力人工智能"智慧动力云助手"及首批数字员工正式上线](https://mp.weixin.qq.com/s/OGcEkjh9DzGO1Tkc9nr7qg)
|
||||
- [MaxKB 应用案例:AI+矿山:DeepSeek助力绿色智慧矿山智慧“升级”](https://mp.weixin.qq.com/s/SZstxTvVoLZg0ECbZbfpIA)
|
||||
- [MaxKB 应用案例:DeepSeek落地弘盛铜业:国产大模型点亮"黑灯工厂"新引擎](https://mp.weixin.qq.com/s/Eczdx574MS5RMF7WfHN7_A)
|
||||
- [MaxKB 应用案例:拥抱智能时代!中国五矿以 “AI+”赋能企业发展](https://mp.weixin.qq.com/s/D5vBtlX2E81pWE3_2OgWSw)
|
||||
- [MaxKB 应用案例:DeepSeek赋能中冶武勘AI智能体](https://mp.weixin.qq.com/s/8m0vxGcWXNdZazziQrLyxg)
|
||||
- [MaxKB 应用案例:重磅!陕西广电网络“秦岭云”平台实现DeepSeek本地化部署](https://mp.weixin.qq.com/s/ZKmEU_wWShK1YDomKJHQeA)
|
||||
- [MaxKB 应用案例:粤海集团完成DeepSeek私有化部署,助力集团智能化管理](https://mp.weixin.qq.com/s/2JbVp0-kr9Hfp-0whH4cvg)
|
||||
- [MaxKB 应用案例:建筑材料工业信息中心完成DeepSeek本地化部署,推动行业数智化转型新发展](https://mp.weixin.qq.com/s/HThGSnND3qDF8ySEqiM4jw)
|
||||
- [MaxKB 应用案例:一起DeepSeek!福建设计以AI大模型开启新篇章](https://mp.weixin.qq.com/s/m67e-H7iQBg3d24NM82UjA)
|
||||
|
|
@ -19,7 +19,7 @@ class ParagraphPipelineModel:
|
|||
|
||||
def __init__(self, _id: str, document_id: str, dataset_id: str, content: str, title: str, status: str,
|
||||
is_active: bool, comprehensive_score: float, similarity: float, dataset_name: str, document_name: str,
|
||||
hit_handling_method: str, directly_return_similarity: float, meta: dict = None):
|
||||
hit_handling_method: str, directly_return_similarity: float):
|
||||
self.id = _id
|
||||
self.document_id = document_id
|
||||
self.dataset_id = dataset_id
|
||||
|
|
@ -33,7 +33,6 @@ class ParagraphPipelineModel:
|
|||
self.document_name = document_name
|
||||
self.hit_handling_method = hit_handling_method
|
||||
self.directly_return_similarity = directly_return_similarity
|
||||
self.meta = meta
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
|
@ -47,8 +46,7 @@ class ParagraphPipelineModel:
|
|||
'comprehensive_score': self.comprehensive_score,
|
||||
'similarity': self.similarity,
|
||||
'dataset_name': self.dataset_name,
|
||||
'document_name': self.document_name,
|
||||
'meta': self.meta,
|
||||
'document_name': self.document_name
|
||||
}
|
||||
|
||||
class builder:
|
||||
|
|
@ -60,7 +58,6 @@ class ParagraphPipelineModel:
|
|||
self.dataset_name = None
|
||||
self.hit_handling_method = None
|
||||
self.directly_return_similarity = 0.9
|
||||
self.meta = {}
|
||||
|
||||
def add_paragraph(self, paragraph):
|
||||
if isinstance(paragraph, Paragraph):
|
||||
|
|
@ -100,10 +97,6 @@ class ParagraphPipelineModel:
|
|||
self.similarity = similarity
|
||||
return self
|
||||
|
||||
def add_meta(self, meta: dict):
|
||||
self.meta = meta
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')),
|
||||
str(self.paragraph.get('dataset_id')),
|
||||
|
|
@ -111,8 +104,7 @@ class ParagraphPipelineModel:
|
|||
self.paragraph.get('status'),
|
||||
self.paragraph.get('is_active'),
|
||||
self.comprehensive_score, self.similarity, self.dataset_name,
|
||||
self.document_name, self.hit_handling_method, self.directly_return_similarity,
|
||||
self.meta)
|
||||
self.document_name, self.hit_handling_method, self.directly_return_similarity)
|
||||
|
||||
|
||||
class IBaseChatPipelineStep:
|
||||
|
|
|
|||
|
|
@ -11,18 +11,14 @@ from functools import reduce
|
|||
from typing import List, Type, Dict
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep
|
||||
from common.handle.base_to_response import BaseToResponse
|
||||
from common.handle.impl.response.system_to_response import SystemToResponse
|
||||
|
||||
|
||||
class PipelineManage:
|
||||
def __init__(self, step_list: List[Type[IBaseChatPipelineStep]],
|
||||
base_to_response: BaseToResponse = SystemToResponse()):
|
||||
def __init__(self, step_list: List[Type[IBaseChatPipelineStep]]):
|
||||
# 步骤执行器
|
||||
self.step_list = [step() for step in step_list]
|
||||
# 上下文
|
||||
self.context = {'message_tokens': 0, 'answer_tokens': 0}
|
||||
self.base_to_response = base_to_response
|
||||
|
||||
def run(self, context: Dict = None):
|
||||
self.context['start_time'] = time.time()
|
||||
|
|
@ -37,21 +33,13 @@ class PipelineManage:
|
|||
filter(lambda r: r is not None,
|
||||
[row.get_details(self) for row in self.step_list])], {})
|
||||
|
||||
def get_base_to_response(self):
|
||||
return self.base_to_response
|
||||
|
||||
class builder:
|
||||
def __init__(self):
|
||||
self.step_list: List[Type[IBaseChatPipelineStep]] = []
|
||||
self.base_to_response = SystemToResponse()
|
||||
|
||||
def append_step(self, step: Type[IBaseChatPipelineStep]):
|
||||
self.step_list.append(step)
|
||||
return self
|
||||
|
||||
def add_base_to_response(self, base_to_response: BaseToResponse):
|
||||
self.base_to_response = base_to_response
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
return PipelineManage(step_list=self.step_list, base_to_response=self.base_to_response)
|
||||
return PipelineManage(step_list=self.step_list)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Type, List
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from langchain.schema import BaseMessage
|
||||
from rest_framework import serializers
|
||||
|
|
@ -24,7 +23,7 @@ from common.util.field_message import ErrMessage
|
|||
class ModelField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, BaseChatModel):
|
||||
self.fail(_('Model type error'), value=data)
|
||||
self.fail('模型类型错误', value=data)
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
|
|
@ -34,7 +33,7 @@ class ModelField(serializers.Field):
|
|||
class MessageField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, BaseMessage):
|
||||
self.fail(_('Message type error'), value=data)
|
||||
self.fail('message类型错误', value=data)
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
|
|
@ -53,42 +52,33 @@ class IChatStep(IBaseChatPipelineStep):
|
|||
class InstanceSerializer(serializers.Serializer):
|
||||
# 对话列表
|
||||
message_list = serializers.ListField(required=True, child=MessageField(required=True),
|
||||
error_messages=ErrMessage.list(_("Conversation list")))
|
||||
model_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid(_("Model id")))
|
||||
error_messages=ErrMessage.list("对话列表"))
|
||||
# 大语言模型
|
||||
chat_model = ModelField(required=False, allow_null=True, error_messages=ErrMessage.list("大语言模型"))
|
||||
# 段落列表
|
||||
paragraph_list = serializers.ListField(error_messages=ErrMessage.list(_("Paragraph List")))
|
||||
paragraph_list = serializers.ListField(error_messages=ErrMessage.list("段落列表"))
|
||||
# 对话id
|
||||
chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("Conversation ID")))
|
||||
chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("对话id"))
|
||||
# 用户问题
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.uuid(_("User Questions")))
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.uuid("用户问题"))
|
||||
# 后置处理器
|
||||
post_response_handler = InstanceField(model_type=PostResponseHandler,
|
||||
error_messages=ErrMessage.base(_("Post-processor")))
|
||||
error_messages=ErrMessage.base("用户问题"))
|
||||
# 补全问题
|
||||
padding_problem_text = serializers.CharField(required=False,
|
||||
error_messages=ErrMessage.base(_("Completion Question")))
|
||||
padding_problem_text = serializers.CharField(required=False, error_messages=ErrMessage.base("补全问题"))
|
||||
# 是否使用流的形式输出
|
||||
stream = serializers.BooleanField(required=False, error_messages=ErrMessage.base(_("Streaming Output")))
|
||||
client_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Client id")))
|
||||
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Client Type")))
|
||||
stream = serializers.BooleanField(required=False, error_messages=ErrMessage.base("流式输出"))
|
||||
client_id = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端id"))
|
||||
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端类型"))
|
||||
# 未查询到引用分段
|
||||
no_references_setting = NoReferencesSetting(required=True,
|
||||
error_messages=ErrMessage.base(_("No reference segment settings")))
|
||||
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
|
||||
|
||||
model_setting = serializers.DictField(required=True, allow_null=True,
|
||||
error_messages=ErrMessage.dict(_("Model settings")))
|
||||
|
||||
model_params_setting = serializers.DictField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.dict(_("Model parameter settings")))
|
||||
no_references_setting = NoReferencesSetting(required=True, error_messages=ErrMessage.base("无引用分段设置"))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
message_list: List = self.initial_data.get('message_list')
|
||||
for message in message_list:
|
||||
if not isinstance(message, BaseMessage):
|
||||
raise Exception(_("message type error"))
|
||||
raise Exception("message 类型错误")
|
||||
|
||||
def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
|
||||
return self.InstanceSerializer
|
||||
|
|
@ -101,10 +91,9 @@ class IChatStep(IBaseChatPipelineStep):
|
|||
def execute(self, message_list: List[BaseMessage],
|
||||
chat_id, problem_text,
|
||||
post_response_handler: PostResponseHandler,
|
||||
model_id: str = None,
|
||||
user_id: str = None,
|
||||
chat_model: BaseChatModel = None,
|
||||
paragraph_list=None,
|
||||
manage: PipelineManage = None,
|
||||
padding_problem_text: str = None, stream: bool = True, client_id=None, client_type=None,
|
||||
no_references_setting=None, model_params_setting=None, model_setting=None, **kwargs):
|
||||
no_references_setting=None, **kwargs):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
@date:2024/1/9 18:25
|
||||
@desc: 对话step Base实现
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
|
|
@ -14,27 +15,22 @@ from typing import List
|
|||
|
||||
from django.db.models import QuerySet
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from langchain.schema import BaseMessage
|
||||
from langchain.schema.messages import HumanMessage, AIMessage
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from rest_framework import status
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
|
||||
from application.chat_pipeline.pipeline_manage import PipelineManage
|
||||
from application.chat_pipeline.step.chat_step.i_chat_step import IChatStep, PostResponseHandler
|
||||
from application.flow.tools import Reasoning
|
||||
from application.models.api_key_model import ApplicationPublicAccessClient
|
||||
from common.constants.authentication_type import AuthenticationType
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
from common.response import result
|
||||
|
||||
|
||||
def add_access_num(client_id=None, client_type=None, application_id=None):
|
||||
if client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value and application_id is not None:
|
||||
application_public_access_client = (QuerySet(ApplicationPublicAccessClient).filter(client_id=client_id,
|
||||
application_id=application_id)
|
||||
.first())
|
||||
def add_access_num(client_id=None, client_type=None):
|
||||
if client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value:
|
||||
application_public_access_client = QuerySet(ApplicationPublicAccessClient).filter(id=client_id).first()
|
||||
if application_public_access_client is not None:
|
||||
application_public_access_client.access_num = application_public_access_client.access_num + 1
|
||||
application_public_access_client.intraday_access_num = application_public_access_client.intraday_access_num + 1
|
||||
|
|
@ -64,54 +60,14 @@ def event_content(response,
|
|||
problem_text: str,
|
||||
padding_problem_text: str = None,
|
||||
client_id=None, client_type=None,
|
||||
is_ai_chat: bool = None,
|
||||
model_setting=None):
|
||||
if model_setting is None:
|
||||
model_setting = {}
|
||||
reasoning_content_enable = model_setting.get('reasoning_content_enable', False)
|
||||
reasoning_content_start = model_setting.get('reasoning_content_start', '<think>')
|
||||
reasoning_content_end = model_setting.get('reasoning_content_end', '</think>')
|
||||
reasoning = Reasoning(reasoning_content_start,
|
||||
reasoning_content_end)
|
||||
is_ai_chat: bool = None):
|
||||
all_text = ''
|
||||
reasoning_content = ''
|
||||
try:
|
||||
response_reasoning_content = False
|
||||
for chunk in response:
|
||||
reasoning_chunk = reasoning.get_reasoning_content(chunk)
|
||||
content_chunk = reasoning_chunk.get('content')
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
response_reasoning_content = True
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
else:
|
||||
reasoning_content_chunk = reasoning_chunk.get('reasoning_content')
|
||||
all_text += content_chunk
|
||||
if reasoning_content_chunk is None:
|
||||
reasoning_content_chunk = ''
|
||||
reasoning_content += reasoning_content_chunk
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], content_chunk,
|
||||
False,
|
||||
0, 0, {'node_is_end': False,
|
||||
'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node',
|
||||
'real_node_id': 'ai-chat-node',
|
||||
'reasoning_content': reasoning_content_chunk if reasoning_content_enable else ''})
|
||||
reasoning_chunk = reasoning.get_end_reasoning_content()
|
||||
all_text += reasoning_chunk.get('content')
|
||||
reasoning_content_chunk = ""
|
||||
if not response_reasoning_content:
|
||||
reasoning_content_chunk = reasoning_chunk.get(
|
||||
'reasoning_content')
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], reasoning_chunk.get('content'),
|
||||
False,
|
||||
0, 0, {'node_is_end': False,
|
||||
'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node',
|
||||
'real_node_id': 'ai-chat-node',
|
||||
'reasoning_content'
|
||||
: reasoning_content_chunk if reasoning_content_enable else ''})
|
||||
all_text += chunk.content
|
||||
yield 'data: ' + json.dumps({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': chunk.content, 'is_end': False}) + "\n\n"
|
||||
|
||||
# 获取token
|
||||
if is_ai_chat:
|
||||
try:
|
||||
|
|
@ -124,34 +80,20 @@ def event_content(response,
|
|||
request_token = 0
|
||||
response_token = 0
|
||||
write_context(step, manage, request_token, response_token, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, step, padding_problem_text, client_id,
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else ''
|
||||
, asker=asker)
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], '', True,
|
||||
request_token, response_token,
|
||||
{'node_is_end': True, 'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node'})
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
all_text, manage, step, padding_problem_text, client_id)
|
||||
yield 'data: ' + json.dumps({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': '', 'is_end': True}) + "\n\n"
|
||||
add_access_num(client_id, client_type)
|
||||
except Exception as e:
|
||||
logging.getLogger("max_kb_error").error(f'{str(e)}:{traceback.format_exc()}')
|
||||
all_text = 'Exception:' + str(e)
|
||||
all_text = '异常' + str(e)
|
||||
write_context(step, manage, 0, 0, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, step, padding_problem_text, client_id, reasoning_content='',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
|
||||
[], all_text,
|
||||
False,
|
||||
0, 0, {'node_is_end': False,
|
||||
'view_type': 'many_view',
|
||||
'node_type': 'ai-chat-node',
|
||||
'real_node_id': 'ai-chat-node',
|
||||
'reasoning_content': ''})
|
||||
all_text, manage, step, padding_problem_text, client_id)
|
||||
add_access_num(client_id, client_type)
|
||||
yield 'data: ' + json.dumps({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': all_text, 'is_end': True}) + "\n\n"
|
||||
|
||||
|
||||
class BaseChatStep(IChatStep):
|
||||
|
|
@ -159,29 +101,22 @@ class BaseChatStep(IChatStep):
|
|||
chat_id,
|
||||
problem_text,
|
||||
post_response_handler: PostResponseHandler,
|
||||
model_id: str = None,
|
||||
user_id: str = None,
|
||||
chat_model: BaseChatModel = None,
|
||||
paragraph_list=None,
|
||||
manage: PipelineManage = None,
|
||||
padding_problem_text: str = None,
|
||||
stream: bool = True,
|
||||
client_id=None, client_type=None,
|
||||
no_references_setting=None,
|
||||
model_params_setting=None,
|
||||
model_setting=None,
|
||||
**kwargs):
|
||||
chat_model = get_model_instance_by_model_user_id(model_id, user_id,
|
||||
**model_params_setting) if model_id is not None else None
|
||||
if stream:
|
||||
return self.execute_stream(message_list, chat_id, problem_text, post_response_handler, chat_model,
|
||||
paragraph_list,
|
||||
manage, padding_problem_text, client_id, client_type, no_references_setting,
|
||||
model_setting)
|
||||
manage, padding_problem_text, client_id, client_type, no_references_setting)
|
||||
else:
|
||||
return self.execute_block(message_list, chat_id, problem_text, post_response_handler, chat_model,
|
||||
paragraph_list,
|
||||
manage, padding_problem_text, client_id, client_type, no_references_setting,
|
||||
model_setting)
|
||||
manage, padding_problem_text, client_id, client_type, no_references_setting)
|
||||
|
||||
def get_details(self, manage, **kwargs):
|
||||
return {
|
||||
|
|
@ -222,8 +157,7 @@ class BaseChatStep(IChatStep):
|
|||
return iter(
|
||||
[AIMessageChunk(content=no_references_setting.get('value').replace('{question}', problem_text))]), False
|
||||
if chat_model is None:
|
||||
return iter([AIMessageChunk(
|
||||
_('Sorry, the AI model is not configured. Please go to the application to set up the AI model first.'))]), False
|
||||
return iter([AIMessageChunk('抱歉,没有配置 AI 模型,无法优化引用分段,请先去应用中设置 AI 模型。')]), False
|
||||
else:
|
||||
return chat_model.stream(message_list), True
|
||||
|
||||
|
|
@ -236,15 +170,14 @@ class BaseChatStep(IChatStep):
|
|||
manage: PipelineManage = None,
|
||||
padding_problem_text: str = None,
|
||||
client_id=None, client_type=None,
|
||||
no_references_setting=None,
|
||||
model_setting=None):
|
||||
no_references_setting=None):
|
||||
chat_result, is_ai_chat = self.get_stream_result(message_list, chat_model, paragraph_list,
|
||||
no_references_setting, problem_text)
|
||||
chat_record_id = uuid.uuid1()
|
||||
r = StreamingHttpResponse(
|
||||
streaming_content=event_content(chat_result, chat_id, chat_record_id, paragraph_list,
|
||||
post_response_handler, manage, self, chat_model, message_list, problem_text,
|
||||
padding_problem_text, client_id, client_type, is_ai_chat, model_setting),
|
||||
padding_problem_text, client_id, client_type, is_ai_chat),
|
||||
content_type='text/event-stream;charset=utf-8')
|
||||
|
||||
r['Cache-Control'] = 'no-cache'
|
||||
|
|
@ -258,17 +191,17 @@ class BaseChatStep(IChatStep):
|
|||
problem_text=None):
|
||||
if paragraph_list is None:
|
||||
paragraph_list = []
|
||||
directly_return_chunk_list = [AIMessageChunk(content=paragraph.content)
|
||||
for paragraph in paragraph_list if (
|
||||
paragraph.hit_handling_method == 'directly_return' and paragraph.similarity >= paragraph.directly_return_similarity)]
|
||||
|
||||
directly_return_chunk_list = [AIMessage(content=paragraph.content)
|
||||
for paragraph in paragraph_list if
|
||||
paragraph.hit_handling_method == 'directly_return']
|
||||
if directly_return_chunk_list is not None and len(directly_return_chunk_list) > 0:
|
||||
return directly_return_chunk_list[0], False
|
||||
elif len(paragraph_list) == 0 and no_references_setting.get(
|
||||
'status') == 'designated_answer':
|
||||
return AIMessage(no_references_setting.get('value').replace('{question}', problem_text)), False
|
||||
if chat_model is None:
|
||||
return AIMessage(
|
||||
_('Sorry, the AI model is not configured. Please go to the application to set up the AI model first.')), False
|
||||
return AIMessage('抱歉,没有配置 AI 模型,无法优化引用分段,请先去应用中设置 AI 模型。'), False
|
||||
else:
|
||||
return chat_model.invoke(message_list), True
|
||||
|
||||
|
|
@ -280,13 +213,7 @@ class BaseChatStep(IChatStep):
|
|||
paragraph_list=None,
|
||||
manage: PipelineManage = None,
|
||||
padding_problem_text: str = None,
|
||||
client_id=None, client_type=None, no_references_setting=None,
|
||||
model_setting=None):
|
||||
reasoning_content_enable = model_setting.get('reasoning_content_enable', False)
|
||||
reasoning_content_start = model_setting.get('reasoning_content_start', '<think>')
|
||||
reasoning_content_end = model_setting.get('reasoning_content_end', '</think>')
|
||||
reasoning = Reasoning(reasoning_content_start,
|
||||
reasoning_content_end)
|
||||
client_id=None, client_type=None, no_references_setting=None):
|
||||
chat_record_id = uuid.uuid1()
|
||||
# 调用模型
|
||||
try:
|
||||
|
|
@ -299,36 +226,16 @@ class BaseChatStep(IChatStep):
|
|||
request_token = 0
|
||||
response_token = 0
|
||||
write_context(self, manage, request_token, response_token, chat_result.content)
|
||||
reasoning_result = reasoning.get_reasoning_content(chat_result)
|
||||
reasoning_result_end = reasoning.get_end_reasoning_content()
|
||||
content = reasoning_result.get('content') + reasoning_result_end.get('content')
|
||||
if 'reasoning_content' in chat_result.response_metadata:
|
||||
reasoning_content = chat_result.response_metadata.get('reasoning_content', '')
|
||||
else:
|
||||
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get(
|
||||
'reasoning_content')
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
content, manage, self, padding_problem_text, client_id,
|
||||
reasoning_content=reasoning_content if reasoning_content_enable else '',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id),
|
||||
content, True,
|
||||
request_token, response_token,
|
||||
{
|
||||
'reasoning_content': reasoning_content if reasoning_content_enable else '',
|
||||
'answer_list': [{
|
||||
'content': content,
|
||||
'reasoning_content': reasoning_content if reasoning_content_enable else ''
|
||||
}]})
|
||||
chat_result.content, manage, self, padding_problem_text, client_id)
|
||||
add_access_num(client_id, client_type)
|
||||
return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': chat_result.content, 'is_end': True})
|
||||
except Exception as e:
|
||||
all_text = 'Exception:' + str(e)
|
||||
all_text = '异常' + str(e)
|
||||
write_context(self, manage, 0, 0, all_text)
|
||||
asker = manage.context.get('form_data', {}).get('asker', None)
|
||||
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
|
||||
all_text, manage, self, padding_problem_text, client_id, reasoning_content='',
|
||||
asker=asker)
|
||||
add_access_num(client_id, client_type, manage.context.get('application_id'))
|
||||
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id), all_text, True, 0,
|
||||
0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
all_text, manage, self, padding_problem_text, client_id)
|
||||
add_access_num(client_id, client_type)
|
||||
return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': all_text, 'is_end': True})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Type, List
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from langchain.schema import BaseMessage
|
||||
from rest_framework import serializers
|
||||
|
||||
|
|
@ -24,26 +23,24 @@ from common.util.field_message import ErrMessage
|
|||
class IGenerateHumanMessageStep(IBaseChatPipelineStep):
|
||||
class InstanceSerializer(serializers.Serializer):
|
||||
# 问题
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char(_("question")))
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char("问题"))
|
||||
# 段落列表
|
||||
paragraph_list = serializers.ListField(child=InstanceField(model_type=ParagraphPipelineModel, required=True),
|
||||
error_messages=ErrMessage.list(_("Paragraph List")))
|
||||
error_messages=ErrMessage.list("段落列表"))
|
||||
# 历史对答
|
||||
history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
|
||||
error_messages=ErrMessage.list(_("History Questions")))
|
||||
error_messages=ErrMessage.list("历史对答"))
|
||||
# 多轮对话数量
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer("多轮对话数量"))
|
||||
# 最大携带知识库段落长度
|
||||
max_paragraph_char_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(
|
||||
_("Maximum length of the knowledge base paragraph")))
|
||||
"最大携带知识库段落长度"))
|
||||
# 模板
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
|
||||
system = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
error_messages=ErrMessage.char(_("System prompt words (role)")))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char("提示词"))
|
||||
# 补齐问题
|
||||
padding_problem_text = serializers.CharField(required=False, error_messages=ErrMessage.char(_("Completion problem")))
|
||||
padding_problem_text = serializers.CharField(required=False, error_messages=ErrMessage.char("补齐问题"))
|
||||
# 未查询到引用分段
|
||||
no_references_setting = NoReferencesSetting(required=True, error_messages=ErrMessage.base(_("No reference segment settings")))
|
||||
no_references_setting = NoReferencesSetting(required=True, error_messages=ErrMessage.base("无引用分段设置"))
|
||||
|
||||
def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
|
||||
return self.InstanceSerializer
|
||||
|
|
@ -62,7 +59,6 @@ class IGenerateHumanMessageStep(IBaseChatPipelineStep):
|
|||
prompt: str,
|
||||
padding_problem_text: str = None,
|
||||
no_references_setting=None,
|
||||
system=None,
|
||||
**kwargs) -> List[BaseMessage]:
|
||||
"""
|
||||
|
||||
|
|
@ -75,7 +71,6 @@ class IGenerateHumanMessageStep(IBaseChatPipelineStep):
|
|||
:param padding_problem_text 用户修改文本
|
||||
:param kwargs: 其他参数
|
||||
:param no_references_setting: 无引用分段设置
|
||||
:param system 系统提示称
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
from typing import List, Dict
|
||||
|
||||
from langchain.schema import BaseMessage, HumanMessage
|
||||
from langchain_core.messages import SystemMessage
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
|
||||
from application.chat_pipeline.step.generate_human_message_step.i_generate_human_message_step import \
|
||||
|
|
@ -28,7 +27,6 @@ class BaseGenerateHumanMessageStep(IGenerateHumanMessageStep):
|
|||
prompt: str,
|
||||
padding_problem_text: str = None,
|
||||
no_references_setting=None,
|
||||
system=None,
|
||||
**kwargs) -> List[BaseMessage]:
|
||||
prompt = prompt if (paragraph_list is not None and len(paragraph_list) > 0) else no_references_setting.get(
|
||||
'value')
|
||||
|
|
@ -37,11 +35,6 @@ class BaseGenerateHumanMessageStep(IGenerateHumanMessageStep):
|
|||
history_message = [[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))]
|
||||
if system is not None and len(system) > 0:
|
||||
return [SystemMessage(system), *flat_map(history_message),
|
||||
self.to_human_message(prompt, exec_problem_text, max_paragraph_char_number, paragraph_list,
|
||||
no_references_setting)]
|
||||
|
||||
return [*flat_map(history_message),
|
||||
self.to_human_message(prompt, exec_problem_text, max_paragraph_char_number, paragraph_list,
|
||||
no_references_setting)]
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Type, List
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep
|
||||
from application.chat_pipeline.pipeline_manage import PipelineManage
|
||||
from application.chat_pipeline.step.chat_step.i_chat_step import ModelField
|
||||
from application.models import ChatRecord
|
||||
from common.field.common import InstanceField
|
||||
from common.util.field_message import ErrMessage
|
||||
|
|
@ -22,16 +23,12 @@ from common.util.field_message import ErrMessage
|
|||
class IResetProblemStep(IBaseChatPipelineStep):
|
||||
class InstanceSerializer(serializers.Serializer):
|
||||
# 问题文本
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.float(_("question")))
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.float("问题文本"))
|
||||
# 历史对答
|
||||
history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
|
||||
error_messages=ErrMessage.list(_("History Questions")))
|
||||
error_messages=ErrMessage.list("历史对答"))
|
||||
# 大语言模型
|
||||
model_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid(_("Model id")))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
|
||||
problem_optimization_prompt = serializers.CharField(required=False, max_length=102400,
|
||||
error_messages=ErrMessage.char(
|
||||
_("Question completion prompt")))
|
||||
chat_model = ModelField(required=False, allow_null=True, error_messages=ErrMessage.base("大语言模型"))
|
||||
|
||||
def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
|
||||
return self.InstanceSerializer
|
||||
|
|
@ -45,13 +42,10 @@ class IResetProblemStep(IBaseChatPipelineStep):
|
|||
manage.context['problem_text'] = source_problem_text
|
||||
manage.context['padding_problem_text'] = padding_problem
|
||||
# 累加tokens
|
||||
manage.context['message_tokens'] = manage.context.get('message_tokens', 0) + self.context.get('message_tokens',
|
||||
0)
|
||||
manage.context['answer_tokens'] = manage.context.get('answer_tokens', 0) + self.context.get('answer_tokens', 0)
|
||||
manage.context['message_tokens'] = manage.context['message_tokens'] + self.context.get('message_tokens')
|
||||
manage.context['answer_tokens'] = manage.context['answer_tokens'] + self.context.get('answer_tokens')
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
|
||||
problem_optimization_prompt=None,
|
||||
user_id=None,
|
||||
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, chat_model: BaseChatModel = None,
|
||||
**kwargs):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -8,33 +8,30 @@
|
|||
"""
|
||||
from typing import List
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from langchain.schema import HumanMessage
|
||||
|
||||
from application.chat_pipeline.step.reset_problem_step.i_reset_problem_step import IResetProblemStep
|
||||
from application.models import ChatRecord
|
||||
from common.util.split_model import flat_map
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
prompt = _(
|
||||
"() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the <data></data> tag")
|
||||
prompt = (
|
||||
'()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在<data></data>标签中')
|
||||
|
||||
|
||||
class BaseResetProblemStep(IResetProblemStep):
|
||||
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
|
||||
problem_optimization_prompt=None,
|
||||
user_id=None,
|
||||
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, chat_model: BaseChatModel = None,
|
||||
**kwargs) -> str:
|
||||
chat_model = get_model_instance_by_model_user_id(model_id, user_id) if model_id is not None else None
|
||||
if chat_model is None:
|
||||
self.context['message_tokens'] = 0
|
||||
self.context['answer_tokens'] = 0
|
||||
return problem_text
|
||||
start_index = len(history_chat_record) - 3
|
||||
history_message = [[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))]
|
||||
reset_prompt = problem_optimization_prompt if problem_optimization_prompt else prompt
|
||||
message_list = [*flat_map(history_message),
|
||||
HumanMessage(content=reset_prompt.replace('{question}', problem_text))]
|
||||
HumanMessage(content=prompt.format(**{'question': problem_text}))]
|
||||
response = chat_model.invoke(message_list)
|
||||
padding_problem = problem_text
|
||||
if response.content.__contains__("<data>") and response.content.__contains__('</data>'):
|
||||
|
|
@ -42,9 +39,6 @@ class BaseResetProblemStep(IResetProblemStep):
|
|||
response.content.index('<data>') + 6:response.content.index('</data>')]
|
||||
if padding_problem_data is not None and len(padding_problem_data.strip()) > 0:
|
||||
padding_problem = padding_problem_data
|
||||
elif len(response.content) > 0:
|
||||
padding_problem = response.content
|
||||
|
||||
try:
|
||||
request_token = chat_model.get_num_tokens_from_messages(message_list)
|
||||
response_token = chat_model.get_num_tokens(padding_problem)
|
||||
|
|
@ -60,8 +54,8 @@ class BaseResetProblemStep(IResetProblemStep):
|
|||
'step_type': 'problem_padding',
|
||||
'run_time': self.context['run_time'],
|
||||
'model_id': str(manage.context['model_id']) if 'model_id' in manage.context else None,
|
||||
'message_tokens': self.context.get('message_tokens', 0),
|
||||
'answer_tokens': self.context.get('answer_tokens', 0),
|
||||
'message_tokens': self.context['message_tokens'],
|
||||
'answer_tokens': self.context['answer_tokens'],
|
||||
'cost': 0,
|
||||
'padding_problem_text': self.context.get('padding_problem_text'),
|
||||
'problem_text': self.context.get("step_args").get('problem_text'),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from abc import abstractmethod
|
|||
from typing import List, Type
|
||||
|
||||
from django.core import validators
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
|
||||
|
|
@ -22,30 +21,28 @@ from common.util.field_message import ErrMessage
|
|||
class ISearchDatasetStep(IBaseChatPipelineStep):
|
||||
class InstanceSerializer(serializers.Serializer):
|
||||
# 原始问题文本
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char(_("question")))
|
||||
problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char("问题"))
|
||||
# 系统补全问题文本
|
||||
padding_problem_text = serializers.CharField(required=False,
|
||||
error_messages=ErrMessage.char(_("System completes question text")))
|
||||
padding_problem_text = serializers.CharField(required=False, error_messages=ErrMessage.char("系统补全问题文本"))
|
||||
# 需要查询的数据集id列表
|
||||
dataset_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
|
||||
error_messages=ErrMessage.list(_("Dataset id list")))
|
||||
error_messages=ErrMessage.list("数据集id列表"))
|
||||
# 需要排除的文档id
|
||||
exclude_document_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
|
||||
error_messages=ErrMessage.list(_("List of document ids to exclude")))
|
||||
error_messages=ErrMessage.list("排除的文档id列表"))
|
||||
# 需要排除向量id
|
||||
exclude_paragraph_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
|
||||
error_messages=ErrMessage.list(_("List of exclusion vector ids")))
|
||||
error_messages=ErrMessage.list("排除向量id列表"))
|
||||
# 需要查询的条数
|
||||
top_n = serializers.IntegerField(required=True,
|
||||
error_messages=ErrMessage.integer(_("Reference segment number")))
|
||||
error_messages=ErrMessage.integer("引用分段数"))
|
||||
# 相似度 0-1之间
|
||||
similarity = serializers.FloatField(required=True, max_value=1, min_value=0,
|
||||
error_messages=ErrMessage.float(_("Similarity")))
|
||||
error_messages=ErrMessage.float("引用分段数"))
|
||||
search_mode = serializers.CharField(required=True, validators=[
|
||||
validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
|
||||
message=_("The type only supports embedding|keywords|blend"), code=500)
|
||||
], error_messages=ErrMessage.char(_("Retrieval Mode")))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
|
||||
message="类型只支持register|reset_password", code=500)
|
||||
], error_messages=ErrMessage.char("检索模式"))
|
||||
|
||||
def get_step_serializer(self, manage: PipelineManage) -> Type[InstanceSerializer]:
|
||||
return self.InstanceSerializer
|
||||
|
|
@ -59,7 +56,6 @@ class ISearchDatasetStep(IBaseChatPipelineStep):
|
|||
def execute(self, problem_text: str, dataset_id_list: list[str], exclude_document_id_list: list[str],
|
||||
exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
|
||||
search_mode: str = None,
|
||||
user_id=None,
|
||||
**kwargs) -> List[ParagraphPipelineModel]:
|
||||
"""
|
||||
关于 用户和补全问题 说明: 补全问题如果有就使用补全问题去查询 反之就用用户原始问题查询
|
||||
|
|
@ -71,7 +67,6 @@ class ISearchDatasetStep(IBaseChatPipelineStep):
|
|||
:param exclude_paragraph_id_list: 需要排除段落id
|
||||
:param padding_problem_text 补全问题
|
||||
:param search_mode 检索模式
|
||||
:param user_id 用户id
|
||||
:return: 段落列表
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -10,54 +10,25 @@ import os
|
|||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
|
||||
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
|
||||
from application.chat_pipeline.step.search_dataset_step.i_search_dataset_step import ISearchDatasetStep
|
||||
from common.config.embedding_config import VectorStore, ModelManage
|
||||
from common.config.embedding_config import VectorStore, EmbeddingModel
|
||||
from common.db.search import native_search
|
||||
from common.util.file_util import get_file_content
|
||||
from dataset.models import Paragraph, DataSet
|
||||
from dataset.models import Paragraph
|
||||
from embedding.models import SearchMode
|
||||
from setting.models import Model
|
||||
from setting.models_provider import get_model
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
|
||||
|
||||
def get_model_by_id(_id, user_id):
|
||||
model = QuerySet(Model).filter(id=_id).first()
|
||||
if model is None:
|
||||
raise Exception(_("Model does not exist"))
|
||||
if model.permission_type == 'PRIVATE' and str(model.user_id) != str(user_id):
|
||||
message = lazy_format(_('No permission to use this model {model_name}'), model_name=model.name)
|
||||
raise Exception(message)
|
||||
return model
|
||||
|
||||
|
||||
def get_embedding_id(dataset_id_list):
|
||||
dataset_list = QuerySet(DataSet).filter(id__in=dataset_id_list)
|
||||
if len(set([dataset.embedding_mode_id for dataset in dataset_list])) > 1:
|
||||
raise Exception(_("The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled."))
|
||||
if len(dataset_list) == 0:
|
||||
raise Exception(_("The knowledge base setting is wrong, please reset the knowledge base"))
|
||||
return dataset_list[0].embedding_mode_id
|
||||
|
||||
|
||||
class BaseSearchDatasetStep(ISearchDatasetStep):
|
||||
|
||||
def execute(self, problem_text: str, dataset_id_list: list[str], exclude_document_id_list: list[str],
|
||||
exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
|
||||
search_mode: str = None,
|
||||
user_id=None,
|
||||
**kwargs) -> List[ParagraphPipelineModel]:
|
||||
if len(dataset_id_list) == 0:
|
||||
return []
|
||||
exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
|
||||
model_id = get_embedding_id(dataset_id_list)
|
||||
model = get_model_by_id(model_id, user_id)
|
||||
self.context['model_name'] = model.name
|
||||
embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model))
|
||||
embedding_model = EmbeddingModel.get_embedding_model()
|
||||
embedding_value = embedding_model.embed_query(exec_problem_text)
|
||||
vector = VectorStore.get_embedding_vector()
|
||||
embedding_list = vector.query(exec_problem_text, embedding_value, dataset_id_list, exclude_document_id_list,
|
||||
|
|
@ -82,7 +53,6 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
|
|||
.add_document_name(paragraph.get('document_name'))
|
||||
.add_hit_handling_method(paragraph.get('hit_handling_method'))
|
||||
.add_directly_return_similarity(paragraph.get('directly_return_similarity'))
|
||||
.add_meta(paragraph.get('meta'))
|
||||
.build())
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -131,7 +101,7 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
|
|||
'run_time': self.context['run_time'],
|
||||
'problem_text': step_args.get(
|
||||
'padding_problem_text') if 'padding_problem_text' in step_args else step_args.get('problem_text'),
|
||||
'model_name': self.context.get('model_name'),
|
||||
'model_name': EmbeddingModel.get_embedding_model().model_name,
|
||||
'message_tokens': 0,
|
||||
'answer_tokens': 0,
|
||||
'cost': 0
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: common.py
|
||||
@date:2024/12/11 17:57
|
||||
@desc:
|
||||
"""
|
||||
|
||||
|
||||
class Answer:
|
||||
def __init__(self, content, view_type, runtime_node_id, chat_record_id, child_node, real_node_id,
|
||||
reasoning_content):
|
||||
self.view_type = view_type
|
||||
self.content = content
|
||||
self.reasoning_content = reasoning_content
|
||||
self.runtime_node_id = runtime_node_id
|
||||
self.chat_record_id = chat_record_id
|
||||
self.child_node = child_node
|
||||
self.real_node_id = real_node_id
|
||||
|
||||
def to_dict(self):
|
||||
return {'view_type': self.view_type, 'content': self.content, 'runtime_node_id': self.runtime_node_id,
|
||||
'chat_record_id': self.chat_record_id,
|
||||
'child_node': self.child_node,
|
||||
'reasoning_content': self.reasoning_content,
|
||||
'real_node_id': self.real_node_id}
|
||||
|
||||
|
||||
class NodeChunk:
|
||||
def __init__(self):
|
||||
self.status = 0
|
||||
self.chunk_list = []
|
||||
|
||||
def add_chunk(self, chunk):
|
||||
self.chunk_list.append(chunk)
|
||||
|
||||
def end(self, chunk=None):
|
||||
if chunk is not None:
|
||||
self.add_chunk(chunk)
|
||||
self.status = 200
|
||||
|
||||
def is_end(self):
|
||||
return self.status == 200
|
||||
|
|
@ -3,29 +3,24 @@
|
|||
{
|
||||
"id": "base-node",
|
||||
"type": "base-node",
|
||||
"x": 360,
|
||||
"y": 2810,
|
||||
"x": 440,
|
||||
"y": 3350,
|
||||
"properties": {
|
||||
"config": {
|
||||
|
||||
},
|
||||
"height": 825.6,
|
||||
"config": {},
|
||||
"height": 517,
|
||||
"stepName": "基本信息",
|
||||
"node_data": {
|
||||
"desc": "",
|
||||
"name": "maxkbapplication",
|
||||
"name": "",
|
||||
"prologue": "您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。\n- MaxKB 主要功能有什么?\n- MaxKB 支持哪些大语言模型?\n- MaxKB 支持哪些文档类型?"
|
||||
},
|
||||
"input_field_list": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "start-node",
|
||||
"type": "start-node",
|
||||
"x": 430,
|
||||
"y": 3660,
|
||||
"x": 440,
|
||||
"y": 3710,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
|
|
@ -36,8 +31,8 @@
|
|||
],
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
"value": "time",
|
||||
"label": "当前时间"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -47,7 +42,7 @@
|
|||
"value": "question"
|
||||
}
|
||||
],
|
||||
"height": 276,
|
||||
"height": 268.533,
|
||||
"stepName": "开始",
|
||||
"globalFields": [
|
||||
{
|
||||
|
|
@ -60,8 +55,8 @@
|
|||
{
|
||||
"id": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"type": "search-dataset-node",
|
||||
"x": 840,
|
||||
"y": 3210,
|
||||
"x": 830,
|
||||
"y": 3470,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
|
|
@ -83,12 +78,10 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"height": 794,
|
||||
"height": 754.8,
|
||||
"stepName": "知识库检索",
|
||||
"node_data": {
|
||||
"dataset_id_list": [
|
||||
|
||||
],
|
||||
"dataset_id_list": [],
|
||||
"dataset_setting": {
|
||||
"top_n": 3,
|
||||
"similarity": 0.6,
|
||||
|
|
@ -98,9 +91,6 @@
|
|||
"question_reference_address": [
|
||||
"start-node",
|
||||
"question"
|
||||
],
|
||||
"source_dataset_id_list": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -108,8 +98,8 @@
|
|||
{
|
||||
"id": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"type": "condition-node",
|
||||
"x": 1490,
|
||||
"y": 3210,
|
||||
"x": 1380,
|
||||
"y": 3470,
|
||||
"properties": {
|
||||
"width": 600,
|
||||
"config": {
|
||||
|
|
@ -120,7 +110,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"height": 543.675,
|
||||
"height": 524.6669999999999,
|
||||
"stepName": "判断器",
|
||||
"node_data": {
|
||||
"branch": [
|
||||
|
|
@ -158,26 +148,24 @@
|
|||
"id": "161",
|
||||
"type": "ELSE",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
|
||||
]
|
||||
"conditions": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"branch_condition_list": [
|
||||
{
|
||||
"index": 0,
|
||||
"height": 121.225,
|
||||
"height": 116.133,
|
||||
"id": "1009"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"height": 121.225,
|
||||
"height": 116.133,
|
||||
"id": "4908"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"height": 44,
|
||||
"height": 40,
|
||||
"id": "161"
|
||||
}
|
||||
]
|
||||
|
|
@ -186,8 +174,8 @@
|
|||
{
|
||||
"id": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"type": "reply-node",
|
||||
"x": 2170,
|
||||
"y": 2480,
|
||||
"x": 2090,
|
||||
"y": 2820,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
|
|
@ -197,7 +185,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"height": 378,
|
||||
"height": 312.267,
|
||||
"stepName": "指定回复",
|
||||
"node_data": {
|
||||
"fields": [
|
||||
|
|
@ -205,16 +193,15 @@
|
|||
"directly_return"
|
||||
],
|
||||
"content": "",
|
||||
"reply_type": "referencing",
|
||||
"is_result": true
|
||||
"reply_type": "referencing"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3200,
|
||||
"x": 2090,
|
||||
"y": 3460,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
|
|
@ -224,22 +211,21 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"height": 681.4,
|
||||
"stepName": "AI 对话",
|
||||
"node_data": {
|
||||
"prompt": "已知信息:\n{{知识库检索.data}}\n问题:\n{{开始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
"dialogue_number": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3970,
|
||||
"x": 2090,
|
||||
"y": 4180,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
|
|
@ -249,14 +235,13 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"height": 681.4,
|
||||
"stepName": "AI 对话1",
|
||||
"node_data": {
|
||||
"prompt": "{{开始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
"dialogue_number": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,32 +253,30 @@
|
|||
"sourceNodeId": "start-node",
|
||||
"targetNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"startPoint": {
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
"x": 600,
|
||||
"y": 3710
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
"x": 670,
|
||||
"y": 3470
|
||||
},
|
||||
"properties": {},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
"x": 600,
|
||||
"y": 3710
|
||||
},
|
||||
{
|
||||
"x": 700,
|
||||
"y": 3660
|
||||
"x": 710,
|
||||
"y": 3710
|
||||
},
|
||||
{
|
||||
"x": 570,
|
||||
"y": 3210
|
||||
"x": 560,
|
||||
"y": 3470
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
"x": 670,
|
||||
"y": 3470
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "start-node_right",
|
||||
|
|
@ -305,32 +288,30 @@
|
|||
"sourceNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"targetNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"startPoint": {
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
"x": 990,
|
||||
"y": 3470
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
"x": 1090,
|
||||
"y": 3470
|
||||
},
|
||||
"properties": {},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
"x": 990,
|
||||
"y": 3470
|
||||
},
|
||||
{
|
||||
"x": 1110,
|
||||
"y": 3210
|
||||
"x": 1100,
|
||||
"y": 3470
|
||||
},
|
||||
{
|
||||
"x": 980,
|
||||
"y": 3470
|
||||
},
|
||||
{
|
||||
"x": 1090,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
"y": 3470
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_right",
|
||||
|
|
@ -342,32 +323,30 @@
|
|||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
"x": 1670,
|
||||
"y": 3340.733
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
},
|
||||
"properties": {
|
||||
|
||||
"x": 1930,
|
||||
"y": 2820
|
||||
},
|
||||
"properties": {},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1670,
|
||||
"y": 3340.733
|
||||
},
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
"y": 3340.733
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3073.775
|
||||
"x": 1820,
|
||||
"y": 2820
|
||||
},
|
||||
{
|
||||
"x": 1900,
|
||||
"y": 2480
|
||||
},
|
||||
{
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
"x": 1930,
|
||||
"y": 2820
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_1009_right",
|
||||
|
|
@ -379,32 +358,30 @@
|
|||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
"x": 1670,
|
||||
"y": 3464.866
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
},
|
||||
"properties": {
|
||||
|
||||
"x": 1930,
|
||||
"y": 3460
|
||||
},
|
||||
"properties": {},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1670,
|
||||
"y": 3464.866
|
||||
},
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
"y": 3464.866
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3203
|
||||
"x": 1820,
|
||||
"y": 3460
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3200
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
"x": 1930,
|
||||
"y": 3460
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_4908_right",
|
||||
|
|
@ -416,32 +393,30 @@
|
|||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
"x": 1670,
|
||||
"y": 3550.9325000000003
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
},
|
||||
"properties": {
|
||||
|
||||
"x": 1930,
|
||||
"y": 4180
|
||||
},
|
||||
"properties": {},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1670,
|
||||
"y": 3550.9325000000003
|
||||
},
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
"y": 3550.9325000000003
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3293.6124999999997
|
||||
"x": 1820,
|
||||
"y": 4180
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3970
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
"x": 1930,
|
||||
"y": 4180
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_161_right",
|
||||
|
|
|
|||
|
|
@ -1,451 +0,0 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "base-node",
|
||||
"type": "base-node",
|
||||
"x": 360,
|
||||
"y": 2810,
|
||||
"properties": {
|
||||
"config": {
|
||||
|
||||
},
|
||||
"height": 825.6,
|
||||
"stepName": "Base",
|
||||
"node_data": {
|
||||
"desc": "",
|
||||
"name": "maxkbapplication",
|
||||
"prologue": "Hello, I am the MaxKB assistant. You can ask me about MaxKB usage issues.\n-What are the main functions of MaxKB?\n-What major language models does MaxKB support?\n-What document types does MaxKB support?"
|
||||
},
|
||||
"input_field_list": [
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "start-node",
|
||||
"type": "start-node",
|
||||
"x": 430,
|
||||
"y": 3660,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"height": 276,
|
||||
"stepName": "Start",
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"type": "search-dataset-node",
|
||||
"x": 840,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "检索结果的分段列表",
|
||||
"value": "paragraph_list"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段列表",
|
||||
"value": "is_hit_handling_method_list"
|
||||
},
|
||||
{
|
||||
"label": "检索结果",
|
||||
"value": "data"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段内容",
|
||||
"value": "directly_return"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 794,
|
||||
"stepName": "Knowledge Search",
|
||||
"node_data": {
|
||||
"dataset_id_list": [
|
||||
|
||||
],
|
||||
"dataset_setting": {
|
||||
"top_n": 3,
|
||||
"similarity": 0.6,
|
||||
"search_mode": "embedding",
|
||||
"max_paragraph_char_number": 5000
|
||||
},
|
||||
"question_reference_address": [
|
||||
"start-node",
|
||||
"question"
|
||||
],
|
||||
"source_dataset_id_list": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"type": "condition-node",
|
||||
"x": 1490,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"width": 600,
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "分支名称",
|
||||
"value": "branch_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 543.675,
|
||||
"stepName": "Conditional Branch",
|
||||
"node_data": {
|
||||
"branch": [
|
||||
{
|
||||
"id": "1009",
|
||||
"type": "IF",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"is_hit_handling_method_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4908",
|
||||
"type": "ELSE IF 1",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"paragraph_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "161",
|
||||
"type": "ELSE",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"branch_condition_list": [
|
||||
{
|
||||
"index": 0,
|
||||
"height": 121.225,
|
||||
"id": "1009"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"height": 121.225,
|
||||
"id": "4908"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"height": 44,
|
||||
"id": "161"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"type": "reply-node",
|
||||
"x": 2170,
|
||||
"y": 2480,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 378,
|
||||
"stepName": "Specified Reply",
|
||||
"node_data": {
|
||||
"fields": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"directly_return"
|
||||
],
|
||||
"content": "",
|
||||
"reply_type": "referencing",
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3200,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI Chat",
|
||||
"node_data": {
|
||||
"prompt": "Known information:\n{{Knowledge Search.data}}\nQuestion:\n{{Start.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3970,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI Chat1",
|
||||
"node_data": {
|
||||
"prompt": "{{Start.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "7d0f166f-c472-41b2-b9a2-c294f4c83d73",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "start-node",
|
||||
"targetNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"startPoint": {
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 700,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 570,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "start-node_right",
|
||||
"targetAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_left"
|
||||
},
|
||||
{
|
||||
"id": "35cb86dd-f328-429e-a973-12fd7218b696",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"targetNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"startPoint": {
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1110,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1090,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_right",
|
||||
"targetAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_left"
|
||||
},
|
||||
{
|
||||
"id": "e8f6cfe6-7e48-41cd-abd3-abfb5304d0d8",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1900,
|
||||
"y": 2480
|
||||
},
|
||||
{
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_1009_right",
|
||||
"targetAnchorId": "4ffe1086-25df-4c85-b168-979b5bbf0a26_left"
|
||||
},
|
||||
{
|
||||
"id": "994ff325-6f7a-4ebc-b61b-10e15519d6d2",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3200
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_4908_right",
|
||||
"targetAnchorId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb_left"
|
||||
},
|
||||
{
|
||||
"id": "19270caf-bb9f-4ba7-9bf8-200aa70fecd5",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3970
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_161_right",
|
||||
"targetAnchorId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7_left"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "base-node",
|
||||
"type": "base-node",
|
||||
"x": 360,
|
||||
"y": 2810,
|
||||
"properties": {
|
||||
"config": {
|
||||
|
||||
},
|
||||
"height": 825.6,
|
||||
"stepName": "基本信息",
|
||||
"node_data": {
|
||||
"desc": "",
|
||||
"name": "maxkbapplication",
|
||||
"prologue": "您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。\n- MaxKB 主要功能有什么?\n- MaxKB 支持哪些大语言模型?\n- MaxKB 支持哪些文档类型?"
|
||||
},
|
||||
"input_field_list": [
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "start-node",
|
||||
"type": "start-node",
|
||||
"x": 430,
|
||||
"y": 3660,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"height": 276,
|
||||
"stepName": "开始",
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"type": "search-dataset-node",
|
||||
"x": 840,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "检索结果的分段列表",
|
||||
"value": "paragraph_list"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段列表",
|
||||
"value": "is_hit_handling_method_list"
|
||||
},
|
||||
{
|
||||
"label": "检索结果",
|
||||
"value": "data"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段内容",
|
||||
"value": "directly_return"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 794,
|
||||
"stepName": "知识库检索",
|
||||
"node_data": {
|
||||
"dataset_id_list": [
|
||||
|
||||
],
|
||||
"dataset_setting": {
|
||||
"top_n": 3,
|
||||
"similarity": 0.6,
|
||||
"search_mode": "embedding",
|
||||
"max_paragraph_char_number": 5000
|
||||
},
|
||||
"question_reference_address": [
|
||||
"start-node",
|
||||
"question"
|
||||
],
|
||||
"source_dataset_id_list": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"type": "condition-node",
|
||||
"x": 1490,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"width": 600,
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "分支名称",
|
||||
"value": "branch_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 543.675,
|
||||
"stepName": "判断器",
|
||||
"node_data": {
|
||||
"branch": [
|
||||
{
|
||||
"id": "1009",
|
||||
"type": "IF",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"is_hit_handling_method_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4908",
|
||||
"type": "ELSE IF 1",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"paragraph_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "161",
|
||||
"type": "ELSE",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"branch_condition_list": [
|
||||
{
|
||||
"index": 0,
|
||||
"height": 121.225,
|
||||
"id": "1009"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"height": 121.225,
|
||||
"id": "4908"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"height": 44,
|
||||
"id": "161"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"type": "reply-node",
|
||||
"x": 2170,
|
||||
"y": 2480,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 378,
|
||||
"stepName": "指定回复",
|
||||
"node_data": {
|
||||
"fields": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"directly_return"
|
||||
],
|
||||
"content": "",
|
||||
"reply_type": "referencing",
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3200,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI 对话",
|
||||
"node_data": {
|
||||
"prompt": "已知信息:\n{{知识库检索.data}}\n问题:\n{{开始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3970,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI 对话1",
|
||||
"node_data": {
|
||||
"prompt": "{{开始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "7d0f166f-c472-41b2-b9a2-c294f4c83d73",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "start-node",
|
||||
"targetNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"startPoint": {
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 700,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 570,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "start-node_right",
|
||||
"targetAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_left"
|
||||
},
|
||||
{
|
||||
"id": "35cb86dd-f328-429e-a973-12fd7218b696",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"targetNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"startPoint": {
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1110,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1090,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_right",
|
||||
"targetAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_left"
|
||||
},
|
||||
{
|
||||
"id": "e8f6cfe6-7e48-41cd-abd3-abfb5304d0d8",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1900,
|
||||
"y": 2480
|
||||
},
|
||||
{
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_1009_right",
|
||||
"targetAnchorId": "4ffe1086-25df-4c85-b168-979b5bbf0a26_left"
|
||||
},
|
||||
{
|
||||
"id": "994ff325-6f7a-4ebc-b61b-10e15519d6d2",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3200
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_4908_right",
|
||||
"targetAnchorId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb_left"
|
||||
},
|
||||
{
|
||||
"id": "19270caf-bb9f-4ba7-9bf8-200aa70fecd5",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3970
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_161_right",
|
||||
"targetAnchorId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7_left"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "base-node",
|
||||
"type": "base-node",
|
||||
"x": 360,
|
||||
"y": 2810,
|
||||
"properties": {
|
||||
"config": {
|
||||
|
||||
},
|
||||
"height": 825.6,
|
||||
"stepName": "基本資訊",
|
||||
"node_data": {
|
||||
"desc": "",
|
||||
"name": "maxkbapplication",
|
||||
"prologue": "您好,我是MaxKB小助手,您可以向我提出MaxKB使用問題。\n- MaxKB主要功能有什麼?\n- MaxKB支持哪些大語言模型?\n- MaxKB支持哪些文檔類型?"
|
||||
},
|
||||
"input_field_list": [
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "start-node",
|
||||
"type": "start-node",
|
||||
"x": 430,
|
||||
"y": 3660,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"label": "用户问题",
|
||||
"value": "question"
|
||||
}
|
||||
],
|
||||
"height": 276,
|
||||
"stepName": "開始",
|
||||
"globalFields": [
|
||||
{
|
||||
"label": "当前时间",
|
||||
"value": "time"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"type": "search-dataset-node",
|
||||
"x": 840,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "检索结果的分段列表",
|
||||
"value": "paragraph_list"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段列表",
|
||||
"value": "is_hit_handling_method_list"
|
||||
},
|
||||
{
|
||||
"label": "检索结果",
|
||||
"value": "data"
|
||||
},
|
||||
{
|
||||
"label": "满足直接回答的分段内容",
|
||||
"value": "directly_return"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 794,
|
||||
"stepName": "知識庫檢索",
|
||||
"node_data": {
|
||||
"dataset_id_list": [
|
||||
|
||||
],
|
||||
"dataset_setting": {
|
||||
"top_n": 3,
|
||||
"similarity": 0.6,
|
||||
"search_mode": "embedding",
|
||||
"max_paragraph_char_number": 5000
|
||||
},
|
||||
"question_reference_address": [
|
||||
"start-node",
|
||||
"question"
|
||||
],
|
||||
"source_dataset_id_list": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"type": "condition-node",
|
||||
"x": 1490,
|
||||
"y": 3210,
|
||||
"properties": {
|
||||
"width": 600,
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "分支名称",
|
||||
"value": "branch_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 543.675,
|
||||
"stepName": "判斷器",
|
||||
"node_data": {
|
||||
"branch": [
|
||||
{
|
||||
"id": "1009",
|
||||
"type": "IF",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"is_hit_handling_method_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4908",
|
||||
"type": "ELSE IF 1",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"field": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"paragraph_list"
|
||||
],
|
||||
"value": "1",
|
||||
"compare": "len_ge"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "161",
|
||||
"type": "ELSE",
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"branch_condition_list": [
|
||||
{
|
||||
"index": 0,
|
||||
"height": 121.225,
|
||||
"id": "1009"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"height": 121.225,
|
||||
"id": "4908"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"height": 44,
|
||||
"id": "161"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"type": "reply-node",
|
||||
"x": 2170,
|
||||
"y": 2480,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 378,
|
||||
"stepName": "指定回覆",
|
||||
"node_data": {
|
||||
"fields": [
|
||||
"b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"directly_return"
|
||||
],
|
||||
"content": "",
|
||||
"reply_type": "referencing",
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3200,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI 對話",
|
||||
"node_data": {
|
||||
"prompt": "已知資訊:\n{{知識庫檢索.data}}\n問題:\n{{開始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"type": "ai-chat-node",
|
||||
"x": 2160,
|
||||
"y": 3970,
|
||||
"properties": {
|
||||
"config": {
|
||||
"fields": [
|
||||
{
|
||||
"label": "AI 回答内容",
|
||||
"value": "answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": 763,
|
||||
"stepName": "AI 對話1",
|
||||
"node_data": {
|
||||
"prompt": "{{開始.question}}",
|
||||
"system": "",
|
||||
"model_id": "",
|
||||
"dialogue_number": 0,
|
||||
"is_result": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "7d0f166f-c472-41b2-b9a2-c294f4c83d73",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "start-node",
|
||||
"targetNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"startPoint": {
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 590,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 700,
|
||||
"y": 3660
|
||||
},
|
||||
{
|
||||
"x": 570,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 680,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "start-node_right",
|
||||
"targetAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_left"
|
||||
},
|
||||
{
|
||||
"id": "35cb86dd-f328-429e-a973-12fd7218b696",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5",
|
||||
"targetNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"startPoint": {
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1110,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1090,
|
||||
"y": 3210
|
||||
},
|
||||
{
|
||||
"x": 1200,
|
||||
"y": 3210
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "b931efe5-5b66-46e0-ae3b-0160cb18eeb5_right",
|
||||
"targetAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_left"
|
||||
},
|
||||
{
|
||||
"id": "e8f6cfe6-7e48-41cd-abd3-abfb5304d0d8",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "4ffe1086-25df-4c85-b168-979b5bbf0a26",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3073.775
|
||||
},
|
||||
{
|
||||
"x": 1900,
|
||||
"y": 2480
|
||||
},
|
||||
{
|
||||
"x": 2010,
|
||||
"y": 2480
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_1009_right",
|
||||
"targetAnchorId": "4ffe1086-25df-4c85-b168-979b5bbf0a26_left"
|
||||
},
|
||||
{
|
||||
"id": "994ff325-6f7a-4ebc-b61b-10e15519d6d2",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3203
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3200
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3200
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_4908_right",
|
||||
"targetAnchorId": "f1f1ee18-5a02-46f6-b4e6-226253cdffbb_left"
|
||||
},
|
||||
{
|
||||
"id": "19270caf-bb9f-4ba7-9bf8-200aa70fecd5",
|
||||
"type": "app-edge",
|
||||
"sourceNodeId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b",
|
||||
"targetNodeId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7",
|
||||
"startPoint": {
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
"endPoint": {
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
},
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"pointsList": [
|
||||
{
|
||||
"x": 1780,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3293.6124999999997
|
||||
},
|
||||
{
|
||||
"x": 1890,
|
||||
"y": 3970
|
||||
},
|
||||
{
|
||||
"x": 2000,
|
||||
"y": 3970
|
||||
}
|
||||
],
|
||||
"sourceAnchorId": "fc60863a-dec2-4854-9e5a-7a44b7187a2b_161_right",
|
||||
"targetAnchorId": "309d0eef-c597-46b5-8d51-b9a28aaef4c7_left"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,42 +7,29 @@
|
|||
@desc:
|
||||
"""
|
||||
import time
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from hashlib import sha1
|
||||
from typing import Type, Dict, List
|
||||
|
||||
from django.core import cache
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError, ErrorDetail
|
||||
|
||||
from application.flow.common import Answer, NodeChunk
|
||||
from application.models import ChatRecord
|
||||
from application.models.api_key_model import ApplicationPublicAccessClient
|
||||
from common.constants.authentication_type import AuthenticationType
|
||||
from common.field.common import InstanceField
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.core import cache
|
||||
|
||||
chat_cache = cache.caches['chat_cache']
|
||||
chat_cache = cache.caches['model_cache']
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'answer' in step_variable:
|
||||
answer = step_variable['answer']
|
||||
yield answer
|
||||
node.answer_text = answer
|
||||
if global_variable is not None:
|
||||
for key in global_variable:
|
||||
workflow.context[key] = global_variable[key]
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def is_interrupt(node, step_variable: Dict, global_variable: Dict):
|
||||
return node.type == 'form-node' and not node.context.get('is_submit', False)
|
||||
|
||||
|
||||
class WorkFlowPostHandler:
|
||||
|
|
@ -61,38 +48,21 @@ class WorkFlowPostHandler:
|
|||
'message_tokens' in row and row.get('message_tokens') is not None])
|
||||
answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
|
||||
'answer_tokens' in row and row.get('answer_tokens') is not None])
|
||||
answer_text_list = workflow.get_answer_text_list()
|
||||
answer_text = '\n\n'.join(
|
||||
'\n\n'.join([a.get('content') for a in answer]) for answer in
|
||||
answer_text_list)
|
||||
if workflow.chat_record is not None:
|
||||
chat_record = workflow.chat_record
|
||||
chat_record.answer_text = answer_text
|
||||
chat_record.details = details
|
||||
chat_record.message_tokens = message_tokens
|
||||
chat_record.answer_tokens = answer_tokens
|
||||
chat_record.answer_text_list = answer_text_list
|
||||
chat_record.run_time = time.time() - workflow.context['start_time']
|
||||
else:
|
||||
chat_record = ChatRecord(id=chat_record_id,
|
||||
chat_id=chat_id,
|
||||
problem_text=question,
|
||||
answer_text=answer_text,
|
||||
details=details,
|
||||
message_tokens=message_tokens,
|
||||
answer_tokens=answer_tokens,
|
||||
answer_text_list=answer_text_list,
|
||||
run_time=time.time() - workflow.context['start_time'],
|
||||
index=0)
|
||||
asker = workflow.context.get('asker', None)
|
||||
self.chat_info.append_chat_record(chat_record, self.client_id, asker)
|
||||
chat_record = ChatRecord(id=chat_record_id,
|
||||
chat_id=chat_id,
|
||||
problem_text=question,
|
||||
answer_text=answer,
|
||||
details=details,
|
||||
message_tokens=message_tokens,
|
||||
answer_tokens=answer_tokens,
|
||||
run_time=time.time() - workflow.context['start_time'],
|
||||
index=0)
|
||||
self.chat_info.append_chat_record(chat_record, self.client_id)
|
||||
# 重新设置缓存
|
||||
chat_cache.set(chat_id,
|
||||
self.chat_info, timeout=60 * 30)
|
||||
if self.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value:
|
||||
application_public_access_client = (QuerySet(ApplicationPublicAccessClient)
|
||||
.filter(client_id=self.client_id,
|
||||
application_id=self.chat_info.application.id).first())
|
||||
application_public_access_client = QuerySet(ApplicationPublicAccessClient).filter(id=self.client_id).first()
|
||||
if application_public_access_client is not None:
|
||||
application_public_access_client.access_num = application_public_access_client.access_num + 1
|
||||
application_public_access_client.intraday_access_num = application_public_access_client.intraday_access_num + 1
|
||||
|
|
@ -100,27 +70,22 @@ class WorkFlowPostHandler:
|
|||
|
||||
|
||||
class NodeResult:
|
||||
def __init__(self, node_variable: Dict, workflow_variable: Dict,
|
||||
_write_context=write_context, _is_interrupt=is_interrupt):
|
||||
def __init__(self, node_variable: Dict, workflow_variable: Dict, _to_response=None, _write_context=write_context):
|
||||
self._write_context = _write_context
|
||||
self.node_variable = node_variable
|
||||
self.workflow_variable = workflow_variable
|
||||
self._is_interrupt = _is_interrupt
|
||||
self._to_response = _to_response
|
||||
|
||||
def write_context(self, node, workflow):
|
||||
return self._write_context(self.node_variable, self.workflow_variable, node, workflow)
|
||||
self._write_context(self.node_variable, self.workflow_variable, node, workflow)
|
||||
|
||||
def to_response(self, chat_id, chat_record_id, node, workflow, post_handler: WorkFlowPostHandler):
|
||||
return self._to_response(chat_id, chat_record_id, self.node_variable, self.workflow_variable, node, workflow,
|
||||
post_handler)
|
||||
|
||||
def is_assertion_result(self):
|
||||
return 'branch_id' in self.node_variable
|
||||
|
||||
def is_interrupt_exec(self, current_node):
|
||||
"""
|
||||
是否中断执行
|
||||
@param current_node:
|
||||
@return:
|
||||
"""
|
||||
return self._is_interrupt(current_node, self.node_variable, self.workflow_variable)
|
||||
|
||||
|
||||
class ReferenceAddressSerializer(serializers.Serializer):
|
||||
node_id = serializers.CharField(required=True, error_messages=ErrMessage.char("节点id"))
|
||||
|
|
@ -146,47 +111,22 @@ class FlowParamsSerializer(serializers.Serializer):
|
|||
|
||||
client_type = serializers.CharField(required=False, error_messages=ErrMessage.char("客户端类型"))
|
||||
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
re_chat = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("换个答案"))
|
||||
|
||||
|
||||
class INode:
|
||||
view_type = 'many_view'
|
||||
|
||||
@abstractmethod
|
||||
def save_context(self, details, workflow_manage):
|
||||
pass
|
||||
|
||||
def get_answer_list(self) -> List[Answer] | None:
|
||||
if self.answer_text is None:
|
||||
return None
|
||||
reasoning_content_enable = self.context.get('model_setting', {}).get('reasoning_content_enable', False)
|
||||
return [
|
||||
Answer(self.answer_text, self.view_type, self.runtime_node_id, self.workflow_params['chat_record_id'], {},
|
||||
self.runtime_node_id, self.context.get('reasoning_content', '') if reasoning_content_enable else '')]
|
||||
|
||||
def __init__(self, node, workflow_params, workflow_manage, up_node_id_list=None,
|
||||
get_node_params=lambda node: node.properties.get('node_data')):
|
||||
def __init__(self, node, workflow_params, workflow_manage):
|
||||
# 当前步骤上下文,用于存储当前步骤信息
|
||||
self.status = 200
|
||||
self.err_message = ''
|
||||
self.node = node
|
||||
self.node_params = get_node_params(node)
|
||||
self.workflow_params = workflow_params
|
||||
self.node_params = node.properties.get('node_data')
|
||||
self.workflow_manage = workflow_manage
|
||||
self.node_params_serializer = None
|
||||
self.flow_params_serializer = None
|
||||
self.context = {}
|
||||
self.answer_text = None
|
||||
self.id = node.id
|
||||
if up_node_id_list is None:
|
||||
up_node_id_list = []
|
||||
self.up_node_id_list = up_node_id_list
|
||||
self.node_chunk = NodeChunk()
|
||||
self.runtime_node_id = sha1(uuid.NAMESPACE_DNS.bytes + bytes(str(uuid.uuid5(uuid.NAMESPACE_DNS,
|
||||
"".join([*sorted(up_node_id_list),
|
||||
node.id]))),
|
||||
"utf-8")).hexdigest()
|
||||
self.valid_args(self.node_params, workflow_params)
|
||||
|
||||
def valid_args(self, node_params, flow_params):
|
||||
flow_params_serializer_class = self.get_flow_params_serializer_class()
|
||||
|
|
@ -197,8 +137,6 @@ class INode:
|
|||
if node_params_serializer_class is not None:
|
||||
self.node_params_serializer = node_params_serializer_class(data=node_params)
|
||||
self.node_params_serializer.is_valid(raise_exception=True)
|
||||
if self.node.properties.get('status', 200) != 200:
|
||||
raise ValidationError(ErrorDetail(f'节点{self.node.properties.get("stepName")} 不可用'))
|
||||
|
||||
def get_reference_field(self, fields: List[str]):
|
||||
return self.get_field(self.context, fields)
|
||||
|
|
@ -222,9 +160,7 @@ class INode:
|
|||
|
||||
def get_write_error_context(self, e):
|
||||
self.status = 500
|
||||
self.answer_text = str(e)
|
||||
self.err_message = str(e)
|
||||
self.context['run_time'] = time.time() - self.context['start_time']
|
||||
|
||||
def write_error_context(answer, status=200):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -7,32 +7,13 @@
|
|||
@desc:
|
||||
"""
|
||||
from .ai_chat_step_node import *
|
||||
from .application_node import BaseApplicationNode
|
||||
from .condition_node import *
|
||||
from .direct_reply_node import *
|
||||
from .form_node import *
|
||||
from .function_lib_node import *
|
||||
from .function_node import *
|
||||
from .question_node import *
|
||||
from .reranker_node import *
|
||||
|
||||
from .document_extract_node import *
|
||||
from .image_understand_step_node import *
|
||||
from .image_generate_step_node import *
|
||||
|
||||
from .search_dataset_node import *
|
||||
from .speech_to_text_step_node import BaseSpeechToTextNode
|
||||
from .start_node import *
|
||||
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
|
||||
from .variable_assign_node import BaseVariableAssignNode
|
||||
from .mcp_node import BaseMcpNode
|
||||
from .direct_reply_node import *
|
||||
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
|
||||
BaseConditionNode, BaseReplyNode,
|
||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||
BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
"""
|
||||
from typing import Type
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
|
|
@ -16,26 +15,12 @@ from common.util.field_message import ErrMessage
|
|||
|
||||
|
||||
class ChatNodeSerializer(serializers.Serializer):
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char("模型id"))
|
||||
system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Role Setting")))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
|
||||
error_messages=ErrMessage.char("角色设定"))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char("提示词"))
|
||||
# 多轮对话数量
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(
|
||||
_("Number of multi-round conversations")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False,
|
||||
error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
model_params_setting = serializers.DictField(required=False,
|
||||
error_messages=ErrMessage.dict(_("Model parameter settings")))
|
||||
model_setting = serializers.DictField(required=False,
|
||||
error_messages=ErrMessage.dict('Model settings'))
|
||||
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Context Type")))
|
||||
mcp_enable = serializers.BooleanField(required=False,
|
||||
error_messages=ErrMessage.boolean(_("Whether to enable MCP")))
|
||||
mcp_servers = serializers.JSONField(required=False, error_messages=ErrMessage.list(_("MCP Server")))
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer("多轮对话数量"))
|
||||
|
||||
|
||||
class IChatNode(INode):
|
||||
|
|
@ -47,12 +32,6 @@ class IChatNode(INode):
|
|||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id,
|
||||
chat_record_id,
|
||||
model_params_setting=None,
|
||||
dialogue_type=None,
|
||||
model_setting=None,
|
||||
mcp_enable=False,
|
||||
mcp_servers=None,
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,55 +6,21 @@
|
|||
@date:2024/6/4 14:30
|
||||
@desc:
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from functools import reduce
|
||||
from types import AsyncGeneratorType
|
||||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
from langchain_core.messages import BaseMessage, AIMessage, AIMessageChunk, ToolMessage
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
from application.flow import tools
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
|
||||
from application.flow.tools import Reasoning
|
||||
from common.util.rsa_util import rsa_long_decrypt
|
||||
from setting.models import Model
|
||||
from setting.models_provider import get_model_credential
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
tool_message_template = """
|
||||
<details>
|
||||
<summary>
|
||||
<strong>Called MCP Tool: <em>%s</em></strong>
|
||||
</summary>
|
||||
|
||||
```json
|
||||
%s
|
||||
```
|
||||
</details>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
|
||||
reasoning_content: str):
|
||||
chat_model = node_variable.get('chat_model')
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
node.context['reasoning_content'] = reasoning_content
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
node.answer_text = answer
|
||||
from setting.models_provider.constants.model_provider_constants import ModelProvideConstants
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -67,73 +33,17 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||
"""
|
||||
response = node_variable.get('result')
|
||||
answer = ''
|
||||
reasoning_content = ''
|
||||
model_setting = node.context.get('model_setting',
|
||||
{'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
|
||||
'reasoning_content_start': '<think>'})
|
||||
reasoning = Reasoning(model_setting.get('reasoning_content_start', '<think>'),
|
||||
model_setting.get('reasoning_content_end', '</think>'))
|
||||
response_reasoning_content = False
|
||||
|
||||
for chunk in response:
|
||||
reasoning_chunk = reasoning.get_reasoning_content(chunk)
|
||||
content_chunk = reasoning_chunk.get('content')
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
response_reasoning_content = True
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
else:
|
||||
reasoning_content_chunk = reasoning_chunk.get('reasoning_content')
|
||||
answer += content_chunk
|
||||
if reasoning_content_chunk is None:
|
||||
reasoning_content_chunk = ''
|
||||
reasoning_content += reasoning_content_chunk
|
||||
yield {'content': content_chunk,
|
||||
'reasoning_content': reasoning_content_chunk if model_setting.get('reasoning_content_enable',
|
||||
False) else ''}
|
||||
|
||||
reasoning_chunk = reasoning.get_end_reasoning_content()
|
||||
answer += reasoning_chunk.get('content')
|
||||
reasoning_content_chunk = ""
|
||||
if not response_reasoning_content:
|
||||
reasoning_content_chunk = reasoning_chunk.get(
|
||||
'reasoning_content')
|
||||
yield {'content': reasoning_chunk.get('content'),
|
||||
'reasoning_content': reasoning_content_chunk if model_setting.get('reasoning_content_enable',
|
||||
False) else ''}
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||
|
||||
|
||||
async def _yield_mcp_response(chat_model, message_list, mcp_servers):
|
||||
async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
|
||||
agent = create_react_agent(chat_model, client.get_tools())
|
||||
response = agent.astream({"messages": message_list}, stream_mode='messages')
|
||||
async for chunk in response:
|
||||
if isinstance(chunk[0], ToolMessage):
|
||||
content = tool_message_template % (chunk[0].name, chunk[0].content)
|
||||
chunk[0].content = content
|
||||
yield chunk[0]
|
||||
if isinstance(chunk[0], AIMessageChunk):
|
||||
yield chunk[0]
|
||||
|
||||
|
||||
def mcp_response_generator(chat_model, message_list, mcp_servers):
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
async_gen = _yield_mcp_response(chat_model, message_list, mcp_servers)
|
||||
while True:
|
||||
try:
|
||||
chunk = loop.run_until_complete(anext_async(async_gen))
|
||||
yield chunk
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'exception: {e}')
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def anext_async(agen):
|
||||
return await agen.__anext__()
|
||||
answer += chunk.content
|
||||
chat_model = node_variable.get('chat_model')
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -145,108 +55,108 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
|
|||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
model_setting = node.context.get('model_setting',
|
||||
{'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
|
||||
'reasoning_content_start': '<think>'})
|
||||
reasoning = Reasoning(model_setting.get('reasoning_content_start'), model_setting.get('reasoning_content_end'))
|
||||
reasoning_result = reasoning.get_reasoning_content(response)
|
||||
reasoning_result_end = reasoning.get_end_reasoning_content()
|
||||
content = reasoning_result.get('content') + reasoning_result_end.get('content')
|
||||
if 'reasoning_content' in response.response_metadata:
|
||||
reasoning_content = response.response_metadata.get('reasoning_content', '')
|
||||
else:
|
||||
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content')
|
||||
_write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content)
|
||||
chat_model = node_variable.get('chat_model')
|
||||
answer = response.content
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
|
||||
|
||||
def get_default_model_params_setting(model_id):
|
||||
model = QuerySet(Model).filter(id=model_id).first()
|
||||
credential = get_model_credential(model.provider, model.model_type, model.model_name)
|
||||
model_params_setting = credential.get_model_params_setting_form(
|
||||
model.model_name).get_default_form_data()
|
||||
return model_params_setting
|
||||
def get_to_response_write_context(node_variable: Dict, node: INode):
|
||||
def _write_context(answer, status=200):
|
||||
chat_model = node_variable.get('chat_model')
|
||||
|
||||
if status == 200:
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
else:
|
||||
answer_tokens = 0
|
||||
message_tokens = 0
|
||||
node.err_message = answer
|
||||
node.status = status
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
return _write_context
|
||||
|
||||
|
||||
def get_node_message(chat_record, runtime_node_id):
|
||||
node_details = chat_record.get_node_details_runtime_node_id(runtime_node_id)
|
||||
if node_details is None:
|
||||
return []
|
||||
return [HumanMessage(node_details.get('question')), AIMessage(node_details.get('answer'))]
|
||||
def to_stream_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将流式数据 转换为 流式响应
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器 输出结果后执行
|
||||
@return: 流式响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_stream_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
def get_workflow_message(chat_record):
|
||||
return [chat_record.get_human_message(), chat_record.get_ai_message()]
|
||||
|
||||
|
||||
def get_message(chat_record, dialogue_type, runtime_node_id):
|
||||
return get_node_message(chat_record, runtime_node_id) if dialogue_type == 'NODE' else get_workflow_message(
|
||||
chat_record)
|
||||
def to_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将结果转换
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器
|
||||
@return: 响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
class BaseChatNode(IChatNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['reasoning_content'] = details.get('reasoning_content')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
dialogue_type=None,
|
||||
model_setting=None,
|
||||
mcp_enable=False,
|
||||
mcp_servers=None,
|
||||
**kwargs) -> NodeResult:
|
||||
if dialogue_type is None:
|
||||
dialogue_type = 'WORKFLOW'
|
||||
|
||||
if model_params_setting is None:
|
||||
model_params_setting = get_default_model_params_setting(model_id)
|
||||
if model_setting is None:
|
||||
model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
|
||||
'reasoning_content_start': '<think>'}
|
||||
self.context['model_setting'] = model_setting
|
||||
chat_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
|
||||
**model_params_setting)
|
||||
history_message = self.get_history_message(history_chat_record, dialogue_number, dialogue_type,
|
||||
self.runtime_node_id)
|
||||
model = QuerySet(Model).filter(id=model_id).first()
|
||||
if model is None:
|
||||
raise Exception("模型不存在")
|
||||
chat_model = ModelProvideConstants[model.provider].value.get_model(model.model_type, model.model_name,
|
||||
json.loads(
|
||||
rsa_long_decrypt(model.credential)),
|
||||
streaming=True)
|
||||
history_message = self.get_history_message(history_chat_record, dialogue_number)
|
||||
self.context['history_message'] = history_message
|
||||
question = self.generate_prompt_question(prompt)
|
||||
self.context['question'] = question.content
|
||||
system = self.workflow_manage.generate_prompt(system)
|
||||
self.context['system'] = system
|
||||
message_list = self.generate_message_list(system, prompt, history_message)
|
||||
self.context['message_list'] = message_list
|
||||
|
||||
if mcp_enable and mcp_servers is not None and '"stdio"' not in mcp_servers:
|
||||
r = mcp_response_generator(chat_model, message_list, mcp_servers)
|
||||
return NodeResult(
|
||||
{'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context_stream)
|
||||
|
||||
if stream:
|
||||
r = chat_model.stream(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context_stream)
|
||||
_write_context=write_context_stream,
|
||||
_to_response=to_stream_response)
|
||||
else:
|
||||
r = chat_model.invoke(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context)
|
||||
_write_context=write_context, _to_response=to_response)
|
||||
|
||||
@staticmethod
|
||||
def get_history_message(history_chat_record, dialogue_number, dialogue_type, runtime_node_id):
|
||||
def get_history_message(history_chat_record, dialogue_number):
|
||||
start_index = len(history_chat_record) - dialogue_number
|
||||
history_message = reduce(lambda x, y: [*x, *y], [
|
||||
get_message(history_chat_record[index], dialogue_type, runtime_node_id)
|
||||
[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||
for message in history_message:
|
||||
if isinstance(message.content, str):
|
||||
message.content = re.sub('<form_rander>[\d\D]*?<\/form_rander>', '', message.content)
|
||||
return history_message
|
||||
|
||||
def generate_prompt_question(self, prompt):
|
||||
|
|
@ -273,13 +183,12 @@ class BaseChatNode(IChatNode):
|
|||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'system': self.context.get('system'),
|
||||
'system': self.node_params.get('system'),
|
||||
'history_message': [{'content': message.content, 'role': message.type} for message in
|
||||
(self.context.get('history_message') if self.context.get(
|
||||
'history_message') is not None else [])],
|
||||
'question': self.context.get('question'),
|
||||
'answer': self.context.get('answer'),
|
||||
'reasoning_content': self.context.get('reasoning_content'),
|
||||
'type': self.node.type,
|
||||
'message_tokens': self.context.get('message_tokens'),
|
||||
'answer_tokens': self.context.get('answer_tokens'),
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
# coding=utf-8
|
||||
from .impl import *
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# coding=utf-8
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ApplicationNodeSerializer(serializers.Serializer):
|
||||
application_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Application ID")))
|
||||
question_reference_address = serializers.ListField(required=True,
|
||||
error_messages=ErrMessage.list(_("User Questions")))
|
||||
api_input_field_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("API Input Fields")))
|
||||
user_input_field_list = serializers.ListField(required=False,
|
||||
error_messages=ErrMessage.uuid(_("User Input Fields")))
|
||||
image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
|
||||
document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
|
||||
audio_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Audio")))
|
||||
child_node = serializers.DictField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.dict(_("Child Nodes")))
|
||||
node_data = serializers.DictField(required=False, allow_null=True, error_messages=ErrMessage.dict(_("Form Data")))
|
||||
|
||||
|
||||
class IApplicationNode(INode):
|
||||
type = 'application-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return ApplicationNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
question = self.workflow_manage.get_reference_field(
|
||||
self.node_params_serializer.data.get('question_reference_address')[0],
|
||||
self.node_params_serializer.data.get('question_reference_address')[1:])
|
||||
kwargs = {}
|
||||
for api_input_field in self.node_params_serializer.data.get('api_input_field_list', []):
|
||||
value = api_input_field.get('value', [''])[0] if api_input_field.get('value') else ''
|
||||
kwargs[api_input_field['variable']] = self.workflow_manage.get_reference_field(value,
|
||||
api_input_field['value'][
|
||||
1:]) if value != '' else ''
|
||||
|
||||
for user_input_field in self.node_params_serializer.data.get('user_input_field_list', []):
|
||||
value = user_input_field.get('value', [''])[0] if user_input_field.get('value') else ''
|
||||
kwargs[user_input_field['field']] = self.workflow_manage.get_reference_field(value,
|
||||
user_input_field['value'][
|
||||
1:]) if value != '' else ''
|
||||
# 判断是否包含这个属性
|
||||
app_document_list = self.node_params_serializer.data.get('document_list', [])
|
||||
if app_document_list and len(app_document_list) > 0:
|
||||
app_document_list = self.workflow_manage.get_reference_field(
|
||||
app_document_list[0],
|
||||
app_document_list[1:])
|
||||
for document in app_document_list:
|
||||
if 'file_id' not in document:
|
||||
raise ValueError(
|
||||
_("Parameter value error: The uploaded document lacks file_id, and the document upload fails"))
|
||||
app_image_list = self.node_params_serializer.data.get('image_list', [])
|
||||
if app_image_list and len(app_image_list) > 0:
|
||||
app_image_list = self.workflow_manage.get_reference_field(
|
||||
app_image_list[0],
|
||||
app_image_list[1:])
|
||||
for image in app_image_list:
|
||||
if 'file_id' not in image:
|
||||
raise ValueError(
|
||||
_("Parameter value error: The uploaded image lacks file_id, and the image upload fails"))
|
||||
|
||||
app_audio_list = self.node_params_serializer.data.get('audio_list', [])
|
||||
if app_audio_list and len(app_audio_list) > 0:
|
||||
app_audio_list = self.workflow_manage.get_reference_field(
|
||||
app_audio_list[0],
|
||||
app_audio_list[1:])
|
||||
for audio in app_audio_list:
|
||||
if 'file_id' not in audio:
|
||||
raise ValueError(
|
||||
_("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails."))
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data,
|
||||
app_document_list=app_document_list, app_image_list=app_image_list,
|
||||
app_audio_list=app_audio_list,
|
||||
message=str(question), **kwargs)
|
||||
|
||||
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
|
||||
app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# coding=utf-8
|
||||
from .base_application_node import BaseApplicationNode
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
# coding=utf-8
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, List
|
||||
|
||||
from application.flow.common import Answer
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.application_node.i_application_node import IApplicationNode
|
||||
from application.models import Chat
|
||||
|
||||
|
||||
def string_to_uuid(input_str):
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_DNS, input_str))
|
||||
|
||||
|
||||
def _is_interrupt_exec(node, node_variable: Dict, workflow_variable: Dict):
|
||||
return node_variable.get('is_interrupt_exec', False)
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
|
||||
reasoning_content: str):
|
||||
result = node_variable.get('result')
|
||||
node.context['application_node_dict'] = node_variable.get('application_node_dict')
|
||||
node.context['node_dict'] = node_variable.get('node_dict', {})
|
||||
node.context['is_interrupt_exec'] = node_variable.get('is_interrupt_exec')
|
||||
node.context['message_tokens'] = result.get('usage', {}).get('prompt_tokens', 0)
|
||||
node.context['answer_tokens'] = result.get('usage', {}).get('completion_tokens', 0)
|
||||
node.context['answer'] = answer
|
||||
node.context['result'] = answer
|
||||
node.context['reasoning_content'] = reasoning_content
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据 (流式)
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
answer = ''
|
||||
reasoning_content = ''
|
||||
usage = {}
|
||||
node_child_node = {}
|
||||
application_node_dict = node.context.get('application_node_dict', {})
|
||||
is_interrupt_exec = False
|
||||
for chunk in response:
|
||||
# 先把流转成字符串
|
||||
response_content = chunk.decode('utf-8')[6:]
|
||||
response_content = json.loads(response_content)
|
||||
content = response_content.get('content', '')
|
||||
runtime_node_id = response_content.get('runtime_node_id', '')
|
||||
chat_record_id = response_content.get('chat_record_id', '')
|
||||
child_node = response_content.get('child_node')
|
||||
view_type = response_content.get('view_type')
|
||||
node_type = response_content.get('node_type')
|
||||
real_node_id = response_content.get('real_node_id')
|
||||
node_is_end = response_content.get('node_is_end', False)
|
||||
_reasoning_content = response_content.get('reasoning_content', '')
|
||||
if node_type == 'form-node':
|
||||
is_interrupt_exec = True
|
||||
answer += content
|
||||
reasoning_content += _reasoning_content
|
||||
node_child_node = {'runtime_node_id': runtime_node_id, 'chat_record_id': chat_record_id,
|
||||
'child_node': child_node}
|
||||
|
||||
if real_node_id is not None:
|
||||
application_node = application_node_dict.get(real_node_id, None)
|
||||
if application_node is None:
|
||||
|
||||
application_node_dict[real_node_id] = {'content': content,
|
||||
'runtime_node_id': runtime_node_id,
|
||||
'chat_record_id': chat_record_id,
|
||||
'child_node': child_node,
|
||||
'index': len(application_node_dict),
|
||||
'view_type': view_type,
|
||||
'reasoning_content': _reasoning_content}
|
||||
else:
|
||||
application_node['content'] += content
|
||||
application_node['reasoning_content'] += _reasoning_content
|
||||
|
||||
yield {'content': content,
|
||||
'node_type': node_type,
|
||||
'runtime_node_id': runtime_node_id, 'chat_record_id': chat_record_id,
|
||||
'reasoning_content': _reasoning_content,
|
||||
'child_node': child_node,
|
||||
'real_node_id': real_node_id,
|
||||
'node_is_end': node_is_end,
|
||||
'view_type': view_type}
|
||||
usage = response_content.get('usage', {})
|
||||
node_variable['result'] = {'usage': usage}
|
||||
node_variable['is_interrupt_exec'] = is_interrupt_exec
|
||||
node_variable['child_node'] = node_child_node
|
||||
node_variable['application_node_dict'] = application_node_dict
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点实例对象
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result', {}).get('data', {})
|
||||
node_variable['result'] = {'usage': {'completion_tokens': response.get('completion_tokens'),
|
||||
'prompt_tokens': response.get('prompt_tokens')}}
|
||||
answer = response.get('content', '') or "抱歉,没有查找到相关内容,请重新描述您的问题或提供更多信息。"
|
||||
reasoning_content = response.get('reasoning_content', '')
|
||||
answer_list = response.get('answer_list', [])
|
||||
node_variable['application_node_dict'] = {answer.get('real_node_id'): {**answer, 'index': index} for answer, index
|
||||
in
|
||||
zip(answer_list, range(len(answer_list)))}
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||
|
||||
|
||||
def reset_application_node_dict(application_node_dict, runtime_node_id, node_data):
|
||||
try:
|
||||
if application_node_dict is None:
|
||||
return
|
||||
for key in application_node_dict:
|
||||
application_node = application_node_dict[key]
|
||||
if application_node.get('runtime_node_id') == runtime_node_id:
|
||||
content: str = application_node.get('content')
|
||||
match = re.search('<form_rander>.*?</form_rander>', content)
|
||||
if match:
|
||||
form_setting_str = match.group().replace('<form_rander>', '').replace('</form_rander>', '')
|
||||
form_setting = json.loads(form_setting_str)
|
||||
form_setting['is_submit'] = True
|
||||
form_setting['form_data'] = node_data
|
||||
value = f'<form_rander>{json.dumps(form_setting)}</form_rander>'
|
||||
res = re.sub('<form_rander>.*?</form_rander>',
|
||||
'${value}', content)
|
||||
application_node['content'] = res.replace('${value}', value)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
class BaseApplicationNode(IApplicationNode):
|
||||
def get_answer_list(self) -> List[Answer] | None:
|
||||
if self.answer_text is None:
|
||||
return None
|
||||
application_node_dict = self.context.get('application_node_dict')
|
||||
if application_node_dict is None or len(application_node_dict) == 0:
|
||||
return [
|
||||
Answer(self.answer_text, self.view_type, self.runtime_node_id, self.workflow_params['chat_record_id'],
|
||||
self.context.get('child_node'), self.runtime_node_id, '')]
|
||||
else:
|
||||
return [Answer(n.get('content'), n.get('view_type'), self.runtime_node_id,
|
||||
self.workflow_params['chat_record_id'], {'runtime_node_id': n.get('runtime_node_id'),
|
||||
'chat_record_id': n.get('chat_record_id')
|
||||
, 'child_node': n.get('child_node')}, n.get('real_node_id'),
|
||||
n.get('reasoning_content', ''))
|
||||
for n in
|
||||
sorted(application_node_dict.values(), key=lambda item: item.get('index'))]
|
||||
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['result'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['type'] = details.get('type')
|
||||
self.context['reasoning_content'] = details.get('reasoning_content')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
|
||||
app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None,
|
||||
**kwargs) -> NodeResult:
|
||||
from application.serializers.chat_message_serializers import ChatMessageSerializer
|
||||
# 生成嵌入应用的chat_id
|
||||
current_chat_id = string_to_uuid(chat_id + application_id)
|
||||
Chat.objects.get_or_create(id=current_chat_id, defaults={
|
||||
'application_id': application_id,
|
||||
'abstract': message[0:1024],
|
||||
'client_id': client_id,
|
||||
})
|
||||
if app_document_list is None:
|
||||
app_document_list = []
|
||||
if app_image_list is None:
|
||||
app_image_list = []
|
||||
if app_audio_list is None:
|
||||
app_audio_list = []
|
||||
runtime_node_id = None
|
||||
record_id = None
|
||||
child_node_value = None
|
||||
if child_node is not None:
|
||||
runtime_node_id = child_node.get('runtime_node_id')
|
||||
record_id = child_node.get('chat_record_id')
|
||||
child_node_value = child_node.get('child_node')
|
||||
application_node_dict = self.context.get('application_node_dict')
|
||||
reset_application_node_dict(application_node_dict, runtime_node_id, node_data)
|
||||
|
||||
response = ChatMessageSerializer(
|
||||
data={'chat_id': current_chat_id, 'message': message,
|
||||
're_chat': re_chat,
|
||||
'stream': stream,
|
||||
'application_id': application_id,
|
||||
'client_id': client_id,
|
||||
'client_type': client_type,
|
||||
'document_list': app_document_list,
|
||||
'image_list': app_image_list,
|
||||
'audio_list': app_audio_list,
|
||||
'runtime_node_id': runtime_node_id,
|
||||
'chat_record_id': record_id,
|
||||
'child_node': child_node_value,
|
||||
'node_data': node_data,
|
||||
'form_data': kwargs}).chat()
|
||||
if response.status_code == 200:
|
||||
if stream:
|
||||
content_generator = response.streaming_content
|
||||
return NodeResult({'result': content_generator, 'question': message}, {},
|
||||
_write_context=write_context_stream, _is_interrupt=_is_interrupt_exec)
|
||||
else:
|
||||
data = json.loads(response.content)
|
||||
return NodeResult({'result': data, 'question': message}, {},
|
||||
_write_context=write_context, _is_interrupt=_is_interrupt_exec)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
global_fields = []
|
||||
for api_input_field in self.node_params_serializer.data.get('api_input_field_list', []):
|
||||
value = api_input_field.get('value', [''])[0] if api_input_field.get('value') else ''
|
||||
global_fields.append({
|
||||
'label': api_input_field['variable'],
|
||||
'key': api_input_field['variable'],
|
||||
'value': self.workflow_manage.get_reference_field(
|
||||
value,
|
||||
api_input_field['value'][1:]
|
||||
) if value != '' else ''
|
||||
})
|
||||
|
||||
for user_input_field in self.node_params_serializer.data.get('user_input_field_list', []):
|
||||
value = user_input_field.get('value', [''])[0] if user_input_field.get('value') else ''
|
||||
global_fields.append({
|
||||
'label': user_input_field['label'],
|
||||
'key': user_input_field['field'],
|
||||
'value': self.workflow_manage.get_reference_field(
|
||||
value,
|
||||
user_input_field['value'][1:]
|
||||
) if value != '' else ''
|
||||
})
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"info": self.node.properties.get('node_data'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'question': self.context.get('question'),
|
||||
'answer': self.context.get('answer'),
|
||||
'reasoning_content': self.context.get('reasoning_content'),
|
||||
'type': self.node.type,
|
||||
'message_tokens': self.context.get('message_tokens'),
|
||||
'answer_tokens': self.context.get('answer_tokens'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'global_fields': global_fields,
|
||||
'document_list': self.workflow_manage.document_list,
|
||||
'image_list': self.workflow_manage.image_list,
|
||||
'audio_list': self.workflow_manage.audio_list,
|
||||
'application_node_dict': self.context.get('application_node_dict')
|
||||
}
|
||||
|
|
@ -9,22 +9,20 @@
|
|||
|
||||
from .contain_compare import *
|
||||
from .equal_compare import *
|
||||
from .ge_compare import *
|
||||
from .gt_compare import *
|
||||
from .is_not_null_compare import *
|
||||
from .is_not_true import IsNotTrueCompare
|
||||
from .is_null_compare import *
|
||||
from .is_true import IsTrueCompare
|
||||
from .ge_compare import *
|
||||
from .le_compare import *
|
||||
from .len_equal_compare import *
|
||||
from .lt_compare import *
|
||||
from .len_ge_compare import *
|
||||
from .len_gt_compare import *
|
||||
from .len_le_compare import *
|
||||
from .len_lt_compare import *
|
||||
from .lt_compare import *
|
||||
from .len_equal_compare import *
|
||||
from .is_not_null_compare import *
|
||||
from .is_null_compare import *
|
||||
from .not_contain_compare import *
|
||||
|
||||
compare_handle_list = [GECompare(), GTCompare(), ContainCompare(), EqualCompare(), LTCompare(), LECompare(),
|
||||
LenLECompare(), LenGECompare(), LenEqualCompare(), LenGTCompare(), LenLTCompare(),
|
||||
IsNullCompare(),
|
||||
IsNotNullCompare(), NotContainCompare(), IsTrueCompare(), IsNotTrueCompare()]
|
||||
IsNotNullCompare(), NotContainCompare()]
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: is_not_true.py
|
||||
@date:2025/4/7 13:44
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from application.flow.step_node.condition_node.compare import Compare
|
||||
|
||||
|
||||
class IsNotTrueCompare(Compare):
|
||||
|
||||
def support(self, node_id, fields: List[str], source_value, compare, target_value):
|
||||
if compare == 'is_not_true':
|
||||
return True
|
||||
|
||||
def compare(self, source_value, compare, target_value):
|
||||
try:
|
||||
return source_value is False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: IsTrue.py
|
||||
@date:2025/4/7 13:38
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from application.flow.step_node.condition_node.compare import Compare
|
||||
|
||||
|
||||
class IsTrueCompare(Compare):
|
||||
|
||||
def support(self, node_id, fields: List[str], source_value, compare, target_value):
|
||||
if compare == 'is_true':
|
||||
return True
|
||||
|
||||
def compare(self, source_value, compare, target_value):
|
||||
try:
|
||||
return source_value is True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
@date:2024/6/7 9:54
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
from typing import Type
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode
|
||||
|
|
@ -16,15 +16,15 @@ from common.util.field_message import ErrMessage
|
|||
|
||||
|
||||
class ConditionSerializer(serializers.Serializer):
|
||||
compare = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Comparator")))
|
||||
value = serializers.CharField(required=True, error_messages=ErrMessage.char(_("value")))
|
||||
field = serializers.ListField(required=True, error_messages=ErrMessage.char(_("Fields")))
|
||||
compare = serializers.CharField(required=True, error_messages=ErrMessage.char("比较器"))
|
||||
value = serializers.CharField(required=True, error_messages=ErrMessage.char(""))
|
||||
field = serializers.ListField(required=True, error_messages=ErrMessage.char("字段"))
|
||||
|
||||
|
||||
class ConditionBranchSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Branch id")))
|
||||
type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Branch Type")))
|
||||
condition = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Condition or|and")))
|
||||
id = serializers.CharField(required=True, error_messages=ErrMessage.char("分支id"))
|
||||
type = serializers.CharField(required=True, error_messages=ErrMessage.char("分支类型"))
|
||||
condition = serializers.CharField(required=True, error_messages=ErrMessage.char("条件or|and"))
|
||||
conditions = ConditionSerializer(many=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ from application.flow.step_node.condition_node.i_condition_node import IConditio
|
|||
|
||||
|
||||
class BaseConditionNode(IConditionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['branch_id'] = details.get('branch_id')
|
||||
self.context['branch_name'] = details.get('branch_name')
|
||||
|
||||
def execute(self, **kwargs) -> NodeResult:
|
||||
branch_list = self.node_params_serializer.data['branch']
|
||||
branch = self._execute(branch_list)
|
||||
|
|
@ -36,15 +32,7 @@ class BaseConditionNode(IConditionNode):
|
|||
return all(condition_list) if condition == 'and' else any(condition_list)
|
||||
|
||||
def assertion(self, field_list: List[str], compare: str, value):
|
||||
try:
|
||||
value = self.workflow_manage.generate_prompt(value)
|
||||
except Exception as e:
|
||||
pass
|
||||
field_value = None
|
||||
try:
|
||||
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
|
||||
except Exception as e:
|
||||
pass
|
||||
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
|
||||
for compare_handler in compare_handle_list:
|
||||
if compare_handler.support(field_list[0], field_list[1:], field_value, compare, value):
|
||||
return compare_handler.compare(field_value, compare, value)
|
||||
|
|
|
|||
|
|
@ -13,26 +13,24 @@ from rest_framework import serializers
|
|||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ReplyNodeParamsSerializer(serializers.Serializer):
|
||||
reply_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Response Type")))
|
||||
fields = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Reference Field")))
|
||||
reply_type = serializers.CharField(required=True, error_messages=ErrMessage.char("回复类型"))
|
||||
fields = serializers.ListField(required=False, error_messages=ErrMessage.list("引用字段"))
|
||||
content = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Direct answer content")))
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
error_messages=ErrMessage.char("直接回答内容"))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
if self.data.get('reply_type') == 'referencing':
|
||||
if 'fields' not in self.data:
|
||||
raise AppApiException(500, _("Reference field cannot be empty"))
|
||||
raise AppApiException(500, "引用字段不能为空")
|
||||
if len(self.data.get('fields')) < 2:
|
||||
raise AppApiException(500, _("Reference field error"))
|
||||
raise AppApiException(500, "引用字段错误")
|
||||
else:
|
||||
if 'content' not in self.data or self.data.get('content') is None:
|
||||
raise AppApiException(500, _("Content cannot be empty"))
|
||||
raise AppApiException(500, "内容不能为空")
|
||||
|
||||
|
||||
class IReplyNode(INode):
|
||||
|
|
|
|||
|
|
@ -6,24 +6,69 @@
|
|||
@date:2024/6/11 17:25
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
|
||||
from application.flow import tools
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.direct_reply_node.i_reply_node import IReplyNode
|
||||
|
||||
|
||||
class BaseReplyNode(IReplyNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
def get_to_response_write_context(node_variable: Dict, node: INode):
|
||||
def _write_context(answer, status=200):
|
||||
node.context['answer'] = answer
|
||||
|
||||
return _write_context
|
||||
|
||||
|
||||
def to_stream_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将流式数据 转换为 流式响应
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器 输出结果后执行
|
||||
@return: 流式响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_stream_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
def to_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将结果转换
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器
|
||||
@return: 响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
class BaseReplyNode(IReplyNode):
|
||||
def execute(self, reply_type, stream, fields=None, content=None, **kwargs) -> NodeResult:
|
||||
if reply_type == 'referencing':
|
||||
result = self.get_reference_content(fields)
|
||||
else:
|
||||
result = self.generate_reply_content(content)
|
||||
return NodeResult({'answer': result}, {})
|
||||
if stream:
|
||||
return NodeResult({'result': iter([AIMessageChunk(content=result)]), 'answer': result}, {},
|
||||
_to_response=to_stream_response)
|
||||
else:
|
||||
return NodeResult({'result': AIMessage(content=result), 'answer': result}, {}, _to_response=to_response)
|
||||
|
||||
def generate_reply_content(self, prompt):
|
||||
return self.workflow_manage.generate_prompt(prompt)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
from .impl import *
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
|
||||
class DocumentExtractNodeSerializer(serializers.Serializer):
|
||||
document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
|
||||
|
||||
|
||||
class IDocumentExtractNode(INode):
|
||||
type = 'document-extract-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return DocumentExtractNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
res = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('document_list')[0],
|
||||
self.node_params_serializer.data.get('document_list')[1:])
|
||||
return self.execute(document=res, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, document, chat_id, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1 +0,0 @@
|
|||
from .base_document_extract_node import BaseDocumentExtractNode
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
# coding=utf-8
|
||||
import io
|
||||
import mimetypes
|
||||
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.document_extract_node.i_document_extract_node import IDocumentExtractNode
|
||||
from dataset.models import File
|
||||
from dataset.serializers.document_serializers import split_handles, parse_table_handle_list, FileBufferHandle
|
||||
from dataset.serializers.file_serializers import FileSerializer
|
||||
|
||||
|
||||
def bytes_to_uploaded_file(file_bytes, file_name="file.txt"):
|
||||
content_type, _ = mimetypes.guess_type(file_name)
|
||||
if content_type is None:
|
||||
# 如果未能识别,设置为默认的二进制文件类型
|
||||
content_type = "application/octet-stream"
|
||||
# 创建一个内存中的字节流对象
|
||||
file_stream = io.BytesIO(file_bytes)
|
||||
|
||||
# 获取文件大小
|
||||
file_size = len(file_bytes)
|
||||
|
||||
# 创建 InMemoryUploadedFile 对象
|
||||
uploaded_file = InMemoryUploadedFile(
|
||||
file=file_stream,
|
||||
field_name=None,
|
||||
name=file_name,
|
||||
content_type=content_type,
|
||||
size=file_size,
|
||||
charset=None,
|
||||
)
|
||||
return uploaded_file
|
||||
|
||||
|
||||
splitter = '\n`-----------------------------------`\n'
|
||||
|
||||
class BaseDocumentExtractNode(IDocumentExtractNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['content'] = details.get('content')
|
||||
|
||||
|
||||
def execute(self, document, chat_id, **kwargs):
|
||||
get_buffer = FileBufferHandle().get_buffer
|
||||
|
||||
self.context['document_list'] = document
|
||||
content = []
|
||||
if document is None or not isinstance(document, list):
|
||||
return NodeResult({'content': ''}, {})
|
||||
|
||||
application = self.workflow_manage.work_flow_post_handler.chat_info.application
|
||||
|
||||
# doc文件中的图片保存
|
||||
def save_image(image_list):
|
||||
for image in image_list:
|
||||
meta = {
|
||||
'debug': False if application.id else True,
|
||||
'chat_id': chat_id,
|
||||
'application_id': str(application.id) if application.id else None,
|
||||
'file_id': str(image.id)
|
||||
}
|
||||
file = bytes_to_uploaded_file(image.image, image.image_name)
|
||||
FileSerializer(data={'file': file, 'meta': meta}).upload()
|
||||
|
||||
for doc in document:
|
||||
file = QuerySet(File).filter(id=doc['file_id']).first()
|
||||
buffer = io.BytesIO(file.get_byte().tobytes())
|
||||
buffer.name = doc['name'] # this is the important line
|
||||
|
||||
for split_handle in (parse_table_handle_list + split_handles):
|
||||
if split_handle.support(buffer, get_buffer):
|
||||
# 回到文件头
|
||||
buffer.seek(0)
|
||||
file_content = split_handle.get_content(buffer, save_image)
|
||||
content.append('### ' + doc['name'] + '\n' + file_content)
|
||||
break
|
||||
|
||||
return NodeResult({'content': splitter.join(content)}, {})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
content = self.context.get('content', '').split(splitter)
|
||||
# 不保存content全部内容,因为content内容可能会很大
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'content': [file_content[:500] for file_content in content],
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'document_list': self.context.get('document_list')
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/11/4 14:48
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_form_node.py
|
||||
@date:2024/11/4 14:48
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class FormNodeParamsSerializer(serializers.Serializer):
|
||||
form_field_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("Form Configuration")))
|
||||
form_content_format = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Form output content')))
|
||||
form_data = serializers.DictField(required=False, allow_null=True, error_messages=ErrMessage.dict(_("Form Data")))
|
||||
|
||||
|
||||
class IFormNode(INode):
|
||||
type = 'form-node'
|
||||
view_type = 'single_view'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FormNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, form_field_list, form_content_format, form_data, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/11/4 14:49
|
||||
@desc:
|
||||
"""
|
||||
from .base_form_node import BaseFormNode
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_form_node.py
|
||||
@date:2024/11/4 14:52
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from application.flow.common import Answer
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.form_node.i_form_node import IFormNode
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'result' in step_variable:
|
||||
result = step_variable['result']
|
||||
yield result
|
||||
node.answer_text = result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
class BaseFormNode(IFormNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
form_data = details.get('form_data', None)
|
||||
self.context['result'] = details.get('result')
|
||||
self.context['form_content_format'] = details.get('form_content_format')
|
||||
self.context['form_field_list'] = details.get('form_field_list')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['start_time'] = details.get('start_time')
|
||||
self.context['form_data'] = form_data
|
||||
self.context['is_submit'] = details.get('is_submit')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('result')
|
||||
if form_data is not None:
|
||||
for key in form_data:
|
||||
self.context[key] = form_data[key]
|
||||
|
||||
def execute(self, form_field_list, form_content_format, form_data, **kwargs) -> NodeResult:
|
||||
if form_data is not None:
|
||||
self.context['is_submit'] = True
|
||||
self.context['form_data'] = form_data
|
||||
for key in form_data:
|
||||
self.context[key] = form_data.get(key)
|
||||
else:
|
||||
self.context['is_submit'] = False
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
|
||||
context = self.workflow_manage.get_workflow_content()
|
||||
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form, context=context)
|
||||
return NodeResult(
|
||||
{'result': value, 'form_field_list': form_field_list, 'form_content_format': form_content_format}, {},
|
||||
_write_context=write_context)
|
||||
|
||||
def get_answer_list(self) -> List[Answer] | None:
|
||||
form_content_format = self.context.get('form_content_format')
|
||||
form_field_list = self.context.get('form_field_list')
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
|
||||
context = self.workflow_manage.get_workflow_content()
|
||||
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form, context=context)
|
||||
return [Answer(value, self.view_type, self.runtime_node_id, self.workflow_params['chat_record_id'], None,
|
||||
self.runtime_node_id, '')]
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
form_content_format = self.context.get('form_content_format')
|
||||
form_field_list = self.context.get('form_field_list')
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
|
||||
context = self.workflow_manage.get_workflow_content()
|
||||
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form, context=context)
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": value,
|
||||
"form_content_format": self.context.get('form_content_format'),
|
||||
"form_field_list": self.context.get('form_field_list'),
|
||||
'form_data': self.context.get('form_data'),
|
||||
'start_time': self.context.get('start_time'),
|
||||
'is_submit': self.context.get('is_submit'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/8/8 17:45
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_function_lib_node.py
|
||||
@date:2024/8/8 16:21
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.field.common import ObjectField
|
||||
from common.util.field_message import ErrMessage
|
||||
from function_lib.models.function import FunctionLib
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class InputField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Variable Name')))
|
||||
value = ObjectField(required=True, error_messages=ErrMessage.char(_("Variable Value")), model_type_list=[str, list])
|
||||
|
||||
|
||||
class FunctionLibNodeParamsSerializer(serializers.Serializer):
|
||||
function_lib_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('Library ID')))
|
||||
input_field_list = InputField(required=True, many=True)
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
f_lib = QuerySet(FunctionLib).filter(id=self.data.get('function_lib_id')).first()
|
||||
if f_lib is None:
|
||||
raise Exception(_('The function has been deleted'))
|
||||
|
||||
|
||||
class IFunctionLibNode(INode):
|
||||
type = 'function-lib-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FunctionLibNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/8/8 17:48
|
||||
@desc:
|
||||
"""
|
||||
from .base_function_lib_node import BaseFunctionLibNodeNode
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_function_lib_node.py
|
||||
@date:2024/8/8 17:49
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from common.util.rsa_util import rsa_long_decrypt
|
||||
from function_lib.models.function import FunctionLib
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'result' in step_variable:
|
||||
result = str(step_variable['result']) + '\n'
|
||||
yield result
|
||||
node.answer_text = result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def get_field_value(debug_field_list, name, is_required):
|
||||
result = [field for field in debug_field_list if field.get('name') == name]
|
||||
if len(result) > 0:
|
||||
return result[-1]['value']
|
||||
if is_required:
|
||||
raise AppApiException(500, _('Field: {name} No value set').format(name=name))
|
||||
return None
|
||||
|
||||
|
||||
def valid_reference_value(_type, value, name):
|
||||
if _type == 'int':
|
||||
instance_type = int | float
|
||||
elif _type == 'float':
|
||||
instance_type = float | int
|
||||
elif _type == 'dict':
|
||||
instance_type = dict
|
||||
elif _type == 'array':
|
||||
instance_type = list
|
||||
elif _type == 'string':
|
||||
instance_type = str
|
||||
else:
|
||||
raise Exception(_('Field: {name} Type: {_type} Value: {value} Unsupported types').format(name=name,
|
||||
_type=_type))
|
||||
if not isinstance(value, instance_type):
|
||||
raise Exception(
|
||||
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
|
||||
value=value))
|
||||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
|
||||
return None
|
||||
if not is_required and source == 'reference' and (value is None or len(value) == 0):
|
||||
return None
|
||||
if source == 'reference':
|
||||
value = node.workflow_manage.get_reference_field(
|
||||
value[0],
|
||||
value[1:])
|
||||
valid_reference_value(_type, value, name)
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
if _type == 'dict':
|
||||
v = json.loads(value)
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
raise Exception(_('type error'))
|
||||
if _type == 'array':
|
||||
v = json.loads(value)
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
raise Exception(_('type error'))
|
||||
return value
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
|
||||
value=value))
|
||||
|
||||
|
||||
def valid_function(function_lib, user_id):
|
||||
if function_lib is None:
|
||||
raise Exception(_('Function does not exist'))
|
||||
if function_lib.permission_type == 'PRIVATE' and str(function_lib.user_id) != str(user_id):
|
||||
raise Exception(_('No permission to use this function {name}').format(name=function_lib.name))
|
||||
if not function_lib.is_active:
|
||||
raise Exception(_('Function {name} is unavailable').format(name=function_lib.name))
|
||||
|
||||
|
||||
class BaseFunctionLibNodeNode(IFunctionLibNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
if self.node_params.get('is_result'):
|
||||
self.answer_text = str(details.get('result'))
|
||||
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
|
||||
valid_function(function_lib, self.flow_params_serializer.data.get('user_id'))
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'),
|
||||
field.get('source'), self)
|
||||
for field in
|
||||
[{'value': get_field_value(input_field_list, field.get('name'), field.get('is_required'),
|
||||
), **field}
|
||||
for field in
|
||||
function_lib.input_field_list]}
|
||||
|
||||
self.context['params'] = params
|
||||
# 合并初始化参数
|
||||
if function_lib.init_params is not None:
|
||||
all_params = json.loads(rsa_long_decrypt(function_lib.init_params)) | params
|
||||
else:
|
||||
all_params = params
|
||||
result = function_executor.exec_code(function_lib.code, all_params)
|
||||
return NodeResult({'result': result}, {}, _write_context=write_context)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": self.context.get('result'),
|
||||
"params": self.context.get('params'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/13 10:43
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_function_lib_node.py
|
||||
@date:2024/8/8 16:21
|
||||
@desc:
|
||||
"""
|
||||
import re
|
||||
from typing import Type
|
||||
|
||||
from django.core import validators
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.field.common import ObjectField
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
|
||||
|
||||
class InputField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Variable Name')))
|
||||
is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean(_("Is this field required")))
|
||||
type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("type")), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
|
||||
message=_("The field only supports string|int|dict|array|float"), code=500)
|
||||
])
|
||||
source = serializers.CharField(required=True, error_messages=ErrMessage.char(_("source")), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^custom|reference$"),
|
||||
message=_("The field only supports custom|reference"), code=500)
|
||||
])
|
||||
value = ObjectField(required=True, error_messages=ErrMessage.char(_("Variable Value")), model_type_list=[str, list])
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
is_required = self.data.get('is_required')
|
||||
if is_required and self.data.get('value') is None:
|
||||
message = lazy_format(_('{field}, this field is required.'), field=self.data.get("name"))
|
||||
raise AppApiException(500, message)
|
||||
|
||||
|
||||
class FunctionNodeParamsSerializer(serializers.Serializer):
|
||||
input_field_list = InputField(required=True, many=True)
|
||||
code = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function")))
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class IFunctionNode(INode):
|
||||
type = 'function-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FunctionNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/13 11:19
|
||||
@desc:
|
||||
"""
|
||||
from .base_function_node import BaseFunctionNodeNode
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_function_lib_node.py
|
||||
@date:2024/8/8 17:49
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.function_node.i_function_node import IFunctionNode
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'result' in step_variable:
|
||||
result = str(step_variable['result']) + '\n'
|
||||
yield result
|
||||
node.answer_text = result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def valid_reference_value(_type, value, name):
|
||||
if _type == 'int':
|
||||
instance_type = int | float
|
||||
elif _type == 'float':
|
||||
instance_type = float | int
|
||||
elif _type == 'dict':
|
||||
instance_type = dict
|
||||
elif _type == 'array':
|
||||
instance_type = list
|
||||
elif _type == 'string':
|
||||
instance_type = str
|
||||
else:
|
||||
raise Exception(500, f'字段:{name}类型:{_type} 不支持的类型')
|
||||
if not isinstance(value, instance_type):
|
||||
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
|
||||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
|
||||
return None
|
||||
if source == 'reference':
|
||||
value = node.workflow_manage.get_reference_field(
|
||||
value[0],
|
||||
value[1:])
|
||||
valid_reference_value(_type, value, name)
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
if _type == 'dict':
|
||||
v = json.loads(value)
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
raise Exception("类型错误")
|
||||
if _type == 'array':
|
||||
v = json.loads(value)
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
raise Exception("类型错误")
|
||||
return value
|
||||
except Exception as e:
|
||||
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
|
||||
|
||||
|
||||
class BaseFunctionNodeNode(IFunctionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = str(details.get('result'))
|
||||
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'), field.get('source'), self)
|
||||
for field in input_field_list}
|
||||
result = function_executor.exec_code(code, params)
|
||||
self.context['params'] = params
|
||||
return NodeResult({'result': result}, {}, _write_context=write_context)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": self.context.get('result'),
|
||||
"params": self.context.get('params'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ImageGenerateNodeSerializer(serializers.Serializer):
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word (positive)")))
|
||||
|
||||
negative_prompt = serializers.CharField(required=False, error_messages=ErrMessage.char(_("Prompt word (negative)")),
|
||||
allow_null=True, allow_blank=True, )
|
||||
# 多轮对话数量
|
||||
dialogue_number = serializers.IntegerField(required=False, default=0,
|
||||
error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
|
||||
|
||||
dialogue_type = serializers.CharField(required=False, default='NODE',
|
||||
error_messages=ErrMessage.char(_("Conversation storage type")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
model_params_setting = serializers.JSONField(required=False, default=dict,
|
||||
error_messages=ErrMessage.json(_("Model parameter settings")))
|
||||
|
||||
|
||||
class IImageGenerateNode(INode):
|
||||
type = 'image-generate-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return ImageGenerateNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_type, history_chat_record, chat_id,
|
||||
model_params_setting,
|
||||
chat_record_id,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_image_generate_node import BaseImageGenerateNode
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# coding=utf-8
|
||||
from functools import reduce
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.image_generate_step_node.i_image_generate_node import IImageGenerateNode
|
||||
from common.util.common import bytes_to_uploaded_file
|
||||
from dataset.serializers.file_serializers import FileSerializer
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
|
||||
class BaseImageGenerateNode(IImageGenerateNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_type, history_chat_record, chat_id,
|
||||
model_params_setting,
|
||||
chat_record_id,
|
||||
**kwargs) -> NodeResult:
|
||||
print(model_params_setting)
|
||||
application = self.workflow_manage.work_flow_post_handler.chat_info.application
|
||||
tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
|
||||
**model_params_setting)
|
||||
history_message = self.get_history_message(history_chat_record, dialogue_number)
|
||||
self.context['history_message'] = history_message
|
||||
question = self.generate_prompt_question(prompt)
|
||||
self.context['question'] = question
|
||||
message_list = self.generate_message_list(question, history_message)
|
||||
self.context['message_list'] = message_list
|
||||
self.context['dialogue_type'] = dialogue_type
|
||||
print(message_list)
|
||||
image_urls = tti_model.generate_image(question, negative_prompt)
|
||||
# 保存图片
|
||||
file_urls = []
|
||||
for image_url in image_urls:
|
||||
file_name = 'generated_image.png'
|
||||
file = bytes_to_uploaded_file(requests.get(image_url).content, file_name)
|
||||
meta = {
|
||||
'debug': False if application.id else True,
|
||||
'chat_id': chat_id,
|
||||
'application_id': str(application.id) if application.id else None,
|
||||
}
|
||||
file_url = FileSerializer(data={'file': file, 'meta': meta}).upload()
|
||||
file_urls.append(file_url)
|
||||
self.context['image_list'] = [{'file_id': path.split('/')[-1], 'url': path} for path in file_urls]
|
||||
answer = ' '.join([f"" for path in file_urls])
|
||||
return NodeResult({'answer': answer, 'chat_model': tti_model, 'message_list': message_list,
|
||||
'image': [{'file_id': path.split('/')[-1], 'url': path} for path in file_urls],
|
||||
'history_message': history_message, 'question': question}, {})
|
||||
|
||||
def generate_history_ai_message(self, chat_record):
|
||||
for val in chat_record.details.values():
|
||||
if self.node.id == val['node_id'] and 'image_list' in val:
|
||||
if val['dialogue_type'] == 'WORKFLOW':
|
||||
return chat_record.get_ai_message()
|
||||
image_list = val['image_list']
|
||||
return AIMessage(content=[
|
||||
*[{'type': 'image_url', 'image_url': {'url': f'{file_url}'}} for file_url in image_list]
|
||||
])
|
||||
return chat_record.get_ai_message()
|
||||
|
||||
def get_history_message(self, history_chat_record, dialogue_number):
|
||||
start_index = len(history_chat_record) - dialogue_number
|
||||
history_message = reduce(lambda x, y: [*x, *y], [
|
||||
[self.generate_history_human_message(history_chat_record[index]),
|
||||
self.generate_history_ai_message(history_chat_record[index])]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||
return history_message
|
||||
|
||||
def generate_history_human_message(self, chat_record):
|
||||
|
||||
for data in chat_record.details.values():
|
||||
if self.node.id == data['node_id'] and 'image_list' in data:
|
||||
image_list = data['image_list']
|
||||
if len(image_list) == 0 or data['dialogue_type'] == 'WORKFLOW':
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
return HumanMessage(content=data['question'])
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
|
||||
def generate_prompt_question(self, prompt):
|
||||
return self.workflow_manage.generate_prompt(prompt)
|
||||
|
||||
def generate_message_list(self, question: str, history_message):
|
||||
return [
|
||||
*history_message,
|
||||
question
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def reset_message_list(message_list: List[BaseMessage], answer_text):
|
||||
result = [{'role': 'user' if isinstance(message, HumanMessage) else 'ai', 'content': message.content} for
|
||||
message
|
||||
in
|
||||
message_list]
|
||||
result.append({'role': 'ai', 'content': answer_text})
|
||||
return result
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'history_message': [{'content': message.content, 'role': message.type} for message in
|
||||
(self.context.get('history_message') if self.context.get(
|
||||
'history_message') is not None else [])],
|
||||
'question': self.context.get('question'),
|
||||
'answer': self.context.get('answer'),
|
||||
'type': self.node.type,
|
||||
'message_tokens': self.context.get('message_tokens'),
|
||||
'answer_tokens': self.context.get('answer_tokens'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'image_list': self.context.get('image_list'),
|
||||
'dialogue_type': self.context.get('dialogue_type')
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ImageUnderstandNodeSerializer(serializers.Serializer):
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Role Setting")))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
|
||||
# 多轮对话数量
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
|
||||
|
||||
dialogue_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Conversation storage type")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
|
||||
|
||||
model_params_setting = serializers.JSONField(required=False, default=dict,
|
||||
error_messages=ErrMessage.json(_("Model parameter settings")))
|
||||
|
||||
|
||||
class IImageUnderstandNode(INode):
|
||||
type = 'image-understand-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return ImageUnderstandNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
res = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('image_list')[0],
|
||||
self.node_params_serializer.data.get('image_list')[1:])
|
||||
return self.execute(image=res, **self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, history_chat_record, stream, chat_id,
|
||||
model_params_setting,
|
||||
chat_record_id,
|
||||
image,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_image_understand_node import BaseImageUnderstandNode
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
# coding=utf-8
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
|
||||
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode
|
||||
from dataset.models import File
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
from imghdr import what
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
|
||||
chat_model = node_variable.get('chat_model')
|
||||
message_tokens = node_variable['usage_metadata']['output_tokens'] if 'usage_metadata' in node_variable else 0
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据 (流式)
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
answer = ''
|
||||
for chunk in response:
|
||||
answer += chunk.content
|
||||
yield chunk.content
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer)
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点实例对象
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
answer = response.content
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer)
|
||||
|
||||
|
||||
def file_id_to_base64(file_id: str):
|
||||
file = QuerySet(File).filter(id=file_id).first()
|
||||
file_bytes = file.get_byte()
|
||||
base64_image = base64.b64encode(file_bytes).decode("utf-8")
|
||||
return [base64_image, what(None, file_bytes.tobytes())]
|
||||
|
||||
|
||||
class BaseImageUnderstandNode(IImageUnderstandNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, history_chat_record, stream, chat_id,
|
||||
model_params_setting,
|
||||
chat_record_id,
|
||||
image,
|
||||
**kwargs) -> NodeResult:
|
||||
# 处理不正确的参数
|
||||
if image is None or not isinstance(image, list):
|
||||
image = []
|
||||
print(model_params_setting)
|
||||
image_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'), **model_params_setting)
|
||||
# 执行详情中的历史消息不需要图片内容
|
||||
history_message = self.get_history_message_for_details(history_chat_record, dialogue_number)
|
||||
self.context['history_message'] = history_message
|
||||
question = self.generate_prompt_question(prompt)
|
||||
self.context['question'] = question.content
|
||||
# 生成消息列表, 真实的history_message
|
||||
message_list = self.generate_message_list(image_model, system, prompt,
|
||||
self.get_history_message(history_chat_record, dialogue_number), image)
|
||||
self.context['message_list'] = message_list
|
||||
self.context['image_list'] = image
|
||||
self.context['dialogue_type'] = dialogue_type
|
||||
if stream:
|
||||
r = image_model.stream(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': image_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context_stream)
|
||||
else:
|
||||
r = image_model.invoke(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': image_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context)
|
||||
|
||||
def get_history_message_for_details(self, history_chat_record, dialogue_number):
|
||||
start_index = len(history_chat_record) - dialogue_number
|
||||
history_message = reduce(lambda x, y: [*x, *y], [
|
||||
[self.generate_history_human_message_for_details(history_chat_record[index]),
|
||||
self.generate_history_ai_message(history_chat_record[index])]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||
return history_message
|
||||
|
||||
def generate_history_ai_message(self, chat_record):
|
||||
for val in chat_record.details.values():
|
||||
if self.node.id == val['node_id'] and 'image_list' in val:
|
||||
if val['dialogue_type'] == 'WORKFLOW':
|
||||
return chat_record.get_ai_message()
|
||||
return AIMessage(content=val['answer'])
|
||||
return chat_record.get_ai_message()
|
||||
|
||||
def generate_history_human_message_for_details(self, chat_record):
|
||||
for data in chat_record.details.values():
|
||||
if self.node.id == data['node_id'] and 'image_list' in data:
|
||||
image_list = data['image_list']
|
||||
if len(image_list) == 0 or data['dialogue_type'] == 'WORKFLOW':
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
file_id_list = [image.get('file_id') for image in image_list]
|
||||
return HumanMessage(content=[
|
||||
{'type': 'text', 'text': data['question']},
|
||||
*[{'type': 'image_url', 'image_url': {'url': f'/api/file/{file_id}'}} for file_id in file_id_list]
|
||||
|
||||
])
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
|
||||
def get_history_message(self, history_chat_record, dialogue_number):
|
||||
start_index = len(history_chat_record) - dialogue_number
|
||||
history_message = reduce(lambda x, y: [*x, *y], [
|
||||
[self.generate_history_human_message(history_chat_record[index]),
|
||||
self.generate_history_ai_message(history_chat_record[index])]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||
return history_message
|
||||
|
||||
def generate_history_human_message(self, chat_record):
|
||||
|
||||
for data in chat_record.details.values():
|
||||
if self.node.id == data['node_id'] and 'image_list' in data:
|
||||
image_list = data['image_list']
|
||||
if len(image_list) == 0 or data['dialogue_type'] == 'WORKFLOW':
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
image_base64_list = [file_id_to_base64(image.get('file_id')) for image in image_list]
|
||||
return HumanMessage(
|
||||
content=[
|
||||
{'type': 'text', 'text': data['question']},
|
||||
*[{'type': 'image_url', 'image_url': {'url': f'data:image/{base64_image[1]};base64,{base64_image[0]}'}} for
|
||||
base64_image in image_base64_list]
|
||||
])
|
||||
return HumanMessage(content=chat_record.problem_text)
|
||||
|
||||
def generate_prompt_question(self, prompt):
|
||||
return HumanMessage(self.workflow_manage.generate_prompt(prompt))
|
||||
|
||||
def generate_message_list(self, image_model, system: str, prompt: str, history_message, image):
|
||||
if image is not None and len(image) > 0:
|
||||
# 处理多张图片
|
||||
images = []
|
||||
for img in image:
|
||||
file_id = img['file_id']
|
||||
file = QuerySet(File).filter(id=file_id).first()
|
||||
image_bytes = file.get_byte()
|
||||
base64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
image_format = what(None, image_bytes.tobytes())
|
||||
images.append({'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
|
||||
messages = [HumanMessage(
|
||||
content=[
|
||||
{'type': 'text', 'text': self.workflow_manage.generate_prompt(prompt)},
|
||||
*images
|
||||
])]
|
||||
else:
|
||||
messages = [HumanMessage(self.workflow_manage.generate_prompt(prompt))]
|
||||
|
||||
if system is not None and len(system) > 0:
|
||||
return [
|
||||
SystemMessage(self.workflow_manage.generate_prompt(system)),
|
||||
*history_message,
|
||||
*messages
|
||||
]
|
||||
else:
|
||||
return [
|
||||
*history_message,
|
||||
*messages
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def reset_message_list(message_list: List[BaseMessage], answer_text):
|
||||
result = [{'role': 'user' if isinstance(message, HumanMessage) else 'ai', 'content': message.content} for
|
||||
message
|
||||
in
|
||||
message_list]
|
||||
result.append({'role': 'ai', 'content': answer_text})
|
||||
return result
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'system': self.node_params.get('system'),
|
||||
'history_message': [{'content': message.content, 'role': message.type} for message in
|
||||
(self.context.get('history_message') if self.context.get(
|
||||
'history_message') is not None else [])],
|
||||
'question': self.context.get('question'),
|
||||
'answer': self.context.get('answer'),
|
||||
'type': self.node.type,
|
||||
'message_tokens': self.context.get('message_tokens'),
|
||||
'answer_tokens': self.context.get('answer_tokens'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'image_list': self.context.get('image_list'),
|
||||
'dialogue_type': self.context.get('dialogue_type')
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class McpNodeSerializer(serializers.Serializer):
|
||||
mcp_servers = serializers.JSONField(required=True,
|
||||
error_messages=ErrMessage.char(_("Mcp servers")))
|
||||
|
||||
mcp_server = serializers.CharField(required=True,
|
||||
error_messages=ErrMessage.char(_("Mcp server")))
|
||||
|
||||
mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
|
||||
|
||||
tool_params = serializers.DictField(required=True,
|
||||
error_messages=ErrMessage.char(_("Tool parameters")))
|
||||
|
||||
|
||||
class IMcpNode(INode):
|
||||
type = 'mcp-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return McpNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_mcp_node import BaseMcpNode
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# coding=utf-8
|
||||
import asyncio
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode
|
||||
|
||||
|
||||
class BaseMcpNode(IMcpNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.context['tool_params'] = details.get('tool_params')
|
||||
self.context['mcp_tool'] = details.get('mcp_tool')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('result')
|
||||
|
||||
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||
servers = json.loads(mcp_servers)
|
||||
params = json.loads(json.dumps(tool_params))
|
||||
params = self.handle_variables(params)
|
||||
|
||||
async def call_tool(s, session, t, a):
|
||||
async with MultiServerMCPClient(s) as client:
|
||||
s = await client.sessions[session].call_tool(t, a)
|
||||
return s
|
||||
|
||||
res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
|
||||
return NodeResult(
|
||||
{'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})
|
||||
|
||||
def handle_variables(self, tool_params):
|
||||
# 处理参数中的变量
|
||||
for k, v in tool_params.items():
|
||||
if type(v) == str:
|
||||
tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k])
|
||||
if type(v) == dict:
|
||||
self.handle_variables(v)
|
||||
if (type(v) == list) and (type(v[0]) == str):
|
||||
tool_params[k] = self.get_reference_content(v)
|
||||
return tool_params
|
||||
|
||||
def get_reference_content(self, fields: List[str]):
|
||||
return str(self.workflow_manage.get_reference_field(
|
||||
fields[0],
|
||||
fields[1:]))
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'type': self.node.type,
|
||||
'mcp_tool': self.context.get('mcp_tool'),
|
||||
'tool_params': self.context.get('tool_params'),
|
||||
'result': self.context.get('result'),
|
||||
}
|
||||
|
|
@ -12,19 +12,15 @@ from rest_framework import serializers
|
|||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class QuestionNodeSerializer(serializers.Serializer):
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
model_id = serializers.CharField(required=True, error_messages=ErrMessage.char("模型id"))
|
||||
system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("Role Setting")))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
|
||||
error_messages=ErrMessage.char("角色设定"))
|
||||
prompt = serializers.CharField(required=True, error_messages=ErrMessage.char("提示词"))
|
||||
# 多轮对话数量
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
model_params_setting = serializers.DictField(required=False, error_messages=ErrMessage.integer(_("Model parameter settings")))
|
||||
dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer("多轮对话数量"))
|
||||
|
||||
|
||||
class IQuestionNode(INode):
|
||||
|
|
@ -37,6 +33,5 @@ class IQuestionNode(INode):
|
|||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
@date:2024/6/4 14:30
|
||||
@desc:
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import List, Dict
|
||||
|
|
@ -15,25 +15,12 @@ from django.db.models import QuerySet
|
|||
from langchain.schema import HumanMessage, SystemMessage
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
from application.flow import tools
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.question_node.i_question_node import IQuestionNode
|
||||
from common.util.rsa_util import rsa_long_decrypt
|
||||
from setting.models import Model
|
||||
from setting.models_provider import get_model_credential
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
|
||||
chat_model = node_variable.get('chat_model')
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
node.answer_text = answer
|
||||
from setting.models_provider.constants.model_provider_constants import ModelProvideConstants
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -48,8 +35,15 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||
answer = ''
|
||||
for chunk in response:
|
||||
answer += chunk.content
|
||||
yield chunk.content
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer)
|
||||
chat_model = node_variable.get('chat_model')
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -61,53 +55,101 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
|
|||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
chat_model = node_variable.get('chat_model')
|
||||
answer = response.content
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer)
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['history_message'] = node_variable['history_message']
|
||||
node.context['question'] = node_variable['question']
|
||||
|
||||
|
||||
def get_default_model_params_setting(model_id):
|
||||
model = QuerySet(Model).filter(id=model_id).first()
|
||||
credential = get_model_credential(model.provider, model.model_type, model.model_name)
|
||||
model_params_setting = credential.get_model_params_setting_form(
|
||||
model.model_name).get_default_form_data()
|
||||
return model_params_setting
|
||||
def get_to_response_write_context(node_variable: Dict, node: INode):
|
||||
def _write_context(answer, status=200):
|
||||
chat_model = node_variable.get('chat_model')
|
||||
|
||||
if status == 200:
|
||||
answer_tokens = chat_model.get_num_tokens(answer)
|
||||
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
|
||||
else:
|
||||
answer_tokens = 0
|
||||
message_tokens = 0
|
||||
node.err_message = answer
|
||||
node.status = status
|
||||
node.context['message_tokens'] = message_tokens
|
||||
node.context['answer_tokens'] = answer_tokens
|
||||
node.context['answer'] = answer
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
return _write_context
|
||||
|
||||
|
||||
def to_stream_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将流式数据 转换为 流式响应
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器 输出结果后执行
|
||||
@return: 流式响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_stream_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
def to_response(chat_id, chat_record_id, node_variable: Dict, workflow_variable: Dict, node, workflow,
|
||||
post_handler):
|
||||
"""
|
||||
将结果转换
|
||||
@param chat_id: 会话id
|
||||
@param chat_record_id: 对话记录id
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 工作流数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
@param post_handler: 后置处理器
|
||||
@return: 响应
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
_write_context = get_to_response_write_context(node_variable, node)
|
||||
return tools.to_response(chat_id, chat_record_id, response, workflow, _write_context, post_handler)
|
||||
|
||||
|
||||
class BaseQuestionNode(IQuestionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['message_tokens'] = details.get('message_tokens')
|
||||
self.context['answer_tokens'] = details.get('answer_tokens')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
if model_params_setting is None:
|
||||
model_params_setting = get_default_model_params_setting(model_id)
|
||||
chat_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
|
||||
**model_params_setting)
|
||||
model = QuerySet(Model).filter(id=model_id).first()
|
||||
if model is None:
|
||||
raise Exception("模型不存在")
|
||||
chat_model = ModelProvideConstants[model.provider].value.get_model(model.model_type, model.model_name,
|
||||
json.loads(
|
||||
rsa_long_decrypt(model.credential)),
|
||||
streaming=True)
|
||||
history_message = self.get_history_message(history_chat_record, dialogue_number)
|
||||
self.context['history_message'] = history_message
|
||||
question = self.generate_prompt_question(prompt)
|
||||
self.context['question'] = question.content
|
||||
system = self.workflow_manage.generate_prompt(system)
|
||||
self.context['system'] = system
|
||||
message_list = self.generate_message_list(system, prompt, history_message)
|
||||
self.context['message_list'] = message_list
|
||||
if stream:
|
||||
r = chat_model.stream(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'get_to_response_write_context': get_to_response_write_context,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context_stream)
|
||||
_write_context=write_context_stream,
|
||||
_to_response=to_stream_response)
|
||||
else:
|
||||
r = chat_model.invoke(message_list)
|
||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||
'history_message': history_message, 'question': question.content}, {},
|
||||
_write_context=write_context)
|
||||
_write_context=write_context, _to_response=to_response)
|
||||
|
||||
@staticmethod
|
||||
def get_history_message(history_chat_record, dialogue_number):
|
||||
|
|
@ -116,9 +158,6 @@ class BaseQuestionNode(IQuestionNode):
|
|||
[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
|
||||
for index in
|
||||
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||
for message in history_message:
|
||||
if isinstance(message.content, str):
|
||||
message.content = re.sub('<form_rander>[\d\D]*?<\/form_rander>', '', message.content)
|
||||
return history_message
|
||||
|
||||
def generate_prompt_question(self, prompt):
|
||||
|
|
@ -145,7 +184,7 @@ class BaseQuestionNode(IQuestionNode):
|
|||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'system': self.context.get('system'),
|
||||
'system': self.node_params.get('system'),
|
||||
'history_message': [{'content': message.content, 'role': message.type} for message in
|
||||
(self.context.get('history_message') if self.context.get(
|
||||
'history_message') is not None else [])],
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/9/4 11:37
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_reranker_node.py
|
||||
@date:2024/9/4 10:40
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class RerankerSettingSerializer(serializers.Serializer):
|
||||
# 需要查询的条数
|
||||
top_n = serializers.IntegerField(required=True,
|
||||
error_messages=ErrMessage.integer(_("Reference segment number")))
|
||||
# 相似度 0-1之间
|
||||
similarity = serializers.FloatField(required=True, max_value=2, min_value=0,
|
||||
error_messages=ErrMessage.float(_("Reference segment number")))
|
||||
max_paragraph_char_number = serializers.IntegerField(required=True,
|
||||
error_messages=ErrMessage.float(_("Maximum number of words in a quoted segment")))
|
||||
|
||||
|
||||
class RerankerStepNodeSerializer(serializers.Serializer):
|
||||
reranker_setting = RerankerSettingSerializer(required=True)
|
||||
|
||||
question_reference_address = serializers.ListField(required=True)
|
||||
reranker_model_id = serializers.UUIDField(required=True)
|
||||
reranker_reference_list = serializers.ListField(required=True, child=serializers.ListField(required=True))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class IRerankerNode(INode):
|
||||
type = 'reranker-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return RerankerStepNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
question = self.workflow_manage.get_reference_field(
|
||||
self.node_params_serializer.data.get('question_reference_address')[0],
|
||||
self.node_params_serializer.data.get('question_reference_address')[1:])
|
||||
reranker_list = [self.workflow_manage.get_reference_field(
|
||||
reference[0],
|
||||
reference[1:]) for reference in
|
||||
self.node_params_serializer.data.get('reranker_reference_list')]
|
||||
return self.execute(**self.node_params_serializer.data, question=str(question),
|
||||
|
||||
reranker_list=reranker_list)
|
||||
|
||||
def execute(self, question, reranker_setting, reranker_list, reranker_model_id,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/9/4 11:39
|
||||
@desc:
|
||||
"""
|
||||
from .base_reranker_node import *
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_reranker_node.py
|
||||
@date:2024/9/4 11:41
|
||||
@desc:
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.reranker_node.i_reranker_node import IRerankerNode
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
|
||||
def merge_reranker_list(reranker_list, result=None):
|
||||
if result is None:
|
||||
result = []
|
||||
for document in reranker_list:
|
||||
if isinstance(document, list):
|
||||
merge_reranker_list(document, result)
|
||||
elif isinstance(document, dict):
|
||||
content = document.get('title', '') + document.get('content', '')
|
||||
title = document.get("title")
|
||||
dataset_name = document.get("dataset_name")
|
||||
document_name = document.get('document_name')
|
||||
result.append(
|
||||
Document(page_content=str(document) if len(content) == 0 else content,
|
||||
metadata={'title': title, 'dataset_name': dataset_name, 'document_name': document_name}))
|
||||
else:
|
||||
result.append(Document(page_content=str(document), metadata={}))
|
||||
return result
|
||||
|
||||
|
||||
def filter_result(document_list: List[Document], max_paragraph_char_number, top_n, similarity):
|
||||
use_len = 0
|
||||
result = []
|
||||
for index in range(len(document_list)):
|
||||
document = document_list[index]
|
||||
if use_len >= max_paragraph_char_number or index >= top_n or document.metadata.get(
|
||||
'relevance_score') < similarity:
|
||||
break
|
||||
content = document.page_content[0:max_paragraph_char_number - use_len]
|
||||
use_len = use_len + len(content)
|
||||
result.append({'page_content': content, 'metadata': document.metadata})
|
||||
return result
|
||||
|
||||
|
||||
def reset_result_list(result_list: List[Document], document_list: List[Document]):
|
||||
r = []
|
||||
document_list = document_list.copy()
|
||||
for result in result_list:
|
||||
filter_result_list = [document for document in document_list if document.page_content == result.page_content]
|
||||
if len(filter_result_list) > 0:
|
||||
item = filter_result_list[0]
|
||||
document_list.remove(item)
|
||||
r.append(Document(page_content=item.page_content,
|
||||
metadata={**item.metadata, 'relevance_score': result.metadata.get('relevance_score')}))
|
||||
else:
|
||||
r.append(result)
|
||||
return r
|
||||
|
||||
|
||||
class BaseRerankerNode(IRerankerNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['document_list'] = details.get('document_list', [])
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['result_list'] = details.get('result_list')
|
||||
self.context['result'] = details.get('result')
|
||||
|
||||
def execute(self, question, reranker_setting, reranker_list, reranker_model_id,
|
||||
**kwargs) -> NodeResult:
|
||||
documents = merge_reranker_list(reranker_list)
|
||||
top_n = reranker_setting.get('top_n', 3)
|
||||
self.context['document_list'] = [{'page_content': document.page_content, 'metadata': document.metadata} for
|
||||
document in documents]
|
||||
self.context['question'] = question
|
||||
reranker_model = get_model_instance_by_model_user_id(reranker_model_id,
|
||||
self.flow_params_serializer.data.get('user_id'),
|
||||
top_n=top_n)
|
||||
result = reranker_model.compress_documents(
|
||||
documents,
|
||||
question)
|
||||
similarity = reranker_setting.get('similarity', 0.6)
|
||||
max_paragraph_char_number = reranker_setting.get('max_paragraph_char_number', 5000)
|
||||
result = reset_result_list(result, documents)
|
||||
r = filter_result(result, max_paragraph_char_number, top_n, similarity)
|
||||
return NodeResult({'result_list': r, 'result': ''.join([item.get('page_content') for item in r])}, {})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'document_list': self.context.get('document_list'),
|
||||
"question": self.context.get('question'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'reranker_setting': self.node_params_serializer.data.get('reranker_setting'),
|
||||
'result_list': self.context.get('result_list'),
|
||||
'result': self.context.get('result'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -15,31 +15,30 @@ from rest_framework import serializers
|
|||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.common import flat_map
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DatasetSettingSerializer(serializers.Serializer):
|
||||
# 需要查询的条数
|
||||
top_n = serializers.IntegerField(required=True,
|
||||
error_messages=ErrMessage.integer(_("Reference segment number")))
|
||||
error_messages=ErrMessage.integer("引用分段数"))
|
||||
# 相似度 0-1之间
|
||||
similarity = serializers.FloatField(required=True, max_value=2, min_value=0,
|
||||
error_messages=ErrMessage.float(_('similarity')))
|
||||
error_messages=ErrMessage.float("引用分段数"))
|
||||
search_mode = serializers.CharField(required=True, validators=[
|
||||
validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
|
||||
message=_("The type only supports embedding|keywords|blend"), code=500)
|
||||
], error_messages=ErrMessage.char(_("Retrieval Mode")))
|
||||
message="类型只支持register|reset_password", code=500)
|
||||
], error_messages=ErrMessage.char("检索模式"))
|
||||
max_paragraph_char_number = serializers.IntegerField(required=True,
|
||||
error_messages=ErrMessage.float(_("Maximum number of words in a quoted segment")))
|
||||
error_messages=ErrMessage.float("最大引用分段字数"))
|
||||
|
||||
|
||||
class SearchDatasetStepNodeSerializer(serializers.Serializer):
|
||||
# 需要查询的数据集id列表
|
||||
dataset_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
|
||||
error_messages=ErrMessage.list(_("Dataset id list")))
|
||||
error_messages=ErrMessage.list("数据集id列表"))
|
||||
dataset_setting = DatasetSettingSerializer(required=True)
|
||||
|
||||
question_reference_address = serializers.ListField(required=True)
|
||||
question_reference_address = serializers.ListField(required=True, )
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
|
@ -66,7 +65,7 @@ class ISearchDatasetStepNode(INode):
|
|||
if self.flow_params_serializer.data.get('re_chat', False):
|
||||
history_chat_record = self.flow_params_serializer.data.get('history_chat_record', [])
|
||||
paragraph_id_list = [p.get('id') for p in flat_map(
|
||||
[get_paragraph_list(chat_record, self.runtime_node_id) for chat_record in history_chat_record if
|
||||
[get_paragraph_list(chat_record, self.node.id) for chat_record in history_chat_record if
|
||||
chat_record.problem_text == question])]
|
||||
exclude_paragraph_id_list = list(set(paragraph_id_list))
|
||||
|
||||
|
|
|
|||
|
|
@ -10,64 +10,23 @@ import os
|
|||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.db import connection
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.search_dataset_node.i_search_dataset_node import ISearchDatasetStepNode
|
||||
from common.config.embedding_config import VectorStore
|
||||
from common.config.embedding_config import EmbeddingModel, VectorStore
|
||||
from common.db.search import native_search
|
||||
from common.util.file_util import get_file_content
|
||||
from dataset.models import Document, Paragraph, DataSet
|
||||
from dataset.models import Document, Paragraph
|
||||
from embedding.models import SearchMode
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
|
||||
|
||||
def get_embedding_id(dataset_id_list):
|
||||
dataset_list = QuerySet(DataSet).filter(id__in=dataset_id_list)
|
||||
if len(set([dataset.embedding_mode_id for dataset in dataset_list])) > 1:
|
||||
raise Exception("关联知识库的向量模型不一致,无法召回分段。")
|
||||
if len(dataset_list) == 0:
|
||||
raise Exception("知识库设置错误,请重新设置知识库")
|
||||
return dataset_list[0].embedding_mode_id
|
||||
|
||||
|
||||
def get_none_result(question):
|
||||
return NodeResult(
|
||||
{'paragraph_list': [], 'is_hit_handling_method': [], 'question': question, 'data': '',
|
||||
'directly_return': ''}, {})
|
||||
|
||||
|
||||
def reset_title(title):
|
||||
if title is None or len(title.strip()) == 0:
|
||||
return ""
|
||||
else:
|
||||
return f"#### {title}\n"
|
||||
|
||||
|
||||
class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
result = details.get('paragraph_list', [])
|
||||
dataset_setting = self.node_params_serializer.data.get('dataset_setting')
|
||||
directly_return = '\n'.join(
|
||||
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if
|
||||
paragraph.get('is_hit_handling_method')])
|
||||
self.context['paragraph_list'] = result
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['is_hit_handling_method_list'] = [row for row in result if row.get('is_hit_handling_method')]
|
||||
self.context['data'] = '\n'.join(
|
||||
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in
|
||||
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)]
|
||||
self.context['directly_return'] = directly_return
|
||||
|
||||
def execute(self, dataset_id_list, dataset_setting, question,
|
||||
exclude_paragraph_id_list=None,
|
||||
**kwargs) -> NodeResult:
|
||||
self.context['question'] = question
|
||||
if len(dataset_id_list) == 0:
|
||||
return get_none_result(question)
|
||||
model_id = get_embedding_id(dataset_id_list)
|
||||
embedding_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'))
|
||||
embedding_model = EmbeddingModel.get_embedding_model()
|
||||
embedding_value = embedding_model.embed_query(question)
|
||||
vector = VectorStore.get_embedding_vector()
|
||||
exclude_document_id_list = [str(document.id) for document in
|
||||
|
|
@ -77,22 +36,16 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
|||
embedding_list = vector.query(question, embedding_value, dataset_id_list, exclude_document_id_list,
|
||||
exclude_paragraph_id_list, True, dataset_setting.get('top_n'),
|
||||
dataset_setting.get('similarity'), SearchMode(dataset_setting.get('search_mode')))
|
||||
# 手动关闭数据库连接
|
||||
connection.close()
|
||||
if embedding_list is None:
|
||||
return get_none_result(question)
|
||||
return NodeResult({'paragraph_list': [], 'is_hit_handling_method': []}, {})
|
||||
paragraph_list = self.list_paragraph(embedding_list, vector)
|
||||
result = [self.reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
|
||||
result = sorted(result, key=lambda p: p.get('similarity'), reverse=True)
|
||||
return NodeResult({'paragraph_list': result,
|
||||
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
|
||||
'data': '\n'.join(
|
||||
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
|
||||
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
|
||||
'directly_return': '\n'.join(
|
||||
[paragraph.get('content') for paragraph in
|
||||
result if
|
||||
paragraph.get('is_hit_handling_method')]),
|
||||
'data': '\n'.join([paragraph.get('content') for paragraph in paragraph_list]),
|
||||
'directly_return': '\n'.join([paragraph.get('content') for paragraph in result if
|
||||
paragraph.get('is_hit_handling_method')]),
|
||||
'question': question},
|
||||
|
||||
{})
|
||||
|
|
@ -107,12 +60,7 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
|||
**paragraph,
|
||||
'similarity': find_embedding.get('similarity'),
|
||||
'is_hit_handling_method': find_embedding.get('similarity') > paragraph.get(
|
||||
'directly_return_similarity') and paragraph.get('hit_handling_method') == 'directly_return',
|
||||
'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'id': str(paragraph.get('id')),
|
||||
'dataset_id': str(paragraph.get('dataset_id')),
|
||||
'document_id': str(paragraph.get('document_id'))
|
||||
'directly_return_similarity') and paragraph.get('hit_handling_method') == 'directly_return'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SpeechToTextNodeSerializer(serializers.Serializer):
|
||||
stt_model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
audio_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("The audio file cannot be empty")))
|
||||
|
||||
|
||||
class ISpeechToTextNode(INode):
|
||||
type = 'speech-to-text-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return SpeechToTextNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
res = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('audio_list')[0],
|
||||
self.node_params_serializer.data.get('audio_list')[1:])
|
||||
for audio in res:
|
||||
if 'file_id' not in audio:
|
||||
raise ValueError(_("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails"))
|
||||
|
||||
return self.execute(audio=res, **self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, stt_model_id, chat_id,
|
||||
audio,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_speech_to_text_node import BaseSpeechToTextNode
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# coding=utf-8
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import io
|
||||
from typing import List, Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from pydub import AudioSegment
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.speech_to_text_step_node.i_speech_to_text_node import ISpeechToTextNode
|
||||
from common.util.common import split_and_transcribe, any_to_mp3
|
||||
from dataset.models import File
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
class BaseSpeechToTextNode(ISpeechToTextNode):
|
||||
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, stt_model_id, chat_id, audio, **kwargs) -> NodeResult:
|
||||
stt_model = get_model_instance_by_model_user_id(stt_model_id, self.flow_params_serializer.data.get('user_id'))
|
||||
audio_list = audio
|
||||
self.context['audio_list'] = audio
|
||||
|
||||
def process_audio_item(audio_item, model):
|
||||
file = QuerySet(File).filter(id=audio_item['file_id']).first()
|
||||
# 根据file_name 吧文件转成mp3格式
|
||||
file_format = file.file_name.split('.')[-1]
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{file_format}') as temp_file:
|
||||
temp_file.write(file.get_byte().tobytes())
|
||||
temp_file_path = temp_file.name
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_amr_file:
|
||||
temp_mp3_path = temp_amr_file.name
|
||||
any_to_mp3(temp_file_path, temp_mp3_path)
|
||||
try:
|
||||
transcription = split_and_transcribe(temp_mp3_path, model)
|
||||
return {file.file_name: transcription}
|
||||
finally:
|
||||
os.remove(temp_file_path)
|
||||
os.remove(temp_mp3_path)
|
||||
|
||||
def process_audio_items(audio_list, model):
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
results = list(executor.map(lambda item: process_audio_item(item, model), audio_list))
|
||||
return results
|
||||
|
||||
result = process_audio_items(audio_list, stt_model)
|
||||
content = []
|
||||
result_content = []
|
||||
for item in result:
|
||||
for key, value in item.items():
|
||||
content.append(f'### {key}\n{value}')
|
||||
result_content.append(value)
|
||||
return NodeResult({'answer': '\n'.join(result_content), 'result': '\n'.join(result_content),
|
||||
'content': content}, {})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'answer': self.context.get('answer'),
|
||||
'content': self.context.get('content'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'audio_list': self.context.get('audio_list'),
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
@date:2024/6/3 16:54
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
|
||||
|
|
@ -13,6 +16,9 @@ from application.flow.i_step_node import INode, NodeResult
|
|||
class IStarNode(INode):
|
||||
type = 'start-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer] | None:
|
||||
return None
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.flow_params_serializer.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,74 +8,20 @@
|
|||
"""
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import List, Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.start_node.i_start_node import IStarNode
|
||||
|
||||
|
||||
def get_default_global_variable(input_field_list: List):
|
||||
return {item.get('variable'): item.get('default_value') for item in input_field_list if
|
||||
item.get('default_value', None) is not None}
|
||||
|
||||
|
||||
def get_global_variable(node):
|
||||
history_chat_record = node.flow_params_serializer.data.get('history_chat_record', [])
|
||||
history_context = [{'question': chat_record.problem_text, 'answer': chat_record.answer_text} for chat_record in
|
||||
history_chat_record]
|
||||
chat_id = node.flow_params_serializer.data.get('chat_id')
|
||||
return {'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'start_time': time.time(),
|
||||
'history_context': history_context, 'chat_id': str(chat_id), **node.workflow_manage.form_data}
|
||||
|
||||
|
||||
class BaseStartStepNode(IStarNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
base_node = self.workflow_manage.get_base_node()
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
workflow_variable = {**default_global_variable, **get_global_variable(self)}
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['document'] = details.get('document_list')
|
||||
self.context['image'] = details.get('image_list')
|
||||
self.context['audio'] = details.get('audio_list')
|
||||
self.context['other'] = details.get('other_list')
|
||||
self.status = details.get('status')
|
||||
self.err_message = details.get('err_message')
|
||||
for key, value in workflow_variable.items():
|
||||
workflow_manage.context[key] = value
|
||||
for item in details.get('global_fields', []):
|
||||
workflow_manage.context[item.get('key')] = item.get('value')
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
pass
|
||||
|
||||
def execute(self, question, **kwargs) -> NodeResult:
|
||||
base_node = self.workflow_manage.get_base_node()
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
workflow_variable = {**default_global_variable, **get_global_variable(self)}
|
||||
"""
|
||||
开始节点 初始化全局变量
|
||||
"""
|
||||
node_variable = {
|
||||
'question': question,
|
||||
'image': self.workflow_manage.image_list,
|
||||
'document': self.workflow_manage.document_list,
|
||||
'audio': self.workflow_manage.audio_list,
|
||||
'other': self.workflow_manage.other_list,
|
||||
}
|
||||
return NodeResult(node_variable, workflow_variable)
|
||||
return NodeResult({'question': question},
|
||||
{'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'start_time': time.time()})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
global_fields = []
|
||||
for field in self.node.properties.get('config')['globalFields']:
|
||||
key = field['value']
|
||||
global_fields.append({
|
||||
'label': field['label'],
|
||||
'key': key,
|
||||
'value': self.workflow_manage.context[key] if key in self.workflow_manage.context else ''
|
||||
})
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
|
|
@ -83,10 +29,5 @@ class BaseStartStepNode(IStarNode):
|
|||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message,
|
||||
'image_list': self.context.get('image'),
|
||||
'document_list': self.context.get('document'),
|
||||
'audio_list': self.context.get('audio'),
|
||||
'other_list': self.context.get('other'),
|
||||
'global_fields': global_fields
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class TextToSpeechNodeSerializer(serializers.Serializer):
|
||||
tts_model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
|
||||
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
|
||||
|
||||
content_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("Text content")))
|
||||
model_params_setting = serializers.DictField(required=False,
|
||||
error_messages=ErrMessage.integer(_("Model parameter settings")))
|
||||
|
||||
|
||||
class ITextToSpeechNode(INode):
|
||||
type = 'text-to-speech-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return TextToSpeechNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
content = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('content_list')[0],
|
||||
self.node_params_serializer.data.get('content_list')[1:])
|
||||
return self.execute(content=content, **self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, tts_model_id, chat_id,
|
||||
content, model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .base_text_to_speech_node import BaseTextToSpeechNode
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
# coding=utf-8
|
||||
import io
|
||||
import mimetypes
|
||||
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
|
||||
from application.flow.i_step_node import NodeResult, INode
|
||||
from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode
|
||||
from application.flow.step_node.text_to_speech_step_node.i_text_to_speech_node import ITextToSpeechNode
|
||||
from dataset.models import File
|
||||
from dataset.serializers.file_serializers import FileSerializer
|
||||
from setting.models_provider.tools import get_model_instance_by_model_user_id
|
||||
|
||||
|
||||
def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
|
||||
content_type, _ = mimetypes.guess_type(file_name)
|
||||
if content_type is None:
|
||||
# 如果未能识别,设置为默认的二进制文件类型
|
||||
content_type = "application/octet-stream"
|
||||
# 创建一个内存中的字节流对象
|
||||
file_stream = io.BytesIO(file_bytes)
|
||||
|
||||
# 获取文件大小
|
||||
file_size = len(file_bytes)
|
||||
|
||||
uploaded_file = InMemoryUploadedFile(
|
||||
file=file_stream,
|
||||
field_name=None,
|
||||
name=file_name,
|
||||
content_type=content_type,
|
||||
size=file_size,
|
||||
charset=None,
|
||||
)
|
||||
return uploaded_file
|
||||
|
||||
|
||||
class BaseTextToSpeechNode(ITextToSpeechNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
if self.node_params.get('is_result', False):
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, tts_model_id, chat_id,
|
||||
content, model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
self.context['content'] = content
|
||||
model = get_model_instance_by_model_user_id(tts_model_id, self.flow_params_serializer.data.get('user_id'),
|
||||
**model_params_setting)
|
||||
audio_byte = model.text_to_speech(content)
|
||||
# 需要把这个音频文件存储到数据库中
|
||||
file_name = 'generated_audio.mp3'
|
||||
file = bytes_to_uploaded_file(audio_byte, file_name)
|
||||
application = self.workflow_manage.work_flow_post_handler.chat_info.application
|
||||
meta = {
|
||||
'debug': False if application.id else True,
|
||||
'chat_id': chat_id,
|
||||
'application_id': str(application.id) if application.id else None,
|
||||
}
|
||||
file_url = FileSerializer(data={'file': file, 'meta': meta}).upload()
|
||||
# 拼接一个audio标签的src属性
|
||||
audio_label = f'<audio src="{file_url}" controls style = "width: 300px; height: 43px"></audio>'
|
||||
file_id = file_url.split('/')[-1]
|
||||
audio_list = [{'file_id': file_id, 'file_name': file_name, 'url': file_url}]
|
||||
return NodeResult({'answer': audio_label, 'result': audio_list}, {})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'content': self.context.get('content'),
|
||||
'err_message': self.err_message,
|
||||
'answer': self.context.get('answer'),
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .impl import *
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
|
||||
class VariableAssignNodeParamsSerializer(serializers.Serializer):
|
||||
variable_list = serializers.ListField(required=True,
|
||||
error_messages=ErrMessage.list(_("Reference Field")))
|
||||
|
||||
|
||||
class IVariableAssignNode(INode):
|
||||
type = 'variable-assign-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return VariableAssignNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, variable_list, stream, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/6/11 17:49
|
||||
@desc:
|
||||
"""
|
||||
from .base_variable_assign_node import *
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# coding=utf-8
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode
|
||||
|
||||
|
||||
class BaseVariableAssignNode(IVariableAssignNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['variable_list'] = details.get('variable_list')
|
||||
self.context['result_list'] = details.get('result_list')
|
||||
|
||||
def execute(self, variable_list, stream, **kwargs) -> NodeResult:
|
||||
#
|
||||
result_list = []
|
||||
for variable in variable_list:
|
||||
if 'fields' not in variable:
|
||||
continue
|
||||
if 'global' == variable['fields'][0]:
|
||||
result = {
|
||||
'name': variable['name'],
|
||||
'input_value': self.get_reference_content(variable['fields']),
|
||||
}
|
||||
if variable['source'] == 'custom':
|
||||
if variable['type'] == 'json':
|
||||
if isinstance(variable['value'], dict) or isinstance(variable['value'], list):
|
||||
val = variable['value']
|
||||
else:
|
||||
val = json.loads(variable['value'])
|
||||
self.workflow_manage.context[variable['fields'][1]] = val
|
||||
result['output_value'] = variable['value'] = val
|
||||
elif variable['type'] == 'string':
|
||||
# 变量解析 例如:{{global.xxx}}
|
||||
val = self.workflow_manage.generate_prompt(variable['value'])
|
||||
self.workflow_manage.context[variable['fields'][1]] = val
|
||||
result['output_value'] = val
|
||||
else:
|
||||
val = variable['value']
|
||||
self.workflow_manage.context[variable['fields'][1]] = val
|
||||
result['output_value'] = val
|
||||
else:
|
||||
reference = self.get_reference_content(variable['reference'])
|
||||
self.workflow_manage.context[variable['fields'][1]] = reference
|
||||
result['output_value'] = reference
|
||||
result_list.append(result)
|
||||
|
||||
return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {})
|
||||
|
||||
def get_reference_content(self, fields: List[str]):
|
||||
return str(self.workflow_manage.get_reference_field(
|
||||
fields[0],
|
||||
fields[1:]))
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'variable_list': self.context.get('variable_list'),
|
||||
'result_list': self.context.get('result_list'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -16,92 +16,6 @@ from application.flow.i_step_node import WorkFlowPostHandler
|
|||
from common.response import result
|
||||
|
||||
|
||||
class Reasoning:
|
||||
def __init__(self, reasoning_content_start, reasoning_content_end):
|
||||
self.content = ""
|
||||
self.reasoning_content = ""
|
||||
self.all_content = ""
|
||||
self.reasoning_content_start_tag = reasoning_content_start
|
||||
self.reasoning_content_end_tag = reasoning_content_end
|
||||
self.reasoning_content_start_tag_len = len(
|
||||
reasoning_content_start) if reasoning_content_start is not None else 0
|
||||
self.reasoning_content_end_tag_len = len(reasoning_content_end) if reasoning_content_end is not None else 0
|
||||
self.reasoning_content_end_tag_prefix = reasoning_content_end[
|
||||
0] if self.reasoning_content_end_tag_len > 0 else ''
|
||||
self.reasoning_content_is_start = False
|
||||
self.reasoning_content_is_end = False
|
||||
self.reasoning_content_chunk = ""
|
||||
|
||||
def get_end_reasoning_content(self):
|
||||
if not self.reasoning_content_is_start and not self.reasoning_content_is_end:
|
||||
r = {'content': self.all_content, 'reasoning_content': ''}
|
||||
self.reasoning_content_chunk = ""
|
||||
return r
|
||||
if self.reasoning_content_is_start and not self.reasoning_content_is_end:
|
||||
r = {'content': '', 'reasoning_content': self.reasoning_content_chunk}
|
||||
self.reasoning_content_chunk = ""
|
||||
return r
|
||||
return {'content': '', 'reasoning_content': ''}
|
||||
|
||||
def get_reasoning_content(self, chunk):
|
||||
# 如果没有开始思考过程标签那么就全是结果
|
||||
if self.reasoning_content_start_tag is None or len(self.reasoning_content_start_tag) == 0:
|
||||
self.content += chunk.content
|
||||
return {'content': chunk.content, 'reasoning_content': ''}
|
||||
# 如果没有结束思考过程标签那么就全部是思考过程
|
||||
if self.reasoning_content_end_tag is None or len(self.reasoning_content_end_tag) == 0:
|
||||
return {'content': '', 'reasoning_content': chunk.content}
|
||||
self.all_content += chunk.content
|
||||
if not self.reasoning_content_is_start and len(self.all_content) >= self.reasoning_content_start_tag_len:
|
||||
if self.all_content.startswith(self.reasoning_content_start_tag):
|
||||
self.reasoning_content_is_start = True
|
||||
self.reasoning_content_chunk = self.all_content[self.reasoning_content_start_tag_len:]
|
||||
else:
|
||||
if not self.reasoning_content_is_end:
|
||||
self.reasoning_content_is_end = True
|
||||
self.content += self.all_content
|
||||
return {'content': self.all_content, 'reasoning_content': ''}
|
||||
else:
|
||||
if self.reasoning_content_is_start:
|
||||
self.reasoning_content_chunk += chunk.content
|
||||
reasoning_content_end_tag_prefix_index = self.reasoning_content_chunk.find(
|
||||
self.reasoning_content_end_tag_prefix)
|
||||
if self.reasoning_content_is_end:
|
||||
self.content += chunk.content
|
||||
return {'content': chunk.content, 'reasoning_content': ''}
|
||||
# 是否包含结束
|
||||
if reasoning_content_end_tag_prefix_index > -1:
|
||||
if len(self.reasoning_content_chunk) - reasoning_content_end_tag_prefix_index >= self.reasoning_content_end_tag_len:
|
||||
reasoning_content_end_tag_index = self.reasoning_content_chunk.find(self.reasoning_content_end_tag)
|
||||
if reasoning_content_end_tag_index > -1:
|
||||
reasoning_content_chunk = self.reasoning_content_chunk[0:reasoning_content_end_tag_index]
|
||||
content_chunk = self.reasoning_content_chunk[
|
||||
reasoning_content_end_tag_index + self.reasoning_content_end_tag_len:]
|
||||
self.reasoning_content += reasoning_content_chunk
|
||||
self.content += content_chunk
|
||||
self.reasoning_content_chunk = ""
|
||||
self.reasoning_content_is_end = True
|
||||
return {'content': content_chunk, 'reasoning_content': reasoning_content_chunk}
|
||||
else:
|
||||
reasoning_content_chunk = self.reasoning_content_chunk[0:reasoning_content_end_tag_prefix_index + 1]
|
||||
self.reasoning_content_chunk = self.reasoning_content_chunk.replace(reasoning_content_chunk, '')
|
||||
self.reasoning_content += reasoning_content_chunk
|
||||
return {'content': '', 'reasoning_content': reasoning_content_chunk}
|
||||
else:
|
||||
return {'content': '', 'reasoning_content': ''}
|
||||
|
||||
else:
|
||||
if self.reasoning_content_is_end:
|
||||
self.content += chunk.content
|
||||
return {'content': chunk.content, 'reasoning_content': ''}
|
||||
else:
|
||||
# aaa
|
||||
result = {'content': '', 'reasoning_content': self.reasoning_content_chunk}
|
||||
self.reasoning_content += self.reasoning_content_chunk
|
||||
self.reasoning_content_chunk = ""
|
||||
return result
|
||||
|
||||
|
||||
def event_content(chat_id, chat_record_id, response, workflow,
|
||||
write_context,
|
||||
post_handler: WorkFlowPostHandler):
|
||||
|
|
@ -171,21 +85,3 @@ def to_response(chat_id, chat_record_id, response: BaseMessage, workflow, write_
|
|||
post_handler.handler(chat_id, chat_record_id, answer, workflow)
|
||||
return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': answer, 'is_end': True})
|
||||
|
||||
|
||||
def to_response_simple(chat_id, chat_record_id, response: BaseMessage, workflow,
|
||||
post_handler: WorkFlowPostHandler):
|
||||
answer = response.content
|
||||
post_handler.handler(chat_id, chat_record_id, answer, workflow)
|
||||
return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
|
||||
'content': answer, 'is_end': True})
|
||||
|
||||
|
||||
def to_stream_response_simple(stream_event):
|
||||
r = StreamingHttpResponse(
|
||||
streaming_content=stream_event,
|
||||
content_type='text/event-stream;charset=utf-8',
|
||||
charset='utf-8')
|
||||
|
||||
r['Cache-Control'] = 'no-cache'
|
||||
return r
|
||||
|
|
|
|||
|
|
@ -6,34 +6,16 @@
|
|||
@date:2024/1/9 17:40
|
||||
@desc:
|
||||
"""
|
||||
import concurrent
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import reduce
|
||||
from typing import List, Dict
|
||||
|
||||
from django.db import close_old_connections
|
||||
from django.db.models import QuerySet
|
||||
from django.utils import translation
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import gettext as _
|
||||
from langchain_core.messages import AIMessageChunk, AIMessage
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||
|
||||
from application.flow import tools
|
||||
from application.flow.i_step_node import INode, WorkFlowPostHandler, NodeResult
|
||||
from application.flow.step_node import get_node
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.handle.base_to_response import BaseToResponse
|
||||
from common.handle.impl.response.system_to_response import SystemToResponse
|
||||
from function_lib.models.function import FunctionLib
|
||||
from setting.models import Model
|
||||
from setting.models_provider import get_model_credential
|
||||
|
||||
executor = ThreadPoolExecutor(max_workers=200)
|
||||
|
||||
|
||||
class Edge:
|
||||
|
|
@ -57,8 +39,7 @@ class Node:
|
|||
self.__setattr__(keyword, kwargs.get(keyword))
|
||||
|
||||
|
||||
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node', 'application-node',
|
||||
'image-understand-node', 'speech-to-text-node', 'text-to-speech-node', 'image-generate-node']
|
||||
end_nodes = ['ai-chat-node', 'reply-node']
|
||||
|
||||
|
||||
class Flow:
|
||||
|
|
@ -82,11 +63,11 @@ class Flow:
|
|||
def get_search_node(self):
|
||||
return [node for node in self.nodes if node.type == 'search-dataset-node']
|
||||
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
校验工作流数据
|
||||
"""
|
||||
self.is_valid_model_params()
|
||||
self.is_valid_start_node()
|
||||
self.is_valid_base_node()
|
||||
self.is_valid_work_flow()
|
||||
|
|
@ -104,14 +85,18 @@ class Flow:
|
|||
edge_list = [edge for edge in self.edges if edge.sourceAnchorId == source_anchor_id]
|
||||
if len(edge_list) == 0:
|
||||
raise AppApiException(500,
|
||||
_('The branch {branch} of the {node} node needs to be connected').format(
|
||||
node=node.properties.get("stepName"), branch=branch.get("type")))
|
||||
f'{node.properties.get("stepName")} 节点的{branch.get("type")}分支需要连接')
|
||||
elif len(edge_list) > 1:
|
||||
raise AppApiException(500,
|
||||
f'{node.properties.get("stepName")} 节点的{branch.get("type")}分支不能连接俩个节点')
|
||||
|
||||
else:
|
||||
edge_list = [edge for edge in self.edges if edge.sourceNodeId == node.id]
|
||||
if len(edge_list) == 0 and not end_nodes.__contains__(node.type):
|
||||
raise AppApiException(500, _("{node} Nodes cannot be considered as end nodes").format(
|
||||
node=node.properties.get("stepName")))
|
||||
raise AppApiException(500, f'{node.properties.get("stepName")} 节点不能当做结束节点')
|
||||
elif len(edge_list) > 1:
|
||||
raise AppApiException(500,
|
||||
f'{node.properties.get("stepName")} 节点不能连接俩个节点')
|
||||
|
||||
def get_next_nodes(self, node: Node):
|
||||
edge_list = [edge for edge in self.edges if edge.sourceNodeId == node.id]
|
||||
|
|
@ -120,7 +105,7 @@ class Flow:
|
|||
[])
|
||||
if len(node_list) == 0 and not end_nodes.__contains__(node.type):
|
||||
raise AppApiException(500,
|
||||
_("The next node that does not exist"))
|
||||
f'不存在的下一个节点')
|
||||
return node_list
|
||||
|
||||
def is_valid_work_flow(self, up_node=None):
|
||||
|
|
@ -134,536 +119,89 @@ class Flow:
|
|||
def is_valid_start_node(self):
|
||||
start_node_list = [node for node in self.nodes if node.id == 'start-node']
|
||||
if len(start_node_list) == 0:
|
||||
raise AppApiException(500, _('The starting node is required'))
|
||||
raise AppApiException(500, '开始节点必填')
|
||||
if len(start_node_list) > 1:
|
||||
raise AppApiException(500, _('There can only be one starting node'))
|
||||
|
||||
def is_valid_model_params(self):
|
||||
node_list = [node for node in self.nodes if (node.type == 'ai-chat-node' or node.type == 'question-node')]
|
||||
for node in node_list:
|
||||
model = QuerySet(Model).filter(id=node.properties.get('node_data', {}).get('model_id')).first()
|
||||
if model is None:
|
||||
raise ValidationError(ErrorDetail(
|
||||
_('The node {node} model does not exist').format(node=node.properties.get("stepName"))))
|
||||
credential = get_model_credential(model.provider, model.model_type, model.model_name)
|
||||
model_params_setting = node.properties.get('node_data', {}).get('model_params_setting')
|
||||
model_params_setting_form = credential.get_model_params_setting_form(
|
||||
model.model_name)
|
||||
if model_params_setting is None:
|
||||
model_params_setting = model_params_setting_form.get_default_form_data()
|
||||
node.properties.get('node_data', {})['model_params_setting'] = model_params_setting
|
||||
if node.properties.get('status', 200) != 200:
|
||||
raise ValidationError(
|
||||
ErrorDetail(_("Node {node} is unavailable").format(node.properties.get("stepName"))))
|
||||
node_list = [node for node in self.nodes if (node.type == 'function-lib-node')]
|
||||
for node in node_list:
|
||||
function_lib_id = node.properties.get('node_data', {}).get('function_lib_id')
|
||||
if function_lib_id is None:
|
||||
raise ValidationError(ErrorDetail(
|
||||
_('The library ID of node {node} cannot be empty').format(node=node.properties.get("stepName"))))
|
||||
f_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
|
||||
if f_lib is None:
|
||||
raise ValidationError(ErrorDetail(_("The function library for node {node} is not available").format(
|
||||
node=node.properties.get("stepName"))))
|
||||
raise AppApiException(500, '开始节点只能有一个')
|
||||
|
||||
def is_valid_base_node(self):
|
||||
base_node_list = [node for node in self.nodes if node.id == 'base-node']
|
||||
if len(base_node_list) == 0:
|
||||
raise AppApiException(500, _('Basic information node is required'))
|
||||
raise AppApiException(500, '基本信息节点必填')
|
||||
if len(base_node_list) > 1:
|
||||
raise AppApiException(500, _('There can only be one basic information node'))
|
||||
|
||||
|
||||
class NodeResultFuture:
|
||||
def __init__(self, r, e, status=200):
|
||||
self.r = r
|
||||
self.e = e
|
||||
self.status = status
|
||||
|
||||
def result(self):
|
||||
if self.status == 200:
|
||||
return self.r
|
||||
else:
|
||||
raise self.e
|
||||
|
||||
|
||||
def await_result(result, timeout=1):
|
||||
try:
|
||||
result.result(timeout)
|
||||
return False
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
|
||||
class NodeChunkManage:
|
||||
|
||||
def __init__(self, work_flow):
|
||||
self.node_chunk_list = []
|
||||
self.current_node_chunk = None
|
||||
self.work_flow = work_flow
|
||||
|
||||
def add_node_chunk(self, node_chunk):
|
||||
self.node_chunk_list.append(node_chunk)
|
||||
|
||||
def contains(self, node_chunk):
|
||||
return self.node_chunk_list.__contains__(node_chunk)
|
||||
|
||||
def pop(self):
|
||||
if self.current_node_chunk is None:
|
||||
try:
|
||||
current_node_chunk = self.node_chunk_list.pop(0)
|
||||
self.current_node_chunk = current_node_chunk
|
||||
except IndexError as e:
|
||||
pass
|
||||
if self.current_node_chunk is not None:
|
||||
try:
|
||||
chunk = self.current_node_chunk.chunk_list.pop(0)
|
||||
return chunk
|
||||
except IndexError as e:
|
||||
if self.current_node_chunk.is_end():
|
||||
self.current_node_chunk = None
|
||||
if self.work_flow.answer_is_not_empty():
|
||||
chunk = self.work_flow.base_to_response.to_stream_chunk_response(
|
||||
self.work_flow.params['chat_id'],
|
||||
self.work_flow.params['chat_record_id'],
|
||||
'\n\n', False, 0, 0)
|
||||
self.work_flow.append_answer('\n\n')
|
||||
return chunk
|
||||
return self.pop()
|
||||
return None
|
||||
raise AppApiException(500, '基本信息节点只能有一个')
|
||||
|
||||
|
||||
class WorkflowManage:
|
||||
def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler,
|
||||
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None,
|
||||
document_list=None,
|
||||
audio_list=None,
|
||||
other_list=None,
|
||||
start_node_id=None,
|
||||
start_node_data=None, chat_record=None, child_node=None):
|
||||
if form_data is None:
|
||||
form_data = {}
|
||||
if image_list is None:
|
||||
image_list = []
|
||||
if document_list is None:
|
||||
document_list = []
|
||||
if audio_list is None:
|
||||
audio_list = []
|
||||
if other_list is None:
|
||||
other_list = []
|
||||
self.start_node_id = start_node_id
|
||||
self.start_node = None
|
||||
self.form_data = form_data
|
||||
self.image_list = image_list
|
||||
self.document_list = document_list
|
||||
self.audio_list = audio_list
|
||||
self.other_list = other_list
|
||||
def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler):
|
||||
self.params = params
|
||||
self.flow = flow
|
||||
self.context = {}
|
||||
self.node_chunk_manage = NodeChunkManage(self)
|
||||
self.node_context = []
|
||||
self.work_flow_post_handler = work_flow_post_handler
|
||||
self.current_node = None
|
||||
self.current_result = None
|
||||
self.answer = ""
|
||||
self.answer_list = ['']
|
||||
self.status = 200
|
||||
self.base_to_response = base_to_response
|
||||
self.chat_record = chat_record
|
||||
self.child_node = child_node
|
||||
self.future_list = []
|
||||
self.lock = threading.Lock()
|
||||
self.field_list = []
|
||||
self.global_field_list = []
|
||||
self.init_fields()
|
||||
if start_node_id is not None:
|
||||
self.load_node(chat_record, start_node_id, start_node_data)
|
||||
else:
|
||||
self.node_context = []
|
||||
|
||||
def init_fields(self):
|
||||
field_list = []
|
||||
global_field_list = []
|
||||
for node in self.flow.nodes:
|
||||
properties = node.properties
|
||||
node_name = properties.get('stepName')
|
||||
node_id = node.id
|
||||
node_config = properties.get('config')
|
||||
if node_config is not None:
|
||||
fields = node_config.get('fields')
|
||||
if fields is not None:
|
||||
for field in fields:
|
||||
field_list.append({**field, 'node_id': node_id, 'node_name': node_name})
|
||||
global_fields = node_config.get('globalFields')
|
||||
if global_fields is not None:
|
||||
for global_field in global_fields:
|
||||
global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name})
|
||||
field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
|
||||
global_field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
|
||||
self.field_list = field_list
|
||||
self.global_field_list = global_field_list
|
||||
|
||||
def append_answer(self, content):
|
||||
self.answer += content
|
||||
self.answer_list[-1] += content
|
||||
|
||||
def answer_is_not_empty(self):
|
||||
return len(self.answer_list[-1]) > 0
|
||||
|
||||
def load_node(self, chat_record, start_node_id, start_node_data):
|
||||
self.node_context = []
|
||||
self.answer = chat_record.answer_text
|
||||
self.answer_list = chat_record.answer_text_list
|
||||
self.answer_list.append('')
|
||||
for node_details in sorted(chat_record.details.values(), key=lambda d: d.get('index')):
|
||||
node_id = node_details.get('node_id')
|
||||
if node_details.get('runtime_node_id') == start_node_id:
|
||||
def get_node_params(n):
|
||||
is_result = False
|
||||
if n.type == 'application-node':
|
||||
is_result = True
|
||||
return {**n.properties.get('node_data'), 'form_data': start_node_data, 'node_data': start_node_data,
|
||||
'child_node': self.child_node, 'is_result': is_result}
|
||||
|
||||
self.start_node = self.get_node_cls_by_id(node_id, node_details.get('up_node_id_list'),
|
||||
get_node_params=get_node_params)
|
||||
self.start_node.valid_args(
|
||||
{**self.start_node.node_params, 'form_data': start_node_data}, self.start_node.workflow_params)
|
||||
if self.start_node.type == 'application-node':
|
||||
application_node_dict = node_details.get('application_node_dict', {})
|
||||
self.start_node.context['application_node_dict'] = application_node_dict
|
||||
self.node_context.append(self.start_node)
|
||||
continue
|
||||
|
||||
node_id = node_details.get('node_id')
|
||||
node = self.get_node_cls_by_id(node_id, node_details.get('up_node_id_list'))
|
||||
node.valid_args(node.node_params, node.workflow_params)
|
||||
node.save_context(node_details, self)
|
||||
node.node_chunk.end()
|
||||
self.node_context.append(node)
|
||||
|
||||
def run(self):
|
||||
close_old_connections()
|
||||
language = get_language()
|
||||
if self.params.get('stream'):
|
||||
return self.run_stream(self.start_node, None, language)
|
||||
return self.run_block(language)
|
||||
|
||||
def run_block(self, language='zh'):
|
||||
"""
|
||||
非流式响应
|
||||
@return: 结果
|
||||
运行工作流
|
||||
"""
|
||||
self.run_chain_async(None, None, language)
|
||||
while self.is_run():
|
||||
pass
|
||||
details = self.get_runtime_details()
|
||||
message_tokens = sum([row.get('message_tokens') for row in details.values() if
|
||||
'message_tokens' in row and row.get('message_tokens') is not None])
|
||||
answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
|
||||
'answer_tokens' in row and row.get('answer_tokens') is not None])
|
||||
answer_text_list = self.get_answer_text_list()
|
||||
answer_text = '\n\n'.join(
|
||||
'\n\n'.join([a.get('content') for a in answer]) for answer in
|
||||
answer_text_list)
|
||||
answer_list = reduce(lambda pre, _n: [*pre, *_n], answer_text_list, [])
|
||||
self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
|
||||
answer_text,
|
||||
self)
|
||||
return self.base_to_response.to_block_response(self.params['chat_id'],
|
||||
self.params['chat_record_id'], answer_text, True
|
||||
, message_tokens, answer_tokens,
|
||||
_status=status.HTTP_200_OK if self.status == 200 else status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
other_params={'answer_list': answer_list})
|
||||
|
||||
def run_stream(self, current_node, node_result_future, language='zh'):
|
||||
"""
|
||||
流式响应
|
||||
@return:
|
||||
"""
|
||||
self.run_chain_async(current_node, node_result_future, language)
|
||||
return tools.to_stream_response_simple(self.await_result())
|
||||
|
||||
def is_run(self, timeout=0.5):
|
||||
future_list_len = len(self.future_list)
|
||||
try:
|
||||
r = concurrent.futures.wait(self.future_list, timeout)
|
||||
if len(r.not_done) > 0:
|
||||
return True
|
||||
while self.has_next_node(self.current_result):
|
||||
self.current_node = self.get_next_node()
|
||||
self.node_context.append(self.current_node)
|
||||
self.current_result = self.current_node.run()
|
||||
if self.has_next_node(self.current_result):
|
||||
self.current_result.write_context(self.current_node, self)
|
||||
else:
|
||||
r = self.current_result.to_response(self.params['chat_id'], self.params['chat_record_id'],
|
||||
self.current_node, self,
|
||||
self.work_flow_post_handler)
|
||||
return r
|
||||
except Exception as e:
|
||||
if self.params.get('stream'):
|
||||
return tools.to_stream_response(self.params['chat_id'], self.params['chat_record_id'],
|
||||
iter([AIMessageChunk(str(e))]), self,
|
||||
self.current_node.get_write_error_context(e),
|
||||
self.work_flow_post_handler)
|
||||
else:
|
||||
if future_list_len == len(self.future_list):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
def await_result(self):
|
||||
try:
|
||||
while self.is_run():
|
||||
while True:
|
||||
chunk = self.node_chunk_manage.pop()
|
||||
if chunk is not None:
|
||||
yield chunk
|
||||
else:
|
||||
break
|
||||
while True:
|
||||
chunk = self.node_chunk_manage.pop()
|
||||
if chunk is None:
|
||||
break
|
||||
yield chunk
|
||||
finally:
|
||||
while self.is_run():
|
||||
pass
|
||||
details = self.get_runtime_details()
|
||||
message_tokens = sum([row.get('message_tokens') for row in details.values() if
|
||||
'message_tokens' in row and row.get('message_tokens') is not None])
|
||||
answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
|
||||
'answer_tokens' in row and row.get('answer_tokens') is not None])
|
||||
self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
|
||||
self.answer,
|
||||
self)
|
||||
yield self.base_to_response.to_stream_chunk_response(self.params['chat_id'],
|
||||
self.params['chat_record_id'],
|
||||
'',
|
||||
[],
|
||||
'', True, message_tokens, answer_tokens, {})
|
||||
|
||||
def run_chain_async(self, current_node, node_result_future, language='zh'):
|
||||
future = executor.submit(self.run_chain_manage, current_node, node_result_future, language)
|
||||
self.future_list.append(future)
|
||||
|
||||
def run_chain_manage(self, current_node, node_result_future, language='zh'):
|
||||
translation.activate(language)
|
||||
if current_node is None:
|
||||
start_node = self.get_start_node()
|
||||
current_node = get_node(start_node.type)(start_node, self.params, self)
|
||||
self.node_chunk_manage.add_node_chunk(current_node.node_chunk)
|
||||
# 添加节点
|
||||
self.append_node(current_node)
|
||||
result = self.run_chain(current_node, node_result_future)
|
||||
if result is None:
|
||||
return
|
||||
node_list = self.get_next_node_list(current_node, result)
|
||||
if len(node_list) == 1:
|
||||
self.run_chain_manage(node_list[0], None, language)
|
||||
elif len(node_list) > 1:
|
||||
sorted_node_run_list = sorted(node_list, key=lambda n: n.node.y)
|
||||
# 获取到可执行的子节点
|
||||
result_list = [{'node': node, 'future': executor.submit(self.run_chain_manage, node, None, language)} for
|
||||
node in
|
||||
sorted_node_run_list]
|
||||
for r in result_list:
|
||||
self.future_list.append(r.get('future'))
|
||||
|
||||
def run_chain(self, current_node, node_result_future=None):
|
||||
if node_result_future is None:
|
||||
node_result_future = self.run_node_future(current_node)
|
||||
try:
|
||||
is_stream = self.params.get('stream', True)
|
||||
result = self.hand_event_node_result(current_node,
|
||||
node_result_future) if is_stream else self.hand_node_result(
|
||||
current_node, node_result_future)
|
||||
return result
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def hand_node_result(self, current_node, node_result_future):
|
||||
try:
|
||||
current_result = node_result_future.result()
|
||||
result = current_result.write_context(current_node, self)
|
||||
if result is not None:
|
||||
# 阻塞获取结果
|
||||
list(result)
|
||||
return current_result
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.status = 500
|
||||
current_node.get_write_error_context(e)
|
||||
self.answer += str(e)
|
||||
finally:
|
||||
current_node.node_chunk.end()
|
||||
|
||||
def append_node(self, current_node):
|
||||
for index in range(len(self.node_context)):
|
||||
n = self.node_context[index]
|
||||
if current_node.id == n.node.id and current_node.runtime_node_id == n.runtime_node_id:
|
||||
self.node_context[index] = current_node
|
||||
return
|
||||
self.node_context.append(current_node)
|
||||
|
||||
def hand_event_node_result(self, current_node, node_result_future):
|
||||
runtime_node_id = current_node.runtime_node_id
|
||||
real_node_id = current_node.runtime_node_id
|
||||
child_node = {}
|
||||
view_type = current_node.view_type
|
||||
try:
|
||||
current_result = node_result_future.result()
|
||||
result = current_result.write_context(current_node, self)
|
||||
if result is not None:
|
||||
if self.is_result(current_node, current_result):
|
||||
for r in result:
|
||||
reasoning_content = ''
|
||||
content = r
|
||||
child_node = {}
|
||||
node_is_end = False
|
||||
view_type = current_node.view_type
|
||||
if isinstance(r, dict):
|
||||
content = r.get('content')
|
||||
child_node = {'runtime_node_id': r.get('runtime_node_id'),
|
||||
'chat_record_id': r.get('chat_record_id')
|
||||
, 'child_node': r.get('child_node')}
|
||||
if r.__contains__('real_node_id'):
|
||||
real_node_id = r.get('real_node_id')
|
||||
if r.__contains__('node_is_end'):
|
||||
node_is_end = r.get('node_is_end')
|
||||
view_type = r.get('view_type')
|
||||
reasoning_content = r.get('reasoning_content')
|
||||
chunk = self.base_to_response.to_stream_chunk_response(self.params['chat_id'],
|
||||
self.params['chat_record_id'],
|
||||
current_node.id,
|
||||
current_node.up_node_id_list,
|
||||
content, False, 0, 0,
|
||||
{'node_type': current_node.type,
|
||||
'runtime_node_id': runtime_node_id,
|
||||
'view_type': view_type,
|
||||
'child_node': child_node,
|
||||
'node_is_end': node_is_end,
|
||||
'real_node_id': real_node_id,
|
||||
'reasoning_content': reasoning_content})
|
||||
current_node.node_chunk.add_chunk(chunk)
|
||||
chunk = (self.base_to_response
|
||||
.to_stream_chunk_response(self.params['chat_id'],
|
||||
self.params['chat_record_id'],
|
||||
current_node.id,
|
||||
current_node.up_node_id_list,
|
||||
'', False, 0, 0, {'node_is_end': True,
|
||||
'runtime_node_id': runtime_node_id,
|
||||
'node_type': current_node.type,
|
||||
'view_type': view_type,
|
||||
'child_node': child_node,
|
||||
'real_node_id': real_node_id,
|
||||
'reasoning_content': ''}))
|
||||
current_node.node_chunk.add_chunk(chunk)
|
||||
else:
|
||||
list(result)
|
||||
return current_result
|
||||
except Exception as e:
|
||||
# 添加节点
|
||||
traceback.print_exc()
|
||||
chunk = self.base_to_response.to_stream_chunk_response(self.params['chat_id'],
|
||||
self.params['chat_record_id'],
|
||||
current_node.id,
|
||||
current_node.up_node_id_list,
|
||||
'Exception:' + str(e), False, 0, 0,
|
||||
{'node_is_end': True,
|
||||
'runtime_node_id': current_node.runtime_node_id,
|
||||
'node_type': current_node.type,
|
||||
'view_type': current_node.view_type,
|
||||
'child_node': {},
|
||||
'real_node_id': real_node_id})
|
||||
current_node.node_chunk.add_chunk(chunk)
|
||||
current_node.get_write_error_context(e)
|
||||
self.status = 500
|
||||
return None
|
||||
finally:
|
||||
current_node.node_chunk.end()
|
||||
|
||||
def run_node_async(self, node):
|
||||
future = executor.submit(self.run_node, node)
|
||||
return future
|
||||
|
||||
def run_node_future(self, node):
|
||||
try:
|
||||
node.valid_args(node.node_params, node.workflow_params)
|
||||
result = self.run_node(node)
|
||||
return NodeResultFuture(result, None, 200)
|
||||
except Exception as e:
|
||||
return NodeResultFuture(None, e, 500)
|
||||
|
||||
def run_node(self, node):
|
||||
result = node.run()
|
||||
return result
|
||||
|
||||
def is_result(self, current_node, current_node_result):
|
||||
return current_node.node_params.get('is_result', not self._has_next_node(
|
||||
current_node, current_node_result)) if current_node.node_params is not None else False
|
||||
|
||||
def get_chunk_content(self, chunk, is_end=False):
|
||||
return 'data: ' + json.dumps(
|
||||
{'chat_id': self.params['chat_id'], 'id': self.params['chat_record_id'], 'operate': True,
|
||||
'content': chunk, 'is_end': is_end}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
def _has_next_node(self, current_node, node_result: NodeResult | None):
|
||||
"""
|
||||
是否有下一个可运行的节点
|
||||
"""
|
||||
if node_result is not None and node_result.is_assertion_result():
|
||||
for edge in self.flow.edges:
|
||||
if (edge.sourceNodeId == current_node.id and
|
||||
f"{edge.sourceNodeId}_{node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
|
||||
return True
|
||||
else:
|
||||
for edge in self.flow.edges:
|
||||
if edge.sourceNodeId == current_node.id:
|
||||
return True
|
||||
return tools.to_response(self.params['chat_id'], self.params['chat_record_id'],
|
||||
AIMessage(str(e)), self, self.current_node.get_write_error_context(e),
|
||||
self.work_flow_post_handler)
|
||||
|
||||
def has_next_node(self, node_result: NodeResult | None):
|
||||
"""
|
||||
是否有下一个可运行的节点
|
||||
"""
|
||||
return self._has_next_node(self.get_start_node() if self.current_node is None else self.current_node,
|
||||
node_result)
|
||||
if self.current_node is None:
|
||||
if self.get_start_node() is not None:
|
||||
return True
|
||||
else:
|
||||
if node_result is not None and node_result.is_assertion_result():
|
||||
for edge in self.flow.edges:
|
||||
if (edge.sourceNodeId == self.current_node.id and
|
||||
f"{edge.sourceNodeId}_{node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
|
||||
return True
|
||||
else:
|
||||
for edge in self.flow.edges:
|
||||
if edge.sourceNodeId == self.current_node.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_runtime_details(self):
|
||||
details_result = {}
|
||||
for index in range(len(self.node_context)):
|
||||
node = self.node_context[index]
|
||||
if self.chat_record is not None and self.chat_record.details is not None:
|
||||
details = self.chat_record.details.get(node.runtime_node_id)
|
||||
if details is not None and self.start_node.runtime_node_id != node.runtime_node_id:
|
||||
details_result[node.runtime_node_id] = details
|
||||
continue
|
||||
details = node.get_details(index)
|
||||
details['node_id'] = node.id
|
||||
details['up_node_id_list'] = node.up_node_id_list
|
||||
details['runtime_node_id'] = node.runtime_node_id
|
||||
details_result[node.runtime_node_id] = details
|
||||
details_result[node.id] = details
|
||||
return details_result
|
||||
|
||||
def get_answer_text_list(self):
|
||||
result = []
|
||||
answer_list = reduce(lambda x, y: [*x, *y],
|
||||
[n.get_answer_list() for n in self.node_context if n.get_answer_list() is not None],
|
||||
[])
|
||||
up_node = None
|
||||
for index in range(len(answer_list)):
|
||||
current_answer = answer_list[index]
|
||||
if len(current_answer.content) > 0:
|
||||
if up_node is None or current_answer.view_type == 'single_view' or (
|
||||
current_answer.view_type == 'many_view' and up_node.view_type == 'single_view'):
|
||||
result.append([current_answer])
|
||||
else:
|
||||
if len(result) > 0:
|
||||
exec_index = len(result) - 1
|
||||
if isinstance(result[exec_index], list):
|
||||
result[exec_index].append(current_answer)
|
||||
else:
|
||||
result.insert(0, [current_answer])
|
||||
up_node = current_answer
|
||||
if len(result) == 0:
|
||||
# 如果没有响应 就响应一个空数据
|
||||
return [[]]
|
||||
return [[item.to_dict() for item in r] for r in result]
|
||||
|
||||
def get_next_node(self):
|
||||
"""
|
||||
获取下一个可运行的所有节点
|
||||
"""
|
||||
if self.current_node is None:
|
||||
node = self.get_start_node()
|
||||
node_instance = get_node(node.type)(node, self.params, self)
|
||||
node_instance = get_node(node.type)(node, self.params, self.context)
|
||||
return node_instance
|
||||
if self.current_result is not None and self.current_result.is_assertion_result():
|
||||
for edge in self.flow.edges:
|
||||
|
|
@ -677,77 +215,9 @@ class WorkflowManage:
|
|||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def dependent_node(up_node_id, node):
|
||||
if not node.node_chunk.is_end():
|
||||
return False
|
||||
if node.id == up_node_id:
|
||||
if node.type == 'form-node':
|
||||
if node.context.get('form_data', None) is not None:
|
||||
return True
|
||||
return False
|
||||
return True
|
||||
|
||||
def dependent_node_been_executed(self, node_id):
|
||||
"""
|
||||
判断依赖节点是否都已执行
|
||||
@param node_id: 需要判断的节点id
|
||||
@return:
|
||||
"""
|
||||
up_node_id_list = [edge.sourceNodeId for edge in self.flow.edges if edge.targetNodeId == node_id]
|
||||
return all([any([self.dependent_node(up_node_id, node) for node in self.node_context]) for up_node_id in
|
||||
up_node_id_list])
|
||||
|
||||
def get_up_node_id_list(self, node_id):
|
||||
up_node_id_list = [edge.sourceNodeId for edge in self.flow.edges if edge.targetNodeId == node_id]
|
||||
return up_node_id_list
|
||||
|
||||
def get_next_node_list(self, current_node, current_node_result):
|
||||
"""
|
||||
获取下一个可执行节点列表
|
||||
@param current_node: 当前可执行节点
|
||||
@param current_node_result: 当前可执行节点结果
|
||||
@return: 可执行节点列表
|
||||
"""
|
||||
# 判断是否中断执行
|
||||
if current_node_result.is_interrupt_exec(current_node):
|
||||
return []
|
||||
node_list = []
|
||||
if current_node_result is not None and current_node_result.is_assertion_result():
|
||||
for edge in self.flow.edges:
|
||||
if (edge.sourceNodeId == current_node.id and
|
||||
f"{edge.sourceNodeId}_{current_node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
|
||||
next_node = [node for node in self.flow.nodes if node.id == edge.targetNodeId]
|
||||
if len(next_node) == 0:
|
||||
continue
|
||||
if next_node[0].properties.get('condition', "AND") == 'AND':
|
||||
if self.dependent_node_been_executed(edge.targetNodeId):
|
||||
node_list.append(
|
||||
self.get_node_cls_by_id(edge.targetNodeId,
|
||||
[*current_node.up_node_id_list, current_node.node.id]))
|
||||
else:
|
||||
node_list.append(
|
||||
self.get_node_cls_by_id(edge.targetNodeId,
|
||||
[*current_node.up_node_id_list, current_node.node.id]))
|
||||
else:
|
||||
for edge in self.flow.edges:
|
||||
if edge.sourceNodeId == current_node.id:
|
||||
next_node = [node for node in self.flow.nodes if node.id == edge.targetNodeId]
|
||||
if len(next_node) == 0:
|
||||
continue
|
||||
if next_node[0].properties.get('condition', "AND") == 'AND':
|
||||
if self.dependent_node_been_executed(edge.targetNodeId):
|
||||
node_list.append(
|
||||
self.get_node_cls_by_id(edge.targetNodeId,
|
||||
[*current_node.up_node_id_list, current_node.node.id]))
|
||||
else:
|
||||
node_list.append(
|
||||
self.get_node_cls_by_id(edge.targetNodeId,
|
||||
[*current_node.up_node_id_list, current_node.node.id]))
|
||||
return node_list
|
||||
|
||||
def get_reference_field(self, node_id: str, fields: List[str]):
|
||||
"""
|
||||
|
||||
@param node_id: 节点id
|
||||
@param fields: 字段
|
||||
@return:
|
||||
|
|
@ -757,37 +227,35 @@ class WorkflowManage:
|
|||
else:
|
||||
return self.get_node_by_id(node_id).get_reference_field(fields)
|
||||
|
||||
def get_workflow_content(self):
|
||||
context = {
|
||||
'global': self.context,
|
||||
}
|
||||
|
||||
for node in self.node_context:
|
||||
context[node.id] = node.context
|
||||
return context
|
||||
|
||||
def reset_prompt(self, prompt: str):
|
||||
placeholder = "{}"
|
||||
for field in self.field_list:
|
||||
globeLabel = f"{field.get('node_name')}.{field.get('value')}"
|
||||
globeValue = f"context.get('{field.get('node_id')}',{placeholder}).get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
for field in self.global_field_list:
|
||||
globeLabel = f"全局变量.{field.get('value')}"
|
||||
globeLabelNew = f"global.{field.get('value')}"
|
||||
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
|
||||
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
|
||||
return prompt
|
||||
|
||||
def generate_prompt(self, prompt: str):
|
||||
"""
|
||||
格式化生成提示词
|
||||
@param prompt: 提示词信息
|
||||
@return: 格式化后的提示词
|
||||
"""
|
||||
context = self.get_workflow_content()
|
||||
prompt = self.reset_prompt(prompt)
|
||||
context = {
|
||||
'global': self.context,
|
||||
}
|
||||
|
||||
for node in self.node_context:
|
||||
properties = node.node.properties
|
||||
node_config = properties.get('config')
|
||||
if node_config is not None:
|
||||
fields = node_config.get('fields')
|
||||
if fields is not None:
|
||||
for field in fields:
|
||||
globeLabel = f"{properties.get('stepName')}.{field.get('value')}"
|
||||
globeValue = f"context['{node.id}'].{field.get('value')}"
|
||||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
global_fields = node_config.get('globalFields')
|
||||
if global_fields is not None:
|
||||
for field in global_fields:
|
||||
globeLabel = f"全局变量.{field.get('value')}"
|
||||
globeValue = f"context['global'].{field.get('value')}"
|
||||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
context[node.id] = node.context
|
||||
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
|
||||
|
||||
value = prompt_template.format(context=context)
|
||||
return value
|
||||
|
||||
|
|
@ -799,20 +267,11 @@ class WorkflowManage:
|
|||
start_node_list = [node for node in self.flow.nodes if node.type == 'start-node']
|
||||
return start_node_list[0]
|
||||
|
||||
def get_base_node(self):
|
||||
"""
|
||||
获取基础节点
|
||||
@return:
|
||||
"""
|
||||
base_node_list = [node for node in self.flow.nodes if node.type == 'base-node']
|
||||
return base_node_list[0]
|
||||
|
||||
def get_node_cls_by_id(self, node_id, up_node_id_list=None,
|
||||
get_node_params=lambda node: node.properties.get('node_data')):
|
||||
def get_node_cls_by_id(self, node_id):
|
||||
for node in self.flow.nodes:
|
||||
if node.id == node_id:
|
||||
node_instance = get_node(node.type)(node,
|
||||
self.params, self, up_node_id_list, get_node_params)
|
||||
self.params, self)
|
||||
return node_instance
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-15 15:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import common.encoder.encoder
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('application', '0009_application_type_application_work_flow_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='chatrecord',
|
||||
name='details',
|
||||
field=models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.2.15 on 2024-08-23 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('application', '0010_alter_chatrecord_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='model_params_setting',
|
||||
field=models.JSONField(default=dict, verbose_name='模型参数相关设置'),
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue