Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661ee79943 | ||
|
|
60ee160131 | ||
|
|
008d0af010 | ||
|
|
f2fb0aedfd | ||
|
|
1dca5edcc6 | ||
|
|
1942cb0d67 | ||
|
|
bf6dbfb245 | ||
|
|
d37433eacd | ||
|
|
a3534407bf | ||
|
|
3091a90df6 | ||
|
|
41b8f4443c | ||
|
|
777f089423 | ||
|
|
b23e00f3e5 | ||
|
|
3b776b6639 | ||
|
|
dd8f2744bf | ||
|
|
7db8d3ea0f | ||
|
|
ad7a17bf40 | ||
|
|
76ac5238b6 | ||
|
|
add73aa2c5 | ||
|
|
bcf9491999 | ||
|
|
d0041a98b4 | ||
|
|
29d152784f | ||
|
|
cd7214ba8d | ||
|
|
6a84e73a82 | ||
|
|
98ce5103a0 | ||
|
|
c65a36d3ab | ||
|
|
b6e49da288 | ||
|
|
45998f9cf5 | ||
|
|
4197f63751 | ||
|
|
ace8134a16 | ||
|
|
7f1fecb84e | ||
|
|
bf172fab81 | ||
|
|
36f5648cae | ||
|
|
ab57bfcc4a | ||
|
|
11848b8f44 | ||
|
|
a11e0bd9c3 | ||
|
|
f6552d0d4f | ||
|
|
38d4db5d5f | ||
|
|
63cd379682 | ||
|
|
9136c9306a | ||
|
|
c9db9f33ea | ||
|
|
3d7178d06f | ||
|
|
a4ff5a3f73 | ||
|
|
814c5b3d3c | ||
|
|
e7e0677291 | ||
|
|
823f4b7ad1 | ||
|
|
a3c77480f7 | ||
|
|
e367265dbb | ||
|
|
7e0deb29e0 | ||
|
|
0d94db4331 | ||
|
|
177482b33a | ||
|
|
63b183a9fe | ||
|
|
858117f8c0 | ||
|
|
ac4355d2e1 | ||
|
|
ce7da2db66 | ||
|
|
0a4a1def1e | ||
|
|
35f4deca76 | ||
|
|
ba1451a0e9 | ||
|
|
40d69e6e20 | ||
|
|
b8ba947ba8 | ||
|
|
06be57815e | ||
|
|
81e37a5736 |
15
.github/imgs/logo-left.svg
vendored
Normal file
15
.github/imgs/logo-left.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
19
.github/workflows/bot-issues-translator.yml
vendored
Normal file
19
.github/workflows/bot-issues-translator.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Github Rebot for issues-translator'
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
BOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -55,8 +55,6 @@ jobs:
|
||||
# Step 4 - Builds the site using Hugo
|
||||
- name: Build
|
||||
run: cd docSite && hugo mod get -u github.com/colinwilson/lotusdocs && hugo -v --minify
|
||||
env:
|
||||
HUGO_BASEURL: ${{ vars.BASE_URL }}
|
||||
|
||||
# Step 5 - Push our generated site to Vercel
|
||||
- name: Deploy to Vercel
|
||||
|
||||
2
.github/workflows/deploy-preview.yml
vendored
2
.github/workflows/deploy-preview.yml
vendored
@@ -55,8 +55,6 @@ jobs:
|
||||
# Step 4 - Builds the site using Hugo
|
||||
- name: Build
|
||||
run: cd docSite && hugo mod get -u github.com/colinwilson/lotusdocs && hugo -v --minify
|
||||
env:
|
||||
HUGO_BASEURL: ${{ vars.BASE_URL }}
|
||||
|
||||
# Step 5 - Push our generated site to Vercel
|
||||
- name: Deploy to Vercel
|
||||
|
||||
98
.github/workflows/docs-image.yml
vendored
Normal file
98
.github/workflows/docs-image.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Build FastGPT docs images and copy image to docker hub
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'docSite/**'
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
jobs:
|
||||
build-fastgpt-docs-images:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Set up QEMU (optional)
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-docs:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Build and publish image for main branch or tag push event
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg name=app \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f docSite/Dockerfile \
|
||||
.
|
||||
push-to-docker-hub:
|
||||
needs: build-fastgpt-docs-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_NAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Pull image from GitHub Container Registry
|
||||
run: docker pull ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{env.IMAGE_TAG}}
|
||||
- name: Tag image with Docker Hub repository name and version tag
|
||||
run: docker tag ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{env.IMAGE_TAG}} ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
- name: Push image to Docker Hub
|
||||
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
|
||||
update-docs-image:
|
||||
needs: build-fastgpt-docs-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions-hub/kubectl@master
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
with:
|
||||
args: rollout restart deployment fastgpt-docs
|
||||
11
.github/workflows/fastgpt-image.yml
vendored
11
.github/workflows/fastgpt-image.yml
vendored
@@ -1,9 +1,10 @@
|
||||
name: Build fastgpt images and copy image to docker hub
|
||||
name: Build FastGPT images and copy image to docker hub
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'projects/app/**'
|
||||
- 'packages/**'
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
@@ -49,12 +50,12 @@ jobs:
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
cd client && \
|
||||
docker buildx build \
|
||||
--build-arg name=app \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
@@ -64,6 +65,7 @@ jobs:
|
||||
push-to-docker-hub:
|
||||
needs: build-fastgpt-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -87,6 +89,7 @@ jobs:
|
||||
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
push-to-ali-hub:
|
||||
needs: build-fastgpt-images
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
55
.github/workflows/preview-image.yml
vendored
Normal file
55
.github/workflows/preview-image.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Preview FastGPT images
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'projects/app/**'
|
||||
- 'packages/**'
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-fastgpt-images:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
submodules: recursive # Fetch submodules
|
||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
- name: Build image for PR
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg name=app \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt-pr image" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f Dockerfile \
|
||||
.
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -33,4 +33,6 @@ dist/
|
||||
|
||||
# hugo
|
||||
**/.hugo_build.lock
|
||||
docSite/public/
|
||||
docSite/public/
|
||||
docSite/resources/_gen/
|
||||
docSite/.vercel
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.mouseWheelZoom": true,
|
||||
"typescript.tsdk": "client/node_modules/typescript/lib",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
"projects/app/public/locales"
|
||||
],
|
||||
"i18n-ally.enabledParsers": ["json"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
|
||||
68
Dockerfile
Normal file
68
Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:18.15-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# copy packages and one project
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY ./packages ./packages
|
||||
COPY ./projects/$name/package.json ./projects/$name/package.json
|
||||
|
||||
RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1)
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:18.15-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# copy common node_modules and one project node_modules
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/packages ./packages
|
||||
COPY ./projects/$name ./projects/$name
|
||||
COPY --from=deps /app/projects/$name/node_modules ./projects/$name/node_modules
|
||||
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm --filter=$name run build
|
||||
|
||||
FROM node:18.15-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# create user and use it
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
# copy running files
|
||||
COPY --from=builder /app/projects/$name/public ./projects/$name/public
|
||||
COPY --from=builder /app/projects/$name/next.config.js ./projects/$name/next.config.js
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/static ./projects/$name/.next/static
|
||||
# copy package.json to version file
|
||||
COPY --from=builder /app/projects/$name/package.json ./package.json
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV serverPath=./projects/$name/server.js
|
||||
|
||||
ENTRYPOINT ["sh","-c","node ${serverPath}"]
|
||||
88
README.md
88
README.md
@@ -4,25 +4,39 @@
|
||||
|
||||
# FastGPT
|
||||
|
||||
<p align="center">
|
||||
<a href="./README_en.md">English</a> |
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景!
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fastgpt.run/">线上体验</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/intro">相关文档</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/development">本地开发</a>
|
||||
·
|
||||
<a href="https://github.com/labring/FastGPT#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">相关项目</a>
|
||||
<a href="https://fastgpt.run/">
|
||||
<img height="21" src="https://img.shields.io/badge/在线使用-d4eaf7?style=flat-square&logo=spoj&logoColor=7d09f1" alt="cloud">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/intro">
|
||||
<img height="21" src="https://img.shields.io/badge/相关文档-7d09f1?style=flat-square" alt="document">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/development">
|
||||
<img height="21" src="https://img.shields.io/badge/本地开发-%23d4eaf7?style=flat-square&logo=xcode&logoColor=7d09f1" alt="development">
|
||||
</a>
|
||||
<a href="/#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">
|
||||
<img height="21" src="https://img.shields.io/badge/相关项目-7d09f1?style=flat-square" alt="project">
|
||||
</a>
|
||||
<a href="https://github.com/labring/FastGPT/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=d4eaf7&color=7d09f1" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409bd33f6d4
|
||||
|
||||
## 🛸 在线体验
|
||||
## 🛸 在线使用
|
||||
|
||||
[fastgpt.run](https://fastgpt.run/)(服务器在新加坡,部分地区可能无法直连)
|
||||
- 🌐 国内版:[ai.fastgpt.in](https://ai.fastgpt.in/)
|
||||
- 🌍 海外版:[fastgpt.run](https://fastgpt.run/)
|
||||
|
||||
| | |
|
||||
| ---------------------------------- | ---------------------------------- |
|
||||
@@ -33,21 +47,21 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
|
||||
1. 强大的可视化编排,轻松构建 AI 应用
|
||||
- [x] 提供简易模式,无需操作编排
|
||||
- [x] 用户对话前引导, 全局字符串变量
|
||||
- [x] 用户对话前引导,全局字符串变量
|
||||
- [x] 知识库搜索
|
||||
- [x] 多 LLM 模型对话
|
||||
- [x] 文本内容提取成结构化数据
|
||||
- [x] HTTP 扩展
|
||||
- [ ] 嵌入 Laf,实现在线编写 HTTP 模块
|
||||
- [ ] 连续对话引导
|
||||
- [x] 对话下一步指引
|
||||
- [ ] 对话多路线选择
|
||||
- [x] 源文件引用追踪
|
||||
- [ ] 自定义文件阅读器
|
||||
- [x] 模块封装,实现多级复用
|
||||
2. 丰富的知识库预处理
|
||||
- [x] 多库复用,混用
|
||||
- [x] chunk 记录修改和删除
|
||||
- [x] 支持 手动输入, 直接分段, QA 拆分导入
|
||||
- [x] 支持 url 读取、 CSV 批量导入
|
||||
- [x] 支持手动输入,直接分段,QA 拆分导入
|
||||
- [x] 支持 url 读取、CSV 批量导入
|
||||
- [x] 支持知识库单独设置向量模型
|
||||
- [x] 源文件存储
|
||||
- [ ] 文件学习 Agent
|
||||
@@ -55,9 +69,9 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
- [x] 知识库单点搜索测试
|
||||
- [x] 对话时反馈引用并可修改与删除
|
||||
- [x] 完整上下文呈现
|
||||
- [ ] 完整模块中间值呈现
|
||||
- [x] 完整模块中间值呈现
|
||||
4. OpenAPI
|
||||
- [x] completions 接口(对齐 GPT 接口)
|
||||
- [x] completions 接口 (对齐 GPT 接口)
|
||||
- [ ] 知识库 CRUD
|
||||
5. 运营功能
|
||||
- [x] 免登录分享窗口
|
||||
@@ -66,7 +80,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
|
||||
## 👨💻 开发
|
||||
|
||||
项目技术栈: NextJs + TS + ChakraUI + Mongo + Postgres(Vector 插件)
|
||||
项目技术栈:NextJs + TS + ChakraUI + Mongo + Postgres (Vector 插件)
|
||||
|
||||
- **⚡ 快速部署**
|
||||
|
||||
@@ -76,12 +90,13 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
|
||||
由于需要部署数据库,部署完后需要等待 2~4 分钟才能正常访问。默认用了最低配置,首次访问时会有些慢。
|
||||
|
||||
* [快开始本地开发](https://doc.fastgpt.run/docs/development/intro/)
|
||||
* [部署 FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
* [系统配置文件说明](https://doc.fastgpt.run/docs/development/configuration/)
|
||||
* [多模型配置](https://doc.fastgpt.run/docs/installation/one-api/)
|
||||
* [版本升级](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
* [API 文档](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh?pre_pathname=%2Fdrive%2Fhome%2F)
|
||||
* [快开始本地开发](https://doc.fastgpt.in/docs/development/intro/)
|
||||
* [部署 FastGPT](https://doc.fastgpt.in/docs/installation)
|
||||
* [系统配置文件说明](https://doc.fastgpt.in/docs/development/configuration/)
|
||||
* [多模型配置](https://doc.fastgpt.in/docs/installation/one-api/)
|
||||
* [版本更新/升级介绍](https://doc.fastgpt.in/docs/installation/upgrading)
|
||||
* [OpenAPI API 文档](https://doc.fastgpt.in/docs/development/openapi/)
|
||||
* [知识库结构详解](https://doc.fastgpt.in/docs/use-cases/datasetengine/)
|
||||
|
||||
## 🏘️ 社区交流群
|
||||
|
||||
@@ -89,23 +104,22 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
|
||||

|
||||
|
||||
## 👀 其他
|
||||
|
||||
- [FastGPT 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [docker 部署教程视频](https://www.bilibili.com/video/BV1jo4y147fT/)
|
||||
- [FastGPT 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||
|
||||
## 💪 相关项目
|
||||
|
||||
- [Laf: 3 分钟快速接入三方应用](https://github.com/labring/laf)
|
||||
- [Sealos: 快速部署集群应用](https://github.com/labring/sealos)
|
||||
- [One API: 多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
|
||||
- [TuShan: 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
|
||||
- [Laf:3 分钟快速接入三方应用](https://github.com/labring/laf)
|
||||
- [Sealos:快速部署集群应用](https://github.com/labring/sealos)
|
||||
- [One API:多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
|
||||
- [TuShan:5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
|
||||
|
||||
## 👀 其他
|
||||
|
||||
- [保姆级 FastGPT 教程](https://www.bilibili.com/video/BV1n34y1A7Bo/?spm_id_from=333.999.0.0)
|
||||
- [接入飞书](https://www.bilibili.com/video/BV1Su4y1r7R3/?spm_id_from=333.999.0.0)
|
||||
- [接入企微](https://www.bilibili.com/video/BV1Tp4y1n72T/?spm_id_from=333.999.0.0)
|
||||
|
||||
## 🤝 第三方生态
|
||||
|
||||
- [OnWeChat 个人微信/企微机器人](https://doc.fastgpt.run/docs/use-cases/onwechat/)
|
||||
- [luolinAI: 企微机器人,开箱即用](https://github.com/luolin-ai/FastGPT-Enterprise-WeChatbot)
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
@@ -115,7 +129,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
|
||||
|
||||
本仓库遵循 [FastGPT Open Source License](./LICENSE) 开源协议。
|
||||
|
||||
1. 允许作为后台服务直接商用,但不允许直接使用 saas 服务商用。
|
||||
2. 需保留相关版权信息。
|
||||
1. 允许作为后台服务直接商用,但不允许提供 SaaS 服务。
|
||||
2. 未经商业授权,任何形式的商用服务均需保留相关版权信息。
|
||||
3. 完整请查看 [FastGPT Open Source License](./LICENSE)
|
||||
4. 联系方式:yujinlong@sealos.io, [点击查看定价策略](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
4. 联系方式:yujinlong@sealos.io,[点击查看商业版定价策略](https://doc.fastgpt.run/docs/commercial)
|
||||
|
||||
87
README_en.md
87
README_en.md
@@ -1,25 +1,39 @@
|
||||
<div align="center">
|
||||
|
||||
<a href="https://fastgpt.run/"><img src="/.github/imgs/logo.svg" width="120" height="120" alt="fastgpt logo"></a>
|
||||
|
||||
# FastGPT
|
||||
|
||||
FastGPT is a knowledge-based question answering system built on the LLM. It offers out-of-the-box data processing and model invocation capabilities. Moreover, it allows for workflow orchestration through Flow visualization, thereby enabling complex question and answer scenarios!
|
||||
<p align="center">
|
||||
<a href="./README_en.md">English</a> |
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
FastGPT is a knowledge-based Q&A system built on the LLM, offers out-of-the-box data processing and model invocation capabilities, allows for workflow orchestration through Flow visualization!
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fastgpt.run/">Online</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/intro">Document</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/development">Development</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/installation">Deploy</a>
|
||||
·
|
||||
<a href="#powered-by">Power By</a>
|
||||
<a href="https://fastgpt.run/">
|
||||
<img height="21" src="https://img.shields.io/badge/在线使用-d4eaf7?style=flat-square&logo=spoj&logoColor=7d09f1" alt="cloud">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/intro">
|
||||
<img height="21" src="https://img.shields.io/badge/相关文档-7d09f1?style=flat-square" alt="document">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/development">
|
||||
<img height="21" src="https://img.shields.io/badge/本地开发-%23d4eaf7?style=flat-square&logo=xcode&logoColor=7d09f1" alt="development">
|
||||
</a>
|
||||
<a href="/#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">
|
||||
<img height="21" src="https://img.shields.io/badge/相关项目-7d09f1?style=flat-square" alt="project">
|
||||
</a>
|
||||
<a href="https://github.com/labring/FastGPT/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=d4eaf7&color=7d09f1" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🛸 Online
|
||||
https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409bd33f6d4
|
||||
|
||||
## 🛸 Use Cloud Services
|
||||
|
||||
[fastgpt.run](https://fastgpt.run/)
|
||||
| | |
|
||||
@@ -29,35 +43,34 @@ FastGPT is a knowledge-based question answering system built on the LLM. It offe
|
||||
|
||||
## 💡 Features
|
||||
|
||||
1. Powerful visual orchestration for easy AI application building
|
||||
1. Powerful visual workflows: Effortlessly craft AI applications
|
||||
|
||||
- [x] Provides a simple mode without the need for orchestration operations
|
||||
- [x] Simple mode on deck - no need for manual arrangement
|
||||
- [x] User dialogue pre-guidance
|
||||
- [x] Global variables
|
||||
- [x] Knowledge base search
|
||||
- [x] Multi-LLM model dialogue
|
||||
- [x] Extraction of text content into structured data
|
||||
- [x] HTTP extension
|
||||
- [ ] Sandbox JS runtime module
|
||||
- [ ] Continuous dialogue guidance
|
||||
- [ ] Dialogue multi-path selection
|
||||
- [ ] Source file reference tracking
|
||||
- [x] Dialogue via multiple LLM models
|
||||
- [x] Text magic - convert to structured data
|
||||
- [x] Extend with HTTP
|
||||
- [ ] Embed Laf for on-the-fly HTTP module crafting
|
||||
- [x] Directions for the next dialogue steps
|
||||
- [x] Tracking source file references
|
||||
- [ ] Custom file reader
|
||||
- [ ] Modules are packaged into plug-ins to achieve reuse
|
||||
|
||||
2. Rich knowledge base preprocessing
|
||||
2. Extensive knowledge base preprocessing
|
||||
|
||||
- [x] Multiple library reuse and mixing
|
||||
- [x] Chunk record modification and deletion
|
||||
- [x] Supports direct segment import
|
||||
- [x] Supports QA split import
|
||||
- [x] Supports manual input content
|
||||
- [ ] Supports URL import reading
|
||||
- [x] Supports batch import of Q&A pairs in CSV format
|
||||
- [ ] Supports separate vector model settings for knowledge bases
|
||||
- [ ] Source file storage
|
||||
- [x] Reuse and mix multiple knowledge bases
|
||||
- [x] Track chunk modifications and deletions
|
||||
- [x] Supports manual entries, direct segmentation, and QA split imports
|
||||
- [x] Supports URL fetching and batch CSV imports
|
||||
- [x] Supports Set unique vector models for knowledge bases
|
||||
- [x] Store original files
|
||||
- [ ] File learning Agent
|
||||
|
||||
3. Multiple effect testing channels
|
||||
|
||||
- [x] Knowledge base single point search testing
|
||||
- [x] Single-point knowledge base search test
|
||||
- [x] Feedback references and ability to modify and delete during dialogue
|
||||
- [x] Complete context presentation
|
||||
- [ ] Complete module intermediate value presentation
|
||||
@@ -77,11 +90,17 @@ FastGPT is a knowledge-based question answering system built on the LLM. It offe
|
||||
|
||||
Project tech stack: NextJs + TS + ChakraUI + Mongo + Postgres (Vector plugin)
|
||||
|
||||
- **⚡ Deployment**
|
||||
|
||||
[](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt)
|
||||
|
||||
Give it a 2-4 minute wait after deployment as it sets up the database. Initially, it might be a tad slow since we're using the basic settings.
|
||||
|
||||
- [Getting Started with Local Development](https://doc.fastgpt.run/docs/development)
|
||||
- [Deploying FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
- [System Configuration File Explanation](https://doc.fastgpt.run/docs/installation/reference)
|
||||
- [Multi-model Configuration](https://doc.fastgpt.run/docs/installation/reference/models)
|
||||
- [V3 Upgrade V4 Initialization](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
- [Guide on System Configs](https://doc.fastgpt.run/docs/installation/reference)
|
||||
- [Configuring Multiple Models](https://doc.fastgpt.run/docs/installation/reference/models)
|
||||
- [Version Updates & Upgrades](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
|
||||
<!-- ## :point_right: RoadMap
|
||||
- [FastGPT RoadMap](https://kjqvjse66l.feishu.cn/docx/RVUxdqE2WolDYyxEKATcM0XXnte) -->
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm fetch || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY pnpm-lock.yaml* ./
|
||||
COPY package.json ./
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN npm install -g pnpm
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && (pnpm --offline install && pnpm run build) || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||
# COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
# COPY --from=builder /app/.env* .
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"FeConfig": {
|
||||
"show_emptyChat": true,
|
||||
"show_register": false,
|
||||
"show_appStore": false,
|
||||
"show_userDetail": false,
|
||||
"show_contact": true,
|
||||
"show_git": true,
|
||||
"show_doc": true,
|
||||
"systemTitle": "FastGPT",
|
||||
"authorText": "Made by FastGPT Team.",
|
||||
"limit": {
|
||||
"exportLimitMinutes": 0
|
||||
},
|
||||
"scripts": []
|
||||
},
|
||||
"SystemParams": {
|
||||
"vectorMaxProcess": 15,
|
||||
"qaMaxProcess": 15,
|
||||
"pgIvfflatProbe": 20
|
||||
},
|
||||
"ChatModels": [
|
||||
{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"name": "GPT35-4k",
|
||||
"contextMaxToken": 4000,
|
||||
"quoteMaxToken": 2000,
|
||||
"maxTemperature": 1.2,
|
||||
"price": 0,
|
||||
"defaultSystem": ""
|
||||
},
|
||||
{
|
||||
"model": "gpt-3.5-turbo-16k",
|
||||
"name": "GPT35-16k",
|
||||
"contextMaxToken": 16000,
|
||||
"quoteMaxToken": 8000,
|
||||
"maxTemperature": 1.2,
|
||||
"price": 0,
|
||||
"defaultSystem": ""
|
||||
},
|
||||
{
|
||||
"model": "gpt-4",
|
||||
"name": "GPT4-8k",
|
||||
"contextMaxToken": 8000,
|
||||
"quoteMaxToken": 4000,
|
||||
"maxTemperature": 1.2,
|
||||
"price": 0,
|
||||
"defaultSystem": ""
|
||||
}
|
||||
],
|
||||
"VectorModels": [
|
||||
{
|
||||
"model": "text-embedding-ada-002",
|
||||
"name": "Embedding-2",
|
||||
"price": 0,
|
||||
"defaultToken": 500,
|
||||
"maxToken": 3000
|
||||
}
|
||||
],
|
||||
"QAModel": {
|
||||
"model": "gpt-3.5-turbo-16k",
|
||||
"name": "GPT35-16k",
|
||||
"maxToken": 16000,
|
||||
"price": 0
|
||||
}
|
||||
}
|
||||
5
client/next-env.d.ts
vendored
5
client/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
12550
client/pnpm-lock.yaml
generated
12550
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
### 常见问题
|
||||
|
||||
- [**Git 地址**,点击查看项目地址](https://github.com/labring/FastGPT)
|
||||
- [本地部署 FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
- [API 文档](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh?pre_pathname=%2Fdrive%2Fhome%2F)
|
||||
- **反馈问卷**: 如果你遇到任何使用问题或有期望的功能,可以[填写该问卷](https://www.wjx.cn/vm/rLIw1uD.aspx#)
|
||||
- **问题文档**: [先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [点击查看商业版文档](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
|
||||
**价格表**
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 0.002 |
|
||||
| FastAI4k - 对话 | 0.015 |
|
||||
| FastAI16k - 对话 | 0.03 |
|
||||
| FastAI-Plus - 对话 | 0.45 |
|
||||
| 文件 QA 拆分 | 0.03 |
|
||||
|
||||
**其他问题**
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
@@ -1,7 +0,0 @@
|
||||
### Fast GPT V4.4.1
|
||||
|
||||
1. 新增 - 知识库目录结构
|
||||
2. 新增 - 分享链接支持配置 IP 限流、过期时间、最大额度等
|
||||
3. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/)
|
||||
4. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
5. [点击查看商业版](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694327751771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4992" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M0 0h1024v1024H0V0z" fill="#202425" opacity=".01" p-id="4993"></path><path d="M136.533333 68.266667a68.266667 68.266667 0 0 0-68.266666 68.266666v428.305067a17.066667 17.066667 0 0 0 28.842666 12.356267l237.738667-226.440534a34.133333 34.133333 0 0 1 42.496-3.6864l268.288 178.858667a17.066667 17.066667 0 0 0 22.766933-3.447467L951.978667 171.4176A17.066667 17.066667 0 0 0 955.733333 160.699733V136.533333a68.266667 68.266667 0 0 0-68.266666-68.266666H136.533333z m819.2 255.3856a17.066667 17.066667 0 0 0-30.344533-10.717867l-221.866667 274.705067a17.066667 17.066667 0 0 0-3.7888 10.717866v340.309334a17.066667 17.066667 0 0 0 17.066667 17.066666h170.666667a68.266667 68.266667 0 0 0 68.266666-68.266666V323.652267zM614.4 955.733333a17.066667 17.066667 0 0 0 17.066667-17.066666v-330.990934a17.066667 17.066667 0 0 0-7.611734-14.199466l-204.8-136.533334a17.066667 17.066667 0 0 0-26.5216 14.199467V938.666667a17.066667 17.066667 0 0 0 17.066667 17.066666h204.8z m-307.2 0a17.066667 17.066667 0 0 0 17.066667-17.066666v-443.733334a17.066667 17.066667 0 0 0-28.842667-12.356266l-221.866667 211.285333a17.066667 17.066667 0 0 0-5.290666 12.3904V887.466667a68.266667 68.266667 0 0 0 68.266666 68.266666h170.666667z" fill="#FFAA44" p-id="4994"></path><path d="M73.557333 693.8624a17.066667 17.066667 0 0 0-5.290666 12.3904V887.466667a68.266667 68.266667 0 0 0 68.266666 68.266666h170.666667a17.066667 17.066667 0 0 0 17.066667-17.066666v-443.733334a17.066667 17.066667 0 0 0-28.842667-12.356266l-221.866667 211.285333zM392.533333 938.666667a17.066667 17.066667 0 0 0 17.066667 17.066666h204.8a17.066667 17.066667 0 0 0 17.066667-17.066666v-330.990934a17.066667 17.066667 0 0 0-7.611734-14.199466l-204.8-136.533334a17.066667 17.066667 0 0 0-26.5216 14.199467V938.666667z m307.2 0a17.066667 17.066667 0 0 0 17.066667 17.066666h170.666667a68.266667 68.266667 0 0 0 68.266666-68.266666V323.6864a17.066667 17.066667 0 0 0-30.344533-10.752l-221.866667 274.705067a17.066667 17.066667 0 0 0-3.7888 10.717866v340.309334z" fill="#11AA66" p-id="4995"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692418843591" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4084" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M511.5 82c-236.6 0-429 192.4-429 429 0 236.5 192.5 429 429 429 236.6 0 429-192.4 429-429 0-236.5-192.4-429-429-429z m377.6 403.8H734.3c-4-139.9-41.4-259.9-97.5-331.9C776.5 203 879 332 889.1 485.8z m-402.8-349v349h-147c5.5-175.5 68.6-322.6 147-349z m0 399.4v349c-78.4-26.4-141.4-173.5-147-349h147z m50.5 349v-349h147c-5.6 175.5-68.6 322.6-147 349z m0-399.4v-349c78.4 26.4 141.4 173.5 147 349h-147zM386.3 153.9c-56.1 72-93.5 192-97.5 331.9H133.9C144.1 332 246.5 203 386.3 153.9zM133.9 536.2h154.8c4 139.9 41.4 259.9 97.5 331.9C246.5 819 144.1 690 133.9 536.2z m502.8 331.9c56.1-72 93.5-192 97.5-331.9H889C879 690 776.5 819 636.7 868.1z" fill="#5F9BEB" p-id="4085"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1006 B |
8
client/src/api/core/dataset/file.d.ts
vendored
8
client/src/api/core/dataset/file.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
import { RequestPaging } from '../../../types/index';
|
||||
|
||||
export type GetFileListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
export type UpdateFileProps = { id: string; name?: string; datasetUsed?: boolean };
|
||||
@@ -1,15 +0,0 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/api/request';
|
||||
import type { FileInfo, KbFileItemType } from '@/types/plugin';
|
||||
|
||||
import type { GetFileListProps, UpdateFileProps } from './file.d';
|
||||
|
||||
export const getDatasetFiles = (data: GetFileListProps) =>
|
||||
POST<KbFileItemType[]>(`/core/dataset/file/list`, data);
|
||||
export const delDatasetFileById = (params: { fileId: string; kbId: string }) =>
|
||||
DELETE(`/core/dataset/file/delById`, params);
|
||||
export const getFileInfoById = (fileId: string) =>
|
||||
GET<FileInfo>(`/core/dataset/file/detail`, { fileId });
|
||||
export const delDatasetEmptyFiles = (kbId: string) =>
|
||||
DELETE(`/core/dataset/file/delEmptyFiles`, { kbId });
|
||||
|
||||
export const updateDatasetFile = (data: UpdateFileProps) => PUT(`/core/dataset/file/update`, data);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
/**
|
||||
* crete a api key
|
||||
*/
|
||||
export const createAOpenApiKey = () => POST<string>('/openapi/postKey');
|
||||
|
||||
/**
|
||||
* get api keys
|
||||
*/
|
||||
export const getOpenApiKeys = () => GET<UserOpenApiKey[]>('/openapi/getKeys');
|
||||
|
||||
/**
|
||||
* delete api by id
|
||||
*/
|
||||
export const delOpenApiById = (id: string) => DELETE(`/openapi/delKey?id=${id}`);
|
||||
@@ -1,6 +0,0 @@
|
||||
import { GET, POST, PUT, DELETE } from '../request';
|
||||
|
||||
import type { FetchResultItem } from '@/types/plugin';
|
||||
|
||||
export const fetchUrls = (urlList: string[]) =>
|
||||
POST<FetchResultItem[]>(`/plugins/urlFetch`, { urlList });
|
||||
@@ -1,106 +0,0 @@
|
||||
import { GET, POST, PUT, DELETE } from '../request';
|
||||
import type { DatasetItemType, KbItemType, KbListItemType, KbPathItemType } from '@/types/plugin';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import {
|
||||
Props as PushDataProps,
|
||||
Response as PushDateResponse
|
||||
} from '@/pages/api/openapi/kb/pushData';
|
||||
import {
|
||||
Props as SearchTestProps,
|
||||
Response as SearchTestResponse
|
||||
} from '@/pages/api/openapi/kb/searchTest';
|
||||
import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
|
||||
import type { KbUpdateParams, CreateKbParams, GetKbDataListProps } from '../request/kb';
|
||||
import { QuoteItemType } from '@/types/chat';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
import { getToken } from '@/utils/user';
|
||||
import download from 'downloadjs';
|
||||
|
||||
/* knowledge base */
|
||||
export const getKbList = (data: { parentId?: string; type?: `${KbTypeEnum}` }) =>
|
||||
GET<KbListItemType[]>(`/plugins/kb/list`, data);
|
||||
export const getAllDataset = () => GET<KbListItemType[]>(`/plugins/kb/allDataset`);
|
||||
|
||||
export const getKbPaths = (parentId?: string) =>
|
||||
GET<KbPathItemType[]>('/plugins/kb/paths', { parentId });
|
||||
|
||||
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
||||
|
||||
export const postCreateKb = (data: CreateKbParams) => POST<string>(`/plugins/kb/create`, data);
|
||||
|
||||
export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, data);
|
||||
|
||||
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
|
||||
|
||||
/* kb data */
|
||||
export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
POST(`/plugins/kb/data/getDataList`, data);
|
||||
|
||||
/**
|
||||
* export and download data
|
||||
*/
|
||||
export const exportDataset = (data: { kbId: string }) =>
|
||||
fetch(`/api/plugins/kb/data/exportAll?kbId=${data.kbId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
token: getToken()
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data?.message || 'Export failed');
|
||||
}
|
||||
return res.blob();
|
||||
})
|
||||
.then((blob) => download(blob, 'dataset.csv', 'text/csv'));
|
||||
|
||||
/**
|
||||
* 获取模型正在拆分数据的数量
|
||||
*/
|
||||
export const getTrainingData = (data: { kbId: string; init: boolean }) =>
|
||||
POST<{
|
||||
qaListLen: number;
|
||||
vectorListLen: number;
|
||||
}>(`/plugins/kb/data/getTrainingData`, data);
|
||||
|
||||
/* get length of system training queue */
|
||||
export const getTrainingQueueLen = () => GET<number>(`/plugins/kb/data/getQueueLen`);
|
||||
|
||||
export const getKbDataItemById = (dataId: string) =>
|
||||
GET<QuoteItemType>(`/plugins/kb/data/getDataById`, { dataId });
|
||||
|
||||
/**
|
||||
* 直接push数据
|
||||
*/
|
||||
export const postKbDataFromList = (data: PushDataProps) =>
|
||||
POST<PushDateResponse>(`/openapi/kb/pushData`, data);
|
||||
|
||||
/**
|
||||
* insert one data to dataset
|
||||
*/
|
||||
export const insertData2Kb = (data: { kbId: string; data: DatasetItemType }) =>
|
||||
POST<string>(`/plugins/kb/data/insertData`, data);
|
||||
|
||||
/**
|
||||
* 更新一条数据
|
||||
*/
|
||||
export const putKbDataById = (data: UpdateDataProps) => PUT('/openapi/kb/updateData', data);
|
||||
/**
|
||||
* 删除一条知识库数据
|
||||
*/
|
||||
export const delOneKbDataByDataId = (dataId: string) =>
|
||||
DELETE(`/openapi/kb/delDataById?dataId=${dataId}`);
|
||||
|
||||
/**
|
||||
* 拆分数据
|
||||
*/
|
||||
export const postSplitData = (data: {
|
||||
kbId: string;
|
||||
chunks: string[];
|
||||
prompt: string;
|
||||
mode: `${TrainingModeEnum}`;
|
||||
}) => POST(`/openapi/text/pushData`, data);
|
||||
|
||||
export const searchText = (data: SearchTestProps) =>
|
||||
POST<SearchTestResponse>(`/openapi/kb/searchTest`, data);
|
||||
6
client/src/api/request/chat.d.ts
vendored
6
client/src/api/request/chat.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export type AdminUpdateFeedbackParams = {
|
||||
chatItemId: string;
|
||||
kbId: string;
|
||||
dataId: string;
|
||||
content: string;
|
||||
};
|
||||
24
client/src/api/request/kb.d.ts
vendored
24
client/src/api/request/kb.d.ts
vendored
@@ -1,24 +0,0 @@
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
import type { RequestPaging } from '@/types';
|
||||
|
||||
export type KbUpdateParams = {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
tags?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
export type CreateKbParams = {
|
||||
parentId?: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
avatar: string;
|
||||
vectorModel?: string;
|
||||
type: `${KbTypeEnum}`;
|
||||
};
|
||||
|
||||
export type GetKbDataListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
fileId: string;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { GET, POST } from './request';
|
||||
|
||||
export const textCensor = (data: { text: string }) =>
|
||||
POST<{ code?: number; message: string }>('/plugins/censor/text_baidu', data).then((res) => {
|
||||
if (res?.code === 5000) {
|
||||
return Promise.reject(res.message);
|
||||
}
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { GET, POST } from '../request';
|
||||
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const uploadImg = (base64Img: string) => POST<string>('/system/uploadImage', { base64Img });
|
||||
|
||||
export const postUploadFiles = (
|
||||
data: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST<string[]>('/support/file/upload', data, {
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const getFileViewUrl = (fileId: string) => GET<string>('/support/file/readUrl', { fileId });
|
||||
@@ -1,4 +0,0 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
import type { InitDateResponse } from '@/pages/api/system/getInitData';
|
||||
|
||||
export const getInitData = () => GET<InitDateResponse>('/system/getInitData');
|
||||
@@ -1,143 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||
import { getErrText, useCopyData } from '@/utils/tools';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import MyIcon from '../Icon';
|
||||
import MyModal from '../MyModal';
|
||||
|
||||
const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { Loading } = useLoading();
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
data: apiKeys = [],
|
||||
isLoading: isGetting,
|
||||
refetch
|
||||
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
|
||||
mutationFn: () => createAOpenApiKey(),
|
||||
onSuccess(res) {
|
||||
setApiKey(res);
|
||||
refetch();
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
|
||||
mutationFn: async (id: string) => delOpenApiById(id),
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} w={'600px'}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
API 秘钥管理
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
如果你不想 API 秘钥被滥用,请勿将秘钥直接放置在前端使用~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
|
||||
<TableContainer mt={2} position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Api Key</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th>最后一次使用时间</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
|
||||
<Tr key={id}>
|
||||
<Td>{apiKey}</Td>
|
||||
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
|
||||
<Td>
|
||||
{lastUsedTime
|
||||
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
|
||||
: '没有使用过'}
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
size={'xs'}
|
||||
aria-label={'delete'}
|
||||
variant={'base'}
|
||||
colorScheme={'gray'}
|
||||
onClick={() => onclickRemove(id)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="base"
|
||||
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
|
||||
onClick={() => onclickCreateApiKey()}
|
||||
>
|
||||
新建秘钥
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
|
||||
<MyModal isOpen={!!apiKey} w={'400px'} onClose={() => setApiKey('')}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
新的 API 秘钥
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
请保管好你的秘钥,秘钥不会再次展示~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalBody>
|
||||
<Flex bg={'myGray.100'} px={3} py={2} cursor={'pointer'} onClick={() => copyData(apiKey)}>
|
||||
<Box flex={1}>{apiKey}</Box>
|
||||
<MyIcon name={'copy'} w={'16px'}></MyIcon>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="base" onClick={() => setApiKey('')}>
|
||||
好的
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default APIKeyModal;
|
||||
@@ -1,151 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
|
||||
import { getKbDataItemById } from '@/api/plugins/kb';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { QuoteItemType } from '@/types/chat';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import InputDataModal, { RawFileText } from '@/pages/kb/detail/components/InputDataModal';
|
||||
import MyModal from '../MyModal';
|
||||
import { KbDataItemType } from '@/types/plugin';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type SearchType = KbDataItemType & {
|
||||
kb_id?: string;
|
||||
};
|
||||
|
||||
const QuoteModal = ({
|
||||
onUpdateQuote,
|
||||
rawSearch = [],
|
||||
onClose
|
||||
}: {
|
||||
onUpdateQuote: (quoteId: string, sourceText?: string) => Promise<void>;
|
||||
rawSearch: SearchType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { setIsLoading, Loading } = useLoading();
|
||||
const [editDataItem, setEditDataItem] = useState<QuoteItemType>();
|
||||
|
||||
const isShare = useMemo(() => router.pathname === '/chat/share', [router.pathname]);
|
||||
|
||||
/**
|
||||
* click edit, get new kbDataItem
|
||||
*/
|
||||
const onclickEdit = useCallback(
|
||||
async (item: SearchType) => {
|
||||
if (!item.id) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await getKbDataItemById(item.id);
|
||||
|
||||
if (!data) {
|
||||
onUpdateQuote(item.id, '已删除');
|
||||
throw new Error('该数据已被删除');
|
||||
}
|
||||
|
||||
setEditDataItem(data);
|
||||
} catch (err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(err)
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, toast, onUpdateQuote]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
h={['90vh', '80vh']}
|
||||
isCentered
|
||||
minW={['90vw', '600px']}
|
||||
title={
|
||||
<>
|
||||
知识库引用({rawSearch.length}条)
|
||||
<Box fontSize={['xs', 'sm']} fontWeight={'normal'}>
|
||||
注意: 修改知识库内容成功后,此处不会显示变更情况。点击编辑后,会显示知识库最新的内容。
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ModalBody
|
||||
pt={0}
|
||||
whiteSpace={'pre-wrap'}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{rawSearch.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
flex={'1 0 0'}
|
||||
p={2}
|
||||
borderRadius={'lg'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
position={'relative'}
|
||||
_hover={{ '& .edit': { display: 'flex' } }}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
{item.source && !isShare && (
|
||||
<RawFileText filename={item.source} fileId={item.file_id} />
|
||||
)}
|
||||
<Box>{item.q}</Box>
|
||||
<Box>{item.a}</Box>
|
||||
{item.id && !isShare && (
|
||||
<Box
|
||||
className="edit"
|
||||
display={'none'}
|
||||
position={'absolute'}
|
||||
right={0}
|
||||
top={0}
|
||||
bottom={0}
|
||||
w={'40px'}
|
||||
bg={'rgba(255,255,255,0.9)'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
boxShadow={'-10px 0 10px rgba(255,255,255,1)'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'myBlue.700'
|
||||
}}
|
||||
onClick={() => onclickEdit(item)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</ModalBody>
|
||||
<Loading fixed={false} />
|
||||
</MyModal>
|
||||
{editDataItem && (
|
||||
<InputDataModal
|
||||
onClose={() => setEditDataItem(undefined)}
|
||||
onSuccess={() => onUpdateQuote(editDataItem.id)}
|
||||
onDelete={() => onUpdateQuote(editDataItem.id, '已删除')}
|
||||
kbId={editDataItem.kb_id}
|
||||
defaultValues={{
|
||||
...editDataItem,
|
||||
dataId: editDataItem.id
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteModal;
|
||||
@@ -1,108 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { ChatModuleEnum } from '@/constants/chat';
|
||||
import { ChatHistoryItemResType, ChatItemType, QuoteItemType } from '@/types/chat';
|
||||
import { Flex, BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Tag from '../Tag';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'), { ssr: false });
|
||||
const ContextModal = dynamic(() => import('./ContextModal'), { ssr: false });
|
||||
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'), { ssr: false });
|
||||
|
||||
const ResponseTags = ({
|
||||
chatId,
|
||||
contentId,
|
||||
responseData = []
|
||||
}: {
|
||||
chatId?: string;
|
||||
contentId?: string;
|
||||
responseData?: ChatHistoryItemResType[];
|
||||
}) => {
|
||||
const { isPc } = useGlobalStore();
|
||||
const { t } = useTranslation();
|
||||
const [quoteModalData, setQuoteModalData] = useState<QuoteItemType[]>();
|
||||
const [contextModalData, setContextModalData] = useState<ChatItemType[]>();
|
||||
const {
|
||||
isOpen: isOpenWholeModal,
|
||||
onOpen: onOpenWholeModal,
|
||||
onClose: onCloseWholeModal
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
quoteList = [],
|
||||
completeMessages = [],
|
||||
tokens = 0
|
||||
} = useMemo(() => {
|
||||
const chatData = responseData.find((item) => item.moduleName === ChatModuleEnum.AIChat);
|
||||
if (!chatData) return {};
|
||||
return {
|
||||
quoteList: chatData.quoteList,
|
||||
completeMessages: chatData.completeMessages,
|
||||
tokens: responseData.reduce((sum, item) => sum + (item.tokens || 0), 0)
|
||||
};
|
||||
}, [responseData]);
|
||||
|
||||
const updateQuote = useCallback(async (quoteId: string, sourceText?: string) => {}, []);
|
||||
|
||||
const TagStyles: BoxProps = {
|
||||
mr: 2,
|
||||
bg: 'transparent'
|
||||
};
|
||||
|
||||
return responseData.length === 0 ? null : (
|
||||
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
|
||||
{quoteList.length > 0 && (
|
||||
<MyTooltip label="查看引用">
|
||||
<Tag
|
||||
colorSchema="blue"
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setQuoteModalData(quoteList)}
|
||||
>
|
||||
{quoteList.length}条引用
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{completeMessages.length > 0 && (
|
||||
<MyTooltip label={'点击查看完整对话记录'}>
|
||||
<Tag
|
||||
colorSchema="green"
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setContextModalData(completeMessages)}
|
||||
>
|
||||
{completeMessages.length}条上下文
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{isPc && tokens > 0 && (
|
||||
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
|
||||
{tokens}Tokens
|
||||
</Tag>
|
||||
)}
|
||||
<MyTooltip label={'点击查看完整响应值'}>
|
||||
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
|
||||
{t('chat.Complete Response')}
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal
|
||||
rawSearch={quoteModalData}
|
||||
onUpdateQuote={updateQuote}
|
||||
onClose={() => setQuoteModalData(undefined)}
|
||||
/>
|
||||
)}
|
||||
{!!contextModalData && (
|
||||
<ContextModal context={contextModalData} onClose={() => setContextModalData(undefined)} />
|
||||
)}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal response={responseData} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseTags;
|
||||
@@ -1,117 +0,0 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
ModalBody,
|
||||
useTheme,
|
||||
ModalFooter,
|
||||
Button,
|
||||
ModalHeader,
|
||||
Box,
|
||||
Card,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '../MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Avatar from '../Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const SelectDataset = ({
|
||||
onSuccess,
|
||||
onClose
|
||||
}: {
|
||||
onSuccess: (kbId: string) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
const { myKbList, loadKbList } = useDatasetStore();
|
||||
const [selectedId, setSelectedId] = useState<string>();
|
||||
|
||||
useQuery(['loadKbList'], () => loadKbList());
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose} w={'100%'} maxW={['90vw', '900px']} isCentered={!isPc}>
|
||||
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
|
||||
<ModalHeader>
|
||||
<Box>{t('chat.Select Mark Kb')}</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}>
|
||||
{t('chat.Select Mark Kb Desc')}
|
||||
</Box>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{myKbList.map((item) =>
|
||||
(() => {
|
||||
const selected = selectedId === item._id;
|
||||
return (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selected
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
setSelectedId(item._id);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
|
||||
<MyIcon mr={1} name="kbTest" w={'12px'} />
|
||||
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={2} onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!selectedId) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('Select value is empty')
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess(selectedId);
|
||||
}}
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectDataset;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, ModalBody, useTheme, Flex } from '@chakra-ui/react';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MyModal from '../MyModal';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
const ResponseModal = ({
|
||||
response,
|
||||
onClose
|
||||
}: {
|
||||
response: ChatHistoryItemResType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const formatResponse = useMemo(
|
||||
() =>
|
||||
response.map((item) => {
|
||||
const copy = { ...item };
|
||||
delete copy.completeMessages;
|
||||
delete copy.quoteList;
|
||||
return copy;
|
||||
}),
|
||||
[response]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
h={['90vh', '80vh']}
|
||||
minW={['90vw', '600px']}
|
||||
title={
|
||||
<Flex alignItems={'center'}>
|
||||
{t('chat.Complete Response')}
|
||||
<MyTooltip
|
||||
label={
|
||||
'moduleName: 模型名\nprice: 价格,倍率:100000\nmodel?: 模型名\ntokens?: token 消耗\n\nanswer?: 回答内容\nquestion?: 问题\ntemperature?: 温度\nmaxToken?: 最大 tokens\n\nsimilarity?: 相似度\nlimit?: 单次搜索结果\n\ncqList?: 问题分类列表\ncqResult?: 分类结果\n\nextractDescription?: 内容提取描述\nextractResult?: 提取结果'
|
||||
}
|
||||
>
|
||||
<QuestionOutlineIcon ml={2} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
}
|
||||
isCentered
|
||||
>
|
||||
<ModalBody>
|
||||
{formatResponse.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p={2}
|
||||
pt={[0, 2]}
|
||||
borderRadius={'lg'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
position={'relative'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
>
|
||||
{JSON.stringify(item, null, 2)}
|
||||
</Box>
|
||||
))}
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseModal;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { getChatModel } from '@/service/utils/data';
|
||||
import { AppModuleItemType, VariableItemType } from '@/types/app';
|
||||
|
||||
export const getSpecialModule = (modules: AppModuleItemType[]) => {
|
||||
const welcomeText: string =
|
||||
modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value || '';
|
||||
|
||||
const variableModules: VariableItemType[] =
|
||||
modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.variable)
|
||||
?.inputs.find((item) => item.key === SystemInputEnum.variables)?.value || [];
|
||||
|
||||
return {
|
||||
welcomeText,
|
||||
variableModules
|
||||
};
|
||||
};
|
||||
export const getChatModelNameList = (modules: AppModuleItemType[]): string[] => {
|
||||
const chatModules = modules.filter((item) => item.flowType === FlowModuleTypeEnum.chatNode);
|
||||
return chatModules
|
||||
.map(
|
||||
(item) => getChatModel(item.inputs.find((input) => input.key === 'model')?.value)?.name || ''
|
||||
)
|
||||
.filter((item) => item);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
.datePicker {
|
||||
--rdp-background-color: #d6e8ff;
|
||||
--rdp-accent-color: #0000ff;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M8.3 5.7a1 1 0 011.4-1.4l7.71 7.7-7.7 7.7a1 1 0 11-1.42-1.4l6.3-6.3-6.3-6.3z" fill-rule="nonzero"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 151 B |
@@ -1,84 +0,0 @@
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export enum FlowInputItemTypeEnum {
|
||||
systemInput = 'systemInput', // history, userChatInput, variableInput
|
||||
input = 'input',
|
||||
textarea = 'textarea',
|
||||
numberInput = 'numberInput',
|
||||
select = 'select',
|
||||
slider = 'slider',
|
||||
custom = 'custom',
|
||||
target = 'target',
|
||||
none = 'none',
|
||||
hidden = 'hidden'
|
||||
}
|
||||
|
||||
export enum FlowOutputItemTypeEnum {
|
||||
answer = 'answer',
|
||||
source = 'source',
|
||||
none = 'none',
|
||||
hidden = 'hidden'
|
||||
}
|
||||
|
||||
export enum FlowModuleTypeEnum {
|
||||
empty = 'empty',
|
||||
variable = 'variable',
|
||||
userGuide = 'userGuide',
|
||||
questionInput = 'questionInput',
|
||||
historyNode = 'historyNode',
|
||||
chatNode = 'chatNode',
|
||||
kbSearchNode = 'kbSearchNode',
|
||||
tfSwitchNode = 'tfSwitchNode',
|
||||
answerNode = 'answerNode',
|
||||
classifyQuestion = 'classifyQuestion',
|
||||
contentExtract = 'contentExtract',
|
||||
httpRequest = 'httpRequest'
|
||||
}
|
||||
|
||||
export enum SpecialInputKeyEnum {
|
||||
'answerText' = 'text',
|
||||
'agents' = 'agents' // cq agent key
|
||||
}
|
||||
|
||||
export enum FlowValueTypeEnum {
|
||||
'string' = 'string',
|
||||
'number' = 'number',
|
||||
'boolean' = 'boolean',
|
||||
'chatHistory' = 'chat_history',
|
||||
'kbQuote' = 'kb_quote',
|
||||
'any' = 'any'
|
||||
}
|
||||
|
||||
export const FlowValueTypeStyle: Record<`${FlowValueTypeEnum}`, BoxProps> = {
|
||||
[FlowValueTypeEnum.string]: {
|
||||
background: '#36ADEF'
|
||||
},
|
||||
[FlowValueTypeEnum.number]: {
|
||||
background: '#FB7C3C'
|
||||
},
|
||||
[FlowValueTypeEnum.boolean]: {
|
||||
background: '#E7D118'
|
||||
},
|
||||
[FlowValueTypeEnum.chatHistory]: {
|
||||
background: '#00A9A6'
|
||||
},
|
||||
[FlowValueTypeEnum.kbQuote]: {
|
||||
background: '#A558C9'
|
||||
},
|
||||
[FlowValueTypeEnum.any]: {
|
||||
background: '#9CA2A8'
|
||||
}
|
||||
};
|
||||
|
||||
export const initModuleType: Record<string, boolean> = {
|
||||
[FlowModuleTypeEnum.historyNode]: true,
|
||||
[FlowModuleTypeEnum.questionInput]: true
|
||||
};
|
||||
|
||||
export const edgeOptions = {
|
||||
style: {
|
||||
strokeWidth: 1.5,
|
||||
stroke: '#5A646Es'
|
||||
}
|
||||
};
|
||||
export const connectionLineStyle = { strokeWidth: 1.5, stroke: '#5A646Es' };
|
||||
@@ -1,25 +0,0 @@
|
||||
import { FlowInputItemType } from '@/types/flow';
|
||||
import { SystemInputEnum } from '../app';
|
||||
import { FlowInputItemTypeEnum, FlowValueTypeEnum } from './index';
|
||||
|
||||
export const Input_Template_TFSwitch: FlowInputItemType = {
|
||||
key: SystemInputEnum.switch,
|
||||
type: FlowInputItemTypeEnum.target,
|
||||
label: '触发器',
|
||||
valueType: FlowValueTypeEnum.any
|
||||
};
|
||||
|
||||
export const Input_Template_History: FlowInputItemType = {
|
||||
key: SystemInputEnum.history,
|
||||
type: FlowInputItemTypeEnum.target,
|
||||
label: '聊天记录',
|
||||
valueType: FlowValueTypeEnum.chatHistory
|
||||
};
|
||||
|
||||
export const Input_Template_UserChatInput: FlowInputItemType = {
|
||||
key: SystemInputEnum.userChatInput,
|
||||
type: FlowInputItemTypeEnum.target,
|
||||
label: '用户问题',
|
||||
required: true,
|
||||
valueType: FlowValueTypeEnum.string
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { KbItemType } from '@/types/plugin';
|
||||
|
||||
export const defaultKbDetail: KbItemType = {
|
||||
_id: '',
|
||||
userId: '',
|
||||
avatar: '/icon/logo.svg',
|
||||
name: '',
|
||||
tags: '',
|
||||
vectorModel: {
|
||||
model: 'text-embedding-ada-002',
|
||||
name: 'Embedding-2',
|
||||
price: 0.2,
|
||||
defaultToken: 500,
|
||||
maxToken: 3000
|
||||
}
|
||||
};
|
||||
|
||||
export enum KbTypeEnum {
|
||||
folder = 'folder',
|
||||
dataset = 'dataset'
|
||||
}
|
||||
export enum FileStatusEnum {
|
||||
embedding = 'embedding',
|
||||
ready = 'ready'
|
||||
}
|
||||
|
||||
export const KbTypeMap = {
|
||||
[KbTypeEnum.folder]: {
|
||||
name: 'folder'
|
||||
},
|
||||
[KbTypeEnum.dataset]: {
|
||||
name: 'dataset'
|
||||
}
|
||||
};
|
||||
|
||||
export const FolderAvatarSrc = '/imgs/files/folder.svg';
|
||||
export const OtherFileId = 'other';
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
|
||||
export const defaultApp: AppSchema = {
|
||||
_id: '',
|
||||
userId: 'userId',
|
||||
name: '模型加载中',
|
||||
type: 'basic',
|
||||
avatar: '/icon/logo.svg',
|
||||
intro: '',
|
||||
updateTime: Date.now(),
|
||||
share: {
|
||||
isShare: false,
|
||||
isShareDetail: false,
|
||||
collection: 0
|
||||
},
|
||||
modules: []
|
||||
};
|
||||
|
||||
export const defaultOutLinkForm: OutLinkEditType = {
|
||||
name: '',
|
||||
responseDetail: false,
|
||||
limit: {
|
||||
QPM: 100,
|
||||
credit: -1
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
export enum TrainingModeEnum {
|
||||
'qa' = 'qa',
|
||||
'index' = 'index'
|
||||
}
|
||||
export const TrainingTypeMap = {
|
||||
[TrainingModeEnum.qa]: 'qa',
|
||||
[TrainingModeEnum.index]: 'index'
|
||||
};
|
||||
|
||||
export const PgDatasetTableName = 'modeldata';
|
||||
@@ -1,58 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 1000 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
const total = await Chat.countDocuments({
|
||||
chatId: { $exists: false }
|
||||
});
|
||||
let promise = Promise.resolve();
|
||||
console.log(total);
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit, skipVal))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number, skip: number) {
|
||||
// 遍历 app
|
||||
const chats = await Chat.find(
|
||||
{
|
||||
chatId: { $exists: false }
|
||||
},
|
||||
'_id'
|
||||
).limit(limit);
|
||||
|
||||
await Promise.all(
|
||||
chats.map((chat) =>
|
||||
Chat.findByIdAndUpdate(chat._id, {
|
||||
chatId: String(chat._id),
|
||||
source: 'online'
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, Chat, ChatItem } from '@/service/mongo';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 100 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
|
||||
const total = await Chat.countDocuments({
|
||||
content: { $exists: true, $not: { $size: 0 } },
|
||||
isInit: { $ne: true }
|
||||
});
|
||||
const totalChat = await Chat.aggregate([
|
||||
{
|
||||
$project: {
|
||||
contentLength: { $size: '$content' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalLength: { $sum: '$contentLength' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('chatLen:', total, totalChat);
|
||||
|
||||
let promise = Promise.resolve();
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number) {
|
||||
// 遍历 app
|
||||
const chats = await Chat.find(
|
||||
{
|
||||
content: { $exists: true, $not: { $size: 0 } },
|
||||
isInit: { $ne: true }
|
||||
},
|
||||
'_id userId appId chatId content'
|
||||
)
|
||||
.sort({ updateTime: -1 })
|
||||
.limit(limit);
|
||||
|
||||
await Promise.all(
|
||||
chats.map(async (chat) => {
|
||||
const inserts = chat.content
|
||||
.map((item) => ({
|
||||
dataId: nanoid(),
|
||||
chatId: chat.chatId,
|
||||
userId: chat.userId,
|
||||
appId: chat.appId,
|
||||
obj: item.obj,
|
||||
value: item.value,
|
||||
responseData: item.responseData
|
||||
}))
|
||||
.filter((item) => item.chatId && item.userId && item.appId && item.obj && item.value);
|
||||
|
||||
try {
|
||||
await Promise.all(inserts.map((item) => ChatItem.create(item)));
|
||||
await Chat.findByIdAndUpdate(chat._id, {
|
||||
isInit: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
await ChatItem.deleteMany({ chatId: chat.chatId });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, OutLink } from '@/service/mongo';
|
||||
import { OutLinkTypeEnum } from '@/constants/chat';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
await OutLink.updateMany(
|
||||
{},
|
||||
{
|
||||
$set: { type: OutLinkTypeEnum.share }
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, App } from '@/service/mongo';
|
||||
import { FlowModuleTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
|
||||
import { TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { FlowInputItemType } from '@/types/flow';
|
||||
|
||||
const chatModelInput = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
kbList: { kbId: string }[];
|
||||
}): FlowInputItemType[] => [
|
||||
{
|
||||
key: 'model',
|
||||
value: model,
|
||||
type: 'custom',
|
||||
label: '对话模型',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
value: temperature,
|
||||
label: '温度',
|
||||
type: 'slider',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'maxToken',
|
||||
value: maxToken,
|
||||
type: 'custom',
|
||||
label: '回复上限',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'systemPrompt',
|
||||
value: systemPrompt,
|
||||
type: 'textarea',
|
||||
label: '系统提示词',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'limitPrompt',
|
||||
label: '限定词',
|
||||
type: 'textarea',
|
||||
value: limitPrompt,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'switch',
|
||||
type: 'target',
|
||||
label: '触发器',
|
||||
connected: kbList.length > 0
|
||||
},
|
||||
{
|
||||
key: 'quoteQA',
|
||||
type: 'target',
|
||||
label: '引用内容',
|
||||
connected: kbList.length > 0
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'target',
|
||||
label: '聊天记录',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'target',
|
||||
label: '用户问题',
|
||||
connected: true
|
||||
}
|
||||
];
|
||||
const chatTemplate = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.questionInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: 'userChatInput'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.historyNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
value: 10,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'history'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.chatNode,
|
||||
inputs: chatModelInput({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList: []
|
||||
}),
|
||||
outputs: [
|
||||
{
|
||||
key: TaskResponseKeyEnum.answerText,
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 981.9682828103937,
|
||||
y: 890.014595014464
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
};
|
||||
const kbTemplate = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList = [],
|
||||
searchSimilarity,
|
||||
searchLimit,
|
||||
searchEmptyText
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
kbList: { kbId: string }[];
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.questionInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
},
|
||||
{
|
||||
moduleId: 'kbSearch',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: 'userChatInput'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.historyNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
value: 10,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'history'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.kbSearchNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'kbList',
|
||||
value: kbList,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'similarity',
|
||||
value: searchSimilarity,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'limit',
|
||||
value: searchLimit,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'switch',
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'isEmpty',
|
||||
targets: searchEmptyText
|
||||
? [
|
||||
{
|
||||
moduleId: 'emptyText',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'unEmpty',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'quoteQA',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'quoteQA'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 956.0838440206068,
|
||||
y: 887.462827870246
|
||||
},
|
||||
moduleId: 'kbSearch'
|
||||
},
|
||||
...(searchEmptyText
|
||||
? [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.answerNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'switch',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: SpecialInputKeyEnum.answerText,
|
||||
value: searchEmptyText,
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
position: {
|
||||
x: 1553.5815811529146,
|
||||
y: 637.8753731306779
|
||||
},
|
||||
moduleId: 'emptyText'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.chatNode,
|
||||
inputs: chatModelInput({ model, temperature, maxToken, systemPrompt, limitPrompt, kbList }),
|
||||
outputs: [
|
||||
{
|
||||
key: TaskResponseKeyEnum.answerText,
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 1551.71405495818,
|
||||
y: 977.4911578918461
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 1000 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
const total = await App.countDocuments();
|
||||
let promise = Promise.resolve();
|
||||
console.log(total);
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit, skipVal))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number, skip: number) {
|
||||
// 遍历 app
|
||||
const apps = await App.find(
|
||||
{
|
||||
chat: { $ne: null },
|
||||
modules: { $exists: false }
|
||||
// userId: '63f9a14228d2a688d8dc9e1b'
|
||||
},
|
||||
'_id chat'
|
||||
).limit(limit);
|
||||
|
||||
return Promise.all(
|
||||
apps.map(async (app) => {
|
||||
if (!app.chat) return app;
|
||||
const modules = (() => {
|
||||
if (app.chat.relatedKbs.length === 0) {
|
||||
return chatTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt
|
||||
});
|
||||
} else {
|
||||
return kbTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt,
|
||||
kbList: app.chat.relatedKbs.map((id) => ({ kbId: id })),
|
||||
searchEmptyText: app.chat.searchEmptyText,
|
||||
searchLimit: app.chat.searchLimit,
|
||||
searchSimilarity: app.chat.searchSimilarity
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await App.findByIdAndUpdate(app.id, {
|
||||
modules
|
||||
});
|
||||
return modules;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
|
||||
const { rowCount } = await PgClient.query(`SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = '${PgDatasetTableName}'
|
||||
AND column_name = 'file_id'`);
|
||||
|
||||
if (rowCount > 0) {
|
||||
return jsonRes(res, {
|
||||
data: '已经存在file_id字段'
|
||||
});
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: await PgClient.query(
|
||||
`ALTER TABLE ${PgDatasetTableName} ADD COLUMN file_id VARCHAR(100)`
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Collection, App } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
|
||||
/* 模型收藏切换 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { appId } = req.query as { appId: string };
|
||||
|
||||
if (!appId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const collectionRecord = await Collection.findOne({
|
||||
userId,
|
||||
modelId: appId
|
||||
});
|
||||
|
||||
if (collectionRecord) {
|
||||
await Collection.findByIdAndRemove(collectionRecord._id);
|
||||
} else {
|
||||
await Collection.create({
|
||||
userId,
|
||||
modelId: appId
|
||||
});
|
||||
}
|
||||
|
||||
await App.findByIdAndUpdate(appId, {
|
||||
'share.collection': await Collection.countDocuments({ modelId: appId })
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, App } from '@/service/mongo';
|
||||
import type { PagingData } from '@/types';
|
||||
import type { ShareAppItem } from '@/types/app';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
/* 获取模型列表 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const {
|
||||
searchText = '',
|
||||
pageNum = 1,
|
||||
pageSize = 20
|
||||
} = req.body as { searchText: string; pageNum: number; pageSize: number };
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const regex = new RegExp(searchText, 'i');
|
||||
|
||||
const where = {
|
||||
$and: [
|
||||
{ 'share.isShare': true },
|
||||
{
|
||||
$or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
|
||||
}
|
||||
]
|
||||
};
|
||||
const pipeline = [
|
||||
{
|
||||
$match: where
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'collections',
|
||||
let: { modelId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$modelId', '$$modelId'] },
|
||||
{
|
||||
$eq: ['$userId', userId ? new Types.ObjectId(userId) : new Types.ObjectId()]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'collections'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
avatar: { $ifNull: ['$avatar', '/icon/logo.svg'] },
|
||||
name: 1,
|
||||
userId: 1,
|
||||
intro: 1,
|
||||
share: 1,
|
||||
isCollection: {
|
||||
$cond: {
|
||||
if: { $gt: [{ $size: '$collections' }, 0] },
|
||||
then: true,
|
||||
else: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { 'share.topNum': -1, 'share.collection': -1 }
|
||||
},
|
||||
{
|
||||
$skip: (pageNum - 1) * pageSize
|
||||
},
|
||||
{
|
||||
$limit: pageSize
|
||||
}
|
||||
];
|
||||
|
||||
// 获取被分享的模型
|
||||
const [models, total] = await Promise.all([
|
||||
// @ts-ignore
|
||||
App.aggregate(pipeline),
|
||||
App.countDocuments(where)
|
||||
]);
|
||||
|
||||
jsonRes<PagingData<ShareAppItem>>(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: models,
|
||||
total
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { fileId, kbId } = req.query as { fileId: string; kbId: string };
|
||||
|
||||
if (!fileId || !kbId) {
|
||||
throw new Error('fileId and kbId is required');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
// other data. Delete only vector data
|
||||
if (fileId === OtherFileId) {
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
"AND (file_id IS NULL OR file_id = '')"
|
||||
]
|
||||
});
|
||||
} else {
|
||||
// auth file
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
|
||||
await gridFs.findAndAuthFile(fileId);
|
||||
|
||||
// delete all pg data
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['file_id', fileId]]
|
||||
});
|
||||
// delete all training data
|
||||
await TrainingData.deleteMany({
|
||||
userId,
|
||||
file_id: fileId
|
||||
});
|
||||
|
||||
// delete file
|
||||
await bucket.delete(new Types.ObjectId(fileId));
|
||||
}
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
import type { FileInfo } from '@/types/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { fileId } = req.query as { kbId: string; fileId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (fileId === OtherFileId) {
|
||||
return jsonRes<FileInfo>(res, {
|
||||
data: {
|
||||
id: OtherFileId,
|
||||
size: 0,
|
||||
filename: 'kb.Other Data',
|
||||
uploadDate: new Date(),
|
||||
encoding: '',
|
||||
contentType: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
|
||||
const file = await gridFs.findAndAuthFile(fileId);
|
||||
|
||||
jsonRes<FileInfo>(res, {
|
||||
data: file
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { FileStatusEnum, OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
let {
|
||||
pageNum = 1,
|
||||
pageSize = 10,
|
||||
kbId,
|
||||
searchText = ''
|
||||
} = req.body as { pageNum: number; pageSize: number; kbId: string; searchText: string };
|
||||
searchText = searchText?.replace(/'/g, '');
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
// find files
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const collection = gridFs.Collection();
|
||||
|
||||
const mongoWhere = {
|
||||
['metadata.kbId']: kbId,
|
||||
['metadata.userId']: userId,
|
||||
['metadata.datasetUsed']: true,
|
||||
...(searchText && { filename: { $regex: searchText } })
|
||||
};
|
||||
const [files, total] = await Promise.all([
|
||||
collection
|
||||
.find(mongoWhere, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
filename: 1,
|
||||
uploadDate: 1,
|
||||
length: 1
|
||||
}
|
||||
})
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.sort({ uploadDate: -1 })
|
||||
.toArray(),
|
||||
collection.countDocuments(mongoWhere)
|
||||
]);
|
||||
|
||||
async function GetOtherData() {
|
||||
return {
|
||||
id: OtherFileId,
|
||||
size: 0,
|
||||
filename: 'kb.Other Data',
|
||||
uploadTime: new Date(),
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: '' }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgDatasetTableName, {
|
||||
fields: ['id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
"AND (file_id IS NULL OR file_id = '')"
|
||||
]
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const data = await Promise.all([
|
||||
GetOtherData(),
|
||||
...files.map(async (file) => {
|
||||
return {
|
||||
id: String(file._id),
|
||||
size: file.length,
|
||||
filename: file.filename,
|
||||
uploadTime: file.uploadDate,
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: file._id }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgDatasetTableName, {
|
||||
fields: ['id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
'AND',
|
||||
['file_id', String(file._id)]
|
||||
]
|
||||
})
|
||||
};
|
||||
})
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: data.flat(),
|
||||
total
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OpenApi } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const findResponse = await OpenApi.find({ userId }).sort({ _id: -1 });
|
||||
|
||||
// jus save four data
|
||||
const apiKeys = findResponse.map<UserOpenApiKey>(
|
||||
({ _id, apiKey, createTime, lastUsedTime }) => {
|
||||
return {
|
||||
id: _id,
|
||||
apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
|
||||
createTime,
|
||||
lastUsedTime
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: apiKeys
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData, KB } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { authKb } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgDatasetTableName, TrainingModeEnum } from '@/constants/plugin';
|
||||
import { startQueue } from '@/service/utils/tools';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
import { DatasetItemType } from '@/types/plugin';
|
||||
import { countPromptTokens } from '@/utils/common/tiktoken';
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
data: DatasetItemType[];
|
||||
mode: `${TrainingModeEnum}`;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
export type Response = {
|
||||
insertLen: number;
|
||||
};
|
||||
|
||||
const modeMap = {
|
||||
[TrainingModeEnum.index]: true,
|
||||
[TrainingModeEnum.qa]: true
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { kbId, data, mode = TrainingModeEnum.index, prompt } = req.body as Props;
|
||||
|
||||
if (!kbId || !Array.isArray(data)) {
|
||||
throw new Error('KbId or data is empty');
|
||||
}
|
||||
|
||||
if (modeMap[mode] === undefined) {
|
||||
throw new Error('Mode is error');
|
||||
}
|
||||
|
||||
if (data.length > 500) {
|
||||
throw new Error('Data is too long, max 500');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
jsonRes<Response>(res, {
|
||||
data: await pushDataToKb({
|
||||
kbId,
|
||||
data,
|
||||
userId,
|
||||
mode,
|
||||
prompt
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export async function pushDataToKb({
|
||||
userId,
|
||||
kbId,
|
||||
data,
|
||||
mode,
|
||||
prompt
|
||||
}: { userId: string } & Props): Promise<Response> {
|
||||
const [kb, vectorModel] = await Promise.all([
|
||||
authKb({
|
||||
userId,
|
||||
kbId
|
||||
}),
|
||||
(async () => {
|
||||
if (mode === TrainingModeEnum.index) {
|
||||
const vectorModel = (await KB.findById(kbId, 'vectorModel'))?.vectorModel;
|
||||
|
||||
return getVectorModel(vectorModel || global.vectorModels[0].model);
|
||||
}
|
||||
return global.vectorModels[0];
|
||||
})()
|
||||
]);
|
||||
|
||||
const modeMaxToken = {
|
||||
[TrainingModeEnum.index]: vectorModel.maxToken * 1.5,
|
||||
[TrainingModeEnum.qa]: global.qaModel.maxToken * 0.8
|
||||
};
|
||||
|
||||
// 过滤重复的 qa 内容
|
||||
const set = new Set();
|
||||
const filterData: DatasetItemType[] = [];
|
||||
|
||||
data.forEach((item) => {
|
||||
if (!item.q) return;
|
||||
|
||||
const text = item.q + item.a;
|
||||
|
||||
// count q token
|
||||
const token = countPromptTokens(item.q, 'system');
|
||||
|
||||
if (token > modeMaxToken[mode]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!set.has(text)) {
|
||||
filterData.push(item);
|
||||
set.add(text);
|
||||
}
|
||||
});
|
||||
|
||||
// 数据库去重
|
||||
const insertData = (
|
||||
await Promise.allSettled(
|
||||
filterData.map(async (data) => {
|
||||
let { q, a } = data;
|
||||
if (mode !== TrainingModeEnum.index) {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
if (!q) {
|
||||
return Promise.reject('q为空');
|
||||
}
|
||||
|
||||
q = q.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
a = a.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
|
||||
// Exactly the same data, not push
|
||||
try {
|
||||
const { rows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgDatasetTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = rows[0]?.exists || false;
|
||||
|
||||
if (exists) {
|
||||
return Promise.reject('已经存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter((item) => item.status === 'fulfilled')
|
||||
.map<DatasetItemType>((item: any) => item.value);
|
||||
|
||||
// 插入记录
|
||||
const insertRes = await TrainingData.insertMany(
|
||||
insertData.map((item) => ({
|
||||
...item,
|
||||
userId,
|
||||
kbId,
|
||||
mode,
|
||||
prompt,
|
||||
vectorModel: vectorModel.model
|
||||
}))
|
||||
);
|
||||
|
||||
insertRes.length > 0 && startQueue();
|
||||
|
||||
return {
|
||||
insertLen: insertRes.length
|
||||
};
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '12mb'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { getVector } from '../plugin/vector';
|
||||
import type { KbTestItemType } from '@/types/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { KB } from '@/service/mongo';
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
text: string;
|
||||
};
|
||||
export type Response = KbTestItemType['results'];
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { kbId, text } = req.body as Props;
|
||||
|
||||
if (!kbId || !text) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const [{ userId }, kb] = await Promise.all([
|
||||
authUser({ req }),
|
||||
KB.findById(kbId, 'vectorModel')
|
||||
]);
|
||||
|
||||
if (!userId || !kb) {
|
||||
throw new Error('缺少用户ID');
|
||||
}
|
||||
|
||||
const { vectors } = await getVector({
|
||||
model: kb.vectorModel,
|
||||
userId,
|
||||
input: [text]
|
||||
});
|
||||
|
||||
const response: any = await PgClient.query(
|
||||
`BEGIN;
|
||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||
select id, q, a, source, file_id, (vector <#> '[${
|
||||
vectors[0]
|
||||
}]') * -1 AS score from ${PgDatasetTableName} where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
|
||||
vectors[0]
|
||||
}]' limit 12;
|
||||
COMMIT;`
|
||||
);
|
||||
|
||||
jsonRes<Response>(res, {
|
||||
data: response?.[2]?.rows || []
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { KB, connectToDatabase } from '@/service/mongo';
|
||||
import { getVector } from '../plugin/vector';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export type Props = {
|
||||
dataId: string;
|
||||
kbId: string;
|
||||
a?: string;
|
||||
q?: string;
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { dataId, a = '', q = '', kbId } = req.body as Props;
|
||||
|
||||
if (!dataId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// auth user and get kb
|
||||
const [{ userId }, kb] = await Promise.all([
|
||||
authUser({ req }),
|
||||
KB.findById(kbId, 'vectorModel')
|
||||
]);
|
||||
|
||||
if (!kb) {
|
||||
throw new Error("Can't find database");
|
||||
}
|
||||
|
||||
// get vector
|
||||
const { vectors = [] } = await (async () => {
|
||||
if (q) {
|
||||
return getVector({
|
||||
userId,
|
||||
input: [q],
|
||||
model: kb.vectorModel
|
||||
});
|
||||
}
|
||||
return { vectors: [[]] };
|
||||
})();
|
||||
|
||||
// 更新 pg 内容.仅修改a,不需要更新向量。
|
||||
await PgClient.update(PgDatasetTableName, {
|
||||
where: [['id', dataId], 'AND', ['user_id', userId]],
|
||||
values: [
|
||||
{ key: 'a', value: a.replace(/'/g, '"') },
|
||||
...(q
|
||||
? [
|
||||
{ key: 'q', value: q.replace(/'/g, '"') },
|
||||
{ key: 'vector', value: `[${vectors[0]}]` }
|
||||
]
|
||||
: [])
|
||||
]
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authBalanceByUid, authUser } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/lib/openai';
|
||||
import { pushGenerateVectorBill } from '@/service/events/pushBill';
|
||||
|
||||
type Props = {
|
||||
model: string;
|
||||
input: string[];
|
||||
};
|
||||
type Response = {
|
||||
tokenLen: number;
|
||||
vectors: number[][];
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { userId } = await authUser({ req });
|
||||
let { input, model } = req.query as Props;
|
||||
|
||||
if (!Array.isArray(input)) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
jsonRes<Response>(res, {
|
||||
data: await getVector({ userId, input, model })
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export async function getVector({
|
||||
model = 'text-embedding-ada-002',
|
||||
userId,
|
||||
input
|
||||
}: { userId?: string } & Props) {
|
||||
userId && (await authBalanceByUid(userId));
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
if (!input[i]) {
|
||||
return Promise.reject({
|
||||
code: 500,
|
||||
message: '向量生成模块输入内容为空'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getAIChatApi();
|
||||
|
||||
// 把输入的内容转成向量
|
||||
const result = await chatAPI
|
||||
.createEmbedding(
|
||||
{
|
||||
model,
|
||||
input
|
||||
},
|
||||
{
|
||||
timeout: 60000,
|
||||
...axiosConfig()
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data?.data?.[0]?.embedding) {
|
||||
console.log(res.data);
|
||||
// @ts-ignore
|
||||
return Promise.reject(res.data?.err?.message || 'Embedding API Error');
|
||||
}
|
||||
return {
|
||||
tokenLen: res.data.usage.total_tokens || 0,
|
||||
vectors: await Promise.all(res.data.data.map((item) => unityDimensional(item.embedding)))
|
||||
};
|
||||
});
|
||||
|
||||
userId &&
|
||||
pushGenerateVectorBill({
|
||||
userId,
|
||||
tokenLen: result.tokenLen,
|
||||
model
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function unityDimensional(vector: number[]) {
|
||||
if (vector.length > 1536) return Promise.reject('向量维度不能超过 1536');
|
||||
let resultVector = vector;
|
||||
const vectorLen = vector.length;
|
||||
|
||||
const zeroVector = new Array(1536 - vectorLen).fill(0);
|
||||
|
||||
return resultVector.concat(zeroVector);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export type Response = {
|
||||
id: string;
|
||||
q: string;
|
||||
a: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { dataId } = req.query as {
|
||||
dataId: string;
|
||||
};
|
||||
if (!dataId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const where: any = [['user_id', userId], 'AND', ['id', dataId]];
|
||||
|
||||
const searchRes = await PgClient.select<KbDataItemType>(PgDatasetTableName, {
|
||||
fields: ['kb_id', 'id', 'q', 'a', 'source', 'file_id'],
|
||||
where,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: searchRes.rows[0]
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
import { startQueue } from '@/service/utils/tools';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { kbId, init = false } = req.body as { kbId: string; init: boolean };
|
||||
if (!kbId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
// split queue data
|
||||
const result = await TrainingData.aggregate([
|
||||
{
|
||||
$match: {
|
||||
userId: new Types.ObjectId(userId),
|
||||
kbId: new Types.ObjectId(kbId)
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$mode',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
qaListLen: result.find((item) => item._id === TrainingModeEnum.qa)?.count || 0,
|
||||
vectorListLen: result.find((item) => item._id === TrainingModeEnum.index)?.count || 0
|
||||
}
|
||||
});
|
||||
|
||||
if (init) {
|
||||
startQueue();
|
||||
}
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { authKb, authUser } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { insertKbItem, PgClient } from '@/service/pg';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
import { DatasetItemType } from '@/types/plugin';
|
||||
import { countPromptTokens } from '@/utils/common/tiktoken';
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
data: DatasetItemType;
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId, data = { q: '', a: '' } } = req.body as Props;
|
||||
|
||||
if (!kbId || !data?.q) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
// auth kb
|
||||
const kb = await authKb({ kbId, userId });
|
||||
|
||||
const q = data?.q?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
const a = data?.a?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
|
||||
// token check
|
||||
const token = countPromptTokens(q, 'system');
|
||||
|
||||
if (token > getVectorModel(kb.vectorModel).maxToken) {
|
||||
throw new Error('Over Tokens');
|
||||
}
|
||||
|
||||
const { rows: existsRows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgDatasetTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = existsRows[0]?.exists || false;
|
||||
|
||||
if (exists) {
|
||||
throw new Error('已经存在完全一致的数据');
|
||||
}
|
||||
|
||||
const { vectors } = await getVector({
|
||||
model: kb.vectorModel,
|
||||
input: [q],
|
||||
userId
|
||||
});
|
||||
|
||||
const response = await insertKbItem({
|
||||
userId,
|
||||
kbId,
|
||||
data: [
|
||||
{
|
||||
q,
|
||||
a,
|
||||
source: data.source,
|
||||
vector: vectors[0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const id = response?.rows?.[0]?.id || '';
|
||||
|
||||
jsonRes(res, {
|
||||
data: id
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { FeConfigsType, SystemEnvType } from '@/types';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { readFileSync } from 'fs';
|
||||
import {
|
||||
type QAModelItemType,
|
||||
type ChatModelItemType,
|
||||
type VectorModelItemType
|
||||
} from '@/types/model';
|
||||
|
||||
export type InitDateResponse = {
|
||||
chatModels: ChatModelItemType[];
|
||||
qaModel: QAModelItemType;
|
||||
vectorModels: VectorModelItemType[];
|
||||
feConfigs: FeConfigsType;
|
||||
systemVersion: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!global.feConfigs) {
|
||||
await getInitConfig();
|
||||
}
|
||||
jsonRes<InitDateResponse>(res, {
|
||||
data: {
|
||||
feConfigs: global.feConfigs,
|
||||
chatModels: global.chatModels,
|
||||
qaModel: global.qaModel,
|
||||
vectorModels: global.vectorModels,
|
||||
systemVersion: global.systemVersion || '0.0.0'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultSystemEnv: SystemEnvType = {
|
||||
vectorMaxProcess: 15,
|
||||
qaMaxProcess: 15,
|
||||
pgIvfflatProbe: 20
|
||||
};
|
||||
const defaultFeConfigs: FeConfigsType = {
|
||||
show_emptyChat: true,
|
||||
show_register: false,
|
||||
show_appStore: false,
|
||||
show_userDetail: false,
|
||||
show_contact: true,
|
||||
show_git: true,
|
||||
show_doc: true,
|
||||
systemTitle: 'FastGPT',
|
||||
authorText: 'Made by FastGPT Team.',
|
||||
limit: {
|
||||
exportLimitMinutes: 0
|
||||
},
|
||||
scripts: []
|
||||
};
|
||||
const defaultChatModels = [
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
name: 'GPT35-4k',
|
||||
contextMaxToken: 4000,
|
||||
quoteMaxToken: 2400,
|
||||
maxTemperature: 1.2,
|
||||
price: 0
|
||||
},
|
||||
{
|
||||
model: 'gpt-3.5-turbo-16k',
|
||||
name: 'GPT35-16k',
|
||||
contextMaxToken: 16000,
|
||||
quoteMaxToken: 8000,
|
||||
maxTemperature: 1.2,
|
||||
price: 0
|
||||
},
|
||||
{
|
||||
model: 'gpt-4',
|
||||
name: 'GPT4-8k',
|
||||
contextMaxToken: 8000,
|
||||
quoteMaxToken: 4000,
|
||||
maxTemperature: 1.2,
|
||||
price: 0
|
||||
}
|
||||
];
|
||||
const defaultQAModel = {
|
||||
model: 'gpt-3.5-turbo-16k',
|
||||
name: 'GPT35-16k',
|
||||
maxToken: 16000,
|
||||
price: 0
|
||||
};
|
||||
|
||||
const defaultVectorModels: VectorModelItemType[] = [
|
||||
{
|
||||
model: 'text-embedding-ada-002',
|
||||
name: 'Embedding-2',
|
||||
price: 0,
|
||||
defaultToken: 500,
|
||||
maxToken: 3000
|
||||
}
|
||||
];
|
||||
|
||||
export async function getInitConfig() {
|
||||
try {
|
||||
if (global.feConfigs) return;
|
||||
|
||||
getSystemVersion();
|
||||
|
||||
const filename =
|
||||
process.env.NODE_ENV === 'development' ? 'data/config.local.json' : '/app/data/config.json';
|
||||
const res = JSON.parse(readFileSync(filename, 'utf-8'));
|
||||
|
||||
console.log(`System Version: ${global.systemVersion}`);
|
||||
|
||||
console.log(res);
|
||||
|
||||
global.systemEnv = res.SystemParams
|
||||
? { ...defaultSystemEnv, ...res.SystemParams }
|
||||
: defaultSystemEnv;
|
||||
global.feConfigs = res.FeConfig ? { ...defaultFeConfigs, ...res.FeConfig } : defaultFeConfigs;
|
||||
global.chatModels = res.ChatModels || defaultChatModels;
|
||||
global.qaModel = res.QAModel || defaultQAModel;
|
||||
global.vectorModels = res.VectorModels || defaultVectorModels;
|
||||
} catch (error) {
|
||||
setDefaultData();
|
||||
console.log('get init config error, set default', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setDefaultData() {
|
||||
global.systemEnv = defaultSystemEnv;
|
||||
global.feConfigs = defaultFeConfigs;
|
||||
global.chatModels = defaultChatModels;
|
||||
global.qaModel = defaultQAModel;
|
||||
global.vectorModels = defaultVectorModels;
|
||||
}
|
||||
|
||||
export function getSystemVersion() {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
global.systemVersion = process.env.npm_package_version || '0.0.0';
|
||||
return;
|
||||
}
|
||||
const packageJson = JSON.parse(readFileSync('/app/package.json', 'utf-8'));
|
||||
|
||||
global.systemVersion = packageJson?.version;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
global.systemVersion = '0.0.0';
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { feConfigs } from '@/store/static';
|
||||
|
||||
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const API = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
||||
const {
|
||||
isOpen: isOpenAPIModal,
|
||||
onOpen: onOpenAPIModal,
|
||||
onClose: onCloseAPIModal
|
||||
} = useDisclosure();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl(`${location.origin}/api/openapi`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} pt={[0, 5]} h={'100%'}>
|
||||
<Flex px={5} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
AppId:
|
||||
<Box
|
||||
as={'span'}
|
||||
ml={2}
|
||||
fontWeight={'bold'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(appId, '已复制 AppId')}
|
||||
>
|
||||
{appId}
|
||||
</Box>
|
||||
</Box>
|
||||
{isPc && (
|
||||
<>
|
||||
<Flex
|
||||
bg={'myWhite.600'}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(baseUrl, '已复制 API 地址')}
|
||||
>
|
||||
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
|
||||
API服务器
|
||||
</Box>
|
||||
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
|
||||
{baseUrl}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
|
||||
variant={'base'}
|
||||
onClick={onOpenAPIModal}
|
||||
>
|
||||
API 秘钥
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider mt={3} />
|
||||
<Box flex={'1 0 0'} h={0}>
|
||||
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
src={
|
||||
feConfigs?.openAPIUrl ||
|
||||
'https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh'
|
||||
}
|
||||
frameBorder="0"
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={() => setIsLoaded(true)}
|
||||
/>
|
||||
</Skeleton>
|
||||
</Box>
|
||||
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default API;
|
||||
@@ -1,138 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Input, Button, Flex } from '@chakra-ui/react';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import type { ClassifyQuestionAgentItemType } from '@/types/app';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
|
||||
import SourceHandle from '../render/SourceHandle';
|
||||
|
||||
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs, onChangeNode } = data;
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
[SpecialInputKeyEnum.agents]: ({
|
||||
key: agentKey,
|
||||
value: agents = [],
|
||||
...props
|
||||
}: {
|
||||
key: string;
|
||||
value?: ClassifyQuestionAgentItemType[];
|
||||
}) => (
|
||||
<Box>
|
||||
{agents.map((item, i) => (
|
||||
<Flex key={item.key} mb={4} alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={2}
|
||||
name={'minus'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'myGray.900' }}
|
||||
onClick={() => {
|
||||
const newInputValue = agents.filter((input) => input.key !== item.key);
|
||||
const newOutputVal = outputs.filter((output) => output.key !== item.key);
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: newInputValue
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'outputs',
|
||||
key: '',
|
||||
value: newOutputVal
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<Box flex={1}>类型{i + 1}</Box>
|
||||
<Box position={'relative'}>
|
||||
<Input
|
||||
mt={1}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
const newVal = agents.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle handleKey={item.key} valueType={FlowValueTypeEnum.boolean} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
const newInputValue = agents.concat({ value: '', key });
|
||||
const newOutputValue = outputs.concat({
|
||||
key,
|
||||
label: '',
|
||||
type: FlowOutputItemTypeEnum.hidden,
|
||||
targets: []
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: newInputValue
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'outputs',
|
||||
key: agentKey,
|
||||
value: newOutputValue
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加问题类型
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCQNode);
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
import MySelect from '@/components/Select';
|
||||
import { chatModelList } from '@/store/static';
|
||||
import MySlider from '@/components/Slider';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
const NodeChat = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs, onChangeNode } = data;
|
||||
const outputsLen = useMemo(
|
||||
() => outputs.filter((item) => item.type !== FlowOutputItemTypeEnum.hidden).length,
|
||||
[outputs]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
model: (inputItem) => {
|
||||
const list = chatModelList.map((item) => {
|
||||
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
|
||||
|
||||
return {
|
||||
value: item.model,
|
||||
label: `${item.name}${priceStr}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
value={inputItem.value}
|
||||
list={list}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: inputItem.key,
|
||||
value: {
|
||||
...inputItem,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
|
||||
// update max tokens
|
||||
const model =
|
||||
chatModelList.find((item) => item.model === e) || chatModelList[0];
|
||||
if (!model) return;
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: 'maxToken',
|
||||
value: {
|
||||
...inputs.find((input) => input.key === 'maxToken'),
|
||||
markList: [
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${model.contextMaxToken}`, value: model.contextMaxToken }
|
||||
],
|
||||
max: model.contextMaxToken,
|
||||
value: model.contextMaxToken / 2
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
maxToken: (inputItem) => {
|
||||
const model = inputs.find((item) => item.key === 'model')?.value;
|
||||
const modelData = chatModelList.find((item) => item.model === model);
|
||||
const maxToken = modelData ? modelData.contextMaxToken : 4000;
|
||||
const markList = [
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${maxToken}`, value: maxToken }
|
||||
];
|
||||
return (
|
||||
<Box pt={5} pb={4} px={2}>
|
||||
<MySlider
|
||||
markList={markList}
|
||||
width={'100%'}
|
||||
min={inputItem.min || 100}
|
||||
max={maxToken}
|
||||
step={inputItem.step || 1}
|
||||
value={inputItem.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: inputItem.key,
|
||||
value: {
|
||||
...inputItem,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
{outputsLen > 0 && (
|
||||
<>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput
|
||||
onChangeNode={onChangeNode}
|
||||
moduleId={moduleId}
|
||||
flowOutputList={outputs}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeChat);
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import { Flex, Box, Button, useTheme, useDisclosure, Grid } from '@chakra-ui/react';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import { KBSelectModal } from '../../../KBSelectModal';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const KBSelect = ({
|
||||
activeKbs = [],
|
||||
onChange
|
||||
}: {
|
||||
activeKbs: SelectedKbType;
|
||||
onChange: (e: SelectedKbType) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { datasets, loadAllDatasets } = useDatasetStore();
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const showKbList = useMemo(
|
||||
() => datasets.filter((item) => activeKbs.find((kb) => kb.kbId === item._id)),
|
||||
[datasets, activeKbs]
|
||||
);
|
||||
|
||||
useQuery(['loadAllDatasets'], loadAllDatasets);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'1fr 1fr'} gridGap={4}>
|
||||
<Button h={'36px'} onClick={onOpenKbSelect}>
|
||||
选择知识库
|
||||
</Button>
|
||||
{showKbList.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
h={'36px'}
|
||||
border={theme.borders.base}
|
||||
px={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'24px'}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
{isOpenKbSelect && (
|
||||
<KBSelectModal activeKbs={activeKbs} onChange={onChange} onClose={onCloseKbSelect} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeKbSearch = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs, onChangeNode } = data;
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
kbList: ({ key, value, ...props }) => (
|
||||
<KBSelect
|
||||
activeKbs={value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key,
|
||||
type: 'inputs',
|
||||
value: {
|
||||
...props,
|
||||
key,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput onChangeNode={onChangeNode} moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeKbSearch);
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from '../modules/Container';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { FlowValueTypeEnum } from '@/constants/flow';
|
||||
import SourceHandle from '../render/SourceHandle';
|
||||
|
||||
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'240px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
|
||||
<Box position={'relative'}>
|
||||
用户问题
|
||||
<SourceHandle
|
||||
handleKey={SystemInputEnum.userChatInput}
|
||||
valueType={FlowValueTypeEnum.string}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(QuestionInputNode);
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import Label from '../modules/Label';
|
||||
|
||||
const NodeTFSwitch = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'220px'} {...data}>
|
||||
<Divider text="输入输出" />
|
||||
<Container h={'100px'} py={0} px={0} display={'flex'} alignItems={'center'}>
|
||||
<Box flex={1} pl={'12px'}>
|
||||
<Label
|
||||
required
|
||||
description="接收到 false、0、null、undefined或空字符串时,执行 False,反之执行 True"
|
||||
>
|
||||
输入
|
||||
</Label>
|
||||
<Handle
|
||||
style={{
|
||||
top: '50%',
|
||||
left: '0',
|
||||
transform: 'translate(-50%,-50%)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
id={SystemInputEnum.switch}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
onConnect={(params) => console.log('input onConnect', params)}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} pr={'12px'}>
|
||||
<Flex alignItems={'center'} justifyContent={'flex-end'} mb={'26px'} position={'relative'}>
|
||||
<Label>True</Label>
|
||||
<Handle
|
||||
style={{
|
||||
top: '0',
|
||||
right: '-12px',
|
||||
transform: 'translate(50%,5px)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
id={'true'}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
onConnect={(params) => console.log('handle onConnect', params)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} justifyContent={'flex-end'} position={'relative'}>
|
||||
<Label>False</Label>
|
||||
<Handle
|
||||
style={{
|
||||
bottom: '0',
|
||||
right: '-12px',
|
||||
transform: 'translate(50%,-5px)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
id={'false'}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
onConnect={(params) => console.log('handle onConnect', params)}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeTFSwitch);
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Flex, Textarea } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from '../modules/Container';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { welcomeTextTip } from '@/constants/flow/ModuleTemplate';
|
||||
|
||||
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { inputs, moduleId, onChangeNode } = data;
|
||||
const welcomeText = useMemo(
|
||||
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
|
||||
<Box>开场白</Box>
|
||||
<MyTooltip label={welcomeTextTip} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
{welcomeText && (
|
||||
<Textarea
|
||||
className="nodrag"
|
||||
rows={6}
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText.value}
|
||||
bg={'myWhite.500'}
|
||||
placeholder={welcomeTextTip}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.welcomeText,
|
||||
type: 'inputs',
|
||||
value: {
|
||||
...welcomeText,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserGuide);
|
||||
@@ -1,126 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
|
||||
import { FlowModuleItemType, FlowModuleTemplateType } from '@/types/flow';
|
||||
import type { Node, XYPosition } from 'reactflow';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
|
||||
const ModuleTemplateList = ({
|
||||
nodes,
|
||||
isOpen,
|
||||
onAddNode,
|
||||
onClose
|
||||
}: {
|
||||
nodes?: Node<FlowModuleItemType>[];
|
||||
isOpen: boolean;
|
||||
onAddNode: (e: { template: FlowModuleTemplateType; position: XYPosition }) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const filterTemplates = useMemo(() => {
|
||||
const guideModulesIndex = ModuleTemplates.findIndex((item) => item.label === '引导模块');
|
||||
const guideModule: {
|
||||
label: string;
|
||||
list: FlowModuleTemplateType[];
|
||||
} = JSON.parse(JSON.stringify(ModuleTemplates[guideModulesIndex]));
|
||||
|
||||
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.userGuide)) {
|
||||
const index = guideModule.list.findIndex(
|
||||
(item) => item.flowType === FlowModuleTypeEnum.userGuide
|
||||
);
|
||||
guideModule.list.splice(index, 1);
|
||||
}
|
||||
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.variable)) {
|
||||
const index = guideModule.list.findIndex(
|
||||
(item) => item.flowType === FlowModuleTypeEnum.variable
|
||||
);
|
||||
guideModule.list.splice(index, 1);
|
||||
}
|
||||
|
||||
return [
|
||||
...ModuleTemplates.slice(0, guideModulesIndex),
|
||||
guideModule,
|
||||
...ModuleTemplates.slice(guideModulesIndex + 1)
|
||||
];
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={'360px'}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 100px)' : '0'}
|
||||
w={isOpen ? ['100%', '360px'] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={['100%', '330px']} py={4} px={5} fontSize={'xl'} fontWeight={'bold'}>
|
||||
系统模块
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
||||
<Box w={['100%', '330px']} mx={'auto'}>
|
||||
{filterTemplates.map((item) =>
|
||||
item.list.map((item) => (
|
||||
<Flex
|
||||
key={item.name}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < 360) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (isPc) return;
|
||||
onClose();
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} objectFit={'contain'} borderRadius={'0'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleTemplateList;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Label = ({
|
||||
required = false,
|
||||
children,
|
||||
description
|
||||
}: {
|
||||
required?: boolean;
|
||||
children: React.ReactNode | string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'}>
|
||||
{children}
|
||||
{required && (
|
||||
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default React.memo(Label);
|
||||
@@ -1,107 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, useTheme, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import type { FlowModuleItemType } from '@/types/flow';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
|
||||
type Props = FlowModuleItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
minW?: string | number;
|
||||
};
|
||||
|
||||
const NodeCard = (props: Props) => {
|
||||
const {
|
||||
children,
|
||||
logo = '/icon/logo.svg',
|
||||
name = '未知模块',
|
||||
description,
|
||||
minW = '300px',
|
||||
onCopyNode,
|
||||
onDelNode,
|
||||
moduleId
|
||||
} = props;
|
||||
const { copyData } = useCopyData();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const menuList = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: 'copy',
|
||||
label: t('common.Copy'),
|
||||
onClick: () => onCopyNode(moduleId)
|
||||
},
|
||||
// {
|
||||
// icon: 'settingLight',
|
||||
// label: t('app.Copy Module Config'),
|
||||
// onClick: () => {
|
||||
// const copyProps = { ...props };
|
||||
// delete copyProps.children;
|
||||
// delete copyProps.children;
|
||||
// console.log(copyProps);
|
||||
// }
|
||||
// },
|
||||
{
|
||||
icon: 'delete',
|
||||
label: t('common.Delete'),
|
||||
onClick: () => onDelNode(moduleId)
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'back',
|
||||
label: t('common.Cancel'),
|
||||
onClick: () => {}
|
||||
}
|
||||
],
|
||||
[moduleId, onCopyNode, onDelNode, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
|
||||
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
||||
<Avatar src={logo} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
|
||||
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
|
||||
{name}
|
||||
</Box>
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon
|
||||
display={['none', 'inline']}
|
||||
transform={'translateY(1px)'}
|
||||
ml={1}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<Menu autoSelect={false} isLazy>
|
||||
<MenuButton
|
||||
className={'nodrag'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'more'} w={'14px'} p={2} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
|
||||
{menuList.map((item) => (
|
||||
<MenuItem key={item.label} onClick={item.onClick} py={[2, 3]}>
|
||||
<MyIcon name={item.icon as any} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{item.label}</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeCard);
|
||||
@@ -1,115 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
FormControl
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyModal from '@/components/MyModal';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { FlowInputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MySelect from '@/components/Select';
|
||||
import { FlowInputItemType } from '@/types/flow';
|
||||
|
||||
const typeSelectList = [
|
||||
{
|
||||
label: '字符串',
|
||||
value: FlowValueTypeEnum.string
|
||||
},
|
||||
{
|
||||
label: '数字',
|
||||
value: FlowValueTypeEnum.number
|
||||
},
|
||||
{
|
||||
label: '布尔',
|
||||
value: FlowValueTypeEnum.boolean
|
||||
},
|
||||
{
|
||||
label: '任意',
|
||||
value: FlowValueTypeEnum.any
|
||||
}
|
||||
];
|
||||
|
||||
const SetInputFieldModal = ({
|
||||
defaultField = {
|
||||
label: '',
|
||||
key: '',
|
||||
type: FlowInputItemTypeEnum.target,
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
description: '',
|
||||
required: false
|
||||
},
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultField?: FlowInputItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: FlowInputItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, getValues, setValue, handleSubmit } = useForm<FlowInputItemType>({
|
||||
defaultValues: defaultField
|
||||
});
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose}>
|
||||
<ModalHeader display={'flex'} alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
|
||||
{t('app.Input Field Settings')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>必填</Box>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段类型</Box>
|
||||
<MySelect
|
||||
w={'288px'}
|
||||
list={typeSelectList}
|
||||
value={getValues('valueType')}
|
||||
onchange={(e: any) => {
|
||||
setValue('valueType', e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段名</Box>
|
||||
<Input
|
||||
placeholder="预约字段/sql语句……"
|
||||
{...register('label', { required: '字段名不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段 key</Box>
|
||||
<Input
|
||||
placeholder="appointment/sql"
|
||||
{...register('key', { required: '字段 key 不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SetInputFieldModal);
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
FormControl
|
||||
} from '@chakra-ui/react';
|
||||
import type { ContextExtractAgentItemType, HttpFieldItemType } from '@/types/app';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyModal from '@/components/MyModal';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MySelect from '@/components/Select';
|
||||
import { FlowOutputItemType } from '@/types/flow';
|
||||
|
||||
const typeSelectList = [
|
||||
{
|
||||
label: '字符串',
|
||||
value: FlowValueTypeEnum.string
|
||||
},
|
||||
{
|
||||
label: '数字',
|
||||
value: FlowValueTypeEnum.number
|
||||
},
|
||||
{
|
||||
label: '布尔',
|
||||
value: FlowValueTypeEnum.boolean
|
||||
},
|
||||
{
|
||||
label: '任意',
|
||||
value: FlowValueTypeEnum.any
|
||||
}
|
||||
];
|
||||
|
||||
const SetInputFieldModal = ({
|
||||
defaultField,
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultField: FlowOutputItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: FlowOutputItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, getValues, setValue, handleSubmit } = useForm<FlowOutputItemType>({
|
||||
defaultValues: defaultField
|
||||
});
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose}>
|
||||
<ModalHeader display={'flex'} alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
|
||||
{t('app.Output Field Settings')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段类型</Box>
|
||||
<MySelect
|
||||
w={'288px'}
|
||||
list={typeSelectList}
|
||||
value={getValues('valueType')}
|
||||
onchange={(e: any) => {
|
||||
setValue('valueType', e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段名</Box>
|
||||
<Input
|
||||
placeholder="预约字段/sql语句……"
|
||||
{...register('label', { required: '字段名不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段 key</Box>
|
||||
<Input
|
||||
placeholder="appointment/sql"
|
||||
{...register('key', { required: '字段 key 不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SetInputFieldModal);
|
||||
@@ -1,272 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { FlowInputItemType, FlowModuleItemType } from '@/types/flow';
|
||||
import {
|
||||
Box,
|
||||
Textarea,
|
||||
Input,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { FlowInputItemTypeEnum } from '@/constants/flow';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import TargetHandle from './TargetHandle';
|
||||
import MyIcon from '@/components/Icon';
|
||||
const SetInputFieldModal = dynamic(() => import('../modules/SetInputFieldModal'));
|
||||
|
||||
export const Label = ({
|
||||
moduleId,
|
||||
inputKey,
|
||||
onChangeNode,
|
||||
...item
|
||||
}: FlowInputItemType & {
|
||||
moduleId: string;
|
||||
inputKey: string;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => {
|
||||
const { required = false, description, edit, label, type, valueType } = item;
|
||||
const [editField, setEditField] = useState<FlowInputItemType>();
|
||||
|
||||
return (
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Box position={'relative'}>
|
||||
{label}
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
{required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-8px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(type === FlowInputItemTypeEnum.target || valueType) && (
|
||||
<TargetHandle handleKey={inputKey} valueType={valueType} />
|
||||
)}
|
||||
|
||||
{edit && (
|
||||
<>
|
||||
<MyIcon
|
||||
name={'settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={3}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
...item,
|
||||
key: inputKey
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delInput',
|
||||
key: inputKey,
|
||||
value: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!editField && (
|
||||
<SetInputFieldModal
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={(data) => {
|
||||
// same key
|
||||
if (editField.key === data.key) {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: inputKey,
|
||||
value: data
|
||||
});
|
||||
} else {
|
||||
// diff key. del and add
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
key: data.key,
|
||||
value: data
|
||||
});
|
||||
setTimeout(() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delInput',
|
||||
key: editField.key,
|
||||
value: ''
|
||||
});
|
||||
});
|
||||
}
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderInput = ({
|
||||
flowInputList,
|
||||
moduleId,
|
||||
CustomComponent = {},
|
||||
onChangeNode
|
||||
}: {
|
||||
flowInputList: FlowInputItemType[];
|
||||
moduleId: string;
|
||||
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{flowInputList.map(
|
||||
(item) =>
|
||||
item.type !== FlowInputItemTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
{!!item.label && (
|
||||
<Label
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
inputKey={item.key}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{item.type === FlowInputItemTypeEnum.numberInput && (
|
||||
<NumberInput
|
||||
defaultValue={item.value}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: Number(e)
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.input && (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.textarea && (
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={item.placeholder}
|
||||
resize={'both'}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={item.list || []}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.slider && (
|
||||
<Box pt={5} pb={4} px={2}>
|
||||
<MySlider
|
||||
markList={item.markList}
|
||||
width={'100%'}
|
||||
min={item.min || 0}
|
||||
max={item.max}
|
||||
step={item.step || 1}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
|
||||
<>{CustomComponent[item.key]({ ...item })}</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderInput);
|
||||
@@ -1,5 +0,0 @@
|
||||
.panel {
|
||||
.react-flow__panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,632 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
XYPosition,
|
||||
Connection,
|
||||
useViewport
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
edgeOptions,
|
||||
connectionLineStyle,
|
||||
FlowModuleTypeEnum,
|
||||
FlowInputItemTypeEnum,
|
||||
FlowValueTypeEnum
|
||||
} from '@/constants/flow';
|
||||
import { appModule2FlowNode, appModule2FlowEdge } from '@/utils/adapt';
|
||||
import {
|
||||
FlowModuleItemType,
|
||||
FlowModuleTemplateType,
|
||||
FlowOutputTargetItemType,
|
||||
type FlowModuleItemChangeProps
|
||||
} from '@/types/flow';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import TemplateList from './components/TemplateList';
|
||||
import ChatTest, { type ChatTestComponentRef } from './components/ChatTest';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./components/ImportSettings'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeChat = dynamic(() => import('./components/Nodes/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeKbSearch = dynamic(() => import('./components/Nodes/NodeKbSearch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeHistory = dynamic(() => import('./components/Nodes/NodeHistory'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeTFSwitch = dynamic(() => import('./components/Nodes/NodeTFSwitch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeAnswer = dynamic(() => import('./components/Nodes/NodeAnswer'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeQuestionInput = dynamic(() => import('./components/Nodes/NodeQuestionInput'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/Nodes/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import { AppTypeEnum } from '@/constants/app';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const nodeTypes = {
|
||||
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
|
||||
[FlowModuleTypeEnum.variable]: NodeVariable,
|
||||
[FlowModuleTypeEnum.questionInput]: NodeQuestionInput,
|
||||
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
|
||||
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
|
||||
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
|
||||
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
|
||||
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
|
||||
[FlowModuleTypeEnum.httpRequest]: NodeHttp
|
||||
// [FlowModuleTypeEnum.empty]: EmptyModule
|
||||
};
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
type Props = { app: AppSchema; onCloseSettings: () => void };
|
||||
|
||||
const AppEdit = ({ app, onCloseSettings }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||
|
||||
const { updateAppDetail } = useUserStore();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
|
||||
|
||||
const onFixView = useCallback(() => {
|
||||
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
|
||||
|
||||
setTimeout(() => {
|
||||
btn && btn.click();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onAddNode = useCallback(
|
||||
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
|
||||
if (!reactFlowWrapper.current) return;
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||
|
||||
setNodes((state) =>
|
||||
state.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
moduleId: nanoid(),
|
||||
position: { x: mouseX, y: mouseY }
|
||||
},
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[x, zoom, y]
|
||||
);
|
||||
const onDelNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
||||
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
const onDelEdge = useCallback(
|
||||
({
|
||||
moduleId,
|
||||
sourceHandle,
|
||||
targetHandle
|
||||
}: {
|
||||
moduleId: string;
|
||||
sourceHandle?: string;
|
||||
targetHandle?: string;
|
||||
}) => {
|
||||
if (!sourceHandle && !targetHandle) return;
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => {
|
||||
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
|
||||
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const onCopyNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((nodes) => {
|
||||
const node = nodes.find((node) => node.id === nodeId);
|
||||
if (!node) return nodes;
|
||||
const template = {
|
||||
logo: node.data.logo,
|
||||
name: node.data.name,
|
||||
intro: node.data.intro,
|
||||
description: node.data.description,
|
||||
flowType: node.data.flowType,
|
||||
inputs: node.data.inputs,
|
||||
outputs: node.data.outputs,
|
||||
showStatus: node.data.showStatus
|
||||
};
|
||||
return nodes.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
moduleId: nanoid(),
|
||||
position: { x: node.position.x + 200, y: node.position.y + 50 }
|
||||
},
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
const onCollectionNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
console.log(nodes.find((node) => node.id === nodeId));
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const flow2AppModules = useCallback(() => {
|
||||
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
||||
moduleId: item.data.moduleId,
|
||||
name: item.data.name,
|
||||
flowType: item.data.flowType,
|
||||
showStatus: item.data.showStatus,
|
||||
position: item.position,
|
||||
inputs: item.data.inputs.map((item) => ({
|
||||
...item,
|
||||
connected: item.type !== FlowInputItemTypeEnum.target
|
||||
})),
|
||||
outputs: item.data.outputs.map((item) => ({
|
||||
...item,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected =
|
||||
input.connected ||
|
||||
!!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
}, [edges, nodes]);
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== moduleId) return node;
|
||||
if (type === 'inputs') {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.map((item) => (item.key === key ? value : item))
|
||||
}
|
||||
};
|
||||
}
|
||||
if (type === 'addInput') {
|
||||
const input = node.data.inputs.find((input) => input.key === value.key);
|
||||
if (input) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.concat(value)
|
||||
}
|
||||
};
|
||||
}
|
||||
if (type === 'delInput') {
|
||||
onDelEdge({ moduleId, targetHandle: key });
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.filter((item) => item.key !== key)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// del output connect
|
||||
const delOutputs = node.data.outputs.filter(
|
||||
(item) => !value.find((output: FlowOutputTargetItemType) => output.key === item.key)
|
||||
);
|
||||
delOutputs.forEach((output) => {
|
||||
onDelEdge({ moduleId, sourceHandle: output.key });
|
||||
});
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
outputs: value
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onDelConnect = useCallback((id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
}, []);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
const source = nodes.find((node) => node.id === connect.source)?.data;
|
||||
const sourceType = (() => {
|
||||
if (source?.flowType === FlowModuleTypeEnum.classifyQuestion) {
|
||||
return FlowValueTypeEnum.boolean;
|
||||
}
|
||||
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
|
||||
})();
|
||||
|
||||
const targetType = nodes
|
||||
.find((node) => node.id === connect.target)
|
||||
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
|
||||
|
||||
if (!sourceType || !targetType) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection is invalid')
|
||||
});
|
||||
}
|
||||
if (
|
||||
sourceType !== FlowValueTypeEnum.any &&
|
||||
targetType !== FlowValueTypeEnum.any &&
|
||||
sourceType !== targetType
|
||||
) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection type is different')
|
||||
});
|
||||
}
|
||||
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: 'buttonedge',
|
||||
animated: true,
|
||||
data: {
|
||||
onDelete: onDelConnect
|
||||
}
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
return updateAppDetail(app._id, {
|
||||
modules: flow2AppModules(),
|
||||
type: AppTypeEnum.advanced
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
errorToast: '保存配置异常',
|
||||
onSuccess() {
|
||||
ChatTestRef.current?.resetChatTest();
|
||||
}
|
||||
});
|
||||
|
||||
const initData = useCallback(
|
||||
(modules: AppModuleItemType[]) => {
|
||||
const edges = appModule2FlowEdge({
|
||||
modules,
|
||||
onDelete: onDelConnect
|
||||
});
|
||||
setEdges(edges);
|
||||
|
||||
setNodes(
|
||||
modules.map((item) =>
|
||||
appModule2FlowNode({
|
||||
item,
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
onFixView();
|
||||
},
|
||||
[
|
||||
onDelConnect,
|
||||
setEdges,
|
||||
setNodes,
|
||||
onFixView,
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(app.modules)));
|
||||
}, [app.modules]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* header */}
|
||||
<Flex
|
||||
py={3}
|
||||
px={[2, 5, 8]}
|
||||
borderBottom={theme.borders.base}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<MyTooltip label={'返回'} offset={[10, 10]}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={() => {
|
||||
onCloseSettings();
|
||||
onFixView();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
|
||||
<MyTooltip label={t('app.Import Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'importLight'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={onOpenImport}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('app.Export Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'export'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={() =>
|
||||
copyData(
|
||||
JSON.stringify(flow2AppModules(), null, 2),
|
||||
t('app.Export Config Successful')
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MyTooltip>
|
||||
|
||||
{testModules ? (
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<SmallCloseIcon fontSize={'25px'} />}
|
||||
variant={'base'}
|
||||
color={'myGray.600'}
|
||||
borderRadius={'lg'}
|
||||
aria-label={''}
|
||||
onClick={() => setTestModules(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<MyTooltip label={'测试对话'}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'chat'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
setTestModules(flow2AppModules());
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
|
||||
<MyTooltip label={'保存配置'}>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box
|
||||
minH={'400px'}
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
w={'38px'}
|
||||
h={'38px'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
className={styles.panel}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls position={'bottom-right'} style={{ display: 'flex' }} showInteractive={false} />
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList
|
||||
isOpen={isOpenTemplate}
|
||||
nodes={nodes}
|
||||
onAddNode={onAddNode}
|
||||
onClose={onCloseTemplate}
|
||||
/>
|
||||
<ChatTest
|
||||
ref={ChatTestRef}
|
||||
modules={testModules}
|
||||
app={app}
|
||||
onClose={() => setTestModules(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
{isOpenImport && (
|
||||
<ImportSettings
|
||||
onClose={onCloseImport}
|
||||
onSuccess={(data) => {
|
||||
setEdges([]);
|
||||
setNodes([]);
|
||||
setTimeout(() => {
|
||||
initData(data);
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Flow = (data: Props) => (
|
||||
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
{!!data.app._id && <AppEdit {...data} />}
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default React.memo(Flow);
|
||||
@@ -1,7 +0,0 @@
|
||||
.intro {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.intro {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
Image,
|
||||
MenuButton,
|
||||
Menu,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { getTrainingData } from '@/api/plugins/kb';
|
||||
import { getDatasetFiles, delDatasetFileById, updateDatasetFile } from '@/api/core/dataset/file';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { debounce } from 'lodash';
|
||||
import { formatFileSize } from '@/utils/tools';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import dayjs from 'dayjs';
|
||||
import { fileImgs } from '@/constants/common';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { FileStatusEnum, OtherFileId } from '@/constants/kb';
|
||||
import { useRouter } from 'next/router';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { KbFileItemType } from '@/types/plugin';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyMenu from '@/components/MyMenu';
|
||||
import { useEditTitle } from '@/hooks/useEditTitle';
|
||||
|
||||
const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
const lastSearch = useRef('');
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { Loading } = useLoading();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('kb.Confirm to delete the file')
|
||||
});
|
||||
|
||||
const {
|
||||
data: files,
|
||||
Pagination,
|
||||
total,
|
||||
getData,
|
||||
isLoading,
|
||||
pageNum,
|
||||
pageSize
|
||||
} = usePagination<KbFileItemType>({
|
||||
api: getDatasetFiles,
|
||||
pageSize: 20,
|
||||
params: {
|
||||
kbId,
|
||||
searchText
|
||||
},
|
||||
onChange() {
|
||||
if (BoxRef.current) {
|
||||
BoxRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// change search
|
||||
const debounceRefetch = useCallback(
|
||||
debounce(() => {
|
||||
getData(1);
|
||||
lastSearch.current = searchText;
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
// add file icon
|
||||
const formatFiles = useMemo(
|
||||
() =>
|
||||
files.map((file) => ({
|
||||
...file,
|
||||
icon: fileImgs.find((item) => new RegExp(item.suffix, 'gi').test(file.filename))?.src
|
||||
})),
|
||||
[files]
|
||||
);
|
||||
|
||||
const { mutate: onDeleteFile } = useRequest({
|
||||
mutationFn: (fileId: string) => {
|
||||
setLoading(true);
|
||||
return delDatasetFileById({
|
||||
fileId,
|
||||
kbId
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
const { mutate: onUpdateFilename } = useRequest({
|
||||
mutationFn: (data: { id: string; name: string }) => {
|
||||
setLoading(true);
|
||||
return updateDatasetFile(data);
|
||||
},
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('Rename')
|
||||
});
|
||||
|
||||
const statusMap = {
|
||||
[FileStatusEnum.embedding]: {
|
||||
color: 'myGray.500',
|
||||
text: t('file.Embedding')
|
||||
},
|
||||
[FileStatusEnum.ready]: {
|
||||
color: 'green.500',
|
||||
text: t('file.Ready')
|
||||
}
|
||||
};
|
||||
|
||||
// training data
|
||||
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch: refetchTrainingData } =
|
||||
useQuery(['getModelSplitDataList', kbId], () => getTrainingData({ kbId, init: false }), {
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
useQuery(
|
||||
['refetchTrainingData', kbId],
|
||||
() => Promise.all([refetchTrainingData(), getData(pageNum)]),
|
||||
{
|
||||
refetchInterval: 8000,
|
||||
enabled: qaListLen > 0 || vectorListLen > 0
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box ref={BoxRef} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex justifyContent={'space-between'} px={[2, 5]}>
|
||||
<Box>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mr={2}>
|
||||
{t('kb.Files', { total })}
|
||||
</Box>
|
||||
<Box as={'span'} fontSize={'sm'}>
|
||||
{(qaListLen > 0 || vectorListLen > 0) && (
|
||||
<>
|
||||
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||
请耐心等待... )
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Flex alignItems={'center'}>
|
||||
<MyInput
|
||||
leftIcon={
|
||||
<MyIcon name="searchLight" position={'absolute'} w={'14px'} color={'myGray.500'} />
|
||||
}
|
||||
w={['100%', '250px']}
|
||||
size={['sm', 'md']}
|
||||
placeholder={t('common.Search') || ''}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
debounceRefetch();
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
getData(1);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
if (e.key === 'Enter') {
|
||||
getData(1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<TableContainer mt={[0, 3]} position={'relative'} minH={'70vh'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('kb.Filename')}</Th>
|
||||
<Th>{t('kb.Chunk Length')}</Th>
|
||||
<Th>{t('kb.Upload Time')}</Th>
|
||||
<Th>{t('kb.File Size')}</Th>
|
||||
<Th>{t('common.Status')}</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{formatFiles.map((file) => (
|
||||
<Tr
|
||||
key={file.id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={'点击查看数据详情'}
|
||||
onClick={() =>
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
fileId: file.id,
|
||||
currentTab: 'dataCard'
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>
|
||||
<Flex alignItems={'center'}>
|
||||
<Image src={file.icon} w={'16px'} mr={2} alt={''} />
|
||||
<Box maxW={['300px', '400px']} className="textEllipsis">
|
||||
{t(file.filename)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td fontSize={'md'} fontWeight={'bold'}>
|
||||
{file.chunkLength}
|
||||
</Td>
|
||||
<Td>{dayjs(file.uploadTime).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td>{formatFileSize(file.size)}</Td>
|
||||
<Td>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
_before={{
|
||||
content: '""',
|
||||
w: '10px',
|
||||
h: '10px',
|
||||
mr: 2,
|
||||
borderRadius: 'lg',
|
||||
bg: statusMap[file.status].color
|
||||
}}
|
||||
>
|
||||
{statusMap[file.status].text}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td onClick={(e) => e.stopPropagation()}>
|
||||
<MyMenu
|
||||
width={100}
|
||||
Button={
|
||||
<MenuButton
|
||||
w={'22px'}
|
||||
h={'22px'}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
color: 'myBlue.600',
|
||||
'& .icon': {
|
||||
bg: 'myGray.100'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
className="icon"
|
||||
name={'more'}
|
||||
h={'16px'}
|
||||
w={'16px'}
|
||||
px={1}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
...(file.id !== OtherFileId
|
||||
? [
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'edit'} w={'14px'} mr={2} />
|
||||
{t('Rename')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
onOpenModal({
|
||||
defaultVal: file.filename,
|
||||
onSuccess: (newName) => {
|
||||
onUpdateFilename({
|
||||
id: file.id,
|
||||
name: newName
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
/>
|
||||
<Box>{t('common.Delete')}</Box>
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
openConfirm(() => {
|
||||
onDeleteFile(file.id);
|
||||
})()
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Loading loading={isLoading && files.length === 0} fixed={false} />
|
||||
{total > pageSize && (
|
||||
<Flex mt={2} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
</TableContainer>
|
||||
<ConfirmModal />
|
||||
<EditTitleModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FileCard);
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, type BoxProps, Flex, Textarea, useTheme } from '@chakra-ui/react';
|
||||
import MyRadio from '@/components/Radio/index';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import ManualImport from './Import/Manual';
|
||||
|
||||
const ChunkImport = dynamic(() => import('./Import/Chunk'), {
|
||||
ssr: true
|
||||
});
|
||||
const QAImport = dynamic(() => import('./Import/QA'), {
|
||||
ssr: true
|
||||
});
|
||||
const CsvImport = dynamic(() => import('./Import/Csv'), {
|
||||
ssr: true
|
||||
});
|
||||
|
||||
enum ImportTypeEnum {
|
||||
manual = 'manual',
|
||||
index = 'index',
|
||||
qa = 'qa',
|
||||
csv = 'csv'
|
||||
}
|
||||
|
||||
const ImportData = ({ kbId }: { kbId: string }) => {
|
||||
const theme = useTheme();
|
||||
const [importType, setImportType] = useState<`${ImportTypeEnum}`>(ImportTypeEnum.manual);
|
||||
const TitleStyle: BoxProps = {
|
||||
fontWeight: 'bold',
|
||||
fontSize: ['md', 'xl'],
|
||||
mb: [3, 5]
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} pt={[1, 5]}>
|
||||
<Box {...TitleStyle} px={[4, 8]}>
|
||||
数据导入方式
|
||||
</Box>
|
||||
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
|
||||
<MyRadio
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2, 350px)']}
|
||||
list={[
|
||||
{
|
||||
icon: 'manualImport',
|
||||
title: '手动输入',
|
||||
desc: '手动输入问答对,是最精准的数据',
|
||||
value: ImportTypeEnum.manual
|
||||
},
|
||||
{
|
||||
icon: 'indexImport',
|
||||
title: '直接分段',
|
||||
desc: '选择文本文件,直接将其按分段进行处理',
|
||||
value: ImportTypeEnum.index
|
||||
},
|
||||
{
|
||||
icon: 'qaImport',
|
||||
title: 'QA拆分',
|
||||
desc: '选择文本文件,让大模型自动生成问答对',
|
||||
value: ImportTypeEnum.qa
|
||||
},
|
||||
{
|
||||
icon: 'csvImport',
|
||||
title: 'CSV 导入',
|
||||
desc: '批量导入问答对,是最精准的数据',
|
||||
value: ImportTypeEnum.csv
|
||||
}
|
||||
]}
|
||||
value={importType}
|
||||
onChange={(e) => setImportType(e as `${ImportTypeEnum}`)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box flex={'1 0 0'} h={0}>
|
||||
{importType === ImportTypeEnum.manual && <ManualImport kbId={kbId} />}
|
||||
{importType === ImportTypeEnum.index && <ChunkImport kbId={kbId} />}
|
||||
{importType === ImportTypeEnum.qa && <QAImport kbId={kbId} />}
|
||||
{importType === ImportTypeEnum.csv && <CsvImport kbId={kbId} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportData;
|
||||
@@ -1,444 +0,0 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
useTheme,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Image
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postKbDataFromList } from '@/api/plugins/kb';
|
||||
import { splitText2Chunks } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import CloseIcon from '@/components/Icon/close';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@/components/Icon/delete';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.txt, .doc, .docx, .pdf, .md';
|
||||
|
||||
const ChunkImport = ({ kbId }: { kbId: string }) => {
|
||||
const { kbDetail } = useDatasetStore();
|
||||
|
||||
const vectorModel = kbDetail.vectorModel;
|
||||
const unitPrice = vectorModel?.price || 0.2;
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [chunkLen, setChunkLen] = useState(vectorModel?.defaultToken || 300);
|
||||
const [showRePreview, setShowRePreview] = useState(false);
|
||||
const [files, setFiles] = useState<FileItemType[]>([]);
|
||||
const [previewFile, setPreviewFile] = useState<FileItemType>();
|
||||
const [successChunks, setSuccessChunks] = useState(0);
|
||||
|
||||
const totalChunk = useMemo(
|
||||
() => files.reduce((sum, file) => sum + file.chunks.length, 0),
|
||||
[files]
|
||||
);
|
||||
const emptyFiles = useMemo(() => files.length === 0, [files]);
|
||||
|
||||
// price count
|
||||
const price = useMemo(() => {
|
||||
return formatPrice(files.reduce((sum, file) => sum + file.tokens, 0) * unitPrice);
|
||||
}, [files, unitPrice]);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: `该任务无法终止,需要一定时间生成索引,请确认导入。如果余额不足,未完成的任务会被暂停,充值后可继续进行。`
|
||||
});
|
||||
|
||||
const { mutate: onclickUpload, isLoading: uploading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const chunks = files.map((file) => file.chunks).flat();
|
||||
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// subsection import
|
||||
let success = 0;
|
||||
const step = 300;
|
||||
for (let i = 0; i < chunks.length; i += step) {
|
||||
const { insertLen } = await postKbDataFromList({
|
||||
kbId,
|
||||
data: chunks.slice(i, i + step),
|
||||
mode: TrainingModeEnum.index
|
||||
});
|
||||
|
||||
success += insertLen;
|
||||
setSuccessChunks(success);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: `去重后共导入 ${success} 条数据,请耐心等待训练.`,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
title: getErrText(err, '导入文件失败'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onRePreview = useCallback(async () => {
|
||||
try {
|
||||
setFiles((state) =>
|
||||
state.map((file) => {
|
||||
const splitRes = splitText2Chunks({
|
||||
text: file.text,
|
||||
maxLen: chunkLen
|
||||
});
|
||||
|
||||
return {
|
||||
...file,
|
||||
tokens: splitRes.tokens,
|
||||
chunks: splitRes.chunks.map((chunk) => ({
|
||||
a: '',
|
||||
source: file.filename,
|
||||
file_id: file.id,
|
||||
q: chunk
|
||||
}))
|
||||
};
|
||||
})
|
||||
);
|
||||
setPreviewFile(undefined);
|
||||
setShowRePreview(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, '文本分段异常')
|
||||
});
|
||||
}
|
||||
}, [chunkLen, toast]);
|
||||
|
||||
const filenameStyles = {
|
||||
className: 'textEllipsis',
|
||||
maxW: '400px'
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
h={'100%'}
|
||||
minW={['auto', '400px']}
|
||||
w={['100%', 0]}
|
||||
p={[4, 8]}
|
||||
>
|
||||
<FileSelect
|
||||
fileExtension={fileExtension}
|
||||
onPushFiles={(files) => {
|
||||
setFiles((state) => files.concat(state));
|
||||
}}
|
||||
chunkLen={chunkLen}
|
||||
py={emptyFiles ? '100px' : 5}
|
||||
/>
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} px={2} minH={['auto', '100px']} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
w={'100%'}
|
||||
_notLast={{ mb: 5 }}
|
||||
px={5}
|
||||
py={2}
|
||||
boxShadow={'1px 1px 5px rgba(0,0,0,0.15)'}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
position={'relative'}
|
||||
alignItems={'center'}
|
||||
_hover={{
|
||||
bg: 'myBlue.100',
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
onClick={() => setPreviewFile(item)}
|
||||
>
|
||||
<Image src={item.icon} w={'16px'} alt={''} />
|
||||
<Box ml={2} flex={'1 0 0'} pr={3} {...filenameStyles}>
|
||||
{item.filename}
|
||||
</Box>
|
||||
<MyIcon
|
||||
position={'absolute'}
|
||||
right={3}
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
display={['block', 'none']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFiles((state) => state.filter((file) => file.id !== item.id));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
{/* chunk size */}
|
||||
<Flex py={5} alignItems={'center'}>
|
||||
<Box>
|
||||
段落长度
|
||||
<MyTooltip
|
||||
label={
|
||||
'按结束标点符号进行分段。前后段落会有 30% 的内容重叠。\n中文文档建议不要超过800,英文不要超过1500'
|
||||
}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Box
|
||||
flex={1}
|
||||
css={{
|
||||
'& > span': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={`范围: 100~${kbDetail.vectorModel.maxToken}`}>
|
||||
<NumberInput
|
||||
ml={4}
|
||||
defaultValue={chunkLen}
|
||||
min={100}
|
||||
max={kbDetail.vectorModel.maxToken}
|
||||
step={10}
|
||||
onChange={(e) => {
|
||||
setChunkLen(+e);
|
||||
setShowRePreview(true);
|
||||
}}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
</Flex>
|
||||
{/* price */}
|
||||
<Flex py={5} alignItems={'center'}>
|
||||
<Box>
|
||||
预估价格
|
||||
<MyTooltip
|
||||
label={`索引生成计费为: ${formatPrice(unitPrice, 1000)}/1k tokens`}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Box ml={4}>{price}元</Box>
|
||||
</Flex>
|
||||
<Flex mt={3}>
|
||||
{showRePreview && (
|
||||
<Button variant={'base'} mr={4} onClick={onRePreview}>
|
||||
重新生成预览
|
||||
</Button>
|
||||
)}
|
||||
<Button isDisabled={uploading} onClick={openConfirm(onclickUpload)}>
|
||||
{uploading ? (
|
||||
<Box>{Math.round((successChunks / totalChunk) * 100)}%</Box>
|
||||
) : (
|
||||
'确认导入'
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{!emptyFiles && (
|
||||
<Box flex={'2 0 0'} w={['100%', 0]} h={'100%'}>
|
||||
{previewFile ? (
|
||||
<Box
|
||||
position={'relative'}
|
||||
display={['block', 'flex']}
|
||||
h={'100%'}
|
||||
flexDirection={'column'}
|
||||
pt={[4, 8]}
|
||||
bg={'myWhite.400'}
|
||||
>
|
||||
<Box px={[4, 8]} fontSize={['lg', 'xl']} fontWeight={'bold'} {...filenameStyles}>
|
||||
{previewFile.filename}
|
||||
</Box>
|
||||
<CloseIcon
|
||||
position={'absolute'}
|
||||
right={[4, 8]}
|
||||
top={4}
|
||||
onClick={() => setPreviewFile(undefined)}
|
||||
/>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
h={['auto', 0]}
|
||||
overflow={'overlay'}
|
||||
px={[4, 8]}
|
||||
my={4}
|
||||
contentEditable
|
||||
dangerouslySetInnerHTML={{ __html: previewFile.text }}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
onBlur={(e) => {
|
||||
// @ts-ignore
|
||||
const val = e.target.innerText;
|
||||
setShowRePreview(true);
|
||||
|
||||
setFiles((state) =>
|
||||
state.map((file) =>
|
||||
file.id === previewFile.id
|
||||
? {
|
||||
...file,
|
||||
text: val
|
||||
}
|
||||
: file
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box h={'100%'} pt={[4, 8]} overflow={'overlay'}>
|
||||
<Flex px={[4, 8]} alignItems={'center'}>
|
||||
<Box fontSize={['lg', 'xl']} fontWeight={'bold'}>
|
||||
分段预览({totalChunk}组)
|
||||
</Box>
|
||||
{totalChunk > 100 && (
|
||||
<Box ml={2} fontSize={'sm'} color={'myhGray.500'}>
|
||||
仅展示部分
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Box px={[4, 8]} overflow={'overlay'}>
|
||||
{files.map((file) =>
|
||||
file.chunks.slice(0, 50).map((chunk, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
py={4}
|
||||
bg={'myWhite.500'}
|
||||
my={2}
|
||||
borderRadius={'md'}
|
||||
fontSize={'sm'}
|
||||
_hover={{ ...hoverDeleteStyles }}
|
||||
>
|
||||
<Flex mb={1} px={4} userSelect={'none'}>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
px={3}
|
||||
py={'1px'}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
# {i + 1}
|
||||
</Box>
|
||||
<Box ml={2} fontSize={'sm'} color={'myhGray.500'} {...filenameStyles}>
|
||||
{file.filename}
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
setFiles((state) =>
|
||||
state.map((stateFile) =>
|
||||
stateFile.id === file.id
|
||||
? {
|
||||
...file,
|
||||
chunks: [
|
||||
...file.chunks.slice(0, i),
|
||||
...file.chunks.slice(i + 1)
|
||||
]
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
px={4}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
contentEditable
|
||||
dangerouslySetInnerHTML={{ __html: chunk.q }}
|
||||
onBlur={(e) => {
|
||||
// @ts-ignore
|
||||
const val = e.target.innerText;
|
||||
|
||||
/* delete file */
|
||||
if (val === '') {
|
||||
setFiles((state) =>
|
||||
state.map((stateFile) =>
|
||||
stateFile.id === file.id
|
||||
? {
|
||||
...file,
|
||||
chunks: [
|
||||
...file.chunks.slice(0, i),
|
||||
...file.chunks.slice(i + 1)
|
||||
]
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// update file
|
||||
setFiles((stateFiles) =>
|
||||
stateFiles.map((stateFile) =>
|
||||
file.id === stateFile.id
|
||||
? {
|
||||
...stateFile,
|
||||
chunks: stateFile.chunks.map((chunk, index) => ({
|
||||
...chunk,
|
||||
q: i === index ? val : chunk.q
|
||||
}))
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChunkImport;
|
||||
@@ -1,233 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Box, Flex, Button, useTheme, Image } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postKbDataFromList } from '@/api/plugins/kb';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@/components/Icon/delete';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.csv';
|
||||
|
||||
const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
const { kbDetail } = useDatasetStore();
|
||||
const maxToken = kbDetail.vectorModel?.maxToken || 2000;
|
||||
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [files, setFiles] = useState<FileItemType[]>([]);
|
||||
const [successChunks, setSuccessChunks] = useState(0);
|
||||
|
||||
const totalChunk = useMemo(
|
||||
() => files.reduce((sum, file) => sum + file.chunks.length, 0),
|
||||
[files]
|
||||
);
|
||||
const emptyFiles = useMemo(() => files.length === 0, [files]);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: `该任务无法终止,需要一定时间生成索引,请确认导入。如果余额不足,未完成的任务会被暂停,充值后可继续进行。`
|
||||
});
|
||||
|
||||
const { mutate: onclickUpload, isLoading: uploading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const chunks = files
|
||||
.map((file) => file.chunks)
|
||||
.flat()
|
||||
.filter((item) => item?.q);
|
||||
|
||||
const filterChunks = chunks.filter((item) => item.q.length < maxToken * 1.5);
|
||||
|
||||
if (filterChunks.length !== chunks.length) {
|
||||
toast({
|
||||
title: `${chunks.length - filterChunks.length}条数据超出长度,已被过滤`,
|
||||
status: 'info'
|
||||
});
|
||||
}
|
||||
|
||||
// subsection import
|
||||
let success = 0;
|
||||
const step = 300;
|
||||
for (let i = 0; i < filterChunks.length; i += step) {
|
||||
const { insertLen } = await postKbDataFromList({
|
||||
kbId,
|
||||
data: filterChunks.slice(i, i + step),
|
||||
mode: TrainingModeEnum.index
|
||||
});
|
||||
|
||||
success += insertLen;
|
||||
setSuccessChunks(success);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: `去重后共导入 ${success} 条数据,请耐心等待训练.`,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
title: getErrText(err, '导入文件失败'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const filenameStyles = {
|
||||
className: 'textEllipsis',
|
||||
maxW: '400px'
|
||||
};
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
h={'100%'}
|
||||
minW={['auto', '400px']}
|
||||
w={['100%', 0]}
|
||||
p={[4, 8]}
|
||||
>
|
||||
<FileSelect
|
||||
fileExtension={fileExtension}
|
||||
tipText={
|
||||
'file.If the imported file is garbled, please convert CSV to UTF-8 encoding format'
|
||||
}
|
||||
onPushFiles={(files) => setFiles((state) => files.concat(state))}
|
||||
showUrlFetch={false}
|
||||
showCreateFile={false}
|
||||
py={emptyFiles ? '100px' : 5}
|
||||
isCsv
|
||||
/>
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} minH={['auto', '100px']} px={2} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
w={'100%'}
|
||||
_notLast={{ mb: 5 }}
|
||||
px={5}
|
||||
py={2}
|
||||
boxShadow={'1px 1px 5px rgba(0,0,0,0.15)'}
|
||||
borderRadius={'md'}
|
||||
position={'relative'}
|
||||
alignItems={'center'}
|
||||
_hover={{ ...hoverDeleteStyles }}
|
||||
>
|
||||
<Image src={'/imgs/files/csv.svg'} w={'16px'} alt={''} />
|
||||
<Box ml={2} flex={'1 0 0'} pr={3} {...filenameStyles}>
|
||||
{item.filename}
|
||||
</Box>
|
||||
<MyIcon
|
||||
position={'absolute'}
|
||||
right={3}
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
display={['block', 'none']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFiles((state) => state.filter((file) => file.id !== item.id));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Flex mt={3}>
|
||||
<Button isDisabled={uploading} onClick={openConfirm(onclickUpload)}>
|
||||
{uploading ? (
|
||||
<Box>{Math.round((successChunks / totalChunk) * 100)}%</Box>
|
||||
) : (
|
||||
'确认导入'
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{!emptyFiles && (
|
||||
<Box flex={'2 0 0'} w={['100%', 0]} h={'100%'} pt={[4, 8]} overflow={'overlay'}>
|
||||
<Flex px={[4, 8]} alignItems={'center'}>
|
||||
<Box fontSize={['lg', 'xl']} fontWeight={'bold'}>
|
||||
分段预览({totalChunk}组)
|
||||
</Box>
|
||||
{totalChunk > 100 && (
|
||||
<Box ml={2} fontSize={'sm'} color={'myhGray.500'}>
|
||||
仅展示部分
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Box px={[4, 8]} overflow={'overlay'}>
|
||||
{files.map((file) =>
|
||||
file.chunks.slice(0, 100).map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
py={4}
|
||||
bg={'myWhite.500'}
|
||||
my={2}
|
||||
borderRadius={'md'}
|
||||
fontSize={'sm'}
|
||||
_hover={{ ...hoverDeleteStyles }}
|
||||
>
|
||||
<Flex mb={1} px={4} userSelect={'none'}>
|
||||
<Box px={3} py={'1px'} border={theme.borders.base} borderRadius={'md'}>
|
||||
# {i + 1}
|
||||
</Box>
|
||||
{item.source && <Box ml={1}>({item.source})</Box>}
|
||||
<Box flex={1} />
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
setFiles((state) =>
|
||||
state.map((stateFile) =>
|
||||
stateFile.id === file.id
|
||||
? {
|
||||
...file,
|
||||
chunks: [...file.chunks.slice(0, i), ...file.chunks.slice(i + 1)]
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box px={4} fontSize={'sm'} whiteSpace={'pre-wrap'} wordBreak={'break-all'}>
|
||||
{`${item.q}\n${item.a}`}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CsvImport;
|
||||
@@ -1,122 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Textarea, Button, Flex } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { postKbDataFromList } from '@/api/plugins/kb';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
|
||||
type ManualFormType = { q: string; a: string };
|
||||
|
||||
const ManualImport = ({ kbId }: { kbId: string }) => {
|
||||
const { kbDetail } = useDatasetStore();
|
||||
const maxToken = kbDetail.vectorModel?.maxToken || 2000;
|
||||
|
||||
const { register, handleSubmit, reset } = useForm({
|
||||
defaultValues: { q: '', a: '' }
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const [qLen, setQLen] = useState(0);
|
||||
|
||||
const { mutate: onImportData, isLoading } = useRequest({
|
||||
mutationFn: async (e: ManualFormType) => {
|
||||
if (e.a.length + e.q.length >= 3000) {
|
||||
toast({
|
||||
title: '总长度超长了',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = {
|
||||
a: e.a,
|
||||
q: e.q,
|
||||
source: '手动录入'
|
||||
};
|
||||
const { insertLen } = await postKbDataFromList({
|
||||
kbId,
|
||||
mode: TrainingModeEnum.index,
|
||||
data: [data]
|
||||
});
|
||||
|
||||
if (insertLen === 0) {
|
||||
toast({
|
||||
title: '已存在完全一致的数据',
|
||||
status: 'warning'
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: '导入数据成功,需要一段时间训练',
|
||||
status: 'success'
|
||||
});
|
||||
reset({
|
||||
a: '',
|
||||
q: ''
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, '出现了点意外~'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box p={[4, 8]} h={'100%'} overflow={'overlay'}>
|
||||
<Box display={'flex'} flexDirection={['column', 'row']}>
|
||||
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']} position={'relative'}>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<MyTooltip label={'被向量化的部分,通常是问题,也可以是一段陈述描述'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={`匹配的知识点。这部分内容会被搜索,请把控内容的质量。最多 ${maxToken} 字。`}
|
||||
maxLength={maxToken}
|
||||
h={['250px', '500px']}
|
||||
{...register(`q`, {
|
||||
required: true,
|
||||
onChange(e) {
|
||||
setQLen(e.target.value.length);
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Box position={'absolute'} color={'myGray.500'} right={5} bottom={3} zIndex={99}>
|
||||
{qLen}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} h={['50%', '100%']}>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'预期答案'}</Box>
|
||||
<MyTooltip
|
||||
label={'匹配的知识点被命中后,这部分内容会随匹配知识点一起注入模型,引导模型回答'}
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'预期答案。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,通常是问题的答案。总和最多 3000 字。'
|
||||
}
|
||||
h={['250px', '500px']}
|
||||
maxLength={3000}
|
||||
{...register('a')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button mt={5} isLoading={isLoading} onClick={handleSubmit((data) => onImportData(data))}>
|
||||
确认导入
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ManualImport);
|
||||
@@ -1,408 +0,0 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, Button, useTheme, Image, Input } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postKbDataFromList } from '@/api/plugins/kb';
|
||||
import { splitText2Chunks } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { qaModel } from '@/store/static';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import CloseIcon from '@/components/Icon/close';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@/components/Icon/delete';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useRouter } from 'next/router';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.txt, .doc, .docx, .pdf, .md';
|
||||
|
||||
const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
const unitPrice = qaModel.price || 3;
|
||||
const chunkLen = qaModel.maxToken * 0.45;
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [files, setFiles] = useState<FileItemType[]>([]);
|
||||
const [showRePreview, setShowRePreview] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<FileItemType>();
|
||||
const [successChunks, setSuccessChunks] = useState(0);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
|
||||
const totalChunk = useMemo(
|
||||
() => files.reduce((sum, file) => sum + file.chunks.length, 0),
|
||||
[files]
|
||||
);
|
||||
const emptyFiles = useMemo(() => files.length === 0, [files]);
|
||||
|
||||
// price count
|
||||
const price = useMemo(() => {
|
||||
const filesToken = files.reduce((sum, file) => sum + file.tokens, 0);
|
||||
const promptTokens = files.reduce((sum, file) => sum + file.chunks.length, 0) * 139;
|
||||
const totalToken = (filesToken + promptTokens) * 2;
|
||||
|
||||
return formatPrice(totalToken * unitPrice);
|
||||
}, [files, unitPrice]);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: `该任务无法终止!导入后会自动调用大模型生成问答对,会有一些细节丢失,请确认!如果余额不足,未完成的任务会被暂停。`
|
||||
});
|
||||
|
||||
const { mutate: onclickUpload, isLoading: uploading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const chunks = files.map((file) => file.chunks).flat();
|
||||
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// subsection import
|
||||
let success = 0;
|
||||
const step = 200;
|
||||
for (let i = 0; i < chunks.length; i += step) {
|
||||
const { insertLen } = await postKbDataFromList({
|
||||
kbId,
|
||||
data: chunks.slice(i, i + step),
|
||||
mode: TrainingModeEnum.qa,
|
||||
prompt: prompt || '下面是一段长文本'
|
||||
});
|
||||
|
||||
success += insertLen;
|
||||
setSuccessChunks(success);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: `共导入 ${success} 条数据,请耐心等待训练.`,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
});
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
title: getErrText(err, '导入文件失败'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onRePreview = useCallback(async () => {
|
||||
try {
|
||||
setFiles((state) =>
|
||||
state.map((file) => {
|
||||
const splitRes = splitText2Chunks({
|
||||
text: file.text,
|
||||
maxLen: chunkLen
|
||||
});
|
||||
return {
|
||||
...file,
|
||||
tokens: splitRes.tokens,
|
||||
chunks: splitRes.chunks.map((chunk) => ({
|
||||
a: '',
|
||||
source: file.filename,
|
||||
file_id: file.id,
|
||||
q: chunk
|
||||
}))
|
||||
};
|
||||
})
|
||||
);
|
||||
setPreviewFile(undefined);
|
||||
setShowRePreview(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, '文本分段异常')
|
||||
});
|
||||
}
|
||||
}, [chunkLen, toast]);
|
||||
|
||||
const filenameStyles = {
|
||||
className: 'textEllipsis',
|
||||
maxW: '400px'
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
h={'100%'}
|
||||
minW={['auto', '400px']}
|
||||
w={['100%', 0]}
|
||||
p={[4, 8]}
|
||||
>
|
||||
<FileSelect
|
||||
fileExtension={fileExtension}
|
||||
onPushFiles={(files) => {
|
||||
setFiles((state) => files.concat(state));
|
||||
}}
|
||||
chunkLen={chunkLen}
|
||||
py={emptyFiles ? '100px' : 5}
|
||||
/>
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} px={2} minH={['auto', '100px']} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
w={'100%'}
|
||||
_notLast={{ mb: 5 }}
|
||||
px={5}
|
||||
py={2}
|
||||
boxShadow={'1px 1px 5px rgba(0,0,0,0.15)'}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
position={'relative'}
|
||||
alignItems={'center'}
|
||||
_hover={{
|
||||
bg: 'myBlue.100',
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
onClick={() => setPreviewFile(item)}
|
||||
>
|
||||
<Image src={item.icon} w={'16px'} alt={''} />
|
||||
<Box ml={2} flex={'1 0 0'} pr={3} {...filenameStyles}>
|
||||
{item.filename}
|
||||
</Box>
|
||||
<MyIcon
|
||||
position={'absolute'}
|
||||
right={3}
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
display={['block', 'none']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFiles((state) => state.filter((file) => file.id !== item.id));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
{/* prompt */}
|
||||
<Box py={5}>
|
||||
<Box mb={2}>
|
||||
QA 拆分引导词{' '}
|
||||
<MyTooltip
|
||||
label={`可输入关于文件内容的范围介绍,例如:\n1. Laf 的介绍\n2. xxx的简历\n最终会补全为: 关于{输入的内容}`}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Flex alignItems={'center'} fontSize={'sm'}>
|
||||
<Box mr={2}>关于</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
placeholder={'Laf 云函数的介绍'}
|
||||
bg={'myWhite.500'}
|
||||
defaultValue={prompt}
|
||||
onBlur={(e) => (e.target.value ? setPrompt(`关于"${e.target.value}"`) : '')}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* price */}
|
||||
<Flex py={5} alignItems={'center'}>
|
||||
<Box>
|
||||
预估价格
|
||||
<MyTooltip
|
||||
label={`索引生成计费为: ${formatPrice(unitPrice, 1000)}/1k tokens`}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Box ml={4}>{price}元</Box>
|
||||
</Flex>
|
||||
<Flex mt={3}>
|
||||
{showRePreview && (
|
||||
<Button variant={'base'} mr={4} onClick={onRePreview}>
|
||||
重新生成预览
|
||||
</Button>
|
||||
)}
|
||||
<Button isDisabled={uploading} onClick={openConfirm(onclickUpload)}>
|
||||
{uploading ? (
|
||||
<Box>{Math.round((successChunks / totalChunk) * 100)}%</Box>
|
||||
) : (
|
||||
'确认导入'
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{!emptyFiles && (
|
||||
<Box flex={'2 0 0'} w={['100%', 0]} h={'100%'}>
|
||||
{previewFile ? (
|
||||
<Box
|
||||
position={'relative'}
|
||||
display={['block', 'flex']}
|
||||
h={'100%'}
|
||||
flexDirection={'column'}
|
||||
pt={[4, 8]}
|
||||
bg={'myWhite.400'}
|
||||
>
|
||||
<Box px={[4, 8]} fontSize={['lg', 'xl']} fontWeight={'bold'} {...filenameStyles}>
|
||||
{previewFile.filename}
|
||||
</Box>
|
||||
<CloseIcon
|
||||
position={'absolute'}
|
||||
right={[4, 8]}
|
||||
top={4}
|
||||
onClick={() => setPreviewFile(undefined)}
|
||||
/>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
h={['auto', 0]}
|
||||
overflow={'overlay'}
|
||||
px={[4, 8]}
|
||||
my={4}
|
||||
contentEditable
|
||||
dangerouslySetInnerHTML={{ __html: previewFile.text }}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
onBlur={(e) => {
|
||||
// @ts-ignore
|
||||
const val = e.target.innerText;
|
||||
setShowRePreview(true);
|
||||
setFiles((state) =>
|
||||
state.map((file) =>
|
||||
file.id === previewFile.id
|
||||
? {
|
||||
...file,
|
||||
text: val
|
||||
}
|
||||
: file
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box h={'100%'} pt={[4, 8]} overflow={'overlay'}>
|
||||
<Flex px={[4, 8]} alignItems={'center'}>
|
||||
<Box fontSize={['lg', 'xl']} fontWeight={'bold'}>
|
||||
分段预览({totalChunk}组)
|
||||
</Box>
|
||||
{totalChunk > 100 && (
|
||||
<Box ml={2} fontSize={'sm'} color={'myhGray.500'}>
|
||||
仅展示部分
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Box px={[4, 8]} overflow={'overlay'}>
|
||||
{files.map((file) =>
|
||||
file.chunks.slice(0, 30).map((chunk, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
py={4}
|
||||
bg={'myWhite.500'}
|
||||
my={2}
|
||||
borderRadius={'md'}
|
||||
fontSize={'sm'}
|
||||
_hover={{ ...hoverDeleteStyles }}
|
||||
>
|
||||
<Flex mb={1} px={4} userSelect={'none'}>
|
||||
<Box px={3} py={'1px'} border={theme.borders.base} borderRadius={'md'}>
|
||||
# {i + 1}
|
||||
</Box>
|
||||
<Box ml={2} fontSize={'sm'} color={'myhGray.500'} {...filenameStyles}>
|
||||
{file.filename}
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
setFiles((state) =>
|
||||
state.map((stateFile) =>
|
||||
stateFile.id === file.id
|
||||
? {
|
||||
...file,
|
||||
chunks: [
|
||||
...file.chunks.slice(0, i),
|
||||
...file.chunks.slice(i + 1)
|
||||
]
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
px={4}
|
||||
fontSize={'sm'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
contentEditable
|
||||
dangerouslySetInnerHTML={{ __html: chunk.q }}
|
||||
onBlur={(e) => {
|
||||
// @ts-ignore
|
||||
const val = e.target.innerText;
|
||||
|
||||
/* delete file */
|
||||
if (val === '') {
|
||||
setFiles((state) =>
|
||||
state.map((stateFile) =>
|
||||
stateFile.id === file.id
|
||||
? {
|
||||
...file,
|
||||
chunks: [
|
||||
...file.chunks.slice(0, i),
|
||||
...file.chunks.slice(i + 1)
|
||||
]
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// update file
|
||||
setFiles((stateFiles) =>
|
||||
stateFiles.map((stateFile) =>
|
||||
file.id === stateFile.id
|
||||
? {
|
||||
...stateFile,
|
||||
chunks: stateFile.chunks.map((chunk, index) => ({
|
||||
...chunk,
|
||||
q: i === index ? val : chunk.q
|
||||
}))
|
||||
}
|
||||
: stateFile
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QAImport;
|
||||
@@ -1,289 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Flex, Button, Textarea, IconButton, BoxProps } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { insertData2Kb, putKbDataById, delOneKbDataByDataId } from '@/api/plugins/kb';
|
||||
import { getFileViewUrl } from '@/api/support/file';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { DatasetItemType } from '@/types/plugin';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
|
||||
export type FormData = { dataId?: string } & DatasetItemType;
|
||||
|
||||
const InputDataModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
onDelete,
|
||||
kbId,
|
||||
defaultValues = {
|
||||
a: '',
|
||||
q: ''
|
||||
}
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: (data: FormData) => void;
|
||||
onDelete?: () => void;
|
||||
kbId: string;
|
||||
defaultValues?: FormData;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { kbDetail, getKbDetail } = useDatasetStore();
|
||||
|
||||
const { getValues, register, handleSubmit, reset } = useForm<FormData>({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const maxToken = kbDetail.vectorModel?.maxToken || 2000;
|
||||
|
||||
/**
|
||||
* 确认导入新数据
|
||||
*/
|
||||
const sureImportData = useCallback(
|
||||
async (e: FormData) => {
|
||||
if (e.q.length >= maxToken) {
|
||||
toast({
|
||||
title: '总长度超长了',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = {
|
||||
dataId: '',
|
||||
a: e.a,
|
||||
q: e.q,
|
||||
source: '手动录入'
|
||||
};
|
||||
data.dataId = await insertData2Kb({
|
||||
kbId,
|
||||
data
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '导入数据成功,需要一段时间训练',
|
||||
status: 'success'
|
||||
});
|
||||
reset({
|
||||
a: '',
|
||||
q: ''
|
||||
});
|
||||
|
||||
onSuccess(data);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, '出现了点意外~'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[kbId, maxToken, onSuccess, reset, toast]
|
||||
);
|
||||
|
||||
const updateData = useCallback(
|
||||
async (e: FormData) => {
|
||||
if (!e.dataId) return;
|
||||
|
||||
if (e.a !== defaultValues.a || e.q !== defaultValues.q) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = {
|
||||
dataId: e.dataId,
|
||||
kbId,
|
||||
a: e.a,
|
||||
q: e.q === defaultValues.q ? '' : e.q
|
||||
};
|
||||
await putKbDataById(data);
|
||||
onSuccess(data);
|
||||
} catch (err) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(err, '更新数据失败')
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '修改数据成功',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
[defaultValues.a, defaultValues.q, kbId, onClose, onSuccess, toast]
|
||||
);
|
||||
|
||||
useQuery(['getKbDetail'], () => {
|
||||
if (kbDetail._id === kbId) return null;
|
||||
return getKbDetail(kbId);
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
isCentered
|
||||
title={defaultValues.dataId ? '变更数据' : '手动导入数据'}
|
||||
w={'90vw'}
|
||||
maxW={'90vw'}
|
||||
h={'90vh'}
|
||||
>
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Box
|
||||
display={'flex'}
|
||||
flexDirection={['column', 'row']}
|
||||
flex={'1 0 0'}
|
||||
h={['100%', 0]}
|
||||
overflow={'overlay'}
|
||||
px={6}
|
||||
pb={2}
|
||||
>
|
||||
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<MyTooltip label={'被向量化的部分,通常是问题,也可以是一段陈述描述'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={`匹配的知识点。这部分内容会被搜索,请把控内容的质量,最多 ${maxToken} 字。`}
|
||||
maxLength={maxToken}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
{...register(`q`, {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} h={['50%', '100%']}>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'预期答案'}</Box>
|
||||
<MyTooltip
|
||||
label={'匹配的知识点被命中后,这部分内容会随匹配知识点一起注入模型,引导模型回答'}
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'预期答案。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,通常是问题的答案。总和最多 3000 字。'
|
||||
}
|
||||
maxLength={3000}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
{...register('a')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Flex px={6} pt={['34px', 2]} pb={4} alignItems={'center'} position={'relative'}>
|
||||
<RawFileText
|
||||
fileId={getValues('file_id')}
|
||||
filename={getValues('source')}
|
||||
position={'absolute'}
|
||||
left={'50%'}
|
||||
top={['16px', '50%']}
|
||||
transform={'translate(-50%,-50%)'}
|
||||
/>
|
||||
|
||||
<Box flex={1}>
|
||||
{defaultValues.dataId && onDelete && (
|
||||
<IconButton
|
||||
variant={'outline'}
|
||||
icon={<MyIcon name={'delete'} w={'16px'} h={'16px'} />}
|
||||
aria-label={''}
|
||||
isLoading={loading}
|
||||
size={'sm'}
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
borderColor: 'red.600'
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (!onDelete || !defaultValues.dataId) return;
|
||||
try {
|
||||
await delOneKbDataByDataId(defaultValues.dataId);
|
||||
onDelete();
|
||||
onClose();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: '记录已删除'
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error)
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant={'base'} mr={3} isLoading={loading} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={loading}
|
||||
onClick={handleSubmit(defaultValues.dataId ? updateData : sureImportData)}
|
||||
>
|
||||
{defaultValues.dataId ? '确认变更' : '确认导入'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputDataModal;
|
||||
|
||||
interface RawFileTextProps extends BoxProps {
|
||||
filename?: string;
|
||||
fileId?: string;
|
||||
}
|
||||
export function RawFileText({ fileId, filename = '', ...props }: RawFileTextProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
return (
|
||||
<MyTooltip label={fileId ? t('file.Click to view file') || '' : ''} shouldWrapChildren={false}>
|
||||
<Box
|
||||
color={'myGray.600'}
|
||||
display={'inline-block'}
|
||||
whiteSpace={'nowrap'}
|
||||
{...(!!fileId
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
onClick: async () => {
|
||||
try {
|
||||
const url = await getFileViewUrl(fileId);
|
||||
const asPath = `${location.origin}${url}`;
|
||||
window.open(asPath, '_blank');
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: getErrText(error, '获取文件地址失败'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})}
|
||||
{...props}
|
||||
>
|
||||
{filename}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.loginPage {
|
||||
background: url('/icon/login-bg.svg') no-repeat;
|
||||
background-size: cover;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import type { IpLimitSchemaType } from '@/types/common/ipLimit';
|
||||
|
||||
const IpLimitSchema = new Schema({
|
||||
eventId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
ip: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
account: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
lastMinute: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
});
|
||||
|
||||
export const IpLimit: Model<IpLimitSchemaType> =
|
||||
models['ip_limit'] || model('ip_limit', IpLimitSchema);
|
||||
@@ -1,228 +0,0 @@
|
||||
import { TrainingData } from '@/service/mongo';
|
||||
import { pushQABill } from '@/service/events/pushBill';
|
||||
import { pushDataToKb } from '@/pages/api/openapi/kb/pushData';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { sendInform } from '@/pages/api/user/inform/send';
|
||||
import { authBalanceByUid } from '../utils/auth';
|
||||
import { axiosConfig, getAIChatApi } from '../lib/openai';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { gptMessage2ChatType } from '@/utils/adapt';
|
||||
import { addLog } from '../utils/tools';
|
||||
import { splitText2Chunks } from '@/utils/file';
|
||||
import { countMessagesTokens } from '@/utils/common/tiktoken';
|
||||
|
||||
const reduceQueue = () => {
|
||||
global.qaQueueLen = global.qaQueueLen > 0 ? global.qaQueueLen - 1 : 0;
|
||||
};
|
||||
|
||||
export async function generateQA(): Promise<any> {
|
||||
if (global.qaQueueLen >= global.systemEnv.qaMaxProcess) return;
|
||||
global.qaQueueLen++;
|
||||
|
||||
let trainingId = '';
|
||||
let userId = '';
|
||||
|
||||
try {
|
||||
const data = await TrainingData.findOneAndUpdate(
|
||||
{
|
||||
mode: TrainingModeEnum.qa,
|
||||
lockTime: { $lte: new Date(Date.now() - 4 * 60 * 1000) }
|
||||
},
|
||||
{
|
||||
lockTime: new Date()
|
||||
}
|
||||
).select({
|
||||
_id: 1,
|
||||
userId: 1,
|
||||
kbId: 1,
|
||||
prompt: 1,
|
||||
q: 1,
|
||||
source: 1,
|
||||
file_id: 1
|
||||
});
|
||||
|
||||
// task preemption
|
||||
if (!data) {
|
||||
reduceQueue();
|
||||
global.qaQueueLen <= 0 && console.log(`【QA】任务完成`);
|
||||
return;
|
||||
}
|
||||
|
||||
trainingId = data._id;
|
||||
userId = String(data.userId);
|
||||
const kbId = String(data.kbId);
|
||||
|
||||
await authBalanceByUid(userId);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const chatAPI = getAIChatApi();
|
||||
|
||||
// 请求 chatgpt 获取回答
|
||||
const response = await Promise.all(
|
||||
[data.q].map((text) => {
|
||||
const modelTokenLimit = global.qaModel.maxToken || 16000;
|
||||
const messages: ChatCompletionRequestMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `我会给你发送一段长文本,${
|
||||
data.prompt ? `是${data.prompt},` : ''
|
||||
}请学习它,并用 markdown 格式给出 25 个问题和答案,问题可以多样化、自由扩展;答案要详细、解读到位,答案包含普通文本、链接、代码、表格、公示、媒体链接等。按下面 QA 问答格式返回:
|
||||
Q1:
|
||||
A1:
|
||||
Q2:
|
||||
A2:
|
||||
……`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: text
|
||||
}
|
||||
];
|
||||
|
||||
const promptsToken = countMessagesTokens({
|
||||
messages: gptMessage2ChatType(messages)
|
||||
});
|
||||
const maxToken = modelTokenLimit - promptsToken;
|
||||
|
||||
return chatAPI
|
||||
.createChatCompletion(
|
||||
{
|
||||
model: global.qaModel.model,
|
||||
temperature: 0.8,
|
||||
messages,
|
||||
stream: false,
|
||||
max_tokens: maxToken
|
||||
},
|
||||
{
|
||||
timeout: 480000,
|
||||
...axiosConfig()
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
const answer = res.data.choices?.[0].message?.content;
|
||||
const totalTokens = res.data.usage?.total_tokens || 0;
|
||||
|
||||
const result = formatSplitText(answer || ''); // 格式化后的QA对
|
||||
console.log(`split result length: `, result.length);
|
||||
// 计费
|
||||
if (result.length > 0) {
|
||||
pushQABill({
|
||||
userId: data.userId,
|
||||
totalTokens,
|
||||
appName: 'QA 拆分'
|
||||
});
|
||||
} else {
|
||||
addLog.info(`QA result 0:`, { answer });
|
||||
}
|
||||
|
||||
return {
|
||||
rawContent: answer,
|
||||
result
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('QA拆分错误');
|
||||
console.log(err.response?.status, err.response?.statusText, err.response?.data);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const responseList = response.map((item) => item.result).flat();
|
||||
|
||||
// 创建 向量生成 队列
|
||||
await pushDataToKb({
|
||||
kbId,
|
||||
data: responseList.map((item) => ({
|
||||
...item,
|
||||
source: data.source,
|
||||
file_id: data.file_id
|
||||
})),
|
||||
userId,
|
||||
mode: TrainingModeEnum.index
|
||||
});
|
||||
|
||||
// delete data from training
|
||||
await TrainingData.findByIdAndDelete(data._id);
|
||||
|
||||
console.log('生成QA成功,time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
reduceQueue();
|
||||
generateQA();
|
||||
} catch (err: any) {
|
||||
reduceQueue();
|
||||
// log
|
||||
if (err?.response) {
|
||||
console.log('openai error: 生成QA错误');
|
||||
console.log(err.response?.status, err.response?.statusText, err.response?.data);
|
||||
} else {
|
||||
addLog.error('生成 QA 错误', err);
|
||||
}
|
||||
|
||||
// message error or openai account error
|
||||
if (err?.message === 'invalid message format') {
|
||||
await TrainingData.findByIdAndRemove(trainingId);
|
||||
}
|
||||
|
||||
// 账号余额不足,删除任务
|
||||
if (userId && err === ERROR_ENUM.insufficientQuota) {
|
||||
sendInform({
|
||||
type: 'system',
|
||||
title: 'QA 任务中止',
|
||||
content:
|
||||
'由于账号余额不足,索引生成任务中止,重新充值后将会继续。暂停的任务将在 7 天后被删除。',
|
||||
userId
|
||||
});
|
||||
console.log('余额不足,暂停向量生成任务');
|
||||
await TrainingData.updateMany(
|
||||
{
|
||||
userId
|
||||
},
|
||||
{
|
||||
lockTime: new Date('2999/5/5')
|
||||
}
|
||||
);
|
||||
return generateQA();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
generateQA();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文本是否按格式返回
|
||||
*/
|
||||
function formatSplitText(text: string) {
|
||||
const regex = /Q\d+:(\s*)(.*)(\s*)A\d+:(\s*)([\s\S]*?)(?=Q|$)/g; // 匹配Q和A的正则表达式
|
||||
const matches = text.matchAll(regex); // 获取所有匹配到的结果
|
||||
|
||||
const result = []; // 存储最终的结果
|
||||
for (const match of matches) {
|
||||
const q = match[2];
|
||||
const a = match[5];
|
||||
if (q && a) {
|
||||
// 如果Q和A都存在,就将其添加到结果中
|
||||
result.push({
|
||||
q,
|
||||
a: a.trim().replace(/\n\s*/g, '\n')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// empty result. direct split chunk
|
||||
if (result.length === 0) {
|
||||
const splitRes = splitText2Chunks({ text: text, maxLen: 500 });
|
||||
splitRes.chunks.forEach((item) => {
|
||||
result.push({
|
||||
q: item,
|
||||
a: ''
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { connectToDatabase, Bill, User, OutLink } from '../mongo';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { getModel } from '../utils/data';
|
||||
import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { addLog } from '../utils/tools';
|
||||
|
||||
export const pushTaskBill = async ({
|
||||
appName,
|
||||
appId,
|
||||
userId,
|
||||
source,
|
||||
shareId,
|
||||
response
|
||||
}: {
|
||||
appName: string;
|
||||
appId: string;
|
||||
userId: string;
|
||||
source: `${BillSourceEnum}`;
|
||||
shareId?: string;
|
||||
response: ChatHistoryItemResType[];
|
||||
}) => {
|
||||
const total = response.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
await Promise.allSettled([
|
||||
Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
appId,
|
||||
total,
|
||||
source,
|
||||
list: response.map((item) => ({
|
||||
moduleName: item.moduleName,
|
||||
amount: item.price || 0,
|
||||
model: item.model,
|
||||
tokenLen: item.tokens
|
||||
}))
|
||||
}),
|
||||
User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
}),
|
||||
...(shareId
|
||||
? [
|
||||
updateShareChatBill({
|
||||
shareId,
|
||||
total
|
||||
})
|
||||
]
|
||||
: [])
|
||||
]);
|
||||
|
||||
addLog.info(`finish completions`, {
|
||||
source,
|
||||
userId,
|
||||
price: formatPrice(total)
|
||||
});
|
||||
};
|
||||
|
||||
export const updateShareChatBill = async ({
|
||||
shareId,
|
||||
total
|
||||
}: {
|
||||
shareId: string;
|
||||
total: number;
|
||||
}) => {
|
||||
try {
|
||||
await OutLink.findOneAndUpdate(
|
||||
{ shareId },
|
||||
{
|
||||
$inc: { total },
|
||||
lastTime: new Date()
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
addLog.error('update shareChat error', err);
|
||||
}
|
||||
};
|
||||
|
||||
export const pushQABill = async ({
|
||||
userId,
|
||||
totalTokens,
|
||||
appName
|
||||
}: {
|
||||
userId: string;
|
||||
totalTokens: number;
|
||||
appName: string;
|
||||
}) => {
|
||||
addLog.info('splitData generate success', { totalTokens });
|
||||
|
||||
let billId;
|
||||
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型单价格, 都是用 gpt35 拆分
|
||||
const unitPrice = global.qaModel.price || 3;
|
||||
// 计算价格
|
||||
const total = unitPrice * totalTokens;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
tokenLen: totalTokens,
|
||||
total
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (err) {
|
||||
addLog.error('Create completions bill error', err);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
};
|
||||
|
||||
export const pushGenerateVectorBill = async ({
|
||||
userId,
|
||||
tokenLen,
|
||||
model
|
||||
}: {
|
||||
userId: string;
|
||||
tokenLen: number;
|
||||
model: string;
|
||||
}) => {
|
||||
let billId;
|
||||
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
try {
|
||||
// 计算价格. 至少为1
|
||||
const vectorModel =
|
||||
global.vectorModels.find((item) => item.model === model) || global.vectorModels[0];
|
||||
const unitPrice = vectorModel.price || 0.2;
|
||||
let total = unitPrice * tokenLen;
|
||||
total = total > 1 ? total : 1;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
model: vectorModel.model,
|
||||
appName: '索引生成',
|
||||
total,
|
||||
list: [
|
||||
{
|
||||
moduleName: '索引生成',
|
||||
amount: total,
|
||||
model: vectorModel.model,
|
||||
tokenLen
|
||||
}
|
||||
]
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (err) {
|
||||
addLog.error('Create generateVector bill error', err);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const countModelPrice = ({ model, tokens }: { model: string; tokens: number }) => {
|
||||
const modelData = getModel(model);
|
||||
if (!modelData) return 0;
|
||||
return modelData.price * tokens;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
|
||||
export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
export const baseUrl = process.env.ONEAPI_URL || openaiBaseUrl;
|
||||
|
||||
export const systemAIChatKey = process.env.CHAT_API_KEY || '';
|
||||
|
||||
export const getAIChatApi = (props?: UserModelSchema['openaiAccount']) => {
|
||||
return new OpenAIApi(
|
||||
new Configuration({
|
||||
basePath: props?.baseUrl || baseUrl,
|
||||
apiKey: props?.key || systemAIChatKey
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/* openai axios config */
|
||||
export const axiosConfig = (props?: UserModelSchema['openaiAccount']) => {
|
||||
return {
|
||||
baseURL: props?.baseUrl || baseUrl, // 此处仅对非 npm 模块有效
|
||||
httpsAgent: global.httpsAgent,
|
||||
headers: {
|
||||
Authorization: `Bearer ${props?.key || systemAIChatKey}`,
|
||||
auth: process.env.OPENAI_BASE_URL_AUTH || ''
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Schema, model, models, Model as MongoModel } from 'mongoose';
|
||||
import { CollectionSchema as CollectionType } from '@/types/mongoSchema';
|
||||
|
||||
const CollectionSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'model',
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
export const Collection: MongoModel<CollectionType> =
|
||||
models['collection'] || model('collection', CollectionSchema);
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { kbSchema as SchemaType } from '@/types/mongoSchema';
|
||||
import { KbTypeMap } from '@/constants/kb';
|
||||
|
||||
const kbSchema = new Schema({
|
||||
parentId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'kb',
|
||||
default: null
|
||||
},
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
updateTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: '/icon/logo.svg'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
vectorModel: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'text-embedding-ada-002'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: Object.keys(KbTypeMap),
|
||||
required: true,
|
||||
default: 'dataset'
|
||||
},
|
||||
tags: {
|
||||
type: [String],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
export const KB: Model<SchemaType> = models['kb'] || model('kb', kbSchema);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { OpenApiSchema } from '@/types/mongoSchema';
|
||||
|
||||
const OpenApiSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
lastUsedTime: {
|
||||
type: Date
|
||||
}
|
||||
});
|
||||
|
||||
export const OpenApi: Model<OpenApiSchema> = models['openapi'] || model('openapi', OpenApiSchema);
|
||||
@@ -1,68 +0,0 @@
|
||||
/* 模型的知识库 */
|
||||
import { Schema, model, models, Model as MongoModel } from 'mongoose';
|
||||
import { TrainingDataSchema as TrainingDateType } from '@/types/mongoSchema';
|
||||
import { TrainingTypeMap } from '@/constants/plugin';
|
||||
|
||||
// pgList and vectorList, Only one of them will work
|
||||
const TrainingDataSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
kbId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'kb',
|
||||
required: true
|
||||
},
|
||||
expireAt: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
lockTime: {
|
||||
type: Date,
|
||||
default: () => new Date('2000/1/1')
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
enum: Object.keys(TrainingTypeMap),
|
||||
required: true
|
||||
},
|
||||
vectorModel: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'text-embedding-ada-002'
|
||||
},
|
||||
prompt: {
|
||||
// qa split prompt
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
q: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
a: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
file_id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
TrainingDataSchema.index({ lockTime: 1 });
|
||||
TrainingDataSchema.index({ userId: 1 });
|
||||
TrainingDataSchema.index({ expireAt: 1 }, { expireAfterSeconds: 7 * 24 * 60 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
export const TrainingData: MongoModel<TrainingDateType> =
|
||||
models['trainingData'] || model('trainingData', TrainingDataSchema);
|
||||
@@ -1,106 +0,0 @@
|
||||
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
|
||||
import { ChatContextFilter } from '@/service/common/tiktoken';
|
||||
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/lib/openai';
|
||||
import type { ClassifyQuestionAgentItemType } from '@/types/app';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { getModel } from '@/service/utils/data';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { SpecialInputKeyEnum } from '@/constants/flow';
|
||||
|
||||
export type CQProps = {
|
||||
systemPrompt?: string;
|
||||
history?: ChatItemType[];
|
||||
[SystemInputEnum.userChatInput]: string;
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
[SpecialInputKeyEnum.agents]: ClassifyQuestionAgentItemType[];
|
||||
};
|
||||
export type CQResponse = {
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const agentModel = 'gpt-3.5-turbo';
|
||||
const agentFunName = 'agent_user_question';
|
||||
const maxTokens = 3000;
|
||||
|
||||
/* request openai chat */
|
||||
export const dispatchClassifyQuestion = async (props: Record<string, any>): Promise<CQResponse> => {
|
||||
const { agents, systemPrompt, history = [], userChatInput, userOpenaiAccount } = props as CQProps;
|
||||
|
||||
if (!userChatInput) {
|
||||
return Promise.reject('Input is empty');
|
||||
}
|
||||
|
||||
const messages: ChatItemType[] = [
|
||||
...(systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: systemPrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...history,
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: userChatInput
|
||||
}
|
||||
];
|
||||
const filterMessages = ChatContextFilter({
|
||||
messages,
|
||||
maxTokens
|
||||
});
|
||||
const adaptMessages = adaptChat2GptMessages({ messages: filterMessages, reserveId: false });
|
||||
|
||||
// function body
|
||||
const agentFunction = {
|
||||
name: agentFunName,
|
||||
description: '判断用户问题的类型属于哪方面,返回对应的枚举字段',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: agents.map((item) => `${item.value},返回:'${item.key}'`).join(';'),
|
||||
enum: agents.map((item) => item.key)
|
||||
}
|
||||
},
|
||||
required: ['type']
|
||||
}
|
||||
};
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: agentModel,
|
||||
temperature: 0,
|
||||
messages: [...adaptMessages],
|
||||
function_call: { name: agentFunName },
|
||||
functions: [agentFunction]
|
||||
},
|
||||
{
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const arg = JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '');
|
||||
|
||||
const tokens = response.data.usage?.total_tokens || 0;
|
||||
|
||||
const result = agents.find((item) => item.key === arg?.type) || agents[0];
|
||||
|
||||
return {
|
||||
[result.key]: 1,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.CQ,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model: agentModel, tokens }),
|
||||
model: getModel(agentModel)?.name || agentModel,
|
||||
tokens,
|
||||
cqList: agents,
|
||||
cqResult: result.value
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
|
||||
import { ChatContextFilter } from '@/service/common/tiktoken';
|
||||
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/lib/openai';
|
||||
import type { ContextExtractAgentItemType } from '@/types/app';
|
||||
import { ContextExtractEnum } from '@/constants/flow/flowField';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { getModel } from '@/service/utils/data';
|
||||
|
||||
export type Props = {
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
history?: ChatItemType[];
|
||||
[ContextExtractEnum.content]: string;
|
||||
[ContextExtractEnum.extractKeys]: ContextExtractAgentItemType[];
|
||||
[ContextExtractEnum.description]: string;
|
||||
};
|
||||
export type Response = {
|
||||
[ContextExtractEnum.success]?: boolean;
|
||||
[ContextExtractEnum.failed]?: boolean;
|
||||
[ContextExtractEnum.fields]: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
};
|
||||
|
||||
const agentModel = 'gpt-3.5-turbo';
|
||||
const agentFunName = 'agent_extract_data';
|
||||
const maxTokens = 4000;
|
||||
|
||||
export async function dispatchContentExtract({
|
||||
userOpenaiAccount,
|
||||
content,
|
||||
extractKeys,
|
||||
history = [],
|
||||
description
|
||||
}: Props): Promise<Response> {
|
||||
if (!content) {
|
||||
return Promise.reject('Input is empty');
|
||||
}
|
||||
const messages: ChatItemType[] = [
|
||||
...history,
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: content
|
||||
}
|
||||
];
|
||||
const filterMessages = ChatContextFilter({
|
||||
messages,
|
||||
maxTokens
|
||||
});
|
||||
const adaptMessages = adaptChat2GptMessages({ messages: filterMessages, reserveId: false });
|
||||
|
||||
const properties: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
> = {};
|
||||
extractKeys.forEach((item) => {
|
||||
properties[item.key] = {
|
||||
type: 'string',
|
||||
description: item.desc
|
||||
};
|
||||
});
|
||||
|
||||
// function body
|
||||
const agentFunction = {
|
||||
name: agentFunName,
|
||||
description: `${description}\n如果内容不存在,返回空字符串。`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties,
|
||||
required: extractKeys.filter((item) => item.required).map((item) => item.key)
|
||||
}
|
||||
};
|
||||
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: agentModel,
|
||||
temperature: 0,
|
||||
messages: [...adaptMessages],
|
||||
function_call: { name: agentFunName },
|
||||
functions: [agentFunction]
|
||||
},
|
||||
{
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const arg: Record<string, any> = (() => {
|
||||
try {
|
||||
return JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '{}');
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
// auth fields
|
||||
let success = !extractKeys.find((item) => !arg[item.key]);
|
||||
// auth empty value
|
||||
if (success) {
|
||||
for (const key in arg) {
|
||||
if (arg[key] === '') {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = response.data.usage?.total_tokens || 0;
|
||||
|
||||
return {
|
||||
[ContextExtractEnum.success]: success ? true : undefined,
|
||||
[ContextExtractEnum.failed]: success ? undefined : true,
|
||||
[ContextExtractEnum.fields]: JSON.stringify(arg),
|
||||
...arg,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.Extract,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model: agentModel, tokens }),
|
||||
model: getModel(agentModel)?.name || agentModel,
|
||||
tokens,
|
||||
extractDescription: description,
|
||||
extractResult: arg
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { sseResponse } from '@/service/utils/tools';
|
||||
import { ChatContextFilter } from '@/service/common/tiktoken';
|
||||
import type { ChatItemType, QuoteItemType } from '@/types/chat';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { SSEParseData, parseStreamChunk } from '@/utils/sse';
|
||||
import { textAdaptGptResponse } from '@/utils/adapt';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/lib/openai';
|
||||
import { TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getChatModel } from '@/service/utils/data';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { ChatModelItemType } from '@/types/model';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { textCensor } from '@/api/service/plugins';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { countMessagesTokens, sliceMessagesTB } from '@/utils/common/tiktoken';
|
||||
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
|
||||
|
||||
export type ChatProps = {
|
||||
res: NextApiResponse;
|
||||
model: string;
|
||||
temperature?: number;
|
||||
maxToken?: number;
|
||||
history?: ChatItemType[];
|
||||
userChatInput: string;
|
||||
stream?: boolean;
|
||||
detail?: boolean;
|
||||
quoteQA?: QuoteItemType[];
|
||||
systemPrompt?: string;
|
||||
limitPrompt?: string;
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
outputs: AppModuleItemType['outputs'];
|
||||
};
|
||||
export type ChatResponse = {
|
||||
[TaskResponseKeyEnum.answerText]: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
finish: boolean;
|
||||
};
|
||||
|
||||
/* request openai chat */
|
||||
export const dispatchChatCompletion = async (props: Record<string, any>): Promise<ChatResponse> => {
|
||||
let {
|
||||
res,
|
||||
model = global.chatModels[0]?.model,
|
||||
temperature = 0,
|
||||
maxToken = 4000,
|
||||
stream = false,
|
||||
detail = false,
|
||||
history = [],
|
||||
quoteQA = [],
|
||||
userChatInput,
|
||||
systemPrompt = '',
|
||||
limitPrompt = '',
|
||||
userOpenaiAccount,
|
||||
outputs
|
||||
} = props as ChatProps;
|
||||
if (!userChatInput) {
|
||||
return Promise.reject('Question is empty');
|
||||
}
|
||||
|
||||
// temperature adapt
|
||||
const modelConstantsData = getChatModel(model);
|
||||
|
||||
if (!modelConstantsData) {
|
||||
return Promise.reject('The chat model is undefined, you need to select a chat model.');
|
||||
}
|
||||
|
||||
const { filterQuoteQA, quotePrompt, hasQuoteOutput } = filterQuote({
|
||||
quoteQA,
|
||||
model: modelConstantsData
|
||||
});
|
||||
|
||||
if (modelConstantsData.censor) {
|
||||
await textCensor({
|
||||
text: `${systemPrompt}
|
||||
${quotePrompt}
|
||||
${limitPrompt}
|
||||
${userChatInput}
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
const { messages, filterMessages } = getChatMessages({
|
||||
model: modelConstantsData,
|
||||
history,
|
||||
quotePrompt,
|
||||
userChatInput,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
hasQuoteOutput
|
||||
});
|
||||
const { max_tokens } = getMaxTokens({
|
||||
model: modelConstantsData,
|
||||
maxToken,
|
||||
filterMessages
|
||||
});
|
||||
// console.log(messages);
|
||||
|
||||
// FastGPT temperature range: 1~10
|
||||
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
|
||||
temperature = Math.max(temperature, 0.01);
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
messages: [
|
||||
...(modelConstantsData.defaultSystem
|
||||
? [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
content: modelConstantsData.defaultSystem
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...messages
|
||||
],
|
||||
stream
|
||||
},
|
||||
{
|
||||
timeout: 480000,
|
||||
responseType: stream ? 'stream' : 'json',
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const { answerText, totalTokens, completeMessages } = await (async () => {
|
||||
if (stream) {
|
||||
// sse response
|
||||
const { answer } = await streamResponse({
|
||||
res,
|
||||
detail,
|
||||
response
|
||||
});
|
||||
// count tokens
|
||||
const completeMessages = filterMessages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer
|
||||
});
|
||||
|
||||
const totalTokens = countMessagesTokens({
|
||||
messages: completeMessages
|
||||
});
|
||||
|
||||
targetResponse({ res, detail, outputs });
|
||||
|
||||
return {
|
||||
answerText: answer,
|
||||
totalTokens,
|
||||
completeMessages
|
||||
};
|
||||
} else {
|
||||
const answer = response.data.choices?.[0].message?.content || '';
|
||||
const totalTokens = response.data.usage?.total_tokens || 0;
|
||||
|
||||
const completeMessages = filterMessages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer
|
||||
});
|
||||
|
||||
return {
|
||||
answerText: answer,
|
||||
totalTokens,
|
||||
completeMessages
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
[TaskResponseKeyEnum.answerText]: answerText,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.AIChat,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model, tokens: totalTokens }),
|
||||
model: modelConstantsData.name,
|
||||
tokens: totalTokens,
|
||||
question: userChatInput,
|
||||
answer: answerText,
|
||||
maxToken: max_tokens,
|
||||
quoteList: filterQuoteQA,
|
||||
completeMessages
|
||||
},
|
||||
finish: true
|
||||
};
|
||||
};
|
||||
|
||||
function filterQuote({
|
||||
quoteQA = [],
|
||||
model
|
||||
}: {
|
||||
quoteQA: ChatProps['quoteQA'];
|
||||
model: ChatModelItemType;
|
||||
}) {
|
||||
const sliceResult = sliceMessagesTB({
|
||||
maxTokens: model.quoteMaxToken,
|
||||
messages: quoteQA.map((item) => ({
|
||||
obj: ChatRoleEnum.System,
|
||||
value: item.a ? `${item.q}\n${item.a}` : item.q
|
||||
}))
|
||||
});
|
||||
|
||||
// slice filterSearch
|
||||
const filterQuoteQA = quoteQA.slice(0, sliceResult.length);
|
||||
|
||||
const quotePrompt =
|
||||
filterQuoteQA.length > 0
|
||||
? `"""${filterQuoteQA
|
||||
.map((item) =>
|
||||
item.a ? `{instruction:"${item.q}",output:"${item.a}"}` : `{instruction:"${item.q}"}`
|
||||
)
|
||||
.join('\n')}"""`
|
||||
: '';
|
||||
|
||||
return {
|
||||
filterQuoteQA,
|
||||
quotePrompt,
|
||||
hasQuoteOutput: !!filterQuoteQA.find((item) => item.a)
|
||||
};
|
||||
}
|
||||
function getChatMessages({
|
||||
quotePrompt,
|
||||
history = [],
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
userChatInput,
|
||||
model,
|
||||
hasQuoteOutput
|
||||
}: {
|
||||
quotePrompt: string;
|
||||
history: ChatProps['history'];
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
userChatInput: string;
|
||||
model: ChatModelItemType;
|
||||
hasQuoteOutput: boolean;
|
||||
}) {
|
||||
const { quoteGuidePrompt } = getDefaultPrompt({ hasQuoteOutput });
|
||||
|
||||
const systemText = `${quotePrompt ? `${quoteGuidePrompt}\n\n` : ''}${systemPrompt}`;
|
||||
|
||||
const messages: ChatItemType[] = [
|
||||
...(systemText
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: systemText
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(quotePrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: quotePrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...history,
|
||||
...(limitPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: limitPrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: userChatInput
|
||||
}
|
||||
];
|
||||
|
||||
const filterMessages = ChatContextFilter({
|
||||
messages,
|
||||
maxTokens: Math.ceil(model.contextMaxToken - 300) // filter token. not response maxToken
|
||||
});
|
||||
|
||||
const adaptMessages = adaptChat2GptMessages({ messages: filterMessages, reserveId: false });
|
||||
|
||||
return {
|
||||
messages: adaptMessages,
|
||||
filterMessages
|
||||
};
|
||||
}
|
||||
function getMaxTokens({
|
||||
maxToken,
|
||||
model,
|
||||
filterMessages = []
|
||||
}: {
|
||||
maxToken: number;
|
||||
model: ChatModelItemType;
|
||||
filterMessages: ChatProps['history'];
|
||||
}) {
|
||||
const tokensLimit = model.contextMaxToken;
|
||||
/* count response max token */
|
||||
|
||||
const promptsToken = countMessagesTokens({
|
||||
messages: filterMessages
|
||||
});
|
||||
maxToken = maxToken + promptsToken > tokensLimit ? tokensLimit - promptsToken : maxToken;
|
||||
|
||||
return {
|
||||
max_tokens: maxToken
|
||||
};
|
||||
}
|
||||
|
||||
function targetResponse({
|
||||
res,
|
||||
outputs,
|
||||
detail
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
outputs: AppModuleItemType['outputs'];
|
||||
detail: boolean;
|
||||
}) {
|
||||
const targets =
|
||||
outputs.find((output) => output.key === TaskResponseKeyEnum.answerText)?.targets || [];
|
||||
|
||||
if (targets.length === 0) return;
|
||||
sseResponse({
|
||||
res,
|
||||
event: detail ? sseResponseEventEnum.answer : undefined,
|
||||
data: textAdaptGptResponse({
|
||||
text: '\n'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function streamResponse({
|
||||
res,
|
||||
detail,
|
||||
response
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
detail: boolean;
|
||||
response: any;
|
||||
}) {
|
||||
let answer = '';
|
||||
let error: any = null;
|
||||
const parseData = new SSEParseData();
|
||||
|
||||
try {
|
||||
for await (const chunk of response.data as any) {
|
||||
if (res.closed) break;
|
||||
const parse = parseStreamChunk(chunk);
|
||||
parse.forEach((item) => {
|
||||
const { data } = parseData.parse(item);
|
||||
if (!data || data === '[DONE]') return;
|
||||
|
||||
const content: string = data?.choices?.[0]?.delta?.content || '';
|
||||
error = data.error;
|
||||
answer += content;
|
||||
|
||||
sseResponse({
|
||||
res,
|
||||
event: detail ? sseResponseEventEnum.answer : undefined,
|
||||
data: textAdaptGptResponse({
|
||||
text: content
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return {
|
||||
answer
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultPrompt({ hasQuoteOutput }: { hasQuoteOutput?: boolean }) {
|
||||
return {
|
||||
quoteGuidePrompt: `三引号引用的内容是我提供给你的知识库,它们拥有最高优先级。instruction 是相关介绍${
|
||||
hasQuoteOutput ? ',output 是预期回答或补充。' : '。'
|
||||
}`
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
export type UserChatInputProps = {
|
||||
[SystemInputEnum.userChatInput]: string;
|
||||
};
|
||||
|
||||
export const dispatchChatInput = (props: Record<string, any>) => {
|
||||
const { userChatInput } = props as UserChatInputProps;
|
||||
return {
|
||||
userChatInput
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user