Compare commits

..

62 Commits

Author SHA1 Message Date
Archer
661ee79943 fix: CQ module output (#445) 2023-10-30 16:45:36 +08:00
Archer
60ee160131 v4.5.2 (#439) 2023-10-30 13:26:42 +08:00
Archer
008d0af010 Quote Modal UI and fix doc (#432) 2023-10-25 20:13:32 +08:00
lizhuang
f2fb0aedfd Update README.md 文档改为https://doc.fastgpt.in网站访问 (#424)
文档改为https://doc.fastgpt.in网站访问
2023-10-24 18:07:30 +08:00
Archer
1dca5edcc6 v4.5.1-3 (#427) 2023-10-24 17:32:36 +08:00
Archer
1942cb0d67 perf: btn color (#423) 2023-10-24 13:19:23 +08:00
Archer
bf6dbfb245 v4.5.1-2 (#421) 2023-10-23 15:05:13 +08:00
Archer
d37433eacd Config file to set doc baseurl (#419) 2023-10-23 08:56:43 +08:00
Archer
a3534407bf v4.5.1 (#417) 2023-10-22 23:54:04 +08:00
Carson Yang
3091a90df6 Update README (#418)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-22 23:47:09 +08:00
Carson Yang
41b8f4443c Docs: update qr for wechat group (#416)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-22 23:28:46 +08:00
Carson Yang
777f089423 Docs: update README (#407)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-18 15:51:51 +08:00
不做了睡大觉
b23e00f3e5 添加Baichuan2-7B-Chat模型接口文件 (#404)
* 更新镜像

* 更新镜像信息

* 更新镜像信息

* Create openai_api.py

* Create requirements.txt
2023-10-18 10:34:22 +08:00
Archer
3b776b6639 v4.5 (#403) 2023-10-17 10:00:32 +08:00
Archer
dd8f2744bf Extraction schema (#398) 2023-10-14 23:02:01 +08:00
左风
7db8d3ea0f Docs: add quick start and video link (#395) 2023-10-13 20:16:18 +08:00
Archer
ad7a17bf40 Optimize the project structure and introduce DDD design (#394) 2023-10-12 17:46:37 +08:00
李启爱
76ac5238b6 Update 447.md (#392) 2023-10-12 14:56:18 +08:00
Carson Yang
add73aa2c5 Docs: use docsearch (#391)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-12 00:04:45 +08:00
Archer
bcf9491999 v4.4.7-2 (#388) 2023-10-11 17:18:43 +08:00
Archer
d0041a98b4 Optimize the file storage structure of the knowledge base (#386) 2023-10-10 22:41:05 +08:00
Carson Yang
29d152784f Docs: delete image cdn for vercel (#385)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-09 15:03:07 +08:00
Carson Yang
cd7214ba8d Docs: update workflow for building docs image (#384)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-09 14:32:47 +08:00
Archer
6a84e73a82 fix: packages (#378) 2023-10-08 09:59:05 +08:00
Archer
98ce5103a0 v4.4.6 (#377) 2023-10-07 18:02:20 +08:00
Carson Yang
c65a36d3ab Docs: hide button for questionnaire on mobile device (#376)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-07 14:57:26 +08:00
Carson Yang
b6e49da288 Docs: update button for questionnaire (#375)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-06 23:52:45 +08:00
Archer
45998f9cf5 README (#372) 2023-10-06 21:19:44 +08:00
Carson Yang
4197f63751 Update README (#371)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-06 14:07:37 +08:00
Carson Yang
ace8134a16 Docs: add Dockerfile for docs (#369)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-06 08:01:16 +08:00
Carson Yang
7f1fecb84e Docs: update theme (#368)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-10-04 22:25:07 +08:00
Archer
bf172fab81 perf: markdown more wrap (#365) 2023-10-02 20:19:09 +08:00
Archer
36f5648cae perf: v4.4.6-1 (#364) 2023-09-28 17:30:05 +08:00
Archer
ab57bfcc4a perf: completions api.fix: new chat question guide (#361) 2023-09-27 12:05:13 +08:00
Archer
11848b8f44 v4.4.5-3 (#357) 2023-09-26 21:17:13 +08:00
epoh
a11e0bd9c3 Update chatglm2.md (#354) 2023-09-26 15:06:38 +08:00
Archer
f6552d0d4f v4.4.5-2 (#355) 2023-09-26 14:31:37 +08:00
epoh
38d4db5d5f Rename requirement.txt to requirements.txt (#352) 2023-09-26 09:38:14 +08:00
Archer
63cd379682 Add share link hook (#351) 2023-09-25 23:12:42 +08:00
Archer
9136c9306a Add OpenAPI docs;Correct the glm document (#346) 2023-09-25 14:28:44 +08:00
Byte Sound
c9db9f33ea Update intro.md (#348)
错别字,市区改为时区
2023-09-25 13:33:30 +08:00
Archer
3d7178d06f monorepo packages (#344) 2023-09-24 18:02:09 +08:00
Archer
a4ff5a3f73 perf: api key (#342) 2023-09-23 20:28:03 +08:00
Archer
814c5b3d3c Add bill of training and rate of file upload (#339) 2023-09-21 21:02:44 +08:00
Chen X
e7e0677291 Docs:add-workflow-case-全能助手 (#334) 2023-09-21 15:57:42 +08:00
Archer
823f4b7ad1 Optimize the structure and naming of projects (#335) 2023-09-21 14:49:56 +08:00
Carson Yang
a3c77480f7 Add action for translating Non-English issues content to English (#333)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-09-21 14:19:54 +08:00
Archer
e367265dbb feat: function call prompt version (#331) 2023-09-21 12:27:48 +08:00
Archer
7e0deb29e0 Add SSE controller; fix share page login failed (#330) 2023-09-20 16:34:32 +08:00
Archer
0d94db4331 fix: ts and default dataset (#329) 2023-09-20 11:43:49 +08:00
Carson Yang
177482b33a Docs: fix code block highlight (#328)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-09-20 11:43:35 +08:00
Archer
63b183a9fe fix: mark modal cannot select folder (#327) 2023-09-20 11:26:17 +08:00
Carson Yang
858117f8c0 Docs: update font to LXGW WenKai (#325)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-09-19 21:22:04 +08:00
Archer
ac4355d2e1 Add modal to show completion response data (#324) 2023-09-19 20:31:45 +08:00
Archer
ce7da2db66 Optimize chat reponse data (#322) 2023-09-19 16:10:30 +08:00
Archer
0a4a1def1e fix: connected error (#318) 2023-09-19 07:54:50 +08:00
Archer
35f4deca76 Revert "Feature: 高级编排自动布局 (#314)" (#319)
This reverts commit ba1451a0e9.
2023-09-18 23:44:44 +08:00
jaden
ba1451a0e9 Feature: 高级编排自动布局 (#314)
* feat: adFlow auto layout

* chore: delete file and build pnpm lock file
2023-09-18 23:39:19 +08:00
Archer
40d69e6e20 version (#317) 2023-09-18 21:56:38 +08:00
Sr
b8ba947ba8 feat: Added defaultOpen Attribute for iframe (#302)
* feat: Added defaultOpen Attribute for iframe

This commit introduces a new attribute `defaultOpen` for the iframe created in `iframe.js`. The `defaultOpen` attribute allows the iframe to be visible by default when the page loads. This new feature enhances the user experience by providing an option to display the chatbot window immediately after the page is loaded, without requiring user interaction.

* Update iframe.js

code standard
2023-09-18 21:27:08 +08:00
Archer
06be57815e v4.4.3 (#316) 2023-09-18 21:26:42 +08:00
Carson Yang
81e37a5736 Update architecture diagram (#315)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2023-09-18 21:26:15 +08:00
863 changed files with 35034 additions and 49746 deletions

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

View 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. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
View 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
View File

@@ -33,4 +33,6 @@ dist/
# hugo
**/.hugo_build.lock
docSite/public/
docSite/public/
docSite/resources/_gen/
docSite/.vercel

View File

@@ -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
View 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}"]

View File

@@ -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 + PostgresVector 插件
项目技术栈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
![](https://otnvvf-imgs.oss.laf.run/wx300.jpg)
## 👀 其他
- [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)
- [Laf3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos快速部署集群应用](https://github.com/labring/sealos)
- [One API多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
- [TuShan5 分钟搭建后台管理系统](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)

View File

@@ -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://cdn.jsdelivr.us/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](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) -->

View File

@@ -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"]

View File

@@ -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
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 |
**其他问题**
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](https://otnvvf-imgs.oss.laf.run/wxqun300.jpg) | ![](https://otnvvf-imgs.oss.laf.run/wx300.jpg) |

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 };

View File

@@ -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);

View File

@@ -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}`);

View File

@@ -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 });

View File

@@ -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);

View File

@@ -1,6 +0,0 @@
export type AdminUpdateFeedbackParams = {
chatItemId: string;
kbId: string;
dataId: string;
content: string;
};

View File

@@ -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;
};

View File

@@ -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);
}
});

View File

@@ -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 });

View File

@@ -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');

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -1,4 +0,0 @@
.datePicker {
--rdp-background-color: #d6e8ff;
--rdp-accent-color: #0000ff;
}

View File

@@ -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

View File

@@ -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' };

View File

@@ -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
};

View File

@@ -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';

View File

@@ -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
}
};

View File

@@ -1,10 +0,0 @@
export enum TrainingModeEnum {
'qa' = 'qa',
'index' = 'index'
}
export const TrainingTypeMap = {
[TrainingModeEnum.qa]: 'qa',
[TrainingModeEnum.index]: 'index'
};
export const PgDatasetTableName = 'modeldata';

View File

@@ -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'
})
)
);
}

View File

@@ -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 });
}
})
);
}

View File

@@ -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
});
}
}

View File

@@ -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;
})
);
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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'
}
}
};

View File

@@ -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
});
}
});

View File

@@ -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
});
}
});

View File

@@ -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);
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
}

View File

@@ -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
});
}
});

View File

@@ -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';
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,5 +0,0 @@
.panel {
.react-flow__panel {
display: none;
}
}

View File

@@ -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);

View File

@@ -1,7 +0,0 @@
.intro {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -1,7 +0,0 @@
.intro {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -1,5 +0,0 @@
.loginPage {
background: url('/icon/login-bg.svg') no-repeat;
background-size: cover;
user-select: none;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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 || ''
}
};
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
}
};
};

View File

@@ -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
}
};
}

View File

@@ -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 是预期回答或补充。' : '。'
}`
};
}

View File

@@ -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