Compare commits
58 Commits
v4.8.12-be
...
v4.8.13-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
608e58ba41 | ||
|
|
044b0c57f7 | ||
|
|
7d7454ef3b | ||
|
|
0d658c0114 | ||
|
|
d58cf44778 | ||
|
|
7537330a3b | ||
|
|
a7f881fc5e | ||
|
|
fc7304d3cd | ||
|
|
aa50174066 | ||
|
|
5b2cc097b0 | ||
|
|
7a933f73b6 | ||
|
|
3e5d7d0d7a | ||
|
|
d15ec1ae69 | ||
|
|
3b82ed0aa1 | ||
|
|
dc95ab1dc1 | ||
|
|
fa2fbc1ddd | ||
|
|
10421d73f4 | ||
|
|
a9ee6e6a5e | ||
|
|
0f1932aadc | ||
|
|
65a39e80b8 | ||
|
|
0db0cbf376 | ||
|
|
f4dbe7c021 | ||
|
|
07b3a0a35d | ||
|
|
fd49ad1342 | ||
|
|
f90803c558 | ||
|
|
49cd2d7a3c | ||
|
|
727bd7144c | ||
|
|
469858877e | ||
|
|
7a929db0a5 | ||
|
|
0645b274da | ||
|
|
cf8786b194 | ||
|
|
be6269688b | ||
|
|
912b264a47 | ||
|
|
7ef1821557 | ||
|
|
4061b11922 | ||
|
|
bc171db945 | ||
|
|
eb365fef44 | ||
|
|
2e7047cb3b | ||
|
|
89a817d1c9 | ||
|
|
e788bcaabe | ||
|
|
9219903341 | ||
|
|
6939899baa | ||
|
|
732b6d7780 | ||
|
|
e361279208 | ||
|
|
946fda0843 | ||
|
|
97216eec59 | ||
|
|
9f4aa3160e | ||
|
|
8e4084f7ee | ||
|
|
ee718750e2 | ||
|
|
1e02544c3a | ||
|
|
98771284e4 | ||
|
|
efc4e860b7 | ||
|
|
e06d72e86e | ||
|
|
b712a821f8 | ||
|
|
4e3d817b63 | ||
|
|
78a85bf847 | ||
|
|
a5b913f1b1 | ||
|
|
7ee1a340e6 |
42
.github/workflows/fastgpt-image.yml
vendored
@@ -90,3 +90,45 @@ jobs:
|
||||
-t ${Docker_Hub_Tag} \
|
||||
-t ${Docker_Hub_Latest} \
|
||||
.
|
||||
build-fastgpt-images-child-route:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
# Set tag
|
||||
- name: Set image name and tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
|
||||
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $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 \
|
||||
-f projects/app/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg base_url=fastai \
|
||||
--label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${Git_Tag} \
|
||||
-t ${Git_Latest} \
|
||||
-t ${Ali_Tag} \
|
||||
-t ${Ali_Latest} \
|
||||
-t ${Docker_Hub_Tag} \
|
||||
-t ${Docker_Hub_Latest} \
|
||||
.
|
||||
|
||||
BIN
docSite/assets/imgs/RAG1.png
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
docSite/assets/imgs/RAG2.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docSite/assets/imgs/RAG3.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-config.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-example-1.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-example-2.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-example-3.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-example-4.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node-example-5.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
docSite/assets/imgs/fastgpt-loop-node.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
331
docSite/content/zh-cn/docs/course/RAG.md
Normal file
@@ -0,0 +1,331 @@
|
||||
---
|
||||
title: '知识库基础原理介绍'
|
||||
description: '本节详细介绍RAG模型的核心机制、应用场景及其在生成任务中的优势与局限性。'
|
||||
icon: 'language'
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 106
|
||||
---
|
||||
|
||||
[RAG文档](https://huggingface.co/docs/transformers/model_doc/rag)
|
||||
|
||||
# 1. 引言
|
||||
|
||||
随着自然语言处理(NLP)技术的迅猛发展,生成式语言模型(如GPT、BART等)在多种文本生成任务中表现卓越,尤其在语言生成和上下文理解方面。然而,纯生成模型在处理事实类任务时存在一些固有的局限性。例如,由于这些模型依赖于固定的预训练数据,它们在回答需要最新或实时信息的问题时,可能会出现“编造”信息的现象,导致生成结果不准确或缺乏事实依据。此外,生成模型在面对长尾问题和复杂推理任务时,常因缺乏特定领域的外部知识支持而表现不佳,难以提供足够的深度和准确性。
|
||||
|
||||
与此同时,检索模型(Retriever)能够通过在海量文档中快速找到相关信息,解决事实查询的问题。然而,传统检索模型(如BM25)在面对模糊查询或跨域问题时,往往只能返回孤立的结果,无法生成连贯的自然语言回答。由于缺乏上下文推理能力,检索模型生成的答案通常不够连贯和完整。
|
||||
|
||||
为了解决这两类模型的不足,检索增强生成模型(Retrieval-Augmented Generation,RAG)应运而生。RAG通过结合生成模型和检索模型的优势,实时从外部知识库中获取相关信息,并将其融入生成任务中,确保生成的文本既具备上下文连贯性,又包含准确的知识。这种混合架构在智能问答、信息检索与推理、以及领域特定的内容生成等场景中表现尤为出色。
|
||||
|
||||
## 1.1 RAG的定义
|
||||
|
||||
RAG是一种将信息检索与生成模型相结合的混合架构。首先,检索器从外部知识库或文档集中获取与用户查询相关的内容片段;然后,生成器基于这些检索到的内容生成自然语言输出,确保生成的内容既信息丰富,又具备高度的相关性和准确性。
|
||||
|
||||
# 2. RAG模型的核心机制
|
||||
|
||||
RAG 模型由两个主要模块构成:检索器(Retriever)与生成器(Generator)。这两个模块相互配合,确保生成的文本既包含外部的相关知识,又具备自然流畅的语言表达。
|
||||
|
||||
## 2.1 检索器(Retriever)
|
||||
|
||||
检索器的主要任务是从一个外部知识库或文档集中获取与输入查询最相关的内容。在RAG中,常用的技术包括:
|
||||
|
||||
- 向量检索:如BERT向量等,它通过将文档和查询转化为向量空间中的表示,并使用相似度计算来进行匹配。向量检索的优势在于能够更好地捕捉语义相似性,而不仅仅是依赖于词汇匹配。
|
||||
- 传统检索算法:如BM25,主要基于词频和逆文档频率(TF-IDF)的加权搜索模型来对文档进行排序和检索。BM25适用于处理较为简单的匹配任务,尤其是当查询和文档中的关键词有直接匹配时。
|
||||
|
||||
RAG中检索器的作用是为生成器提供一个上下文背景,使生成器能够基于这些检索到的文档片段生成更为相关的答案。
|
||||
|
||||
## 2.2 生成器(Generator)
|
||||
|
||||
生成器负责生成最终的自然语言输出。在RAG系统中,常用的生成器包括:
|
||||
|
||||
- BART:BART是一种序列到序列的生成模型,专注于文本生成任务,可以通过不同层次的噪声处理来提升生成的质量 。
|
||||
- GPT系列:GPT是一个典型的预训练语言模型,擅长生成流畅自然的文本。它通过大规模数据训练,能够生成相对准确的回答,尤其在任务-生成任务中表现尤为突出 。
|
||||
|
||||
生成器在接收来自检索器的文档片段后,会利用这些片段作为上下文,并结合输入的查询,生成相关且自然的文本回答。这确保了模型的生成结果不仅仅基于已有的知识,还能够结合外部最新的信息。
|
||||
|
||||
## 2.3 RAG的工作流程
|
||||
|
||||
RAG模型的工作流程可以总结为以下几个步骤:
|
||||
|
||||
1. 输入查询:用户输入问题,系统将其转化为向量表示。
|
||||
2. 文档检索:检索器从知识库中提取与查询最相关的文档片段,通常使用向量检索技术或BM25等传统技术进行。
|
||||
3. 生成答案:生成器接收检索器提供的片段,并基于这些片段生成自然语言答案。生成器不仅基于原始的用户查询,还会利用检索到的片段提供更加丰富、上下文相关的答案。
|
||||
4. 输出结果:生成的答案反馈给用户,这个过程确保了用户能够获得基于最新和相关信息的准确回答。
|
||||
|
||||
# 3. RAG模型的工作原理
|
||||
|
||||
## 3.1 检索阶段
|
||||
|
||||
在RAG模型中,用户的查询首先被转化为向量表示,然后在知识库中执行向量检索。通常,检索器采用诸如BERT等预训练模型生成查询和文档片段的向量表示,并通过相似度计算(如余弦相似度)匹配最相关的文档片段。RAG的检索器不仅仅依赖简单的关键词匹配,而是采用语义级别的向量表示,从而在面对复杂问题或模糊查询时,能够更加准确地找到相关知识。这一步骤对于最终生成的回答至关重要,因为检索的效率和质量直接决定了生成器可利用的上下文信息 。
|
||||
|
||||
## 3.2 生成阶段
|
||||
|
||||
生成阶段是RAG模型的核心部分,生成器负责基于检索到的内容生成连贯且自然的文本回答。RAG中的生成器,如BART或GPT等模型,结合用户输入的查询和检索到的文档片段,生成更加精准且丰富的答案。与传统生成模型相比,RAG的生成器不仅能够生成语言流畅的回答,还可以根据外部知识库中的实际信息提供更具事实依据的内容,从而提高了生成的准确性 。
|
||||
|
||||
## 3.3 多轮交互与反馈机制
|
||||
|
||||
RAG模型在对话系统中能够有效支持多轮交互。每一轮的查询和生成结果会作为下一轮的输入,系统通过分析和学习用户的反馈,逐步优化后续查询的上下文。通过这种循环反馈机制,RAG能够更好地调整其检索和生成策略,使得在多轮对话中生成的答案越来越符合用户的期望。此外,多轮交互还增强了RAG在复杂对话场景中的适应性,使其能够处理跨多轮的知识整合和复杂推理 。
|
||||
|
||||
# 4. RAG的优势与局限
|
||||
|
||||
## 4.1 优势
|
||||
|
||||
- 信息完整性:RAG 模型结合了检索与生成技术,使得生成的文本不仅语言自然流畅,还能够准确利用外部知识库提供的实时信息。这种方法能够显著提升生成任务的准确性,特别是在知识密集型场景下,如医疗问答或法律意见生成。通过从知识库中检索相关文档,RAG 模型避免了生成模型“编造”信息的风险,确保输出更具真实性 。
|
||||
- 知识推理能力:RAG 能够利用大规模的外部知识库进行高效检索,并结合这些真实数据进行推理,生成基于事实的答案。相比传统生成模型,RAG 能处理更为复杂的任务,特别是涉及跨领域或跨文档的推理任务。例如,法律领域的复杂判例推理或金融领域的分析报告生成都可以通过RAG的推理能力得到优化 。
|
||||
- 领域适应性强:RAG 具有良好的跨领域适应性,能够根据不同领域的知识库进行特定领域内的高效检索和生成。例如,在医疗、法律、金融等需要实时更新和高度准确性的领域,RAG 模型的表现优于仅依赖预训练的生成模型 。
|
||||
|
||||
## 4.2 局限
|
||||
|
||||
RAG(检索增强生成)模型通过结合检索器和生成器,实现了在多种任务中知识密集型内容生成的突破性进展。然而,尽管其具有较强的应用潜力和跨领域适应能力,但在实际应用中仍然面临着一些关键局限,限制了其在大规模系统中的部署和优化。以下是RAG模型的几个主要局限性:
|
||||
|
||||
#### 4.2.1 检索器的依赖性与质量问题
|
||||
|
||||
RAG模型的性能很大程度上取决于检索器返回的文档质量。由于生成器主要依赖检索器提供的上下文信息,如果检索到的文档片段不相关、不准确,生成的文本可能出现偏差,甚至产生误导性的结果。尤其在多模糊查询或跨领域检索的情况下,检索器可能无法找到合适的片段,这将直接影响生成内容的连贯性和准确性。
|
||||
|
||||
- 挑战:当知识库庞大且内容多样时,如何提高检索器在复杂问题下的精确度是一大挑战。当前的方法如BM25等在特定任务上有局限,尤其是在面对语义模糊的查询时,传统的关键词匹配方式可能无法提供语义上相关的内容。
|
||||
- 解决途径:引入混合检索技术,如结合稀疏检索(BM25)与密集检索(如向量检索)。例如,[Faiss](https://fael3z0zfze.feishu.cn/wiki/LULawsUufitGvWkDjx3cKJqHnle?from=from_copylink)的底层实现允许通过BERT等模型生成密集向量表示,显著提升语义级别的匹配效果。通过这种方式,检索器可以捕捉深层次的语义相似性,减少无关文档对生成器的负面影响。
|
||||
|
||||
#### 4.2.2 生成器的计算复杂度与性能瓶颈
|
||||
|
||||
RAG模型将检索和生成模块结合,尽管生成结果更加准确,但也大大增加了模型的计算复杂度。尤其在处理大规模数据集或长文本时,生成器需要处理来自多个文档片段的信息,导致生成时间明显增加,推理速度下降。对于实时问答系统或其他需要快速响应的应用场景,这种高计算复杂度是一个主要瓶颈。
|
||||
|
||||
- 挑战:当知识库规模扩大时,检索过程中的计算开销以及生成器在多片段上的整合能力都会显著影响系统的效率。同时,生成器也面临着资源消耗的问题,尤其是在多轮对话或复杂生成任务中,GPU和内存的消耗会成倍增加。
|
||||
- 解决途径:使用模型压缩技术和知识蒸馏来减少生成器的复杂度和推理时间。此外,分布式计算与模型并行化技术的引入,如[DeepSpeed](https://www.deepspeed.ai/)和模型压缩工具,可以有效应对生成任务的高计算复杂度,提升大规模应用场景中的推理效率。
|
||||
|
||||
#### 4.2.3 知识库的更新与维护
|
||||
|
||||
RAG模型通常依赖于一个预先建立的外部知识库,该知识库可能包含文档、论文、法律条款等各类信息。然而,知识库内容的时效性和准确性直接影响到RAG生成结果的可信度。随着时间推移,知识库中的内容可能过时,导致生成的回答不能反映最新的信息。这对于需要实时信息的场景(如医疗、金融)尤其明显。
|
||||
|
||||
- 挑战:知识库需要频繁更新,但手动更新知识库既耗时又容易出错。如何在不影响系统性能的情况下实现知识库的持续自动更新是当前的一大挑战。
|
||||
- 解决途径:利用自动化爬虫和信息提取系统,可以实现对知识库的自动化更新,例如,Scrapy等爬虫框架可以自动抓取网页数据并更新知识库。结合[动态索引技术](https://arxiv.org/pdf/2102.03315),可以帮助检索器实时更新索引,确保知识库反映最新信息。同时,结合增量学习技术,生成器可以逐步吸收新增的信息,避免生成过时答案。此外,动态索引技术也可以帮助检索器实时更新索引,确保知识库检索到的文档反映最新的内容。
|
||||
|
||||
#### 4.2.4 生成内容的可控性与透明度
|
||||
|
||||
RAG模型结合了检索与生成模块,在生成内容的可控性和透明度上存在一定问题。特别是在复杂任务或多义性较强的用户输入情况下,生成器可能会基于不准确的文档片段生成错误的推理,导致生成的答案偏离实际问题。此外,由于RAG模型的“黑箱”特性,用户难以理解生成器如何利用检索到的文档信息,这在高敏感领域如法律或医疗中尤为突出,可能导致用户对生成内容产生不信任感。
|
||||
|
||||
- 挑战:模型透明度不足使得用户难以验证生成答案的来源和可信度。对于需要高可解释性的任务(如医疗问诊、法律咨询等),无法追溯生成答案的知识来源会导致用户不信任模型的决策。
|
||||
- 解决途径:为提高透明度,可以引入可解释性AI(XAI)技术,如LIME或SHAP([链接](https://github.com/marcotcr/lime)),为每个生成答案提供详细的溯源信息,展示所引用的知识片段。这种方法能够帮助用户理解模型的推理过程,从而增强对模型输出的信任。此外,针对生成内容的控制,可以通过加入规则约束或用户反馈机制,逐步优化生成器的输出,确保生成内容更加可信。
|
||||
|
||||
# 5. RAG整体改进方向
|
||||
|
||||
RAG模型的整体性能依赖于知识库的准确性和检索的效率,因此在数据采集、内容分块、精准检索和回答生成等环节进行优化,是提升模型效果的关键。通过加强数据来源、改进内容管理、优化检索策略及提升回答生成的准确性,RAG模型能够更加适应复杂且动态的实际应用需求。
|
||||
|
||||
## 5.1 数据采集与知识库构建
|
||||
|
||||
RAG模型的核心依赖在于知识库的数据质量和广度,知识库在某种程度上充当着“外部记忆”的角色。因此,高质量的知识库不仅应包含广泛领域的内容,更要确保数据来源的权威性、可靠性以及时效性。知识库的数据源应涵盖多种可信的渠道,例如科学文献数据库(如PubMed、IEEE Xplore)、权威新闻媒体、行业标准和报告等,这样才能提供足够的背景信息支持RAG在不同任务中的应用。此外,为了确保RAG模型能够提供最新的回答,知识库需要具备自动化更新的能力,以避免数据内容老旧,导致回答失准或缺乏现实参考。
|
||||
|
||||
- 挑战:
|
||||
- 尽管数据采集是构建知识库的基础,但在实际操作中仍存在以下几方面的不足:
|
||||
- 数据采集来源单一或覆盖不全
|
||||
1. RAG模型依赖多领域数据的支持,然而某些知识库过度依赖单一或有限的数据源,通常集中在某些领域,导致在多任务需求下覆盖不足。例如,依赖医学领域数据而缺乏法律和金融数据会使RAG模型在跨领域问答中表现不佳。这种局限性削弱了RAG模型在处理不同主题或多样化查询时的准确性,使得系统在应对复杂或跨领域任务时能力欠缺。
|
||||
- 数据质量参差不齐
|
||||
1. 数据源的质量差异直接影响知识库的可靠性。一些数据可能来源于非权威或低质量渠道,存在偏见、片面或不准确的内容。这些数据若未经筛选录入知识库,会导致RAG模型生成偏差或不准确的回答。例如,在医学领域中,如果引入未经验证的健康信息,可能导致模型给出误导性回答,产生负面影响。数据质量不一致的知识库会大大降低模型输出的可信度和适用性。
|
||||
- 缺乏定期更新机制
|
||||
1. 许多知识库缺乏自动化和频繁的更新机制,特别是在信息变动频繁的领域,如法律、金融和科技。若知识库长期未更新,则RAG模型无法提供最新信息,生成的回答可能过时或不具备实时参考价值。对于用户而言,特别是在需要实时信息的场景下,滞后的知识库会显著影响RAG模型的可信度和用户体验。
|
||||
- 数据处理耗时且易出错
|
||||
1. 数据的采集、清洗、分类和结构化处理是一项繁琐而复杂的任务,尤其是当数据量巨大且涉及多种格式时。通常,大量数据需要人工参与清洗和结构化,而自动化处理流程也存在缺陷,可能会产生错误或遗漏关键信息。低效和易出错的数据处理流程会导致知识库内容不准确、不完整,进而影响RAG模型生成的答案的准确性和连贯性。
|
||||
- 数据敏感性和隐私问题
|
||||
1. 一些特定领域的数据(如医疗、法律、金融)包含敏感信息,未经适当的隐私保护直接引入知识库可能带来隐私泄露的风险。此外,某些敏感数据需要严格的授权和安全存储,以确保在知识库使用中避免违规或隐私泄漏。若未能妥善处理数据隐私问题,不仅会影响系统的合规性,还可能对用户造成严重后果。
|
||||
- 改进:
|
||||
- 针对以上不足,可以从以下几个方面进行改进,以提高数据采集和知识库构建的有效性:
|
||||
- 扩大数据源覆盖范围,增加数据的多样性
|
||||
1. 具体实施:将知识库的数据源扩展至多个重要领域,确保包含医疗、法律、金融等关键领域的专业数据库,如PubMed、LexisNexis和金融数据库。使用具有开放许可的开源数据库和经过认证的数据,确保来源多样化且权威性强。
|
||||
2. 目的与效果:通过跨领域数据覆盖,知识库的广度和深度得以增强,确保RAG模型能够在多任务场景下提供可靠回答。借助多领域合作机构的数据支持,在应对多样化需求时将更具优势。
|
||||
- 构建数据质量审查与过滤机制
|
||||
1. 具体实施:采用自动化数据质量检测算法,如文本相似度检查、情感偏差检测等工具,结合人工审查过滤不符合标准的数据。为数据打分并构建“数据可信度评分”,基于来源可信度、内容完整性等指标筛选数据。
|
||||
2. 目的与效果:减少低质量、偏见数据的干扰,确保知识库内容的可靠性。此方法保障了RAG模型输出的权威性,特别在回答复杂或专业问题时,用户能够获得更加精准且中立的答案。
|
||||
- 实现知识库的自动化更新
|
||||
1. 具体实施:引入自动化数据更新系统,如网络爬虫,定期爬取可信站点、行业数据库的最新数据,并利用变化检测算法筛选出与已有知识库重复或已失效的数据。更新机制可以结合智能筛选算法,仅采纳与用户查询高相关性或时效性强的数据。
|
||||
2. 目的与效果:知识库保持及时更新,确保模型在快速变化的领域(如金融、政策、科技)中提供最新信息。用户体验将因此大幅提升,特别是在需要动态或最新信息的领域,输出的内容将更具时效性。
|
||||
- 采用高效的数据清洗与分类流程
|
||||
1. 具体实施:使用自然语言处理技术,如BERT等模型进行数据分类、实体识别和文本去噪,结合去重算法清理重复内容。采用自动化的数据标注和分类算法,将不同数据类型分领域存储。
|
||||
2. 目的与效果:数据清洗和分领域管理可以大幅提高数据处理的准确性,减少低质量数据的干扰。此改进确保RAG模型的回答生成更流畅、上下文更连贯,提升用户对生成内容的理解和信赖。
|
||||
- 强化数据安全与隐私保护措施
|
||||
1. 具体实施:针对医疗、法律等敏感数据,采用去标识化处理技术(如数据脱敏、匿名化等),并结合差分隐私保护。建立数据权限管理和加密存储机制,对敏感信息进行严格管控。
|
||||
2. 目的与效果:在保护用户隐私的前提下,确保使用的数据合规、安全,适用于涉及个人或敏感数据的应用场景。此措施进一步保证了系统的法律合规性,并有效防止隐私泄露风险。
|
||||
- 优化数据格式与结构的标准化
|
||||
1. 具体实施:建立统一的数据格式与标准编码格式,例如使用JSON、XML或知识图谱形式组织结构化数据,以便于检索系统在查询时高效利用。同时,使用知识图谱等结构化工具,将复杂数据间的关系进行系统化存储。
|
||||
2. 目的与效果:提高数据检索效率,确保模型在生成回答时能够高效使用数据的关键信息。标准化的数据结构支持高效的跨领域检索,并提高了RAG模型的内容准确性和知识关系的透明度。
|
||||
- 用户反馈机制
|
||||
1. 具体实施:通过用户反馈系统,记录用户对回答的满意度、反馈意见及改进建议。使用机器学习算法从反馈中识别知识库中的盲区与信息误差,反馈至数据管理流程中进行更新和优化。
|
||||
2. 目的与效果:利用用户反馈作为数据质量的调整依据,帮助知识库持续优化内容。此方法不仅提升了RAG模型的实际效用,还使知识库更贴合用户需求,确保输出内容始终符合用户期望。
|
||||
|
||||
## 5.2 数据分块与内容管理
|
||||
|
||||
RAG模型的数据分块与内容管理是优化检索与生成流程的关键。合理的分块策略能够帮助模型高效定位目标信息,并在回答生成时提供清晰的上下文支持。通常情况下,将数据按段落、章节或主题进行分块,不仅有助于检索效率的提升,还能避免冗余数据对生成内容造成干扰。尤其在复杂、长文本中,适当的分块策略可保证模型生成的答案具备连贯性、精确性,避免出现内容跳跃或上下文断裂的问题。
|
||||
|
||||
- 挑战:
|
||||
- 在实际操作中,数据分块与内容管理环节存在以下问题:
|
||||
- 分块不合理导致的信息断裂
|
||||
1. 部分文本过度切割或分块策略不合理,可能导致信息链条被打断,使得模型在回答生成时缺乏必要的上下文支持。这会使生成内容显得零散,不具备连贯性,影响用户对答案的理解。例如,将法律文本或技术文档随意切割成小段落会导致重要的上下文关系丢失,降低模型的回答质量。
|
||||
- 冗余数据导致生成内容重复或信息过载
|
||||
1. 数据集中往往包含重复信息,若不去重或优化整合,冗余数据可能导致生成内容的重复或信息过载。这不仅影响用户体验,还会浪费计算资源。例如,在新闻数据或社交媒体内容中,热点事件的描述可能重复出现,模型在生成回答时可能反复引用相同信息。
|
||||
- 分块粒度选择不当影响检索精度
|
||||
1. 如果分块粒度过细,模型可能因缺乏足够的上下文而生成不准确的回答;若分块过大,检索时将难以定位具体信息,导致回答内容冗长且含有无关信息。选择适当的分块粒度对生成答案的准确性和相关性至关重要,特别是在问答任务中需要精确定位答案的情况下,粗放的分块策略会明显影响用户的阅读体验和回答的可读性。
|
||||
- 难以实现基于主题或内容逻辑的分块
|
||||
1. 某些复杂文本难以直接按主题或逻辑结构进行分块,尤其是内容密集或领域专业性较强的数据。基于关键字或简单的规则切割往往难以识别不同主题和信息层次,导致模型在回答生成时信息杂乱。对内容逻辑或主题的错误判断,尤其是在医学、金融等场景下,会大大影响生成答案的准确度和专业性。
|
||||
- 改进:
|
||||
- 为提高数据分块和内容管理的有效性,可以从以下几方面进行优化:
|
||||
- 引入NLP技术进行自动化分块和上下文分析
|
||||
1. 具体实施:借助自然语言处理(NLP)技术,通过句法分析、语义分割等方式对文本进行逻辑切割,以确保分块的合理性。可以基于BERT等预训练模型实现主题识别和上下文分析,确保每个片段均具备完整的信息链,避免信息断裂。
|
||||
2. 目的与效果:确保文本切割基于逻辑或语义关系,避免信息链条被打断,生成答案时能够更具连贯性,尤其适用于长文本和复杂结构的内容,使模型在回答时上下文更加完整、连贯。
|
||||
- 去重与信息整合,优化内容简洁性
|
||||
1. 具体实施:利用相似度算法(如TF-IDF、余弦相似度)识别冗余内容,并结合聚类算法自动合并重复信息。针对内容频繁重复的情况,可设置内容标记或索引,避免生成时多次引用相同片段。
|
||||
2. 目的与效果:通过去重和信息整合,使数据更具简洁性,避免生成答案中出现重复信息。减少冗余信息的干扰,使用户获得简明扼要的回答,增强阅读体验,同时提升生成过程的计算效率。
|
||||
- 根据任务需求动态调整分块粒度
|
||||
1. 具体实施:根据模型任务的不同,设置动态分块策略。例如,在问答任务中对关键信息较短的内容可采用小粒度分块,而在长文本或背景性内容中采用较大粒度。分块策略可基于查询需求或内容复杂度自动调整。
|
||||
2. 目的与效果:分块粒度的动态调整确保模型在检索和生成时既能准确定位关键内容,又能为回答提供足够的上下文支持,提升生成内容的精准性和相关性,确保用户获取的信息既准确又不冗长。
|
||||
- 引入基于主题的分块方法以提升上下文完整性
|
||||
1. 具体实施:使用主题模型(如LDA)或嵌入式文本聚类技术,对文本内容按主题进行自动分类与分块。基于相同主题内容的聚合分块,有助于模型识别不同内容层次,尤其适用于复杂的学术文章或多章节的长篇报告。
|
||||
2. 目的与效果:基于主题的分块确保同一主题的内容保持在一个片段内,提升模型在回答生成时的上下文连贯性。适用于主题复杂、层次清晰的内容场景,提高回答的专业性和条理性,使用户更容易理解生成内容的逻辑关系。
|
||||
- 实时评估分块策略与内容呈现效果的反馈机制
|
||||
1. 具体实施:通过用户反馈机制和生成质量评估系统实时监测生成内容的连贯性和准确性。对用户反馈中涉及分块效果差的部分进行重新分块,通过用户使用数据优化分块策略。
|
||||
2. 目的与效果:用户反馈帮助识别不合理的分块和内容呈现问题,实现分块策略的动态优化,持续提升生成内容的质量和用户满意度。
|
||||
|
||||
## 5.3 检索优化
|
||||
|
||||
在RAG模型中,检索模块决定了生成答案的相关性和准确性。有效的检索策略可确保模型获取到最适合的上下文片段,使生成的回答更加精准且贴合查询需求。常用的混合检索策略(如BM25和DPR结合)能够在关键词匹配和语义检索方面实现优势互补:BM25适合高效地处理关键字匹配任务,而DPR在理解深层语义上表现更为优异。因此,合理选用检索策略有助于在不同任务场景下达到计算资源和检索精度的平衡,以高效提供相关上下文供生成器使用。
|
||||
|
||||
- 挑战:
|
||||
- 检索优化过程中,仍面临以下不足之处:
|
||||
- 检索策略单一导致的回答偏差
|
||||
1. 当仅依赖BM25或DPR等单一技术时,模型可能难以平衡关键词匹配与语义理解。BM25在处理具象关键字时表现良好,但在面对复杂、含义丰富的语义查询时效果欠佳;相反,DPR虽然具备深度语义匹配能力,但对高频关键词匹配的敏感度较弱。检索策略单一将导致模型难以适应复杂的用户查询,回答中出现片面性或不够精准的情况。
|
||||
- 检索效率与资源消耗的矛盾
|
||||
1. 检索模块需要在短时间内处理大量查询,而语义检索(如DPR)需要进行大量的计算和存储操作,计算资源消耗高,影响系统响应速度。特别是对于需要实时响应的应用场景,DPR的计算复杂度往往难以满足实际需求,因此在实时性和资源利用率上亟需优化。
|
||||
- 检索结果的冗余性导致内容重复
|
||||
1. 当检索策略未对结果进行去重或排序优化时,RAG模型可能从知识库中检索出相似度高但内容冗余的文档片段。这会导致生成的回答中包含重复信息,影响阅读体验,同时增加无效信息的比例,使用户难以迅速获取核心答案。
|
||||
- 不同任务需求下检索策略的适配性差
|
||||
1. RAG模型应用场景丰富,但不同任务对检索精度、速度和上下文长度的需求不尽相同。固定检索策略难以灵活应对多样化的任务需求,导致在应对不同任务时,模型检索效果受限。例如,面向精确性较高的医疗问答场景时,检索策略应偏向语义准确性,而在热点新闻场景中则应偏重检索速度。
|
||||
- 改进:
|
||||
- 针对上述不足,可以从以下几个方面优化检索模块:
|
||||
- 结合BM25与DPR的混合检索策略
|
||||
1. 具体实施:采用BM25进行关键词初筛,快速排除无关信息,然后使用DPR进行深度语义匹配筛选。这样可以有效提升检索精度,平衡关键词匹配和语义理解。
|
||||
2. 目的与效果:通过多层筛选过程,确保检索结果在语义理解和关键词匹配方面互补,提升生成内容的准确性,特别适用于多意图查询或复杂的长文本检索。
|
||||
- 优化检索效率,控制计算资源消耗
|
||||
1. 具体实施:利用缓存机制存储近期高频查询结果,避免对相似查询的重复计算。同时,可基于分布式计算结构,将DPR的语义计算任务分散至多节点并行处理。
|
||||
2. 目的与效果:缓存与分布式计算结合可显著减少检索计算压力,使系统能够在有限资源下提高响应速度,适用于高并发、实时性要求高的应用场景。
|
||||
- 引入去重和排序优化算法
|
||||
1. 具体实施:在检索结果中应用余弦相似度去重算法,筛除冗余内容,并基于用户偏好或时间戳对检索结果排序,以确保输出内容的丰富性和新鲜度。
|
||||
2. 目的与效果:通过去重和优化排序,确保生成内容更加简洁、直接,减少重复信息的干扰,提高用户获取信息的效率和体验。
|
||||
- 动态调整检索策略适应多任务需求
|
||||
1. 具体实施:设置不同检索策略模板,根据任务类型自动调整检索权重、片段长度和策略组合。例如在医疗场景中偏向语义检索,而在金融新闻场景中更重视快速关键词匹配。
|
||||
2. 目的与效果:动态调整检索策略使RAG模型更加灵活,能够适应不同任务需求,确保检索的精准性和生成答案的上下文适配性,显著提升多场景下的用户体验。
|
||||
- 借助Haystack等检索优化框架
|
||||
1. 具体实施:在RAG模型中集成Haystack框架,以实现更高效的检索效果,并利用框架中的插件生态系统来增强检索模块的可扩展性和可调节性。
|
||||
2. 目的与效果:Haystack提供了检索和生成的整合接口,有助于快速优化检索模块,并适应复杂多样的用户需求,在多任务环境中提供更稳定的性能表现。
|
||||
|
||||
## 5.4 回答生成与优化
|
||||
|
||||
在RAG模型中,生成器负责基于检索模块提供的上下文,为用户查询生成自然语言答案。生成内容的准确性和逻辑性直接决定了用户的体验,因此优化生成器的表现至关重要。通过引入知识图谱等结构化信息,生成器能够更准确地理解和关联上下文,从而生成逻辑连贯、准确的回答。此外,生成器的生成逻辑可结合用户反馈持续优化,使回答风格和内容更加符合用户需求。
|
||||
|
||||
- 挑战:
|
||||
- 在回答生成过程中,RAG模型仍面临以下不足:
|
||||
- 上下文不充分导致的逻辑不连贯
|
||||
1. 当生成器在上下文缺失或信息不完整的情况下生成回答时,生成内容往往不够连贯,特别是在处理复杂、跨领域任务时。这种缺乏上下文支持的问题,容易导致生成器误解或忽略关键信息,最终生成内容的逻辑性和完整性欠佳。如在医学场景中,若生成器缺少对病例或症状的全面理解,可能导致回答不准确或不符合逻辑,影响专业性和用户信任度。
|
||||
- 专业领域回答的准确性欠佳
|
||||
1. 在医学、法律等高专业领域中,生成器的回答需要高度的准确性。然而,生成器可能因缺乏特定知识而生成不符合领域要求的回答,出现内容偏差或理解错误,尤其在涉及专业术语和复杂概念时更为明显。如在法律咨询中,生成器可能未能正确引用相关法条或判例,导致生成的答案不够精确,甚至可能产生误导。
|
||||
- 难以有效整合多轮用户反馈
|
||||
1. 生成器缺乏有效机制来利用多轮用户反馈进行自我优化。用户反馈可能涉及回答内容的准确性、逻辑性以及风格适配等方面,但生成器在连续对话中缺乏充分的调节机制,难以持续调整生成策略和回答风格。如在客服场景中,生成器可能连续生成不符合用户需求的回答,降低了用户满意度。
|
||||
- 生成内容的可控性和一致性不足
|
||||
1. 在特定领域回答生成中,生成器的输出往往不具备足够的可控性和一致性。由于缺乏领域特定的生成规则和约束,生成内容的专业性和风格一致性欠佳,难以满足高要求的应用场景。如在金融报告生成中,生成内容需要确保一致的风格和术语使用,否则会影响输出的专业性和可信度。
|
||||
- 改进:
|
||||
- 针对以上不足,可以从以下方面优化回答生成模块:
|
||||
- 引入知识图谱与结构化数据,增强上下文理解
|
||||
1. 具体实施:结合知识图谱或知识库,将医学、法律等专业领域的信息整合到生成过程中。生成器在生成回答时,可以从知识图谱中提取关键信息和关联知识点,确保回答具备连贯的逻辑链条。
|
||||
2. 目的与效果:知识图谱的引入提升了生成内容的连贯性和准确性,尤其在高专业性领域中,通过丰富的上下文理解,使生成器能够产生符合逻辑的回答。
|
||||
- 设计专业领域特定的生成规则和约束
|
||||
1. 具体实施:在生成模型中加入领域特定的生成规则和用语约束,特别针对医学、法律等领域的常见问答场景,设定回答模板、术语库等,以提高生成内容的准确性和一致性。
|
||||
2. 目的与效果:生成内容更具领域特征,输出风格和内容的专业性增强,有效降低了生成器在专业领域中的回答偏差,满足用户对专业性和可信度的要求。
|
||||
- 优化用户反馈机制,实现动态生成逻辑调整
|
||||
1. 具体实施:利用机器学习算法对用户反馈进行分析,从反馈中提取生成错误或用户需求的调整信息,动态调节生成器的生成逻辑和策略。同时,在多轮对话中逐步适应用户的需求和风格偏好。
|
||||
2. 目的与效果:用户反馈的高效利用能够帮助生成器优化生成内容,提高连续对话中的响应质量,提升用户体验,并使回答更贴合用户需求。
|
||||
- 引入生成器与检索器的协同优化机制
|
||||
1. 具体实施:通过协同优化机制,在生成器生成答案之前,允许生成器请求检索器补充缺失的上下文信息。生成器可基于回答需求自动向检索器发起上下文补充请求,从而获取完整的上下文。
|
||||
2. 目的与效果:协同优化机制保障了生成器在回答时拥有足够的上下文支持,避免信息断层或缺失,提升回答的完整性和准确性。
|
||||
- 实施生成内容的一致性检测和语义校正
|
||||
1. 具体实施:通过一致性检测算法对生成内容进行术语、风格的统一管理,并结合语义校正模型检测生成内容是否符合用户需求的逻辑结构。在复杂回答生成中,使用语义校正对不符合逻辑的生成内容进行自动优化。
|
||||
2. 目的与效果:生成内容具备高度一致性和逻辑性,特别是在多轮对话和专业领域生成中,保障了内容的稳定性和专业水准,提高了生成答案的可信度和用户满意度。
|
||||
|
||||
## 5.5 RAG流程
|
||||
|
||||

|
||||
|
||||
1. 数据加载与查询输入:
|
||||
1. 用户通过界面或API提交自然语言查询,系统接收查询作为输入。
|
||||
2. 输入被传递至向量化器,利用向量化技术(如BERT或Sentence Transformer)将自然语言查询转换为向量表示。
|
||||
2. 文档检索:
|
||||
1. 向量化后的查询会传递给检索器,检索器通过在知识库中查找最相关的文档片段。
|
||||
2. 检索可以基于稀疏检索技术(如BM25)或密集检索技术(如DPR)来提高匹配效率和精度。
|
||||
3. 生成器处理与自然语言生成:
|
||||
1. 检索到的文档片段作为生成器的输入,生成器(如GPT、BART或T5)基于查询和文档内容生成自然语言回答。
|
||||
2. 生成器结合了外部检索结果和预训练模型的语言知识,使回答更加精准、自然。
|
||||
4. 结果输出:
|
||||
1. 系统生成的答案通过API或界面返回给用户,确保答案连贯且知识准确。
|
||||
5. 反馈与优化:
|
||||
1. 用户可以对生成的答案进行反馈,系统根据反馈优化检索与生成过程。
|
||||
2. 通过微调模型参数或调整检索权重,系统逐步改进其性能,确保未来查询时更高的准确性与效率。
|
||||
|
||||
# 6. RAG相关案例整合
|
||||
|
||||
[各种分类领域下的RAG](https://github.com/hymie122/RAG-Survey)
|
||||
|
||||
# 7. RAG模型的应用
|
||||
|
||||
RAG模型已在多个领域得到广泛应用,主要包括:
|
||||
|
||||
## 7.1 智能问答系统中的应用
|
||||
|
||||
- RAG通过实时检索外部知识库,生成包含准确且详细的答案,避免传统生成模型可能产生的错误信息。例如,在医疗问答系统中,RAG能够结合最新的医学文献,生成包含最新治疗方案的准确答案,避免生成模型提供过时或错误的建议。这种方法帮助医疗专家快速获得最新的研究成果和诊疗建议,提升医疗决策的质量。
|
||||
- [医疗问答系统案例](https://www.apexon.com/blog/empowering-discovery-the-role-of-rag-architecture-generative-ai-in-healthcare-life-sciences/)
|
||||
- 
|
||||
- 用户通过Web应用程序发起查询:
|
||||
1. 用户在一个Web应用上输入查询请求,这个请求进入后端系统,启动了整个数据处理流程。
|
||||
- 使用Azure AD进行身份验证:
|
||||
1. 系统通过Azure Active Directory (Azure AD) 对用户进行身份验证,确保只有经过授权的用户才能访问系统和数据。
|
||||
- 用户权限检查:
|
||||
1. 系统根据用户的组权限(由Azure AD管理)过滤用户能够访问的内容。这个步骤保证了用户只能看到他们有权限查看的信息。
|
||||
- Azure AI搜索服务:
|
||||
1. 过滤后的用户查询被传递给Azure AI搜索服务,该服务会在已索引的数据库或文档中查找与查询相关的内容。这个搜索引擎通过语义搜索技术检索最相关的信息。
|
||||
- 文档智能处理:
|
||||
1. 系统使用OCR(光学字符识别)和文档提取等技术处理输入的文档,将非结构化数据转换为结构化、可搜索的数据,便于Azure AI进行检索。
|
||||
- 文档来源:
|
||||
1. 这些文档来自预先存储的输入文档集合,这些文档在被用户查询之前已经通过文档智能处理进行了准备和索引。
|
||||
- Azure Open AI生成响应:
|
||||
1. 在检索到相关信息后,数据会被传递到Azure Open AI,该模块利用自然语言生成(NLG)技术,根据用户的查询和检索结果生成连贯的回答。
|
||||
- 响应返回用户:
|
||||
1. 最终生成的回答通过Web应用程序返回给用户,完成整个查询到响应的流程。
|
||||
- 整个流程展示了Azure AI技术的集成,通过文档检索、智能处理以及自然语言生成来处理复杂的查询,并确保了数据的安全和合规性。
|
||||
|
||||
## 7.2 信息检索与文本生成
|
||||
|
||||
- 文本生成:RAG不仅可以检索相关文档,还能根据这些文档生成总结、报告或文档摘要,从而增强生成内容的连贯性和准确性。例如,法律领域中,RAG可以整合相关法条和判例,生成详细的法律意见书,确保内容的全面性和严谨性。这在法律咨询和文件生成过程中尤为重要,可以帮助律师和法律从业者提高工作效率。
|
||||
- [法律领域检索增强生成案例](https://www.apexon.com/blog/empowering-discovery-the-role-of-rag-architecture-generative-ai-in-healthcare-life-sciences/)
|
||||
- 内容总结:
|
||||
- 背景: 传统的大语言模型 (LLMs) 在生成任务中表现优异,但在处理法律领域中的复杂任务时存在局限。法律文档具有独特的结构和术语,标准的检索评估基准往往无法充分捕捉这些领域特有的复杂性。为了弥补这一不足,LegalBench-RAG 旨在提供一个评估法律文档检索效果的专用基准。
|
||||
- LegalBench-RAG 的结构:
|
||||
1. 
|
||||
2. 工作流程:
|
||||
3. 用户输入问题(Q: ?,A: ?):用户通过界面输入查询问题,提出需要答案的具体问题。
|
||||
4. 嵌入与检索模块(Embed + Retrieve):该模块接收到用户的查询后,会对问题进行嵌入(将其转化为向量),并在外部知识库或文档中执行相似度检索。通过检索算法,系统找到与查询相关的文档片段或信息。
|
||||
5. 生成答案(A):基于检索到的最相关信息,生成模型(如GPT或类似的语言模型)根据检索的结果生成连贯的自然语言答案。
|
||||
6. 对比和返回结果:生成的答案会与之前的相关问题答案进行对比,并最终将生成的答案返回给用户。
|
||||
7. 该基准基于 LegalBench 的数据集,构建了 6858 个查询-答案对,并追溯到其原始法律文档的确切位置。
|
||||
8. LegalBench-RAG 侧重于精确地检索法律文本中的小段落,而非宽泛的、上下文不相关的片段。
|
||||
9. 数据集涵盖了合同、隐私政策等不同类型的法律文档,确保涵盖多个法律应用场景。
|
||||
- 意义: LegalBench-RAG 是第一个专门针对法律检索系统的公开可用的基准。它为研究人员和公司提供了一个标准化的框架,用于比较不同的检索算法的效果,特别是在需要高精度的法律任务中,例如判决引用、条款解释等。
|
||||
- 关键挑战:
|
||||
1. RAG 系统的生成部分依赖检索到的信息,错误的检索结果可能导致错误的生成输出。
|
||||
2. 法律文档的长度和术语复杂性增加了模型检索和生成的难度。
|
||||
- 质量控制: 数据集的构建过程确保了高质量的人工注释和文本精确性,特别是在映射注释类别和文档ID到具体文本片段时进行了多次人工校验。
|
||||
|
||||
## 7.3 其它应用场景
|
||||
|
||||
RAG还可以应用于多模态生成场景,如图像、音频和3D内容生成。例如,跨模态应用如ReMoDiffuse和Make-An-Audio利用RAG技术实现不同数据形式的生成。此外,在企业决策支持中,RAG能够快速检索外部资源(如行业报告、市场数据),生成高质量的前瞻性报告,从而提升企业战略决策的能力。
|
||||
|
||||
## 8 总结
|
||||
|
||||
本文档系统阐述了检索增强生成(RAG)模型的核心机制、优势与应用场景。通过结合生成模型与检索模型,RAG解决了传统生成模型在面对事实性任务时的“编造”问题和检索模型难以生成连贯自然语言输出的不足。RAG模型能够实时从外部知识库获取信息,使生成内容既包含准确的知识,又具备流畅的语言表达,适用于医疗、法律、智能问答系统等多个知识密集型领域。
|
||||
|
||||
在应用实践中,RAG模型虽然有着信息完整性、推理能力和跨领域适应性等显著优势,但也面临着数据质量、计算资源消耗和知识库更新等挑战。为进一步提升RAG的性能,提出了针对数据采集、内容分块、检索策略优化以及回答生成的全面改进措施,如引入知识图谱、优化用户反馈机制、实施高效去重算法等,以增强模型的适用性和效率。
|
||||
|
||||
RAG在智能问答、信息检索与文本生成等领域展现了出色的应用潜力,并在不断发展的技术支持下进一步拓展至多模态生成和企业决策支持等场景。通过引入混合检索技术、知识图谱以及动态反馈机制,RAG能够更加灵活地应对复杂的用户需求,生成具有事实支撑和逻辑连贯性的回答。未来,RAG将通过增强模型透明性与可控性,进一步提升在专业领域中的可信度和实用性,为智能信息检索与内容生成提供更广泛的应用空间。
|
||||
@@ -45,7 +45,7 @@ curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "导演是谁",
|
||||
"content": "导演是谁"
|
||||
}
|
||||
]
|
||||
}'
|
||||
@@ -526,7 +526,8 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/getHistories
|
||||
--data-raw '{
|
||||
"appId": "appId",
|
||||
"offset": 0,
|
||||
"pageSize": 20
|
||||
"pageSize": 20,
|
||||
"source: "api"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -540,6 +541,7 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/getHistories
|
||||
- appId - 应用 Id
|
||||
- offset - 偏移量,即从第几条数据开始取
|
||||
- pageSize - 记录数量
|
||||
- source - 对话源
|
||||
{{% /alert %}}
|
||||
|
||||
{{< /markdownify >}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'V4.8.12(进行中)'
|
||||
title: 'V4.8.12(需要初始化)'
|
||||
description: 'FastGPT V4.8.12 更新说明'
|
||||
icon: 'upgrade'
|
||||
draft: false
|
||||
@@ -13,14 +13,14 @@ weight: 812
|
||||
|
||||
### 2. 修改镜像
|
||||
|
||||
- 更新 FastGPT 镜像 tag: v4.8.12-beta
|
||||
- 更新 FastGPT 商业版镜像 tag: v4.8.12-beta
|
||||
- 更新 FastGPT 镜像 tag: v4.8.12-fix
|
||||
- 更新 FastGPT 管理端镜像 tag: v4.8.12 (fastgpt-pro镜像)
|
||||
- Sandbox 镜像,可以不更新
|
||||
|
||||
|
||||
### 3. 商业版执行初始化
|
||||
|
||||
从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`;{{host}} 替换成**FastGPT 商业版域名**。
|
||||
从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`;{{host}} 替换成**FastGPT 管理端域名**。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://{{host}}/api/admin/init/4812' \
|
||||
@@ -30,6 +30,18 @@ curl --location --request POST 'https://{{host}}/api/admin/init/4812' \
|
||||
|
||||
会初始化应用和知识库的成员组数据。
|
||||
|
||||
### 4. 重构 Milvus 数据
|
||||
|
||||
由于 js int64 精度丢失问题,之前私有化使用 milvus 或者 zilliz 的用户,如果存在数据精度丢失的问题,需要重构 Milvus 数据。(可以查看 dataset_datas 表中,indexes 中的 dataId 是否末尾精度丢失)。使用 PG 的用户不需要操作。
|
||||
|
||||
从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`;{{host}} 替换成**FastGPT 主域名**。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://{{host}}/api/admin/resetMilvus' \
|
||||
--header 'rootkey: {{rootkey}}' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## 更新说明
|
||||
|
||||
1. 新增 - 全局变量支持数字类型,支持配置默认值和部分输入框参数。
|
||||
@@ -45,10 +57,12 @@ curl --location --request POST 'https://{{host}}/api/admin/init/4812' \
|
||||
11. 新增 - HTTP 节点支持 JSONPath 表达式
|
||||
12. 新增 - 应用和知识库支持成员组配置权限
|
||||
13. 优化 - 循环节点支持选择外部节点的变量
|
||||
14. 修复 - 文件后缀判断,去除 query 影响。
|
||||
15. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。
|
||||
16. 修复 - 用户交互节点未阻塞流程。
|
||||
17. 修复 - 新建 APP,有时候会导致空指针报错。
|
||||
18. 修复 - 拥有多个循环节点时,错误运行。
|
||||
19. 修复 - 循环节点中修改变量,无法传递。
|
||||
20. 修复 - 非 stream 模式,嵌套子应用/插件执行时无法获取子应用响应。
|
||||
14. 优化 - Docx 文件读取中, HTML to Markdown 优化,提高速度和大幅度降低内存消耗。
|
||||
15. 修复 - 文件后缀判断,去除 query 影响。
|
||||
16. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。
|
||||
17. 修复 - 用户交互节点未阻塞流程。
|
||||
18. 修复 - 新建 APP,有时候会导致空指针报错。
|
||||
19. 修复 - 拥有多个循环节点时,错误运行。
|
||||
20. 修复 - 循环节点中修改变量,无法传递。
|
||||
21. 修复 - 非 stream 模式,嵌套子应用/插件执行时无法获取子应用响应。
|
||||
22. 修复 - 数据分块策略,同时将每个 Markdown 独立分块。
|
||||
|
||||
25
docSite/content/zh-cn/docs/development/upgrading/4813.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: 'V4.8.13(进行中)'
|
||||
description: 'FastGPT V4.8.13 更新说明'
|
||||
icon: 'upgrade'
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 811
|
||||
---
|
||||
|
||||
## 更新说明
|
||||
|
||||
1. 新增 - 数组变量选择支持多选,可以选多个数组或对应的单一数据类型,会自动按选择顺序进行合并。
|
||||
2. 新增 - 文件上传方案调整,节点直接支持接收文件链接,插件自定义变量支持文件上传。
|
||||
3. 新增 - 对话记录增加时间显示。
|
||||
4. 新增 - 工作流校验错误时,跳转至错误节点。
|
||||
5. 新增 - 循环节点增加下标值。
|
||||
6. 新增 - 部分对话错误提醒增加翻译。
|
||||
7. 优化 - 合并多个 system 提示词成 1 个,避免部分模型不支持多个 system 提示词。
|
||||
8. 优化 - 知识库上传文件,优化报错提示。
|
||||
9. 优化 - 全文检索语句,减少一轮查询。
|
||||
10. 优化 - 修改 findLast 为 [...array].reverse().find,适配旧版浏览器。
|
||||
11. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。
|
||||
12. 优化 - 工作流上下文拆分,性能优化。
|
||||
13. 修复 - Dockerfile pnpm install 支持代理。
|
||||
14. 修复 - BI 图表生成无法写入文件。
|
||||
297
docSite/content/zh-cn/docs/workflow/modules/loop.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
title: "循环运行"
|
||||
description: "FastGPT 循环运行节点介绍和使用"
|
||||
icon: "input"
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 366
|
||||
---
|
||||
|
||||
## 节点概述
|
||||
|
||||
【**循环运行**】节点是 FastGPT V4.8.11 版本新增的一个重要功能模块。它允许工作流对数组类型的输入数据进行迭代处理,每次处理数组中的一个元素,并自动执行后续节点,直到完成整个数组的处理。
|
||||
|
||||
这个节点的设计灵感来自编程语言中的循环结构,但以可视化的方式呈现。
|
||||
|
||||

|
||||
|
||||
> 在程序中,节点可以理解为一个个 Function 或者接口。可以理解为它就是一个**步骤**。将多个节点一个个拼接起来,即可一步步的去实现最终的 AI 输出。
|
||||
|
||||
【**循环运行**】节点本质上也是一个 Function,它的主要职责是自动化地重复执行特定的工作流程。
|
||||
|
||||
## 核心特性
|
||||
|
||||
1. **数组批量处理**
|
||||
- 支持输入数组类型数据
|
||||
- 自动遍历数组元素
|
||||
- 保持处理顺序
|
||||
- 支持并行处理 (性能优化)
|
||||
|
||||
2. **自动迭代执行**
|
||||
- 自动触发后续节点
|
||||
- 支持条件终止
|
||||
- 支持循环计数
|
||||
- 维护执行上下文
|
||||
|
||||
3. **与其他节点协同**
|
||||
- 支持与 AI 对话节点配合
|
||||
- 支持与 HTTP 节点配合
|
||||
- 支持与内容提取节点配合
|
||||
- 支持与判断器节点配合
|
||||
|
||||
## 应用场景
|
||||
|
||||
【**循环运行**】节点的主要作用是通过自动化的方式扩展工作流的处理能力,使 FastGPT 能够更好地处理批量任务和复杂的数据处理流程。特别是在处理大规模数据或需要多轮迭代的场景下,循环运行节点能显著提升工作流的效率和自动化程度。
|
||||
|
||||
【**循环运行**】节点特别适合以下场景:
|
||||
|
||||
1. **批量数据处理**
|
||||
- 批量翻译文本
|
||||
- 批量总结文档
|
||||
- 批量生成内容
|
||||
|
||||
2. **数据流水线处理**
|
||||
- 对搜索结果逐条分析
|
||||
- 对知识库检索结果逐条处理
|
||||
- 对 HTTP 请求返回的数组数据逐项处理
|
||||
|
||||
3. **递归或迭代任务**
|
||||
- 长文本分段处理
|
||||
- 多轮优化内容
|
||||
- 链式数据处理
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 输入参数设置
|
||||
|
||||
【**循环运行**】节点需要配置两个核心输入参数:
|
||||
|
||||
1. **数组 (必填)**:接收一个数组类型的输入,可以是:
|
||||
- 字符串数组 (`Array<string>`)
|
||||
- 数字数组 (`Array<number>`)
|
||||
- 布尔数组 (`Array<boolean>`)
|
||||
- 对象数组 (`Array<object>`)
|
||||
|
||||
2. **循环体 (必填)**:定义每次循环需要执行的节点流程,包含:
|
||||
- 循环体开始:标记循环开始的位置。
|
||||
- 循环体结束:标记循环结束的位置,并可选择输出结果变量。
|
||||
|
||||
### 循环体配置
|
||||
|
||||

|
||||
|
||||
1. 在循环体内部,可以添加任意类型的节点,如:
|
||||
- AI 对话节点
|
||||
- HTTP 请求节点
|
||||
- 内容提取节点
|
||||
- 文本加工节点等
|
||||
|
||||
2. 循环体结束节点配置:
|
||||
- 通过下拉菜单选择要输出的变量
|
||||
- 该变量将作为当前循环的结果被收集
|
||||
- 所有循环的结果将组成一个新的数组作为最终输出
|
||||
|
||||
## 场景示例
|
||||
|
||||
### 批量处理数组
|
||||
|
||||
假设我们有一个包含多个文本的数组,需要对每个文本进行 AI 处理。这是循环运行节点最基础也最常见的应用场景。
|
||||
|
||||
#### 实现步骤
|
||||
|
||||
1. 准备输入数组
|
||||
|
||||

|
||||
|
||||
使用【代码运行】节点创建测试数组:
|
||||
|
||||
```javascript
|
||||
const texts = [
|
||||
"这是第一段文本",
|
||||
"这是第二段文本",
|
||||
"这是第三段文本"
|
||||
];
|
||||
return { textArray: texts };
|
||||
```
|
||||
|
||||
2. 配置循环运行节点
|
||||
|
||||

|
||||
|
||||
- 数组输入:选择上一步代码运行节点的输出变量 `textArray`。
|
||||
- 循环体内添加一个【AI 对话】节点,用于处理每个文本。这里我们输入的 prompt 为:`请将这段文本翻译成英文`。
|
||||
- 再添加一个【指定回复】节点,用于输出翻译后的文本。
|
||||
- 循环体结束节点选择输出变量为 AI 回复内容。
|
||||
|
||||
#### 运行流程
|
||||
|
||||

|
||||
|
||||
1. 【代码运行】节点执行,生成测试数组
|
||||
2. 【循环运行】节点接收数组,开始遍历
|
||||
3. 对每个数组元素:
|
||||
- 【AI 对话】节点处理当前元素
|
||||
- 【指定回复】节点输出翻译后的文本
|
||||
- 【循环体结束】节点收集处理结果
|
||||
4. 完成所有元素处理后,输出结果数组
|
||||
|
||||
### 长文本翻译
|
||||
|
||||
在处理长文本翻译时,我们经常会遇到以下挑战:
|
||||
|
||||
- 文本长度超出 LLM 的 token 限制
|
||||
- 需要保持翻译风格的一致性
|
||||
- 需要维护上下文的连贯性
|
||||
- 翻译质量需要多轮优化
|
||||
|
||||
【**循环运行**】节点可以很好地解决这些问题。
|
||||
|
||||
#### 实现步骤
|
||||
|
||||
1. 文本预处理与分段
|
||||
|
||||

|
||||
|
||||
使用【代码运行】节点进行文本分段,代码如下:
|
||||
|
||||
```javascript
|
||||
const MAX_HEADING_LENGTH = 7; // 最大标题长度
|
||||
const MAX_HEADING_CONTENT_LENGTH = 200; // 最大标题内容长度
|
||||
const MAX_HEADING_UNDERLINE_LENGTH = 200; // 最大标题下划线长度
|
||||
const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100; // 最大HTML标题属性长度
|
||||
const MAX_LIST_ITEM_LENGTH = 200; // 最大列表项长度
|
||||
const MAX_NESTED_LIST_ITEMS = 6; // 最大嵌套列表项数
|
||||
const MAX_LIST_INDENT_SPACES = 7; // 最大列表缩进空格数
|
||||
const MAX_BLOCKQUOTE_LINE_LENGTH = 200; // 最大块引用行长度
|
||||
const MAX_BLOCKQUOTE_LINES = 15; // 最大块引用行数
|
||||
const MAX_CODE_BLOCK_LENGTH = 1500; // 最大代码块长度
|
||||
const MAX_CODE_LANGUAGE_LENGTH = 20; // 最大代码语言长度
|
||||
const MAX_INDENTED_CODE_LINES = 20; // 最大缩进代码行数
|
||||
const MAX_TABLE_CELL_LENGTH = 200; // 最大表格单元格长度
|
||||
const MAX_TABLE_ROWS = 20; // 最大表格行数
|
||||
const MAX_HTML_TABLE_LENGTH = 2000; // 最大HTML表格长度
|
||||
const MIN_HORIZONTAL_RULE_LENGTH = 3; // 最小水平分隔线长度
|
||||
const MAX_SENTENCE_LENGTH = 400; // 最大句子长度
|
||||
const MAX_QUOTED_TEXT_LENGTH = 300; // 最大引用文本长度
|
||||
const MAX_PARENTHETICAL_CONTENT_LENGTH = 200; // 最大括号内容长度
|
||||
const MAX_NESTED_PARENTHESES = 5; // 最大嵌套括号数
|
||||
const MAX_MATH_INLINE_LENGTH = 100; // 最大行内数学公式长度
|
||||
const MAX_MATH_BLOCK_LENGTH = 500; // 最大数学公式块长度
|
||||
const MAX_PARAGRAPH_LENGTH = 1000; // 最大段落长度
|
||||
const MAX_STANDALONE_LINE_LENGTH = 800; // 最大独立行长度
|
||||
const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100; // 最大HTML标签属性长度
|
||||
const MAX_HTML_TAG_CONTENT_LENGTH = 1000; // 最大HTML标签内容长度
|
||||
const LOOKAHEAD_RANGE = 100; // 向前查找句子边界的字符数
|
||||
|
||||
const AVOID_AT_START = `[\\s\\]})>,']`; // 避免在开头匹配的字符
|
||||
const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`; // 标点符号
|
||||
const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`; // 引号结束
|
||||
const SENTENCE_END = `(?:${PUNCTUATION}(?<!${AVOID_AT_START}(?=${PUNCTUATION}))|${QUOTE_END})(?=\\S|$)`; // 句子结束
|
||||
const SENTENCE_BOUNDARY = `(?:${SENTENCE_END}|(?=[\\r\\n]|$))`; // 句子边界
|
||||
const LOOKAHEAD_PATTERN = `(?:(?!${SENTENCE_END}).){1,${LOOKAHEAD_RANGE}}${SENTENCE_END}`; // 向前查找句子结束的模式
|
||||
const NOT_PUNCTUATION_SPACE = `(?!${PUNCTUATION}\\s)`; // 非标点符号空格
|
||||
const SENTENCE_PATTERN = `${NOT_PUNCTUATION_SPACE}(?:[^\\r\\n]{1,{MAX_LENGTH}}${SENTENCE_BOUNDARY}|[^\\r\\n]{1,{MAX_LENGTH}}(?=${PUNCTUATION}|$ {QUOTE_END})(?:${LOOKAHEAD_PATTERN})?)${AVOID_AT_START}*`; // 句子模式
|
||||
|
||||
const regex = new RegExp(
|
||||
"(" +
|
||||
// 1. Headings (Setext-style, Markdown, and HTML-style, with length constraints)
|
||||
`(?:^(?:[#*=-]{1,${MAX_HEADING_LENGTH}}|\\w[^\\r\\n]{0,${MAX_HEADING_CONTENT_LENGTH}}\\r?\\n[-=]{2,${MAX_HEADING_UNDERLINE_LENGTH}}|<h[1-6][^>] {0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:</h[1-6]>)?(?:\\r?\\n|$))` +
|
||||
"|" +
|
||||
// New pattern for citations
|
||||
`(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` +
|
||||
"|" +
|
||||
// 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}` +
|
||||
`(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` +
|
||||
`(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` +
|
||||
"|" +
|
||||
// 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints)
|
||||
`(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,$ {MAX_BLOCKQUOTE_LINES}})` +
|
||||
"|" +
|
||||
// 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` +
|
||||
`|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,$ {MAX_INDENTED_CODE_LINES}}\\r?\\n?)` +
|
||||
`|(?:<pre>(?:<code>)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:</code>)?</pre>))` +
|
||||
"|" +
|
||||
// 5. Tables (Markdown, grid tables, and HTML tables, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,$ {MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` +
|
||||
`|<table>[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?</table>))` +
|
||||
"|" +
|
||||
// 6. Horizontal rules (Markdown and HTML hr tag)
|
||||
`(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|<hr\\s*/?>)` +
|
||||
"|" +
|
||||
// 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints)
|
||||
`(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_STANDALONE_LINE_LENGTH))}(?:</[a-zA-Z]+>)?(?:\\r?\\n|$))` +
|
||||
"|" +
|
||||
// 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation)
|
||||
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` +
|
||||
"|" +
|
||||
// 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints)
|
||||
"(?:" +
|
||||
`(?<!\\w)\"\"\"[^\"]{0,${MAX_QUOTED_TEXT_LENGTH}}\"\"\"(?!\\w)` +
|
||||
`|(?<!\\w)(?:['\"\`'"])[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}\\1(?!\\w)` +
|
||||
`|(?<!\\w)\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}'(?!\\w)` +
|
||||
`|(?<!\\w)\`\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}''(?!\\w)` +
|
||||
`|\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\)[^\\r\\n()]{0,$ {MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\)` +
|
||||
`|\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\][^\\r\\n\\[\\]]{0,$ {MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\]` +
|
||||
`|\\$[^\\r\\n$]{0,${MAX_MATH_INLINE_LENGTH}}\\$` +
|
||||
`|\`[^\`\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\`` +
|
||||
")" +
|
||||
"|" +
|
||||
// 9. Paragraphs (with length constraints)
|
||||
`(?!${AVOID_AT_START})(?:(?:^|\\r?\\n\\r?\\n)(?:<p>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:</p>)?(?=\\r? \\n\\r?\\n|$))` +
|
||||
"|" +
|
||||
// 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints)
|
||||
`(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?</[a-zA-Z]+>|\\s*/>))` +
|
||||
"|" +
|
||||
// 12. LaTeX-style math expressions (inline and block, with length constraints)
|
||||
`(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` +
|
||||
"|" +
|
||||
// 14. Fallback for any remaining content (with length constraints)
|
||||
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` +
|
||||
")",
|
||||
"gmu"
|
||||
);
|
||||
|
||||
function main({text}){
|
||||
const chunks = [];
|
||||
let currentChunk = '';
|
||||
const tokens = countToken(text)
|
||||
|
||||
const matches = text.match(regex);
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
if (currentChunk.length + match.length <= 1000) {
|
||||
currentChunk += match;
|
||||
} else {
|
||||
if (currentChunk) {
|
||||
chunks.push(currentChunk);
|
||||
}
|
||||
currentChunk = match;
|
||||
}
|
||||
});
|
||||
if (currentChunk) {
|
||||
chunks.push(currentChunk);
|
||||
}
|
||||
}
|
||||
|
||||
return {chunks, tokens};
|
||||
}
|
||||
```
|
||||
|
||||
这里我们用到了 [Jina AI 开源的一个强大的正则表达式](https://x.com/JinaAI_/status/1823756993108304135),它能利用所有可能的边界线索和启发式方法来精确切分文本。
|
||||
|
||||
2. 配置循环运行节点
|
||||
|
||||

|
||||
|
||||
- 数组输入:选择上一步代码运行节点的输出变量 `chunks`。
|
||||
- 循环体内添加一个【代码运行】节点,对源文本进行格式化。
|
||||
- 添加一个【搜索词库】节点,将专有名词的词库作为知识库,在翻译前进行搜索。
|
||||
- 添加一个【AI 对话】节点,使用 CoT 思维链,让 LLM 显式地、系统地生成推理链条,展示翻译的完整思考过程。
|
||||
- 添加一个【代码运行】节点,将【AI 对话】节点最后一轮的翻译结果提取出来。
|
||||
- 添加一个【指定回复】节点,输出翻译后的文本。
|
||||
- 循环体结束节点选择输出变量为【取出翻译文本】的输出变量 `result`。
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
{{ $.Scratch.Delete "social_list" }}
|
||||
{{ $.Scratch.Set "pathName" (printf "%s" (.Site.Params.docs.pathName | default "docs")) }}
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-W9HPZZ22');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<!-- social_list -->
|
||||
<!-- change -->
|
||||
{{ $social_params := slice "github" "twitter" "instagram" "rss" "wechat" "lark" }}
|
||||
@@ -12,6 +19,10 @@
|
||||
<html lang="{{ site.LanguageCode }}">
|
||||
{{- partial (printf "%s/%s" ($.Scratch.Get "pathName") "head.html") . -}}
|
||||
<body>
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-W9HPZZ22"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
<div class="content">
|
||||
<div class="page-wrapper toggled">
|
||||
{{- partial (printf "%s/%s" ($.Scratch.Get "pathName") "sidebar.html") . -}}
|
||||
|
||||
@@ -139,6 +139,8 @@ services:
|
||||
- OPENAI_BASE_URL=http://oneapi:3000/v1
|
||||
# AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改)
|
||||
- CHAT_API_KEY=sk-fastgpt
|
||||
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
|
||||
- MULTIPLE_DATA_TO_BASE64=false
|
||||
# 数据库最大连接数
|
||||
- DB_MAX_LINK=30
|
||||
# 登录凭证密钥
|
||||
|
||||
@@ -97,6 +97,8 @@ services:
|
||||
- OPENAI_BASE_URL=http://oneapi:3000/v1
|
||||
# AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改)
|
||||
- CHAT_API_KEY=sk-fastgpt
|
||||
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
|
||||
- MULTIPLE_DATA_TO_BASE64=false
|
||||
# 数据库最大连接数
|
||||
- DB_MAX_LINK=30
|
||||
# 登录凭证密钥
|
||||
|
||||
@@ -77,6 +77,8 @@ services:
|
||||
- OPENAI_BASE_URL=http://oneapi:3000/v1
|
||||
# AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改)
|
||||
- CHAT_API_KEY=sk-fastgpt
|
||||
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
|
||||
- MULTIPLE_DATA_TO_BASE64=false
|
||||
# 数据库最大连接数
|
||||
- DB_MAX_LINK=30
|
||||
# 登录凭证密钥
|
||||
|
||||
@@ -16,6 +16,8 @@ export const bucketNameMap = {
|
||||
}
|
||||
};
|
||||
|
||||
export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}/api/common/file/read`;
|
||||
export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}${process.env.NEXT_PUBLIC_BASE_URL}/api/common/file/read`;
|
||||
|
||||
export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx';
|
||||
export const imageFileType =
|
||||
'.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif';
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { detect } from 'jschardet';
|
||||
import { documentFileType, imageFileType } from './constants';
|
||||
import { ChatFileTypeEnum } from '../../core/chat/constants';
|
||||
import { UserChatItemValueItemType } from '../../core/chat/type';
|
||||
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
@@ -13,3 +16,40 @@ export const formatFileSize = (bytes: number): string => {
|
||||
export const detectFileEncoding = (buffer: Buffer) => {
|
||||
return detect(buffer.slice(0, 200))?.encoding?.toLocaleLowerCase();
|
||||
};
|
||||
|
||||
// Url => user upload file type
|
||||
export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file'] | undefined => {
|
||||
if (typeof url !== 'string') return;
|
||||
const parseUrl = new URL(url, 'https://locaohost:3000');
|
||||
|
||||
const filename = (() => {
|
||||
// Old version file url: https://xxx.com/file/read?filename=xxx.pdf
|
||||
const filenameQuery = parseUrl.searchParams.get('filename');
|
||||
if (filenameQuery) return filenameQuery;
|
||||
|
||||
// Common file: https://xxx.com/xxx.pdf?xxxx=xxx
|
||||
const pathname = parseUrl.pathname;
|
||||
if (pathname) return pathname.split('/').pop();
|
||||
})();
|
||||
|
||||
if (!filename) return;
|
||||
|
||||
const extension = filename.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
if (!extension) return;
|
||||
|
||||
if (documentFileType.includes(extension)) {
|
||||
return {
|
||||
type: ChatFileTypeEnum.file,
|
||||
name: filename,
|
||||
url
|
||||
};
|
||||
}
|
||||
if (imageFileType.includes(extension)) {
|
||||
return {
|
||||
type: ChatFileTypeEnum.image,
|
||||
name: filename,
|
||||
url
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,9 +92,9 @@ ${mdSplitString}
|
||||
};
|
||||
|
||||
/*
|
||||
1. 自定义分隔符:不需要重叠
|
||||
2. Markdown 标题:不需要重叠;标题嵌套共享。
|
||||
3. 特殊 markdown 语法:不需要重叠
|
||||
1. 自定义分隔符:不需要重叠,不需要小块合并
|
||||
2. Markdown 标题:不需要重叠;标题嵌套共享,不需要小块合并
|
||||
3. 特殊 markdown 语法:不需要重叠,需要小块合并
|
||||
4. 段落:尽可能保证它是一个完整的段落。
|
||||
5. 标点分割:重叠
|
||||
*/
|
||||
@@ -118,10 +118,10 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
reg: new RegExp(`(${replaceRegChars(text)})`, 'g'),
|
||||
maxLen: chunkLen * 1.4
|
||||
})),
|
||||
{ reg: /^(#\s[^\n]+)\n/gm, maxLen: chunkLen * 1.2 },
|
||||
{ reg: /^(##\s[^\n]+)\n/gm, maxLen: chunkLen * 1.2 },
|
||||
{ reg: /^(###\s[^\n]+)\n/gm, maxLen: chunkLen * 1.2 },
|
||||
{ reg: /^(####\s[^\n]+)\n/gm, maxLen: chunkLen * 1.2 },
|
||||
{ reg: /^(#\s[^\n]+\n)/gm, maxLen: chunkLen * 1.2 },
|
||||
{ reg: /^(##\s[^\n]+\n)/gm, maxLen: chunkLen * 1.4 },
|
||||
{ reg: /^(###\s[^\n]+\n)/gm, maxLen: chunkLen * 1.6 },
|
||||
{ reg: /^(####\s[^\n]+\n)/gm, maxLen: chunkLen * 1.8 },
|
||||
|
||||
{ reg: /([\n]([`~]))/g, maxLen: chunkLen * 4 }, // code block
|
||||
{ reg: /([\n](?!\s*[\*\-|>0-9]))/g, maxLen: chunkLen * 2 }, // 增大块,尽可能保证它是一个完整的段落。 (?![\*\-|>`0-9]): markdown special char
|
||||
@@ -137,7 +137,6 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
const customRegLen = customReg.length;
|
||||
const checkIsCustomStep = (step: number) => step < customRegLen;
|
||||
const checkIsMarkdownSplit = (step: number) => step >= customRegLen && step <= 3 + customRegLen;
|
||||
const checkIndependentChunk = (step: number) => step >= customRegLen && step <= 4 + customRegLen;
|
||||
const checkForbidOverlap = (step: number) => step <= 6 + customRegLen;
|
||||
|
||||
// if use markdown title split, Separate record title
|
||||
@@ -153,7 +152,6 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
|
||||
const isCustomStep = checkIsCustomStep(step);
|
||||
const isMarkdownSplit = checkIsMarkdownSplit(step);
|
||||
const independentChunk = checkIndependentChunk(step);
|
||||
|
||||
const { reg } = stepReges[step];
|
||||
|
||||
@@ -162,7 +160,7 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
reg,
|
||||
(() => {
|
||||
if (isCustomStep) return splitMarker;
|
||||
if (independentChunk) return `${splitMarker}$1`;
|
||||
if (isMarkdownSplit) return `${splitMarker}$1`;
|
||||
return `$1${splitMarker}`;
|
||||
})()
|
||||
)
|
||||
@@ -178,7 +176,7 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
title: matchTitle
|
||||
};
|
||||
})
|
||||
.filter((item) => item.text.trim());
|
||||
.filter((item) => item.text?.trim());
|
||||
};
|
||||
|
||||
/* Gets the overlap at the end of a text as the beginning of the next block */
|
||||
@@ -214,15 +212,16 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
text = '',
|
||||
step,
|
||||
lastText,
|
||||
mdTitle = ''
|
||||
parentTitle = ''
|
||||
}: {
|
||||
text: string;
|
||||
step: number;
|
||||
lastText: string;
|
||||
mdTitle: string;
|
||||
lastText: string; // 上一个分块末尾数据会通过这个参数传入。
|
||||
parentTitle: string;
|
||||
}): string[] => {
|
||||
const independentChunk = checkIndependentChunk(step);
|
||||
const isMarkdownStep = checkIsMarkdownSplit(step);
|
||||
const isCustomStep = checkIsCustomStep(step);
|
||||
const forbidConcat = isMarkdownStep || isCustomStep; // forbid=true时候,lastText肯定为空
|
||||
|
||||
// oversize
|
||||
if (step >= stepReges.length) {
|
||||
@@ -232,7 +231,7 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
// use slice-chunkLen to split text
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < text.length; i += chunkLen - overlapLen) {
|
||||
chunks.push(`${mdTitle}${text.slice(i, i + chunkLen)}`);
|
||||
chunks.push(`${parentTitle}${text.slice(i, i + chunkLen)}`);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
@@ -242,67 +241,78 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
|
||||
const maxLen = splitTexts.length > 1 ? stepReges[step].maxLen : chunkLen;
|
||||
const minChunkLen = chunkLen * 0.7;
|
||||
const miniChunkLen = 30;
|
||||
// console.log(splitTexts, stepReges[step].reg);
|
||||
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < splitTexts.length; i++) {
|
||||
const item = splitTexts[i];
|
||||
const currentTitle = `${mdTitle}${item.title}`;
|
||||
|
||||
const lastTextLen = lastText.length;
|
||||
const currentText = item.text;
|
||||
const currentTextLen = currentText.length;
|
||||
const lastTextLen = lastText.length;
|
||||
const newText = lastText + currentText;
|
||||
const newTextLen = lastTextLen + currentTextLen;
|
||||
|
||||
// newText is too large(now, The lastText must be smaller than chunkLen)
|
||||
if (newTextLen > maxLen) {
|
||||
if (newTextLen > maxLen || isMarkdownStep) {
|
||||
// lastText greater minChunkLen, direct push it to chunks, not add to next chunk. (large lastText)
|
||||
if (lastTextLen > minChunkLen) {
|
||||
chunks.push(`${currentTitle}${lastText}`);
|
||||
lastText = getOneTextOverlapText({ text: lastText, step }); // next chunk will start with overlayText
|
||||
i--;
|
||||
chunks.push(lastText);
|
||||
|
||||
lastText = getOneTextOverlapText({ text: lastText, step }); // next chunk will start with overlayText
|
||||
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 说明是新的文本块比较大,需要进一步拆分
|
||||
|
||||
// split new Text, split chunks must will greater 1 (small lastText)
|
||||
const innerChunks = splitTextRecursively({
|
||||
text: newText,
|
||||
step: step + 1,
|
||||
lastText: '',
|
||||
mdTitle: currentTitle
|
||||
parentTitle: parentTitle + item.title
|
||||
});
|
||||
const lastChunk = innerChunks[innerChunks.length - 1];
|
||||
|
||||
if (!lastChunk) continue;
|
||||
|
||||
if (forbidConcat) {
|
||||
chunks.push(
|
||||
...innerChunks.map(
|
||||
(chunk) => (step === 3 + customRegLen ? `${parentTitle}${chunk}` : chunk) // 合并进 Markdown 分块时,需要补标题
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// last chunk is too small, concat it to lastText(next chunk start)
|
||||
if (!independentChunk && lastChunk.length < minChunkLen) {
|
||||
if (lastChunk.length < minChunkLen) {
|
||||
chunks.push(...innerChunks.slice(0, -1));
|
||||
lastText = lastChunk;
|
||||
} else {
|
||||
chunks.push(...innerChunks);
|
||||
// compute new overlapText
|
||||
lastText = getOneTextOverlapText({
|
||||
text: lastChunk,
|
||||
step
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Last chunk is large enough
|
||||
chunks.push(...innerChunks);
|
||||
// compute new overlapText
|
||||
lastText = getOneTextOverlapText({
|
||||
text: lastChunk,
|
||||
step
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// size less than chunkLen, push text to last chunk. now, text definitely less than maxLen
|
||||
lastText = newText;
|
||||
// new text is small
|
||||
|
||||
// markdown paragraph block: Direct addition; If the chunk size reaches, add a chunk
|
||||
if (
|
||||
isCustomStep ||
|
||||
(independentChunk && newTextLen > miniChunkLen) ||
|
||||
newTextLen >= chunkLen
|
||||
) {
|
||||
chunks.push(`${currentTitle}${lastText}`);
|
||||
|
||||
lastText = getOneTextOverlapText({ text: lastText, step });
|
||||
// Not overlap
|
||||
if (forbidConcat) {
|
||||
chunks.push(`${parentTitle}${item.title}${item.text}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
lastText += item.text;
|
||||
}
|
||||
|
||||
/* If the last chunk is independent, it needs to be push chunks. */
|
||||
@@ -310,9 +320,10 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
if (lastText.length < chunkLen * 0.4) {
|
||||
chunks[chunks.length - 1] = chunks[chunks.length - 1] + lastText;
|
||||
} else {
|
||||
chunks.push(`${mdTitle}${lastText}`);
|
||||
chunks.push(lastText);
|
||||
}
|
||||
} else if (lastText && chunks.length === 0) {
|
||||
// 只分出一个很小的块,则直接追加到末尾(如果大于 1 个块,说明这个小块内容已经被上一个块拿到了)
|
||||
chunks.push(lastText);
|
||||
}
|
||||
|
||||
@@ -324,8 +335,8 @@ const commonSplit = (props: SplitProps): SplitResponse => {
|
||||
text,
|
||||
step: 0,
|
||||
lastText: '',
|
||||
mdTitle: ''
|
||||
}).map((chunk) => chunk?.replaceAll(codeBlockMarker, '\n') || ''); // restore code block
|
||||
parentTitle: ''
|
||||
}).map((chunk) => chunk?.replaceAll(codeBlockMarker, '\n')?.trim() || ''); // restore code block
|
||||
|
||||
const chars = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import dayjs from 'dayjs';
|
||||
import cronParser from 'cron-parser';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import { i18nT } from '../../../web/i18n/utils';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
@@ -23,31 +24,51 @@ export const formatTimeToChatTime = (time: Date) => {
|
||||
|
||||
// 如果传入时间小于60秒,返回刚刚
|
||||
if (now.diff(target, 'second') < 60) {
|
||||
return '刚刚';
|
||||
return i18nT('common:just_now');
|
||||
}
|
||||
|
||||
// 如果时间是今天,展示几时:几分
|
||||
//用#占位,i18n生效后replace成:
|
||||
if (now.isSame(target, 'day')) {
|
||||
return target.format('HH : mm');
|
||||
return target.format('HH#mm');
|
||||
}
|
||||
|
||||
// 如果是昨天,展示昨天
|
||||
if (now.subtract(1, 'day').isSame(target, 'day')) {
|
||||
return '昨天';
|
||||
}
|
||||
|
||||
// 如果是前天,展示前天
|
||||
if (now.subtract(2, 'day').isSame(target, 'day')) {
|
||||
return '前天';
|
||||
return i18nT('common:yesterday');
|
||||
}
|
||||
|
||||
// 如果是今年,展示某月某日
|
||||
if (now.isSame(target, 'year')) {
|
||||
return target.format('MM/DD');
|
||||
return target.format('MM-DD');
|
||||
}
|
||||
|
||||
// 如果是更久之前,展示某年某月某日
|
||||
return target.format('YYYY/M/D');
|
||||
return target.format('YYYY-M-D');
|
||||
};
|
||||
|
||||
export const formatTimeToChatItemTime = (time: Date) => {
|
||||
const now = dayjs();
|
||||
const target = dayjs(time);
|
||||
const detailTime = target.format('HH#mm');
|
||||
|
||||
// 如果时间是今天,展示几时:几分
|
||||
if (now.isSame(target, 'day')) {
|
||||
return detailTime;
|
||||
}
|
||||
|
||||
// 如果是昨天,展示昨天+几时:几分
|
||||
if (now.subtract(1, 'day').isSame(target, 'day')) {
|
||||
return i18nT('common:yesterday_detail_time');
|
||||
}
|
||||
|
||||
// 如果是今年,展示某月某日+几时:几分
|
||||
if (now.isSame(target, 'year')) {
|
||||
return target.format('MM-DD') + ' ' + detailTime;
|
||||
}
|
||||
|
||||
// 如果是更久之前,展示某年某月某日+几时:几分
|
||||
return target.format('YYYY-M-D') + ' ' + detailTime;
|
||||
};
|
||||
|
||||
/* cron time parse */
|
||||
|
||||
@@ -207,8 +207,8 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
|
||||
];
|
||||
|
||||
// Document quote prompt
|
||||
export const Prompt_DocumentQuote = `将 <Reference></Reference> 中的内容作为本次对话的参考:
|
||||
<Reference>
|
||||
export const Prompt_DocumentQuote = `将 <FilesContent></FilesContent> 中的内容作为本次对话的参考:
|
||||
<FilesContent>
|
||||
{{quote}}
|
||||
</Reference>
|
||||
</FilesContent>
|
||||
`;
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
ChatCompletionToolMessageParam
|
||||
} from '../../core/ai/type.d';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
|
||||
|
||||
const GPT2Chat = {
|
||||
[ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System,
|
||||
[ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human,
|
||||
@@ -61,14 +60,14 @@ export const chats2GPTMessages = ({
|
||||
return {
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: item.file?.url || ''
|
||||
url: item.file.url
|
||||
}
|
||||
};
|
||||
} else if (item.file?.type === ChatFileTypeEnum.file) {
|
||||
return {
|
||||
type: 'file_url',
|
||||
name: item.file?.name || '',
|
||||
url: item.file?.url || ''
|
||||
url: item.file.url
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/global/core/chat/type.d.ts
vendored
@@ -126,6 +126,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
|
||||
moduleName?: string;
|
||||
ttsBuffer?: Uint8Array;
|
||||
responseData?: ChatHistoryItemResType[];
|
||||
time?: Date;
|
||||
} & ChatBoxInputType &
|
||||
ResponseTagItemType;
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue
|
||||
// Keep the first n and last n characters
|
||||
export const getHistoryPreview = (
|
||||
completeMessages: ChatItemType[],
|
||||
size = 100
|
||||
size = 100,
|
||||
useVision = false
|
||||
): {
|
||||
obj: `${ChatRoleEnum}`;
|
||||
value: string;
|
||||
@@ -48,7 +49,8 @@ export const getHistoryPreview = (
|
||||
item.value
|
||||
?.map((item) => {
|
||||
if (item?.text?.content) return item?.text?.content;
|
||||
if (item.file?.type === 'image') return 'Input an image';
|
||||
if (item.file?.type === 'image' && useVision)
|
||||
return `}...)`;
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
4
packages/global/core/dataset/type.d.ts
vendored
@@ -191,7 +191,7 @@ export type DatasetDataItemType = {
|
||||
chunkIndex: number;
|
||||
indexes: DatasetDataIndexItemType[];
|
||||
isOwner: boolean;
|
||||
canWrite: boolean;
|
||||
// permission: DatasetPermission;
|
||||
};
|
||||
|
||||
/* --------------- file ---------------------- */
|
||||
@@ -212,7 +212,7 @@ export type DatasetFileSchema = {
|
||||
/* ============= search =============== */
|
||||
export type SearchDataResponseItemType = Omit<
|
||||
DatasetDataItemType,
|
||||
'teamId' | 'indexes' | 'isOwner' | 'canWrite'
|
||||
'teamId' | 'indexes' | 'isOwner'
|
||||
> & {
|
||||
score: { type: `${SearchScoreTypeEnum}`; value: number; index: number }[];
|
||||
// score: number;
|
||||
|
||||
@@ -201,6 +201,7 @@ export enum NodeInputKeyEnum {
|
||||
nodeHeight = 'nodeHeight',
|
||||
// loop start
|
||||
loopStartInput = 'loopStartInput',
|
||||
loopStartIndex = 'loopStartIndex',
|
||||
// loop end
|
||||
loopEndInput = 'loopEndInput',
|
||||
|
||||
@@ -256,9 +257,9 @@ export enum NodeOutputKeyEnum {
|
||||
|
||||
// loop
|
||||
loopArray = 'loopArray',
|
||||
|
||||
// loop start
|
||||
loopStartInput = 'loopStartInput',
|
||||
loopStartIndex = 'loopStartIndex',
|
||||
|
||||
// form input
|
||||
formInputResult = 'formInputResult'
|
||||
@@ -334,3 +335,21 @@ export enum ContentTypes {
|
||||
xml = 'xml',
|
||||
raw = 'raw-text'
|
||||
}
|
||||
|
||||
export const ArrayTypeMap: Record<WorkflowIOValueTypeEnum, WorkflowIOValueTypeEnum> = {
|
||||
[WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString,
|
||||
[WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber,
|
||||
[WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean,
|
||||
[WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.arrayString,
|
||||
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.arrayNumber,
|
||||
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.arrayBoolean,
|
||||
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.chatHistory]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.datasetQuote]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.dynamic]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.selectDataset]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.selectApp]: WorkflowIOValueTypeEnum.arrayObject,
|
||||
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.arrayAny,
|
||||
[WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ export enum FlowNodeInputTypeEnum { // render ui
|
||||
settingDatasetQuotePrompt = 'settingDatasetQuotePrompt',
|
||||
|
||||
hidden = 'hidden',
|
||||
custom = 'custom'
|
||||
custom = 'custom',
|
||||
|
||||
fileSelect = 'fileSelect'
|
||||
}
|
||||
export const FlowNodeInputMap: Record<
|
||||
FlowNodeInputTypeEnum,
|
||||
@@ -85,6 +87,9 @@ export const FlowNodeInputMap: Record<
|
||||
},
|
||||
[FlowNodeInputTypeEnum.textarea]: {
|
||||
icon: 'core/workflow/inputType/textarea'
|
||||
},
|
||||
[FlowNodeInputTypeEnum.fileSelect]: {
|
||||
icon: 'core/workflow/inputType/file'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,43 +142,43 @@ export enum FlowNodeTypeEnum {
|
||||
// node IO value type
|
||||
export const FlowValueTypeMap = {
|
||||
[WorkflowIOValueTypeEnum.string]: {
|
||||
label: 'string',
|
||||
label: 'String',
|
||||
value: WorkflowIOValueTypeEnum.string
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.number]: {
|
||||
label: 'number',
|
||||
label: 'Number',
|
||||
value: WorkflowIOValueTypeEnum.number
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.boolean]: {
|
||||
label: 'boolean',
|
||||
label: 'Boolean',
|
||||
value: WorkflowIOValueTypeEnum.boolean
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.object]: {
|
||||
label: 'object',
|
||||
label: 'Object',
|
||||
value: WorkflowIOValueTypeEnum.object
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.arrayString]: {
|
||||
label: 'array<string>',
|
||||
label: 'Array<string>',
|
||||
value: WorkflowIOValueTypeEnum.arrayString
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.arrayNumber]: {
|
||||
label: 'array<number>',
|
||||
label: 'Array<number>',
|
||||
value: WorkflowIOValueTypeEnum.arrayNumber
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.arrayBoolean]: {
|
||||
label: 'array<boolean>',
|
||||
label: 'Array<boolean>',
|
||||
value: WorkflowIOValueTypeEnum.arrayBoolean
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.arrayObject]: {
|
||||
label: 'array<object>',
|
||||
label: 'Array<object>',
|
||||
value: WorkflowIOValueTypeEnum.arrayObject
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.arrayAny]: {
|
||||
label: 'array',
|
||||
label: 'Array',
|
||||
value: WorkflowIOValueTypeEnum.arrayAny
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.any]: {
|
||||
label: 'any',
|
||||
label: 'Any',
|
||||
value: WorkflowIOValueTypeEnum.any
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.chatHistory]: {
|
||||
|
||||
@@ -135,6 +135,9 @@ export type DispatchNodeResponseType = {
|
||||
extensionResult?: string;
|
||||
extensionTokens?: number;
|
||||
|
||||
// dataset concat
|
||||
concatLength?: number;
|
||||
|
||||
// cq
|
||||
cqList?: ClassifyQuestionAgentItemType[];
|
||||
cqResult?: string;
|
||||
@@ -216,5 +219,7 @@ export type AIChatNodeProps = {
|
||||
[NodeInputKeyEnum.aiChatQuoteTemplate]?: string;
|
||||
[NodeInputKeyEnum.aiChatQuotePrompt]?: string;
|
||||
[NodeInputKeyEnum.aiChatVision]?: boolean;
|
||||
|
||||
[NodeInputKeyEnum.stringQuoteText]?: string;
|
||||
[NodeInputKeyEnum.fileUrlList]?: string[];
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ import { StoreNodeItemType } from '../type/node';
|
||||
import { StoreEdgeItemType } from '../type/edge';
|
||||
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
|
||||
import { VARIABLE_NODE_ID } from '../constants';
|
||||
import { isReferenceValue } from '../utils';
|
||||
import { FlowNodeOutputItemType, ReferenceValueProps } from '../type/io';
|
||||
import { isValidReferenceValueFormat } from '../utils';
|
||||
import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
|
||||
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
|
||||
|
||||
@@ -34,7 +34,7 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
|
||||
2. Check that the workflow starts at the interaction node
|
||||
*/
|
||||
export const getLastInteractiveValue = (histories: ChatItemType[]) => {
|
||||
const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI);
|
||||
const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI);
|
||||
|
||||
if (lastAIMessage) {
|
||||
const lastValue = lastAIMessage.value[lastAIMessage.value.length - 1];
|
||||
@@ -225,37 +225,129 @@ export const checkNodeRunStatus = ({
|
||||
return 'wait';
|
||||
};
|
||||
|
||||
/*
|
||||
Get the value of the reference variable/node output
|
||||
1. [string,string]
|
||||
2. [string,string][]
|
||||
*/
|
||||
export const getReferenceVariableValue = ({
|
||||
value,
|
||||
nodes,
|
||||
variables
|
||||
}: {
|
||||
value: ReferenceValueProps;
|
||||
value?: ReferenceValueType;
|
||||
nodes: RuntimeNodeItemType[];
|
||||
variables: Record<string, any>;
|
||||
}) => {
|
||||
const nodeIds = nodes.map((node) => node.nodeId);
|
||||
if (!isReferenceValue(value, nodeIds)) {
|
||||
return value;
|
||||
}
|
||||
const sourceNodeId = value[0];
|
||||
const outputId = value[1];
|
||||
if (!value) return value;
|
||||
|
||||
if (sourceNodeId === VARIABLE_NODE_ID && outputId) {
|
||||
return variables[outputId];
|
||||
// handle single reference value
|
||||
if (isValidReferenceValueFormat(value)) {
|
||||
const sourceNodeId = value[0];
|
||||
const outputId = value[1];
|
||||
|
||||
if (sourceNodeId === VARIABLE_NODE_ID) {
|
||||
if (!outputId) return undefined;
|
||||
return variables[outputId];
|
||||
}
|
||||
|
||||
const node = nodes.find((node) => node.nodeId === sourceNodeId);
|
||||
if (!node) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return node.outputs.find((output) => output.id === outputId)?.value;
|
||||
}
|
||||
|
||||
const node = nodes.find((node) => node.nodeId === sourceNodeId);
|
||||
// handle reference array
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value.every((item) => isValidReferenceValueFormat(item))
|
||||
) {
|
||||
const result = value.map<any>((val) => {
|
||||
return getReferenceVariableValue({
|
||||
value: val,
|
||||
nodes,
|
||||
variables
|
||||
});
|
||||
});
|
||||
|
||||
if (!node) {
|
||||
return undefined;
|
||||
return result.flat().filter((item) => item !== undefined);
|
||||
}
|
||||
|
||||
const outputValue = node.outputs.find((output) => output.id === outputId)?.value;
|
||||
|
||||
return outputValue;
|
||||
return value;
|
||||
};
|
||||
|
||||
// replace {{$xx.xx$}} variables for text
|
||||
export function replaceEditorVariable({
|
||||
text,
|
||||
nodes,
|
||||
variables,
|
||||
runningNode
|
||||
}: {
|
||||
text: any;
|
||||
nodes: RuntimeNodeItemType[];
|
||||
variables: Record<string, any>; // global variables
|
||||
runningNode: RuntimeNodeItemType;
|
||||
}) {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
const globalVariables = Object.keys(variables).map((key) => {
|
||||
return {
|
||||
nodeId: VARIABLE_NODE_ID,
|
||||
id: key,
|
||||
value: variables[key]
|
||||
};
|
||||
});
|
||||
|
||||
// Upstream node outputs
|
||||
const nodeVariables = nodes
|
||||
.map((node) => {
|
||||
return node.outputs.map((output) => {
|
||||
return {
|
||||
nodeId: node.nodeId,
|
||||
id: output.id,
|
||||
value: output.value
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
|
||||
// Get runningNode inputs(Will be replaced with reference)
|
||||
const customInputs = runningNode.inputs.flatMap((item) => {
|
||||
return [
|
||||
{
|
||||
id: item.key,
|
||||
value: getReferenceVariableValue({
|
||||
value: item.value,
|
||||
nodes,
|
||||
variables
|
||||
}),
|
||||
nodeId: runningNode.nodeId
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const allVariables = [...globalVariables, ...nodeVariables, ...customInputs];
|
||||
|
||||
// Replace {{$xxx.xxx$}} to value
|
||||
for (const key in allVariables) {
|
||||
const variable = allVariables[key];
|
||||
const val = variable.value;
|
||||
const formatVal = (() => {
|
||||
if (val === undefined) return '';
|
||||
if (val === null) return 'null';
|
||||
|
||||
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
||||
})();
|
||||
|
||||
const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g');
|
||||
text = text.replace(regex, formatVal);
|
||||
}
|
||||
return text || '';
|
||||
}
|
||||
|
||||
export const textAdaptGptResponse = ({
|
||||
text,
|
||||
model = '',
|
||||
|
||||
@@ -75,10 +75,17 @@ export const Input_Template_Text_Quote: FlowNodeInputItemType = {
|
||||
description: i18nT('app:document_quote_tip'),
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
};
|
||||
|
||||
export const Input_Template_File_Link_Prompt: FlowNodeInputItemType = {
|
||||
key: NodeInputKeyEnum.fileUrlList,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input],
|
||||
label: i18nT('app:file_quote_link'),
|
||||
debugLabel: i18nT('app:file_quote_link'),
|
||||
valueType: WorkflowIOValueTypeEnum.arrayString
|
||||
};
|
||||
export const Input_Template_File_Link: FlowNodeInputItemType = {
|
||||
key: NodeInputKeyEnum.fileUrlList,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
required: true,
|
||||
label: i18nT('app:workflow.user_file_input'),
|
||||
debugLabel: i18nT('app:workflow.user_file_input'),
|
||||
description: i18nT('app:workflow.user_file_input_desc'),
|
||||
@@ -104,7 +111,7 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = {
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
label: '',
|
||||
value: 900
|
||||
value: 600
|
||||
};
|
||||
|
||||
export const Input_Template_Stream_MODE: FlowNodeInputItemType = {
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
Input_Template_History,
|
||||
Input_Template_System_Prompt,
|
||||
Input_Template_UserChatInput,
|
||||
Input_Template_Text_Quote
|
||||
Input_Template_Text_Quote,
|
||||
Input_Template_File_Link_Prompt
|
||||
} from '../../input';
|
||||
import { chatNodeSystemPromptTip, systemPromptTip } from '../../tip';
|
||||
import { getHandleConfig } from '../../utils';
|
||||
@@ -55,7 +56,7 @@ export const AiChatModule: FlowNodeTemplateType = {
|
||||
showStatus: true,
|
||||
isTool: true,
|
||||
courseUrl: '/docs/workflow/modules/ai_chat/',
|
||||
version: '481',
|
||||
version: '4813',
|
||||
inputs: [
|
||||
Input_Template_SettingAiModel,
|
||||
// --- settings modal
|
||||
@@ -89,7 +90,7 @@ export const AiChatModule: FlowNodeTemplateType = {
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
label: '',
|
||||
valueType: WorkflowIOValueTypeEnum.boolean,
|
||||
value: false
|
||||
value: true
|
||||
},
|
||||
// settings modal ---
|
||||
{
|
||||
@@ -100,7 +101,7 @@ export const AiChatModule: FlowNodeTemplateType = {
|
||||
},
|
||||
Input_Template_History,
|
||||
Input_Template_Dataset_Quote,
|
||||
Input_Template_Text_Quote,
|
||||
Input_Template_File_Link_Prompt,
|
||||
|
||||
{ ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') }
|
||||
],
|
||||
|
||||
@@ -25,7 +25,7 @@ export const getOneQuoteInputTemplate = ({
|
||||
}): FlowNodeInputItemType => ({
|
||||
key,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
label: `${i18nT('workflow:quote_num')},{ num: ${index} }`,
|
||||
label: `${i18nT('workflow:quote_num')}-${index}`,
|
||||
debugLabel: i18nT('workflow:knowledge_base_reference'),
|
||||
canEdit: true,
|
||||
valueType: WorkflowIOValueTypeEnum.datasetQuote
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ReferenceValueProps } from 'core/workflow/type/io';
|
||||
import { ReferenceItemValueType } from '../../../type/io';
|
||||
import { VariableConditionEnum } from './constant';
|
||||
|
||||
export type IfElseConditionType = 'AND' | 'OR';
|
||||
export type ConditionListItemType = {
|
||||
variable?: ReferenceValueProps;
|
||||
variable?: ReferenceItemValueType;
|
||||
condition?: VariableConditionEnum;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '../../../node/constant';
|
||||
import { FlowNodeTemplateType } from '../../../type/node.d';
|
||||
import {
|
||||
FlowNodeTemplateTypeEnum,
|
||||
NodeInputKeyEnum,
|
||||
NodeOutputKeyEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '../../../constants';
|
||||
import { getHandleConfig } from '../../utils';
|
||||
@@ -28,7 +33,21 @@ export const LoopStartNode: FlowNodeTemplateType = {
|
||||
label: '',
|
||||
required: true,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: NodeInputKeyEnum.loopStartIndex,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
label: i18nT('workflow:Array_element_index')
|
||||
}
|
||||
],
|
||||
outputs: []
|
||||
outputs: [
|
||||
{
|
||||
id: NodeOutputKeyEnum.loopStartIndex,
|
||||
key: NodeOutputKeyEnum.loopStartIndex,
|
||||
label: i18nT('workflow:Array_element_index'),
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
valueType: WorkflowIOValueTypeEnum.number
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ReadFilesNode: FlowNodeTemplateType = {
|
||||
name: i18nT('app:workflow.read_files'),
|
||||
intro: i18nT('app:workflow.read_files_tip'),
|
||||
showStatus: true,
|
||||
version: '489',
|
||||
version: '4812',
|
||||
isTool: true,
|
||||
inputs: [
|
||||
{
|
||||
|
||||
@@ -24,17 +24,8 @@ export const TextEditorNode: FlowNodeTemplateType = {
|
||||
name: i18nT('workflow:text_concatenation'),
|
||||
intro: i18nT('workflow:intro_text_concatenation'),
|
||||
courseUrl: '/docs/workflow/modules/text_editor/',
|
||||
version: '486',
|
||||
version: '4813',
|
||||
inputs: [
|
||||
{
|
||||
...Input_Template_DynamicInput,
|
||||
description: i18nT('workflow:dynamic_input_description_concat'),
|
||||
customInputConfig: {
|
||||
selectValueTypeList: Object.values(WorkflowIOValueTypeEnum),
|
||||
showDescription: false,
|
||||
showDefaultValue: false
|
||||
}
|
||||
},
|
||||
{
|
||||
key: NodeInputKeyEnum.textareaInput,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.textarea],
|
||||
|
||||
@@ -20,6 +20,7 @@ import { chatNodeSystemPromptTip, systemPromptTip } from '../tip';
|
||||
import { LLMModelTypeEnum } from '../../../ai/constants';
|
||||
import { getHandleConfig } from '../utils';
|
||||
import { i18nT } from '../../../../../web/i18n/utils';
|
||||
import { Input_Template_File_Link_Prompt } from '../input';
|
||||
|
||||
export const ToolModule: FlowNodeTemplateType = {
|
||||
id: FlowNodeTypeEnum.tools,
|
||||
@@ -32,7 +33,7 @@ export const ToolModule: FlowNodeTemplateType = {
|
||||
intro: i18nT('workflow:template.tool_call_intro'),
|
||||
showStatus: true,
|
||||
courseUrl: '/docs/workflow/modules/tool/',
|
||||
version: '481',
|
||||
version: '4813',
|
||||
inputs: [
|
||||
{
|
||||
...Input_Template_SettingAiModel,
|
||||
@@ -67,6 +68,7 @@ export const ToolModule: FlowNodeTemplateType = {
|
||||
placeholder: chatNodeSystemPromptTip
|
||||
},
|
||||
Input_Template_History,
|
||||
Input_Template_File_Link_Prompt,
|
||||
Input_Template_UserChatInput
|
||||
],
|
||||
outputs: [
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FlowNodeInputTypeEnum } from '../../../node/constant';
|
||||
import { ReferenceValueProps } from '../../..//type/io';
|
||||
import { ReferenceItemValueType, ReferenceValueType } from '../../..//type/io';
|
||||
import { WorkflowIOValueTypeEnum } from '../../../constants';
|
||||
|
||||
export type TUpdateListItem = {
|
||||
variable?: ReferenceValueProps;
|
||||
value: ReferenceValueProps;
|
||||
variable?: ReferenceItemValueType;
|
||||
value?: ReferenceValueType; // input: ['',value], reference: [nodeId,outputId]
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,3 @@ export const WorkflowStart: FlowNodeTemplateType = {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const isWorkflowStartOutput = (key?: string) =>
|
||||
!!WorkflowStart.outputs.find((output) => output.key === key);
|
||||
|
||||
9
packages/global/core/workflow/type/io.d.ts
vendored
@@ -56,6 +56,11 @@ export type FlowNodeInputItemType = InputComponentPropsType & {
|
||||
canEdit?: boolean; // dynamic inputs
|
||||
isPro?: boolean; // Pro version field
|
||||
isToolOutput?: boolean;
|
||||
|
||||
// file
|
||||
canSelectFile?: boolean;
|
||||
canSelectImg?: boolean;
|
||||
maxFiles?: number;
|
||||
};
|
||||
|
||||
export type FlowNodeOutputItemType = {
|
||||
@@ -75,4 +80,6 @@ export type FlowNodeOutputItemType = {
|
||||
customFieldConfig?: CustomFieldConfigType;
|
||||
};
|
||||
|
||||
export type ReferenceValueProps = [string, string | undefined];
|
||||
export type ReferenceItemValueType = [string, string | undefined];
|
||||
export type ReferenceArrayValueType = ReferenceItemValueType[];
|
||||
export type ReferenceValueType = ReferenceItemValueType | ReferenceArrayValueType;
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
VARIABLE_NODE_ID,
|
||||
NodeOutputKeyEnum
|
||||
} from './constants';
|
||||
import { FlowNodeInputItemType, FlowNodeOutputItemType, ReferenceValueProps } from './type/io.d';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType,
|
||||
ReferenceArrayValueType,
|
||||
ReferenceItemValueType
|
||||
} from './type/io.d';
|
||||
import { StoreNodeItemType } from './type/node';
|
||||
import type {
|
||||
VariableItemType,
|
||||
@@ -30,8 +35,8 @@ import {
|
||||
} from '../app/constants';
|
||||
import { IfElseResultEnum } from './template/system/ifElse/constant';
|
||||
import { RuntimeNodeItemType } from './runtime/type';
|
||||
import { getReferenceVariableValue } from './runtime/utils';
|
||||
import {
|
||||
Input_Template_File_Link,
|
||||
Input_Template_History,
|
||||
Input_Template_Stream_MODE,
|
||||
Input_Template_UserChatInput
|
||||
@@ -261,8 +266,10 @@ export const appData2FlowNodeIO = ({
|
||||
inputs: [
|
||||
Input_Template_Stream_MODE,
|
||||
Input_Template_History,
|
||||
...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg
|
||||
? [Input_Template_File_Link]
|
||||
: []),
|
||||
Input_Template_UserChatInput,
|
||||
// ...(showFileLink ? [Input_Template_File_Link] : []),
|
||||
...variableInput
|
||||
],
|
||||
outputs: [
|
||||
@@ -298,9 +305,37 @@ export const formatEditorVariablePickerIcon = (
|
||||
}));
|
||||
};
|
||||
|
||||
export const isReferenceValue = (value: any, nodeIds: string[]): boolean => {
|
||||
const validIdList = [VARIABLE_NODE_ID, ...nodeIds];
|
||||
return Array.isArray(value) && value.length === 2 && validIdList.includes(value[0]);
|
||||
// Check the value is a valid reference value format: [variableId, outputId]
|
||||
export const isValidReferenceValueFormat = (value: any): value is ReferenceItemValueType => {
|
||||
return Array.isArray(value) && value.length === 2 && typeof value[0] === 'string';
|
||||
};
|
||||
/*
|
||||
Check whether the value([variableId, outputId]) value is a valid reference value:
|
||||
1. The value must be an array of length 2
|
||||
2. The first item of the array must be one of VARIABLE_NODE_ID or nodeIds
|
||||
*/
|
||||
export const isValidReferenceValue = (
|
||||
value: any,
|
||||
nodeIds: string[]
|
||||
): value is ReferenceItemValueType => {
|
||||
if (!isValidReferenceValueFormat(value)) return false;
|
||||
|
||||
const validIdSet = new Set([VARIABLE_NODE_ID, ...nodeIds]);
|
||||
return validIdSet.has(value[0]);
|
||||
};
|
||||
/*
|
||||
Check whether the value([variableId, outputId][]) value is a valid reference value array:
|
||||
1. The value must be an array
|
||||
2. The array must contain at least one element
|
||||
3. Each element in the array must be a valid reference value
|
||||
*/
|
||||
export const isValidArrayReferenceValue = (
|
||||
value: any,
|
||||
nodeIds: string[]
|
||||
): value is ReferenceArrayValueType => {
|
||||
if (!Array.isArray(value)) return false;
|
||||
|
||||
return value.every((item) => isValidReferenceValue(item, nodeIds));
|
||||
};
|
||||
|
||||
export const getElseIFLabel = (i: number) => {
|
||||
@@ -342,79 +377,6 @@ export const updatePluginInputByVariables = (
|
||||
);
|
||||
};
|
||||
|
||||
// replace {{$xx.xx$}} variables for text
|
||||
export function replaceEditorVariable({
|
||||
text,
|
||||
nodes,
|
||||
variables,
|
||||
runningNode
|
||||
}: {
|
||||
text: any;
|
||||
nodes: RuntimeNodeItemType[];
|
||||
variables: Record<string, any>; // global variables
|
||||
runningNode: RuntimeNodeItemType;
|
||||
}) {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
const globalVariables = Object.keys(variables).map((key) => {
|
||||
return {
|
||||
nodeId: VARIABLE_NODE_ID,
|
||||
id: key,
|
||||
value: variables[key]
|
||||
};
|
||||
});
|
||||
|
||||
// Upstream node outputs
|
||||
const nodeVariables = nodes
|
||||
.map((node) => {
|
||||
return node.outputs.map((output) => {
|
||||
return {
|
||||
nodeId: node.nodeId,
|
||||
id: output.id,
|
||||
value: output.value
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
|
||||
// Get runningNode inputs(Will be replaced with reference)
|
||||
const customInputs = runningNode.inputs.flatMap((item) => {
|
||||
if (Array.isArray(item.value)) {
|
||||
return [
|
||||
{
|
||||
id: item.key,
|
||||
value: getReferenceVariableValue({
|
||||
value: item.value as ReferenceValueProps,
|
||||
nodes,
|
||||
variables
|
||||
}),
|
||||
nodeId: runningNode.nodeId
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: item.key,
|
||||
value: item.value,
|
||||
nodeId: runningNode.nodeId
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const allVariables = [...globalVariables, ...nodeVariables, ...customInputs];
|
||||
|
||||
// Replace {{$xxx.xxx$}} to value
|
||||
for (const key in allVariables) {
|
||||
const variable = allVariables[key];
|
||||
const val = variable.value;
|
||||
const formatVal = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
||||
|
||||
const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g');
|
||||
text = text.replace(regex, formatVal);
|
||||
}
|
||||
return text || '';
|
||||
}
|
||||
|
||||
/* Get plugin runtime input user query */
|
||||
export const getPluginRunUserQuery = ({
|
||||
pluginInputs,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as echarts from 'echarts';
|
||||
import json5 from 'json5';
|
||||
import { getFileSavePath } from '../../../utils';
|
||||
import * as fs from 'fs';
|
||||
import { SystemPluginSpecialResponse } from '../../../type.d';
|
||||
|
||||
type Props = {
|
||||
@@ -82,25 +80,23 @@ const generateChart = async (title: string, xAxis: string, yAxis: string, chartT
|
||||
|
||||
chart.setOption(option);
|
||||
// 生成 Base64 图像
|
||||
const base64Image = chart.getDataURL();
|
||||
const svgData = decodeURIComponent(base64Image.split(',')[1]);
|
||||
const base64Image = chart.getDataURL({
|
||||
type: 'png',
|
||||
pixelRatio: 2 // 可以设置更高的像素比以获得更清晰的图像
|
||||
});
|
||||
const svgContent = decodeURIComponent(base64Image.split(',')[1]);
|
||||
const base64 = `data:image/svg+xml;base64,${Buffer.from(svgContent).toString('base64')}`;
|
||||
|
||||
const fileName = `chart_${Date.now()}.svg`;
|
||||
const filePath = getFileSavePath(fileName);
|
||||
fs.writeFileSync(filePath, svgData);
|
||||
// 释放图表实例
|
||||
chart.dispose();
|
||||
|
||||
return filePath;
|
||||
return base64;
|
||||
};
|
||||
|
||||
const main = async ({ title, xAxis, yAxis, chartType }: Props): Response => {
|
||||
const filePath = await generateChart(title, xAxis, yAxis, chartType);
|
||||
const base64 = await generateChart(title, xAxis, yAxis, chartType);
|
||||
return {
|
||||
result: {
|
||||
type: 'SYSTEM_PLUGIN_FILE',
|
||||
path: filePath,
|
||||
contentType: 'image/svg+xml'
|
||||
type: 'SYSTEM_PLUGIN_BASE64',
|
||||
value: base64,
|
||||
extension: 'svg'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
6
packages/plugins/type.d.ts
vendored
@@ -4,9 +4,9 @@ import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type
|
||||
|
||||
export type SystemPluginResponseType = Promise<Record<string, any>>;
|
||||
export type SystemPluginSpecialResponse = {
|
||||
type: 'SYSTEM_PLUGIN_FILE';
|
||||
path: string;
|
||||
contentType: string;
|
||||
type: 'SYSTEM_PLUGIN_BASE64';
|
||||
value: string;
|
||||
extension: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export const getFileSavePath = (name: string) => {
|
||||
if (isProduction) {
|
||||
return `/app/plugin_file/${name}`;
|
||||
}
|
||||
const filePath = path.join(process.cwd(), 'local', 'plugin_file', name);
|
||||
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
|
||||
return filePath;
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { gridFsStream2Buffer, stream2Encoding } from './utils';
|
||||
import { addLog } from '../../system/log';
|
||||
import { readFromSecondary } from '../../mongo/utils';
|
||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export function getGFSCollection(bucket: `${BucketNameEnum}`) {
|
||||
MongoDatasetFileSchema;
|
||||
@@ -76,6 +77,59 @@ export async function uploadFile({
|
||||
|
||||
return String(stream.id);
|
||||
}
|
||||
export async function uploadFileFromBase64Img({
|
||||
bucketName,
|
||||
teamId,
|
||||
tmbId,
|
||||
base64,
|
||||
filename,
|
||||
metadata = {}
|
||||
}: {
|
||||
bucketName: `${BucketNameEnum}`;
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
base64: string;
|
||||
filename: string;
|
||||
metadata?: Record<string, any>;
|
||||
}) {
|
||||
if (!base64) return Promise.reject(`filePath is empty`);
|
||||
if (!filename) return Promise.reject(`filename is empty`);
|
||||
|
||||
const base64Data = base64.split(',')[1];
|
||||
const contentType = base64.split(',')?.[0]?.split?.(':')?.[1];
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
const readableStream = new Readable({
|
||||
read() {
|
||||
this.push(buffer);
|
||||
this.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
const { stream: readStream, encoding } = await stream2Encoding(readableStream);
|
||||
|
||||
// Add default metadata
|
||||
metadata.teamId = teamId;
|
||||
metadata.tmbId = tmbId;
|
||||
metadata.encoding = encoding;
|
||||
|
||||
// create a gridfs bucket
|
||||
const bucket = getGridBucket(bucketName);
|
||||
|
||||
const stream = bucket.openUploadStream(filename, {
|
||||
metadata,
|
||||
contentType
|
||||
});
|
||||
|
||||
// save to gridfs
|
||||
await new Promise((resolve, reject) => {
|
||||
readStream
|
||||
.pipe(stream as any)
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
|
||||
return String(stream.id);
|
||||
}
|
||||
|
||||
export async function getFileById({
|
||||
bucketName,
|
||||
@@ -159,7 +213,6 @@ export const readFileContentFromMongo = async ({
|
||||
getFileById({ bucketName, fileId }),
|
||||
getDownloadStream({ bucketName, fileId })
|
||||
]);
|
||||
// console.log('get file stream', Date.now() - start);
|
||||
if (!file) {
|
||||
return Promise.reject(CommonErrEnum.fileNotFound);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { markdownProcess } from '@fastgpt/global/common/string/markdown';
|
||||
import { uploadMongoImg } from '../image/controller';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { addHours } from 'date-fns';
|
||||
import FormData from 'form-data';
|
||||
|
||||
import { WorkerNameEnum, runWorker } from '../../../worker/utils';
|
||||
@@ -10,6 +8,8 @@ import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
|
||||
import type { ReadFileResponse } from '../../../worker/readFile/type';
|
||||
import axios from 'axios';
|
||||
import { addLog } from '../../system/log';
|
||||
import { batchRun } from '@fastgpt/global/common/fn/utils';
|
||||
import { addHours } from 'date-fns';
|
||||
|
||||
export type readRawTextByLocalFileParams = {
|
||||
teamId: string;
|
||||
@@ -53,21 +53,6 @@ export const readRawContentByFileBuffer = async ({
|
||||
encoding: string;
|
||||
metadata?: Record<string, any>;
|
||||
}) => {
|
||||
// Upload image in markdown
|
||||
const matchMdImgTextAndUpload = ({ teamId, md }: { md: string; teamId: string }) =>
|
||||
markdownProcess({
|
||||
rawText: md,
|
||||
uploadImgController: (base64Img) =>
|
||||
uploadMongoImg({
|
||||
type: MongoImageTypeEnum.collectionImage,
|
||||
base64Img,
|
||||
teamId,
|
||||
metadata,
|
||||
expiredTime: addHours(new Date(), 1)
|
||||
})
|
||||
});
|
||||
|
||||
/* If */
|
||||
const customReadfileUrl = process.env.CUSTOM_READ_FILE_URL;
|
||||
const customReadFileExtension = process.env.CUSTOM_READ_FILE_EXTENSION || '';
|
||||
const ocrParse = process.env.CUSTOM_READ_FILE_OCR || 'false';
|
||||
@@ -111,19 +96,29 @@ export const readRawContentByFileBuffer = async ({
|
||||
};
|
||||
};
|
||||
|
||||
let { rawText, formatText } =
|
||||
let { rawText, formatText, imageList } =
|
||||
(await readFileFromCustomService()) ||
|
||||
(await runWorker<ReadFileResponse>(WorkerNameEnum.readFile, {
|
||||
extension,
|
||||
encoding,
|
||||
buffer
|
||||
buffer,
|
||||
teamId
|
||||
}));
|
||||
|
||||
// markdown data format
|
||||
if (['md', 'html', 'docx', ...customReadFileExtension.split(',')].includes(extension)) {
|
||||
rawText = await matchMdImgTextAndUpload({
|
||||
teamId: teamId,
|
||||
md: rawText
|
||||
if (imageList) {
|
||||
await batchRun(imageList, async (item) => {
|
||||
const src = await uploadMongoImg({
|
||||
type: MongoImageTypeEnum.collectionImage,
|
||||
base64Img: `data:${item.mime};base64,${item.base64}`,
|
||||
teamId,
|
||||
expiredTime: addHours(new Date(), 1),
|
||||
metadata: {
|
||||
...metadata,
|
||||
mime: item.mime
|
||||
}
|
||||
});
|
||||
rawText = rawText.replace(item.uuid, src);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { jsonRes } from '../response';
|
||||
export function useReqFrequencyLimit(seconds: number, limit: number) {
|
||||
return async (req: ApiRequestProps, res: NextApiResponse) => {
|
||||
const ip = requestIp.getClientIp(req);
|
||||
if (!ip && process.env.USE_IP_LIMIT !== 'true') {
|
||||
if (!ip || process.env.USE_IP_LIMIT !== 'true') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { simpleMarkdownText } from '@fastgpt/global/common/string/markdown';
|
||||
import { WorkerNameEnum, runWorker } from '../../worker/utils';
|
||||
import { ImageType } from '../../worker/readFile/type';
|
||||
|
||||
export const htmlToMarkdown = async (html?: string | null) => {
|
||||
const md = await runWorker<string>(WorkerNameEnum.htmlStr2Md, { html: html || '' });
|
||||
const md = await runWorker<{
|
||||
rawText: string;
|
||||
imageList: ImageType[];
|
||||
}>(WorkerNameEnum.htmlStr2Md, { html: html || '' });
|
||||
|
||||
return simpleMarkdownText(md);
|
||||
return simpleMarkdownText(md.rawText);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from '../controller.d';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import { customNanoid } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
export class MilvusCtrl {
|
||||
constructor() {}
|
||||
@@ -63,7 +64,7 @@ export class MilvusCtrl {
|
||||
name: 'id',
|
||||
data_type: DataType.Int64,
|
||||
is_primary_key: true,
|
||||
autoID: true
|
||||
autoID: false // disable auto id, and we need to set id in insert
|
||||
},
|
||||
{
|
||||
name: 'vector',
|
||||
@@ -127,11 +128,21 @@ export class MilvusCtrl {
|
||||
const client = await this.getClient();
|
||||
const { teamId, datasetId, collectionId, vector, retry = 3 } = props;
|
||||
|
||||
const generateId = () => {
|
||||
// in js, the max safe integer is 2^53 - 1: 9007199254740991
|
||||
// so we can generate a random number between 1-8 as the first digit
|
||||
// and the rest 15 digits can be random
|
||||
const firstDigit = customNanoid('12345678', 1);
|
||||
const restDigits = customNanoid('1234567890', 15);
|
||||
return Number(`${firstDigit}${restDigits}`);
|
||||
};
|
||||
const id = generateId();
|
||||
try {
|
||||
const result = await client.insert({
|
||||
collection_name: DatasetVectorTableName,
|
||||
data: [
|
||||
{
|
||||
id,
|
||||
vector,
|
||||
teamId: String(teamId),
|
||||
datasetId: String(datasetId),
|
||||
|
||||
@@ -7,14 +7,20 @@ import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
1. Commercial plugin: n points per times
|
||||
2. Other plugin: sum of children points
|
||||
*/
|
||||
export const computedPluginUsage = async (
|
||||
plugin: PluginRuntimeType,
|
||||
childrenUsage: ChatNodeUsageType[]
|
||||
) => {
|
||||
export const computedPluginUsage = async ({
|
||||
plugin,
|
||||
childrenUsage,
|
||||
error
|
||||
}: {
|
||||
plugin: PluginRuntimeType;
|
||||
childrenUsage: ChatNodeUsageType[];
|
||||
error?: boolean;
|
||||
}) => {
|
||||
const { source } = await splitCombinePluginId(plugin.id);
|
||||
|
||||
// Commercial plugin: n points per times
|
||||
if (source === PluginSourceEnum.commercial) {
|
||||
if (error) return 0;
|
||||
return plugin.currentCost ?? 0;
|
||||
}
|
||||
|
||||
|
||||
155
packages/service/core/chat/pushChatLog.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { addLog } from '../../common/system/log';
|
||||
import { MongoChatItem } from './chatItemSchema';
|
||||
import { MongoChat } from './chatSchema';
|
||||
import axios from 'axios';
|
||||
import { AIChatItemType, ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
|
||||
export type Metadata = {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const pushChatLog = ({
|
||||
chatId,
|
||||
chatItemIdHuman,
|
||||
chatItemIdAi,
|
||||
appId,
|
||||
metadata
|
||||
}: {
|
||||
chatId: string;
|
||||
chatItemIdHuman: string;
|
||||
chatItemIdAi: string;
|
||||
appId: string;
|
||||
metadata?: Metadata;
|
||||
}) => {
|
||||
const interval = Number(process.env.CHAT_LOG_INTERVAL);
|
||||
const url = process.env.CHAT_LOG_URL;
|
||||
if (interval > 0 && url) {
|
||||
addLog.info(`[ChatLogPush] push chat log after ${interval}ms`, {
|
||||
appId,
|
||||
chatItemIdHuman,
|
||||
chatItemIdAi
|
||||
});
|
||||
setTimeout(() => {
|
||||
pushChatLogInternal({ chatId, chatItemIdHuman, chatItemIdAi, appId, url, metadata });
|
||||
}, interval);
|
||||
}
|
||||
};
|
||||
|
||||
type ChatItem = ChatItemType & {
|
||||
userGoodFeedback?: string;
|
||||
userBadFeedback?: string;
|
||||
chatId: string;
|
||||
responseData: {
|
||||
moduleType: string;
|
||||
runningTime: number; //s
|
||||
historyPreview: { obj: string; value: string }[];
|
||||
}[];
|
||||
time: Date;
|
||||
};
|
||||
|
||||
type ChatLog = {
|
||||
title: string;
|
||||
feedback: 'like' | 'dislike' | null;
|
||||
chatItemId: string;
|
||||
uid: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
chatId: string;
|
||||
responseTime: number;
|
||||
metadata: string;
|
||||
sourceName: string;
|
||||
createdAt: number;
|
||||
sourceId: string;
|
||||
};
|
||||
|
||||
const pushChatLogInternal = async ({
|
||||
chatId,
|
||||
chatItemIdHuman,
|
||||
chatItemIdAi,
|
||||
appId,
|
||||
url,
|
||||
metadata
|
||||
}: {
|
||||
chatId: string;
|
||||
chatItemIdHuman: string;
|
||||
chatItemIdAi: string;
|
||||
appId: string;
|
||||
url: string;
|
||||
metadata?: Metadata;
|
||||
}) => {
|
||||
try {
|
||||
const [chatItemHuman, chatItemAi] = await Promise.all([
|
||||
MongoChatItem.findById(chatItemIdHuman).lean(),
|
||||
MongoChatItem.findById(chatItemIdAi).lean() as Promise<AIChatItemType>
|
||||
]);
|
||||
|
||||
if (!chatItemHuman || !chatItemAi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = await MongoChat.findOne({ chatId }).lean();
|
||||
|
||||
// addLog.warn('ChatLogDebug', chat);
|
||||
// addLog.warn('ChatLogDebug', { chatItemHuman, chatItemAi });
|
||||
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataString = JSON.stringify(metadata ?? {});
|
||||
|
||||
const uid = chat.outLinkUid || chat.tmbId;
|
||||
// Pop last two items
|
||||
const question = chatItemHuman.value[chatItemHuman.value.length - 1]?.text?.content;
|
||||
const answer = chatItemAi.value[chatItemAi.value.length - 1]?.text?.content;
|
||||
if (!question || !answer) {
|
||||
addLog.error('[ChatLogPush] question or answer is empty', {
|
||||
question: chatItemHuman.value,
|
||||
answer: chatItemAi.value
|
||||
});
|
||||
return;
|
||||
}
|
||||
const responseData = chatItemAi.responseData;
|
||||
const responseTime =
|
||||
responseData?.reduce((acc, item) => acc + (item?.runningTime ?? 0), 0) || 0;
|
||||
|
||||
const sourceIdPrefix = process.env.SOURCE_ID_PREFIX ?? '';
|
||||
|
||||
const chatLog: ChatLog = {
|
||||
title: chat.title,
|
||||
feedback: (() => {
|
||||
if (chatItemAi.userGoodFeedback) {
|
||||
return 'like';
|
||||
} else if (chatItemAi.userBadFeedback) {
|
||||
return 'dislike';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
chatItemId: `${chatItemIdHuman},${chatItemIdAi}`,
|
||||
uid,
|
||||
question,
|
||||
answer,
|
||||
chatId,
|
||||
responseTime: responseTime * 1000,
|
||||
metadata: metadataString,
|
||||
sourceName: chat.source ?? '-',
|
||||
// @ts-ignore
|
||||
createdAt: new Date(chatItemAi.time).getTime(),
|
||||
sourceId: `${sourceIdPrefix}${appId}`
|
||||
};
|
||||
await axios
|
||||
.post(`${url}/api/chat/push`, chatLog)
|
||||
.then((res) => {
|
||||
addLog.info('[ChatLogPush] push success', res.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
addLog.error('[ChatLogPush] push failed', { e, resData: e.response?.data });
|
||||
});
|
||||
} catch (e) {
|
||||
addLog.error('[ChatLogPush] error', e);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type {
|
||||
AIChatItemType,
|
||||
ChatItemType,
|
||||
UserChatItemType
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
import axios from 'axios';
|
||||
import { MongoApp } from '../app/schema';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
@@ -13,6 +18,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
|
||||
import { pushChatLog } from './pushChatLog';
|
||||
|
||||
type Props = {
|
||||
chatId: string;
|
||||
@@ -67,7 +73,7 @@ export async function saveChat({
|
||||
});
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoChatItem.insertMany(
|
||||
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany(
|
||||
content.map((item) => ({
|
||||
chatId,
|
||||
teamId,
|
||||
@@ -105,6 +111,13 @@ export async function saveChat({
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
|
||||
pushChatLog({
|
||||
chatId,
|
||||
chatItemIdHuman: String(chatItemIdHuman),
|
||||
chatItemIdAi: String(chatItemIdAi),
|
||||
appId
|
||||
});
|
||||
});
|
||||
|
||||
if (isUpdateUseTime) {
|
||||
|
||||
@@ -104,9 +104,12 @@ export const loadRequestMessages = async ({
|
||||
}) => {
|
||||
// Load image to base64
|
||||
const loadImageToBase64 = async (messages: ChatCompletionContentPart[]) => {
|
||||
if (process.env.MULTIPLE_DATA_TO_BASE64 === 'false') {
|
||||
return messages;
|
||||
}
|
||||
return Promise.all(
|
||||
messages.map(async (item) => {
|
||||
if (item.type === 'image_url') {
|
||||
if (item.type === 'image_url' && process.env.MULTIPLE_DATA_TO_BASE64 === 'true') {
|
||||
// Remove url origin
|
||||
const imgUrl = (() => {
|
||||
if (origin && item.image_url.url.startsWith(origin)) {
|
||||
@@ -115,38 +118,51 @@ export const loadRequestMessages = async ({
|
||||
return item.image_url.url;
|
||||
})();
|
||||
|
||||
// If imgUrl is a local path, load image from local, and set url to base64
|
||||
if (imgUrl.startsWith('/')) {
|
||||
addLog.debug('Load image from local server', {
|
||||
baseUrl: serverRequestBaseUrl,
|
||||
requestUrl: imgUrl
|
||||
});
|
||||
const response = await axios.get(imgUrl, {
|
||||
baseURL: serverRequestBaseUrl,
|
||||
responseType: 'arraybuffer',
|
||||
proxy: false
|
||||
});
|
||||
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||
const imageType =
|
||||
getFileContentTypeFromHeader(response.headers['content-type']) ||
|
||||
guessBase64ImageType(base64);
|
||||
try {
|
||||
// If imgUrl is a local path, load image from local, and set url to base64
|
||||
if (imgUrl.startsWith('/')) {
|
||||
addLog.debug('Load image from local server', {
|
||||
baseUrl: serverRequestBaseUrl,
|
||||
requestUrl: imgUrl
|
||||
});
|
||||
const response = await axios.get(imgUrl, {
|
||||
baseURL: serverRequestBaseUrl,
|
||||
responseType: 'arraybuffer',
|
||||
proxy: false
|
||||
});
|
||||
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||
const imageType =
|
||||
getFileContentTypeFromHeader(response.headers['content-type']) ||
|
||||
guessBase64ImageType(base64);
|
||||
|
||||
return {
|
||||
...item,
|
||||
image_url: {
|
||||
...item.image_url,
|
||||
url: `data:${imageType};base64,${base64}`
|
||||
}
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
image_url: {
|
||||
...item.image_url,
|
||||
url: `data:${imageType};base64,${base64}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 检查下这个图片是否可以被访问,如果不行的话,则过滤掉
|
||||
const response = await axios.head(imgUrl, {
|
||||
timeout: 10000
|
||||
});
|
||||
if (response.status < 200 || response.status >= 400) {
|
||||
addLog.info(`Filter invalid image: ${imgUrl}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
).then((res) => res.filter(Boolean) as ChatCompletionContentPart[]);
|
||||
};
|
||||
// Split question text and image
|
||||
const parseStringWithImages = (input: string): ChatCompletionContentPart[] => {
|
||||
if (!useVision) {
|
||||
if (!useVision || input.length > 500) {
|
||||
return [{ type: 'text', text: input || '' }];
|
||||
}
|
||||
|
||||
@@ -167,8 +183,8 @@ export const loadRequestMessages = async ({
|
||||
});
|
||||
});
|
||||
|
||||
// Too many images or too long text, return text
|
||||
if (httpsImages.length > 4 || input.length > 1000) {
|
||||
// Too many images return text
|
||||
if (httpsImages.length > 4) {
|
||||
return [{ type: 'text', text: input || '' }];
|
||||
}
|
||||
|
||||
@@ -176,7 +192,7 @@ export const loadRequestMessages = async ({
|
||||
result.push({ type: 'text', text: input });
|
||||
return result;
|
||||
};
|
||||
// Parse user content(text and img)
|
||||
// Parse user content(text and img) Store history => api messages
|
||||
const parseUserContent = async (content: string | ChatCompletionContentPart[]) => {
|
||||
if (typeof content === 'string') {
|
||||
return loadImageToBase64(parseStringWithImages(content));
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DatasetDataWithCollectionType,
|
||||
SearchDataResponseItemType
|
||||
} from '@fastgpt/global/core/dataset/type';
|
||||
import { DatasetColCollectionName, MongoDatasetCollection } from '../collection/schema';
|
||||
import { MongoDatasetCollection } from '../collection/schema';
|
||||
import { reRankRecall } from '../../../core/ai/rerank';
|
||||
import { countPromptTokens } from '../../../common/string/tiktoken/index';
|
||||
import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils';
|
||||
@@ -320,11 +320,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
|
||||
const fullTextRecall = async ({
|
||||
query,
|
||||
limit,
|
||||
filterCollectionIdList
|
||||
filterCollectionIdList,
|
||||
forbidCollectionIdList
|
||||
}: {
|
||||
query: string;
|
||||
limit: number;
|
||||
filterCollectionIdList?: string[];
|
||||
forbidCollectionIdList: string[];
|
||||
}): Promise<{
|
||||
fullTextRecallResults: SearchDataResponseItemType[];
|
||||
tokenLen: number;
|
||||
@@ -351,6 +353,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
|
||||
$in: filterCollectionIdList.map((id) => new Types.ObjectId(id))
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(forbidCollectionIdList && forbidCollectionIdList.length > 0
|
||||
? {
|
||||
collectionId: {
|
||||
$nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id))
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
},
|
||||
@@ -367,31 +376,6 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
|
||||
{
|
||||
$limit: limit
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: DatasetColCollectionName,
|
||||
let: { collectionId: '$collectionId' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ['$_id', '$$collectionId'] },
|
||||
forbid: { $eq: true } // 匹配被禁用的数据
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1 // 只需要_id字段来确认匹配
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'collection'
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
collection: { $eq: [] } // 没有 forbid=true 的数据
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
@@ -509,7 +493,8 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
|
||||
fullTextRecall({
|
||||
query,
|
||||
limit: fullTextLimit,
|
||||
filterCollectionIdList
|
||||
filterCollectionIdList,
|
||||
forbidCollectionIdList
|
||||
})
|
||||
]);
|
||||
totalTokens += tokens;
|
||||
|
||||
@@ -63,7 +63,7 @@ const TrainingDataSchema = new Schema({
|
||||
},
|
||||
q: {
|
||||
type: String,
|
||||
required: true
|
||||
default: ''
|
||||
},
|
||||
a: {
|
||||
type: String,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils
|
||||
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
|
||||
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { i18nT } from '../../../../../../web/i18n/utils';
|
||||
|
||||
type FunctionRunResponseType = {
|
||||
toolRunResponse: DispatchFlowResponse;
|
||||
@@ -549,7 +550,7 @@ async function streamResponse({
|
||||
}
|
||||
|
||||
if (!textAnswer && functionCalls.length === 0) {
|
||||
return Promise.reject('LLM api response empty');
|
||||
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
|
||||
}
|
||||
|
||||
return { answer: textAnswer, functionCalls };
|
||||
|
||||
@@ -25,45 +25,16 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools';
|
||||
import { getMultiplePrompt, Prompt_Tool_Call } from './constants';
|
||||
import { filterToolResponseToPreview } from './utils';
|
||||
import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { getFileContentFromLinks, getHistoryFileLinks } from '../../tools/readFiles';
|
||||
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
|
||||
import { Prompt_DocumentQuote } from '@fastgpt/global/core/ai/prompt/AIChat';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
type Response = DispatchNodeResultType<{
|
||||
[NodeOutputKeyEnum.answerText]: string;
|
||||
[DispatchNodeResponseKeyEnum.interactive]?: InteractiveNodeResponseType;
|
||||
}>;
|
||||
|
||||
/*
|
||||
Tool call, auth add file prompt to question。
|
||||
Guide the LLM to call tool.
|
||||
*/
|
||||
export const toolCallMessagesAdapt = ({
|
||||
userInput
|
||||
}: {
|
||||
userInput: UserChatItemValueItemType[];
|
||||
}) => {
|
||||
const files = userInput.filter((item) => item.type === 'file');
|
||||
|
||||
if (files.length > 0) {
|
||||
return userInput.map((item) => {
|
||||
if (item.type === 'text') {
|
||||
const filesCount = files.filter((file) => file.file?.type === 'file').length;
|
||||
const imgCount = files.filter((file) => file.file?.type === 'image').length;
|
||||
const text = item.text?.content || '';
|
||||
|
||||
return {
|
||||
...item,
|
||||
text: {
|
||||
content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return userInput;
|
||||
};
|
||||
|
||||
export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<Response> => {
|
||||
const {
|
||||
node: { nodeId, name, isEntry },
|
||||
@@ -71,11 +42,21 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
runtimeEdges,
|
||||
histories,
|
||||
query,
|
||||
|
||||
params: { model, systemPrompt, userChatInput, history = 6 }
|
||||
requestOrigin,
|
||||
chatConfig,
|
||||
runningAppInfo: { teamId },
|
||||
params: {
|
||||
model,
|
||||
systemPrompt,
|
||||
userChatInput,
|
||||
history = 6,
|
||||
fileUrlList: fileLinks,
|
||||
aiChatVision
|
||||
}
|
||||
} = props;
|
||||
|
||||
const toolModel = getLLMModel(model);
|
||||
const useVision = aiChatVision && toolModel.vision;
|
||||
const chatHistories = getHistories(history, histories);
|
||||
|
||||
const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges });
|
||||
@@ -109,18 +90,43 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
}
|
||||
})();
|
||||
props.node.isEntry = false;
|
||||
const hasReadFilesTool = toolNodes.some(
|
||||
(item) => item.flowNodeType === FlowNodeTypeEnum.readFiles
|
||||
);
|
||||
|
||||
const globalFiles = chatValue2RuntimePrompt(query).files;
|
||||
const { documentQuoteText, userFiles } = await getMultiInput({
|
||||
histories: chatHistories,
|
||||
requestOrigin,
|
||||
maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20,
|
||||
teamId,
|
||||
fileLinks,
|
||||
inputFiles: globalFiles
|
||||
});
|
||||
|
||||
const concatenateSystemPrompt = [
|
||||
toolModel.defaultSystemChatPrompt,
|
||||
systemPrompt,
|
||||
documentQuoteText
|
||||
? replaceVariable(Prompt_DocumentQuote, {
|
||||
quote: documentQuoteText
|
||||
})
|
||||
: ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n===---===---===\n\n');
|
||||
|
||||
const messages: ChatItemType[] = (() => {
|
||||
const value: ChatItemType[] = [
|
||||
...getSystemPrompt_ChatItemType(toolModel.defaultSystemChatPrompt),
|
||||
...getSystemPrompt_ChatItemType(systemPrompt),
|
||||
...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
|
||||
// Add file input prompt to histories
|
||||
...chatHistories.map((item) => {
|
||||
if (item.obj === ChatRoleEnum.Human) {
|
||||
return {
|
||||
...item,
|
||||
value: toolCallMessagesAdapt({
|
||||
userInput: item.value
|
||||
userInput: item.value,
|
||||
skip: !hasReadFilesTool
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -129,9 +135,10 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: toolCallMessagesAdapt({
|
||||
skip: !hasReadFilesTool,
|
||||
userInput: runtimePrompt2ChatsValue({
|
||||
text: userChatInput,
|
||||
files: chatValue2RuntimePrompt(query).files
|
||||
files: userFiles
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -185,7 +192,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
// array, replace last element
|
||||
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||
if (lastText.type === 'text') {
|
||||
lastMessage.content = replaceVariable(Prompt_Tool_Call, {
|
||||
lastText.text = replaceVariable(Prompt_Tool_Call, {
|
||||
question: lastText.text
|
||||
});
|
||||
} else {
|
||||
@@ -237,7 +244,11 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
childTotalPoints: flatUsages.reduce((sum, item) => sum + item.totalPoints, 0),
|
||||
model: modelName,
|
||||
query: userChatInput,
|
||||
historyPreview: getHistoryPreview(GPTMessages2Chats(completeMessages, false), 10000),
|
||||
historyPreview: getHistoryPreview(
|
||||
GPTMessages2Chats(completeMessages, false),
|
||||
10000,
|
||||
useVision
|
||||
),
|
||||
toolDetail: childToolResponse,
|
||||
mergeSignId: nodeId
|
||||
},
|
||||
@@ -253,3 +264,88 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
[DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse
|
||||
};
|
||||
};
|
||||
|
||||
const getMultiInput = async ({
|
||||
histories,
|
||||
fileLinks,
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId,
|
||||
inputFiles
|
||||
}: {
|
||||
histories: ChatItemType[];
|
||||
fileLinks?: string[];
|
||||
requestOrigin?: string;
|
||||
maxFiles: number;
|
||||
teamId: string;
|
||||
inputFiles: UserChatItemValueItemType['file'][];
|
||||
}) => {
|
||||
// Not file quote
|
||||
if (!fileLinks) {
|
||||
return {
|
||||
documentQuoteText: '',
|
||||
userFiles: inputFiles
|
||||
};
|
||||
}
|
||||
|
||||
const filesFromHistories = getHistoryFileLinks(histories);
|
||||
const urls = [...fileLinks, ...filesFromHistories];
|
||||
|
||||
if (urls.length === 0) {
|
||||
return {
|
||||
documentQuoteText: '',
|
||||
userFiles: []
|
||||
};
|
||||
}
|
||||
|
||||
// Get files from histories
|
||||
const { text } = await getFileContentFromLinks({
|
||||
// Concat fileUrlList and filesFromHistories; remove not supported files
|
||||
urls,
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId
|
||||
});
|
||||
|
||||
return {
|
||||
documentQuoteText: text,
|
||||
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
Tool call, auth add file prompt to question。
|
||||
Guide the LLM to call tool.
|
||||
*/
|
||||
const toolCallMessagesAdapt = ({
|
||||
userInput,
|
||||
skip
|
||||
}: {
|
||||
userInput: UserChatItemValueItemType[];
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
if (skip) return userInput;
|
||||
|
||||
const files = userInput.filter((item) => item.type === 'file');
|
||||
|
||||
if (files.length > 0) {
|
||||
return userInput.map((item) => {
|
||||
if (item.type === 'text') {
|
||||
const filesCount = files.filter((file) => file.file?.type === 'file').length;
|
||||
const imgCount = files.filter((file) => file.file?.type === 'image').length;
|
||||
const text = item.text?.content || '';
|
||||
|
||||
return {
|
||||
...item,
|
||||
text: {
|
||||
content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return userInput;
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ import { WorkflowResponseType } from '../../type';
|
||||
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
|
||||
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { i18nT } from '../../../../../../web/i18n/utils';
|
||||
|
||||
type FunctionCallCompletion = {
|
||||
id: string;
|
||||
@@ -176,17 +177,29 @@ export const runToolWithPromptCall = async (
|
||||
);
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (typeof lastMessage.content !== 'string') {
|
||||
if (typeof lastMessage.content === 'string') {
|
||||
lastMessage.content = replaceVariable(lastMessage.content, {
|
||||
toolsPrompt
|
||||
});
|
||||
} else if (Array.isArray(lastMessage.content)) {
|
||||
// array, replace last element
|
||||
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||
if (lastText.type === 'text') {
|
||||
lastText.text = replaceVariable(lastText.text, {
|
||||
toolsPrompt
|
||||
});
|
||||
} else {
|
||||
return Promise.reject('Prompt call invalid input');
|
||||
}
|
||||
} else {
|
||||
return Promise.reject('Prompt call invalid input');
|
||||
}
|
||||
lastMessage.content = replaceVariable(lastMessage.content, {
|
||||
toolsPrompt
|
||||
});
|
||||
|
||||
const filterMessages = await filterGPTMessageByMaxTokens({
|
||||
messages,
|
||||
maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken
|
||||
});
|
||||
|
||||
const [requestMessages, max_tokens] = await Promise.all([
|
||||
loadRequestMessages({
|
||||
messages: filterMessages,
|
||||
@@ -398,11 +411,27 @@ export const runToolWithPromptCall = async (
|
||||
: undefined;
|
||||
|
||||
// get the next user prompt
|
||||
lastMessage.content += `${replaceAnswer}
|
||||
if (typeof lastMessage.content === 'string') {
|
||||
lastMessage.content += `${replaceAnswer}
|
||||
TOOL_RESPONSE: """
|
||||
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
|
||||
"""
|
||||
ANSWER: `;
|
||||
} else if (Array.isArray(lastMessage.content)) {
|
||||
// array, replace last element
|
||||
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||
if (lastText.type === 'text') {
|
||||
lastText.text += `${replaceAnswer}
|
||||
TOOL_RESPONSE: """
|
||||
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
|
||||
"""
|
||||
ANSWER: `;
|
||||
} else {
|
||||
return Promise.reject('Prompt call invalid input');
|
||||
}
|
||||
} else {
|
||||
return Promise.reject('Prompt call invalid input');
|
||||
}
|
||||
|
||||
const runTimes = (response?.runTimes || 0) + toolsRunResponse.toolResponse.runTimes;
|
||||
const toolNodeTokens = response?.toolNodeTokens ? response.toolNodeTokens + tokens : tokens;
|
||||
@@ -509,7 +538,7 @@ async function streamResponse({
|
||||
}
|
||||
|
||||
if (!textAnswer) {
|
||||
return Promise.reject('LLM api response empty');
|
||||
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
|
||||
}
|
||||
return { answer: textAnswer.trim() };
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { addLog } from '../../../../../common/system/log';
|
||||
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
|
||||
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { i18nT } from '../../../../../../web/i18n/utils';
|
||||
|
||||
type ToolRunResponseType = {
|
||||
toolRunResponse: DispatchFlowResponse;
|
||||
@@ -268,7 +269,7 @@ export const runToolWithToolChoice = async (
|
||||
},
|
||||
toolModel
|
||||
);
|
||||
// console.log(JSON.stringify(requestMessages, null, 2), '==requestBody');
|
||||
// console.log(JSON.stringify(requestBody, null, 2), '==requestBody');
|
||||
/* Run llm */
|
||||
const ai = getAIApi({
|
||||
timeout: 480000
|
||||
@@ -656,7 +657,7 @@ async function streamResponse({
|
||||
}
|
||||
|
||||
if (!textAnswer && toolCalls.length === 0) {
|
||||
return Promise.reject('LLM api response empty');
|
||||
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
|
||||
}
|
||||
|
||||
return { answer: textAnswer, toolCalls };
|
||||
|
||||
@@ -21,6 +21,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.aiChatTemperature]: number;
|
||||
[NodeInputKeyEnum.aiChatMaxToken]: number;
|
||||
[NodeInputKeyEnum.aiChatVision]?: boolean;
|
||||
[NodeInputKeyEnum.fileUrlList]?: string[];
|
||||
}> & {
|
||||
messages: ChatCompletionMessageParam[];
|
||||
toolNodes: ToolNodeItemType[];
|
||||
|
||||
@@ -5,11 +5,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { getAIApi } from '../../../ai/config';
|
||||
import type {
|
||||
ChatCompletion,
|
||||
ChatCompletionMessageParam,
|
||||
StreamChatType
|
||||
} from '@fastgpt/global/core/ai/type.d';
|
||||
import type { ChatCompletion, StreamChatType } from '@fastgpt/global/core/ai/type.d';
|
||||
import { formatModelChars2Points } from '../../../../support/wallet/usage/utils';
|
||||
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
|
||||
import { postTextCensor } from '../../../../common/api/requestPlusApi';
|
||||
@@ -45,6 +41,10 @@ import { computedMaxToken, llmCompletionsBodyFormat } from '../../../ai/utils';
|
||||
import { WorkflowResponseType } from '../type';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import { AiChatQuoteRoleType } from '@fastgpt/global/core/workflow/template/system/aiChat/type';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { getFileContentFromLinks, getHistoryFileLinks } from '../tools/readFiles';
|
||||
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
|
||||
import { i18nT } from '../../../../../web/i18n/utils';
|
||||
|
||||
export type ChatProps = ModuleDispatchProps<
|
||||
AIChatNodeProps & {
|
||||
@@ -68,7 +68,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
histories,
|
||||
node: { name },
|
||||
query,
|
||||
runningAppInfo: { teamId },
|
||||
workflowStreamResponse,
|
||||
chatConfig,
|
||||
params: {
|
||||
model,
|
||||
temperature = 0,
|
||||
@@ -82,14 +84,12 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
quoteTemplate,
|
||||
quotePrompt,
|
||||
aiChatVision,
|
||||
stringQuoteText
|
||||
fileUrlList: fileLinks, // node quote file links
|
||||
stringQuoteText //abandon
|
||||
}
|
||||
} = props;
|
||||
const { files: inputFiles } = chatValue2RuntimePrompt(query);
|
||||
const { files: inputFiles } = chatValue2RuntimePrompt(query); // Chat box input files
|
||||
|
||||
if (!userChatInput && inputFiles.length === 0) {
|
||||
return Promise.reject('Question is empty');
|
||||
}
|
||||
stream = stream && isResponseAnswerText;
|
||||
|
||||
const chatHistories = getHistories(history, histories);
|
||||
@@ -99,11 +99,26 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
return Promise.reject('The chat model is undefined, you need to select a chat model.');
|
||||
}
|
||||
|
||||
const { datasetQuoteText } = await filterDatasetQuote({
|
||||
quoteQA,
|
||||
model: modelConstantsData,
|
||||
quoteTemplate
|
||||
});
|
||||
const [{ datasetQuoteText }, { documentQuoteText, userFiles }] = await Promise.all([
|
||||
filterDatasetQuote({
|
||||
quoteQA,
|
||||
model: modelConstantsData,
|
||||
quoteTemplate
|
||||
}),
|
||||
getMultiInput({
|
||||
histories: chatHistories,
|
||||
inputFiles,
|
||||
fileLinks,
|
||||
stringQuoteText,
|
||||
requestOrigin,
|
||||
maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20,
|
||||
teamId
|
||||
})
|
||||
]);
|
||||
|
||||
if (!userChatInput && !documentQuoteText && userFiles.length === 0) {
|
||||
return Promise.reject(i18nT('chat:AI_input_is_empty'));
|
||||
}
|
||||
|
||||
const [{ filterMessages }] = await Promise.all([
|
||||
getChatMessages({
|
||||
@@ -114,9 +129,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
aiChatQuoteRole,
|
||||
datasetQuotePrompt: quotePrompt,
|
||||
userChatInput,
|
||||
inputFiles,
|
||||
systemPrompt,
|
||||
stringQuoteText
|
||||
userFiles,
|
||||
documentQuoteText
|
||||
}),
|
||||
(() => {
|
||||
// censor model and system key
|
||||
@@ -131,22 +146,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
})()
|
||||
]);
|
||||
|
||||
// Get the request messages
|
||||
const concatMessages = [
|
||||
...(modelConstantsData.defaultSystemChatPrompt
|
||||
? [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
content: modelConstantsData.defaultSystemChatPrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...filterMessages
|
||||
] as ChatCompletionMessageParam[];
|
||||
|
||||
const [requestMessages, max_tokens] = await Promise.all([
|
||||
loadRequestMessages({
|
||||
messages: concatMessages,
|
||||
messages: filterMessages,
|
||||
useVision: modelConstantsData.vision && aiChatVision,
|
||||
origin: requestOrigin
|
||||
}),
|
||||
@@ -194,7 +196,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
throw new Error('LLM model response empty');
|
||||
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -241,7 +243,11 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
tokens,
|
||||
query: `${userChatInput}`,
|
||||
maxToken: max_tokens,
|
||||
historyPreview: getHistoryPreview(chatCompleteMessages, 10000),
|
||||
historyPreview: getHistoryPreview(
|
||||
chatCompleteMessages,
|
||||
10000,
|
||||
modelConstantsData.vision && aiChatVision
|
||||
),
|
||||
contextTotalLen: completeMessages.length
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
|
||||
@@ -262,7 +268,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
});
|
||||
|
||||
if (user.openaiAccount?.baseUrl) {
|
||||
return Promise.reject(`您的 OpenAI key 出错了: ${JSON.stringify(requestBody)}`);
|
||||
return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
@@ -301,7 +307,70 @@ async function filterDatasetQuote({
|
||||
datasetQuoteText
|
||||
};
|
||||
}
|
||||
|
||||
async function getMultiInput({
|
||||
histories,
|
||||
inputFiles,
|
||||
fileLinks,
|
||||
stringQuoteText,
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId
|
||||
}: {
|
||||
histories: ChatItemType[];
|
||||
inputFiles: UserChatItemValueItemType['file'][];
|
||||
fileLinks?: string[];
|
||||
stringQuoteText?: string; // file quote
|
||||
requestOrigin?: string;
|
||||
maxFiles: number;
|
||||
teamId: string;
|
||||
}) {
|
||||
// 旧版本适配====>
|
||||
if (stringQuoteText) {
|
||||
return {
|
||||
documentQuoteText: stringQuoteText,
|
||||
userFiles: inputFiles
|
||||
};
|
||||
}
|
||||
|
||||
// 没有引用文件参考,但是可能用了图片识别
|
||||
if (!fileLinks) {
|
||||
return {
|
||||
documentQuoteText: '',
|
||||
userFiles: inputFiles
|
||||
};
|
||||
}
|
||||
// 旧版本适配<====
|
||||
|
||||
// If fileLinks params is not empty, it means it is a new version, not get the global file.
|
||||
|
||||
// Get files from histories
|
||||
const filesFromHistories = getHistoryFileLinks(histories);
|
||||
const urls = [...fileLinks, ...filesFromHistories];
|
||||
|
||||
if (urls.length === 0) {
|
||||
return {
|
||||
documentQuoteText: '',
|
||||
userFiles: []
|
||||
};
|
||||
}
|
||||
|
||||
const { text } = await getFileContentFromLinks({
|
||||
// Concat fileUrlList and filesFromHistories; remove not supported files
|
||||
urls,
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId
|
||||
});
|
||||
|
||||
return {
|
||||
documentQuoteText: text,
|
||||
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
|
||||
};
|
||||
}
|
||||
|
||||
async function getChatMessages({
|
||||
model,
|
||||
aiChatQuoteRole,
|
||||
datasetQuotePrompt = '',
|
||||
datasetQuoteText,
|
||||
@@ -309,10 +378,10 @@ async function getChatMessages({
|
||||
histories = [],
|
||||
systemPrompt,
|
||||
userChatInput,
|
||||
inputFiles,
|
||||
model,
|
||||
stringQuoteText
|
||||
userFiles,
|
||||
documentQuoteText
|
||||
}: {
|
||||
model: LLMModelItemType;
|
||||
// dataset quote
|
||||
aiChatQuoteRole: AiChatQuoteRoleType; // user: replace user prompt; system: replace system prompt
|
||||
datasetQuotePrompt?: string;
|
||||
@@ -322,10 +391,11 @@ async function getChatMessages({
|
||||
histories: ChatItemType[];
|
||||
systemPrompt: string;
|
||||
userChatInput: string;
|
||||
inputFiles: UserChatItemValueItemType['file'][];
|
||||
model: LLMModelItemType;
|
||||
stringQuoteText?: string; // file quote
|
||||
|
||||
userFiles: UserChatItemValueItemType['file'][];
|
||||
documentQuoteText?: string; // document quote
|
||||
}) {
|
||||
// Dataset prompt ====>
|
||||
// User role or prompt include question
|
||||
const quoteRole =
|
||||
aiChatQuoteRole === 'user' || datasetQuotePrompt.includes('{{question}}') ? 'user' : 'system';
|
||||
@@ -336,6 +406,7 @@ async function getChatMessages({
|
||||
? Prompt_userQuotePromptList[0].value
|
||||
: Prompt_systemQuotePromptList[0].value;
|
||||
|
||||
// Reset user input, add dataset quote to user input
|
||||
const replaceInputValue =
|
||||
useDatasetQuote && quoteRole === 'user'
|
||||
? replaceVariable(datasetQuotePromptTemplate, {
|
||||
@@ -343,31 +414,33 @@ async function getChatMessages({
|
||||
question: userChatInput
|
||||
})
|
||||
: userChatInput;
|
||||
// Dataset prompt <====
|
||||
|
||||
const replaceSystemPrompt =
|
||||
// Concat system prompt
|
||||
const concatenateSystemPrompt = [
|
||||
model.defaultSystemChatPrompt,
|
||||
systemPrompt,
|
||||
useDatasetQuote && quoteRole === 'system'
|
||||
? `${systemPrompt ? systemPrompt + '\n\n------\n\n' : ''}${replaceVariable(
|
||||
datasetQuotePromptTemplate,
|
||||
{
|
||||
quote: datasetQuoteText
|
||||
}
|
||||
)}`
|
||||
: systemPrompt;
|
||||
? replaceVariable(datasetQuotePromptTemplate, {
|
||||
quote: datasetQuoteText
|
||||
})
|
||||
: '',
|
||||
documentQuoteText
|
||||
? replaceVariable(Prompt_DocumentQuote, {
|
||||
quote: documentQuoteText
|
||||
})
|
||||
: ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n===---===---===\n\n');
|
||||
|
||||
const messages: ChatItemType[] = [
|
||||
...getSystemPrompt_ChatItemType(replaceSystemPrompt),
|
||||
...(stringQuoteText // file quote
|
||||
? getSystemPrompt_ChatItemType(
|
||||
replaceVariable(Prompt_DocumentQuote, {
|
||||
quote: stringQuoteText
|
||||
})
|
||||
)
|
||||
: []),
|
||||
...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
|
||||
...histories,
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: runtimePrompt2ChatsValue({
|
||||
files: inputFiles,
|
||||
files: userFiles,
|
||||
text: replaceInputValue
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type {
|
||||
DispatchNodeResultType,
|
||||
ModuleDispatchProps
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils';
|
||||
import { filterSearchResultsByMaxChars } from '../../utils';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
|
||||
type DatasetConcatProps = ModuleDispatchProps<
|
||||
{
|
||||
[NodeInputKeyEnum.datasetMaxTokens]: number;
|
||||
} & { [key: string]: SearchDataResponseItemType[] }
|
||||
>;
|
||||
type DatasetConcatResponse = {
|
||||
type DatasetConcatResponse = DispatchNodeResultType<{
|
||||
[NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[];
|
||||
};
|
||||
}>;
|
||||
|
||||
export async function dispatchDatasetConcat(
|
||||
props: DatasetConcatProps
|
||||
@@ -30,6 +34,12 @@ export async function dispatchDatasetConcat(
|
||||
);
|
||||
|
||||
return {
|
||||
[NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars(rrfConcatResults, limit)
|
||||
[NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars(
|
||||
rrfConcatResults,
|
||||
limit
|
||||
),
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
concatLength: rrfConcatResults.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { datasetSearchQueryExtension } from '../../../dataset/search/utils';
|
||||
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||
import { checkTeamReRankPermission } from '../../../../support/permission/teamLimit';
|
||||
import { MongoDataset } from '../../../dataset/schema';
|
||||
import { i18nT } from '../../../../../web/i18n/utils';
|
||||
|
||||
type DatasetSearchProps = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
|
||||
@@ -56,15 +57,15 @@ export async function dispatchDatasetSearch(
|
||||
} = props as DatasetSearchProps;
|
||||
|
||||
if (!Array.isArray(datasets)) {
|
||||
return Promise.reject('Quote type error');
|
||||
return Promise.reject(i18nT('chat:dataset_quote_type error'));
|
||||
}
|
||||
|
||||
if (datasets.length === 0) {
|
||||
return Promise.reject('core.chat.error.Select dataset empty');
|
||||
return Promise.reject(i18nT('common:core.chat.error.Select dataset empty'));
|
||||
}
|
||||
|
||||
if (!userChatInput) {
|
||||
return Promise.reject('core.chat.error.User input empty');
|
||||
return Promise.reject(i18nT('common:core.chat.error.User input empty'));
|
||||
}
|
||||
|
||||
// query extension
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools';
|
||||
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
|
||||
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
|
||||
|
||||
import { dispatchWorkflowStart } from './init/workflowStart';
|
||||
import { dispatchChatCompletion } from './chat/oneapi';
|
||||
@@ -38,11 +37,12 @@ import { dispatchQueryExtension } from './tools/queryExternsion';
|
||||
import { dispatchRunPlugin } from './plugin/run';
|
||||
import { dispatchPluginInput } from './plugin/runInput';
|
||||
import { dispatchPluginOutput } from './plugin/runOutput';
|
||||
import { removeSystemVariable, valueTypeFormat } from './utils';
|
||||
import { formatHttpError, removeSystemVariable, valueTypeFormat } from './utils';
|
||||
import {
|
||||
filterWorkflowEdges,
|
||||
checkNodeRunStatus,
|
||||
textAdaptGptResponse
|
||||
textAdaptGptResponse,
|
||||
replaceEditorVariable
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||
import { dispatchRunTools } from './agent/runTool/index';
|
||||
@@ -72,6 +72,7 @@ import { dispatchLoopEnd } from './loop/runLoopEnd';
|
||||
import { dispatchLoopStart } from './loop/runLoopStart';
|
||||
import { dispatchFormInput } from './interactive/formInput';
|
||||
import { dispatchToolParams } from './agent/runTool/toolParams';
|
||||
import { responseWrite } from '../../../common/response';
|
||||
|
||||
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
|
||||
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
|
||||
@@ -386,6 +387,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
node,
|
||||
runtimeEdges
|
||||
});
|
||||
|
||||
const nodeRunResult = await (() => {
|
||||
if (status === 'run') {
|
||||
nodeRunBeforeHook(node);
|
||||
@@ -481,8 +483,16 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
: {};
|
||||
|
||||
node.inputs.forEach((input) => {
|
||||
// Special input, not format
|
||||
if (input.key === dynamicInput?.key) return;
|
||||
|
||||
// Skip some special key
|
||||
if (input.key === NodeInputKeyEnum.childrenNodeIdList) {
|
||||
params[input.key] = input.value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// replace {{xx}} variables
|
||||
let value = replaceVariable(input.value, variables);
|
||||
|
||||
@@ -505,7 +515,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
if (input.canEdit && dynamicInput && params[dynamicInput.key]) {
|
||||
params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType);
|
||||
}
|
||||
|
||||
params[input.key] = valueTypeFormat(value, input.valueType);
|
||||
});
|
||||
|
||||
@@ -548,7 +557,21 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
// run module
|
||||
const dispatchRes: Record<string, any> = await (async () => {
|
||||
if (callbackMap[node.flowNodeType]) {
|
||||
return callbackMap[node.flowNodeType](dispatchData);
|
||||
try {
|
||||
return await callbackMap[node.flowNodeType](dispatchData);
|
||||
} catch (error) {
|
||||
// Get source handles of outgoing edges
|
||||
const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId);
|
||||
const skipHandleIds = targetEdges.map((item) => item.sourceHandle);
|
||||
|
||||
// Skip all edges and return error
|
||||
return {
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
error: formatHttpError(error)
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.skipHandleId]: skipHandleIds
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
})();
|
||||
|
||||
@@ -25,7 +25,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
user,
|
||||
node: { name }
|
||||
} = props;
|
||||
const { loopInputArray = [], childrenNodeIdList } = params;
|
||||
const { loopInputArray = [], childrenNodeIdList = [] } = params;
|
||||
|
||||
if (!Array.isArray(loopInputArray)) {
|
||||
return Promise.reject('Input value is not an array');
|
||||
@@ -43,23 +43,24 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
let totalPoints = 0;
|
||||
let newVariables: Record<string, any> = props.variables;
|
||||
|
||||
for await (const item of loopInputArray) {
|
||||
let index = 0;
|
||||
for await (const item of loopInputArray.filter(Boolean)) {
|
||||
runtimeNodes.forEach((node) => {
|
||||
if (
|
||||
childrenNodeIdList.includes(node.nodeId) &&
|
||||
node.flowNodeType === FlowNodeTypeEnum.loopStart
|
||||
) {
|
||||
node.isEntry = true;
|
||||
node.inputs = node.inputs.map((input) =>
|
||||
input.key === NodeInputKeyEnum.loopStartInput
|
||||
? {
|
||||
...input,
|
||||
value: item
|
||||
}
|
||||
: input
|
||||
);
|
||||
node.inputs.forEach((input) => {
|
||||
if (input.key === NodeInputKeyEnum.loopStartInput) {
|
||||
input.value = item;
|
||||
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
|
||||
input.value = index++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const response = await dispatchWorkFlow({
|
||||
...props,
|
||||
runtimeEdges: cloneDeep(runtimeEdges)
|
||||
@@ -69,11 +70,13 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
(res) => res.moduleType === FlowNodeTypeEnum.loopEnd
|
||||
)?.loopOutputValue;
|
||||
|
||||
// Concat runtime response
|
||||
outputValueArr.push(loopOutputValue);
|
||||
loopDetail.push(...response.flowResponses);
|
||||
assistantResponses.push(...response.assistantResponses);
|
||||
totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
|
||||
|
||||
totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
|
||||
// Concat new variables
|
||||
newVariables = {
|
||||
...newVariables,
|
||||
...response.newVariables
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.loopStartInput]: any;
|
||||
[NodeInputKeyEnum.loopStartIndex]: number;
|
||||
}>;
|
||||
type Response = DispatchNodeResultType<{
|
||||
[NodeOutputKeyEnum.loopStartInput]: any;
|
||||
[NodeOutputKeyEnum.loopStartIndex]: number;
|
||||
}>;
|
||||
|
||||
export const dispatchLoopStart = async (props: Props): Promise<Response> => {
|
||||
@@ -18,6 +20,7 @@ export const dispatchLoopStart = async (props: Props): Promise<Response> => {
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
loopInputValue: params.loopStartInput
|
||||
},
|
||||
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput
|
||||
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput,
|
||||
[NodeOutputKeyEnum.loopStartIndex]: params.loopStartIndex
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,7 +112,11 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
|
||||
output.moduleLogo = plugin.avatar;
|
||||
}
|
||||
|
||||
const usagePoints = await computedPluginUsage(plugin, flowUsages);
|
||||
const usagePoints = await computedPluginUsage({
|
||||
plugin,
|
||||
childrenUsage: flowUsages,
|
||||
error: !!output?.pluginOutput?.error
|
||||
});
|
||||
|
||||
return {
|
||||
// 嵌套运行时,如果 childApp stream=false,实际上不会有任何内容输出给用户,所以不需要存储
|
||||
|
||||
@@ -17,12 +17,14 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty
|
||||
import { authAppByTmbId } from '../../../../support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { getAppVersionById } from '../../../app/version/controller';
|
||||
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.userChatInput]: string;
|
||||
[NodeInputKeyEnum.history]?: ChatItemType[] | number;
|
||||
[NodeInputKeyEnum.fileUrlList]?: string[];
|
||||
[NodeInputKeyEnum.forbidStream]?: boolean;
|
||||
[NodeInputKeyEnum.fileUrlList]?: string[];
|
||||
}>;
|
||||
type Response = DispatchNodeResultType<{
|
||||
[NodeOutputKeyEnum.answerText]: string;
|
||||
@@ -40,8 +42,24 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
|
||||
variables
|
||||
} = props;
|
||||
|
||||
const { system_forbid_stream = false, userChatInput, history, ...childrenAppVariables } = params;
|
||||
if (!userChatInput) {
|
||||
const {
|
||||
system_forbid_stream = false,
|
||||
userChatInput,
|
||||
history,
|
||||
fileUrlList,
|
||||
...childrenAppVariables
|
||||
} = params;
|
||||
const { files } = chatValue2RuntimePrompt(query);
|
||||
|
||||
const userInputFiles = (() => {
|
||||
if (fileUrlList) {
|
||||
return fileUrlList.map((url) => parseUrlToFileType(url));
|
||||
}
|
||||
// Adapt version 4.8.13 upgrade
|
||||
return files;
|
||||
})();
|
||||
|
||||
if (!userChatInput && !userInputFiles) {
|
||||
return Promise.reject('Input is empty');
|
||||
}
|
||||
if (!appId) {
|
||||
@@ -72,7 +90,6 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
|
||||
}
|
||||
|
||||
const chatHistories = getHistories(history, histories);
|
||||
const { files } = chatValue2RuntimePrompt(query);
|
||||
|
||||
// Rewrite children app variables
|
||||
const systemVariables = filterSystemVariables(variables);
|
||||
@@ -102,7 +119,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
|
||||
histories: chatHistories,
|
||||
variables: childrenRunVariables,
|
||||
query: runtimePrompt2ChatsValue({
|
||||
files,
|
||||
files: userInputFiles,
|
||||
text: userChatInput
|
||||
}),
|
||||
chatConfig
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
@@ -11,6 +12,26 @@ export const dispatchPluginInput = (props: PluginInputProps) => {
|
||||
const { params, query } = props;
|
||||
const { files } = chatValue2RuntimePrompt(query);
|
||||
|
||||
/*
|
||||
对 params 中文件类型数据进行处理
|
||||
* 插件单独运行时,这里会是一个特殊的数组
|
||||
* 插件调用的话,这个参数是一个 string[] 不会进行处理
|
||||
* 硬性要求:API 单独调用插件时,要避免这种特殊类型冲突
|
||||
|
||||
TODO: 需要 filter max files
|
||||
*/
|
||||
for (const key in params) {
|
||||
const val = params[key];
|
||||
if (
|
||||
Array.isArray(val) &&
|
||||
val.every(
|
||||
(item) => item.type === ChatFileTypeEnum.file || item.type === ChatFileTypeEnum.image
|
||||
)
|
||||
) {
|
||||
params[key] = val.map((item) => item.url);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {},
|
||||
|
||||
@@ -14,15 +14,17 @@ import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
|
||||
import { addLog } from '../../../../common/system/log';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import {
|
||||
textAdaptGptResponse,
|
||||
replaceEditorVariable
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { getSystemPluginCb } from '../../../../../plugins/register';
|
||||
import { ContentTypes } from '@fastgpt/global/core/workflow/constants';
|
||||
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
|
||||
import { uploadFile } from '../../../../common/file/gridfs/controller';
|
||||
import { uploadFileFromBase64Img } from '../../../../common/file/gridfs/controller';
|
||||
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
|
||||
import { createFileToken } from '../../../../support/permission/controller';
|
||||
import { removeFilesByPaths } from '../../../../common/file/utils';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import type { SystemPluginSpecialResponse } from '../../../../../plugins/type';
|
||||
|
||||
type PropsArrType = {
|
||||
key: string;
|
||||
@@ -235,18 +237,29 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
|
||||
node.outputs
|
||||
.filter(
|
||||
(item) =>
|
||||
item.key !== NodeOutputKeyEnum.error && item.key !== NodeOutputKeyEnum.httpRawResponse
|
||||
item.id !== NodeOutputKeyEnum.error &&
|
||||
item.id !== NodeOutputKeyEnum.httpRawResponse &&
|
||||
item.id !== NodeOutputKeyEnum.addOutputParam
|
||||
)
|
||||
.forEach((item) => {
|
||||
const key = item.key.startsWith('$') ? item.key : `$.${item.key}`;
|
||||
results[item.key] = (() => {
|
||||
// 检查是否是简单的属性访问或单一索引访问
|
||||
if (/^\$(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/.test(key) || /^\$(\[\d+\])+$/.test(key)) {
|
||||
return JSONPath({ path: key, json: formatResponse })[0];
|
||||
const result = JSONPath({ path: key, json: formatResponse });
|
||||
|
||||
// 如果结果为空,返回 undefined
|
||||
if (!result || result.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 如果无法确定,默认返回数组
|
||||
return JSONPath({ path: key, json: formatResponse });
|
||||
// 以下情况返回数组:
|
||||
// 1. 使用通配符 *
|
||||
// 2. 使用数组切片 [start:end]
|
||||
// 3. 使用过滤表达式 [?(...)]
|
||||
// 4. 使用递归下降 ..
|
||||
// 5. 使用多个结果运算符 ,
|
||||
const needArrayResult = /[*]|[\[][:?]|\.\.|\,/.test(key);
|
||||
|
||||
return needArrayResult ? result : result[0];
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -367,27 +380,25 @@ async function replaceSystemPluginResponse({
|
||||
tmbId: string;
|
||||
}) {
|
||||
for await (const key of Object.keys(response)) {
|
||||
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_FILE') {
|
||||
const fileObj = response[key];
|
||||
const filename = fileObj.path.split('/').pop() || `${tmbId}-${Date.now()}`;
|
||||
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_BASE64') {
|
||||
const fileObj = response[key] as SystemPluginSpecialResponse;
|
||||
const filename = `${tmbId}-${Date.now()}.${fileObj.extension}`;
|
||||
try {
|
||||
const fileId = await uploadFile({
|
||||
const fileId = await uploadFileFromBase64Img({
|
||||
teamId,
|
||||
tmbId,
|
||||
bucketName: 'chat',
|
||||
path: fileObj.path,
|
||||
base64: fileObj.value,
|
||||
filename,
|
||||
contentType: fileObj.contentType,
|
||||
metadata: {}
|
||||
});
|
||||
response[key] = `${ReadFileBaseUrl}?filename=${filename}&token=${await createFileToken({
|
||||
response[key] = `${ReadFileBaseUrl}/${filename}?token=${await createFileToken({
|
||||
bucketName: 'chat',
|
||||
teamId,
|
||||
tmbId,
|
||||
fileId
|
||||
})}`;
|
||||
} catch (error) {}
|
||||
removeFilesByPaths([fileObj.path]);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -2,16 +2,15 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import axios from 'axios';
|
||||
import { serverRequestBaseUrl } from '../../../../common/api/serverRequest';
|
||||
import { MongoRawTextBuffer } from '../../../../common/buffer/rawText/schema';
|
||||
import { readFromSecondary } from '../../../../common/mongo/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
|
||||
import { detectFileEncoding, parseUrlToFileType } from '@fastgpt/global/common/file/tools';
|
||||
import { readRawContentByFileBuffer } from '../../../../common/file/read/utils';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
@@ -48,12 +47,41 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
|
||||
runningAppInfo: { teamId },
|
||||
histories,
|
||||
chatConfig,
|
||||
node: { version },
|
||||
params: { fileUrlList = [] }
|
||||
} = props;
|
||||
const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 20;
|
||||
|
||||
// Get files from histories
|
||||
const filesFromHistories = histories
|
||||
const filesFromHistories = version !== '489' ? [] : getHistoryFileLinks(histories);
|
||||
|
||||
const { text, readFilesResult } = await getFileContentFromLinks({
|
||||
// Concat fileUrlList and filesFromHistories; remove not supported files
|
||||
urls: [...fileUrlList, ...filesFromHistories],
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId
|
||||
});
|
||||
|
||||
return {
|
||||
[NodeOutputKeyEnum.text]: text,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
readFiles: readFilesResult.map((item) => ({
|
||||
name: item?.filename || '',
|
||||
url: item?.url || ''
|
||||
})),
|
||||
readFilesResult: readFilesResult
|
||||
.map((item) => item?.nodeResponsePreviewText ?? '')
|
||||
.join('\n******\n')
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.toolResponses]: {
|
||||
fileContent: text
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getHistoryFileLinks = (histories: ChatItemType[]) => {
|
||||
return histories
|
||||
.filter((item) => {
|
||||
if (item.obj === ChatRoleEnum.Human) {
|
||||
return item.value.filter((value) => value.type === 'file');
|
||||
@@ -70,28 +98,38 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
|
||||
return files;
|
||||
})
|
||||
.flat();
|
||||
};
|
||||
|
||||
// Concat fileUrlList and filesFromHistories; remove not supported files
|
||||
const parseUrlList = [...fileUrlList, ...filesFromHistories]
|
||||
export const getFileContentFromLinks = async ({
|
||||
urls,
|
||||
requestOrigin,
|
||||
maxFiles,
|
||||
teamId
|
||||
}: {
|
||||
urls: string[];
|
||||
requestOrigin?: string;
|
||||
maxFiles: number;
|
||||
teamId: string;
|
||||
}) => {
|
||||
const parseUrlList = urls
|
||||
// Remove invalid urls
|
||||
.filter((url) => {
|
||||
if (typeof url !== 'string') return false;
|
||||
|
||||
// 检查相对路径
|
||||
const validPrefixList = ['/', 'http', 'ws'];
|
||||
if (validPrefixList.some((prefix) => url.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
// Just get the document type file
|
||||
.filter((url) => parseUrlToFileType(url)?.type === 'file')
|
||||
.map((url) => {
|
||||
try {
|
||||
// Avoid "/api/xxx" file error.
|
||||
const origin = requestOrigin ?? 'http://localhost:3000';
|
||||
|
||||
// Check is system upload file
|
||||
if (url.startsWith('/') || (requestOrigin && url.startsWith(requestOrigin))) {
|
||||
// Parse url, get filename query. Keep only documents that can be parsed
|
||||
const parseUrl = new URL(url, origin);
|
||||
const filenameQuery = parseUrl.searchParams.get('filename');
|
||||
|
||||
// Not document
|
||||
if (filenameQuery) {
|
||||
const extensionQuery = filenameQuery.split('.').pop()?.toLowerCase() || '';
|
||||
if (!documentFileType.includes(extensionQuery)) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the origin(Make intranet requests directly)
|
||||
if (requestOrigin && url.startsWith(requestOrigin)) {
|
||||
url = url.replace(requestOrigin, '');
|
||||
@@ -123,7 +161,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get file buffer
|
||||
// Get file buffer data
|
||||
const response = await axios.get(url, {
|
||||
baseURL: serverRequestBaseUrl,
|
||||
responseType: 'arraybuffer'
|
||||
@@ -197,18 +235,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
|
||||
const text = readFilesResult.map((item) => item?.text ?? '').join('\n******\n');
|
||||
|
||||
return {
|
||||
[NodeOutputKeyEnum.text]: text,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
readFiles: readFilesResult.map((item) => ({
|
||||
name: item?.filename || '',
|
||||
url: item?.url || ''
|
||||
})),
|
||||
readFilesResult: readFilesResult
|
||||
.map((item) => item?.nodeResponsePreviewText ?? '')
|
||||
.join('\n******\n')
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.toolResponses]: {
|
||||
fileContent: text
|
||||
}
|
||||
text,
|
||||
readFilesResult
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
SseResponseEventEnum
|
||||
} from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import {
|
||||
getReferenceVariableValue,
|
||||
replaceEditorVariable
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
|
||||
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { removeSystemVariable, valueTypeFormat } from '../utils';
|
||||
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
|
||||
import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils';
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.updateList]: TUpdateListItem[];
|
||||
@@ -19,15 +22,24 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
|
||||
const { params, variables, runtimeNodes, workflowStreamResponse, node } = props;
|
||||
|
||||
const { updateList } = params;
|
||||
const result = updateList.map((item) => {
|
||||
const varNodeId = item.variable?.[0];
|
||||
const varKey = item.variable?.[1];
|
||||
const nodeIds = runtimeNodes.map((node) => node.nodeId);
|
||||
|
||||
if (!varNodeId || !varKey) {
|
||||
const result = updateList.map((item) => {
|
||||
const variable = item.variable;
|
||||
|
||||
if (!isValidReferenceValue(variable, nodeIds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const varNodeId = variable[0];
|
||||
const varKey = variable[1];
|
||||
|
||||
if (!varKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = (() => {
|
||||
// If first item is empty, it means it is a input value
|
||||
if (!item.value?.[0]) {
|
||||
const formatValue = valueTypeFormat(item.value?.[1], item.valueType);
|
||||
|
||||
@@ -48,6 +60,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
|
||||
}
|
||||
})();
|
||||
|
||||
// Update node output
|
||||
// Global variable
|
||||
if (varNodeId === VARIABLE_NODE_ID) {
|
||||
variables[varKey] = value;
|
||||
@@ -72,6 +85,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
|
||||
});
|
||||
|
||||
return {
|
||||
[DispatchNodeResponseKeyEnum.newVariables]: variables,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
updateVarResult: result
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { countPromptTokens } from '../../common/string/tiktoken/index';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
getPluginInputsFromStoreNodes,
|
||||
getPluginRunContent
|
||||
} from '@fastgpt/global/core/app/plugin/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { RuntimeUserPromptType, UserChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt';
|
||||
|
||||
/* filter search result */
|
||||
export const filterSearchResultsByMaxChars = async (
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"pg": "^8.10.0",
|
||||
"request-ip": "^3.3.0",
|
||||
"tiktoken": "^1.0.15",
|
||||
"tiktoken": "1.0.17",
|
||||
"tunnel": "^0.0.6",
|
||||
"turndown": "^7.1.2"
|
||||
},
|
||||
|
||||
@@ -72,6 +72,12 @@ export const authAppByTmbId = async ({
|
||||
const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId);
|
||||
|
||||
const { Per } = await (async () => {
|
||||
if (isOwner) {
|
||||
return {
|
||||
Per: new AppPermission({ isOwner: true })
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
AppFolderTypeList.includes(app.type) ||
|
||||
app.inheritPermission === false ||
|
||||
|
||||
@@ -64,6 +64,11 @@ export const authDatasetByTmbId = async ({
|
||||
|
||||
// get dataset permission or inherit permission from parent folder.
|
||||
const { Per } = await (async () => {
|
||||
if (isOwner) {
|
||||
return {
|
||||
Per: new DatasetPermission({ isOwner: true })
|
||||
};
|
||||
}
|
||||
if (
|
||||
dataset.type === DatasetTypeEnum.folder ||
|
||||
dataset.inheritPermission === false ||
|
||||
@@ -152,6 +157,7 @@ export const authDataset = async ({
|
||||
dataset
|
||||
};
|
||||
};
|
||||
|
||||
// the temporary solution for authDatasetCollection is getting the
|
||||
export async function authDatasetCollection({
|
||||
collectionId,
|
||||
@@ -189,55 +195,58 @@ export async function authDatasetCollection({
|
||||
};
|
||||
}
|
||||
|
||||
export async function authDatasetFile({
|
||||
fileId,
|
||||
per,
|
||||
...props
|
||||
}: AuthModeType & {
|
||||
fileId: string;
|
||||
}): Promise<
|
||||
AuthResponseType<DatasetPermission> & {
|
||||
file: DatasetFileSchema;
|
||||
}
|
||||
> {
|
||||
const { teamId, tmbId, isRoot } = await parseHeaderCert(props);
|
||||
// export async function authDatasetFile({
|
||||
// fileId,
|
||||
// per,
|
||||
// ...props
|
||||
// }: AuthModeType & {
|
||||
// fileId: string;
|
||||
// }): Promise<
|
||||
// AuthResponseType<DatasetPermission> & {
|
||||
// file: DatasetFileSchema;
|
||||
// }
|
||||
// > {
|
||||
// const { teamId, tmbId, isRoot } = await parseHeaderCert(props);
|
||||
|
||||
const [file, collection] = await Promise.all([
|
||||
getFileById({ bucketName: BucketNameEnum.dataset, fileId }),
|
||||
MongoDatasetCollection.findOne({
|
||||
teamId,
|
||||
fileId
|
||||
})
|
||||
]);
|
||||
// const [file, collection] = await Promise.all([
|
||||
// getFileById({ bucketName: BucketNameEnum.dataset, fileId }),
|
||||
// MongoDatasetCollection.findOne({
|
||||
// teamId,
|
||||
// fileId
|
||||
// })
|
||||
// ]);
|
||||
|
||||
if (!file) {
|
||||
return Promise.reject(CommonErrEnum.fileNotFound);
|
||||
}
|
||||
// if (!file) {
|
||||
// return Promise.reject(CommonErrEnum.fileNotFound);
|
||||
// }
|
||||
|
||||
if (!collection) {
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
}
|
||||
// if (!collection) {
|
||||
// return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
// }
|
||||
|
||||
try {
|
||||
const { permission } = await authDatasetCollection({
|
||||
...props,
|
||||
collectionId: collection._id,
|
||||
per,
|
||||
isRoot
|
||||
});
|
||||
// try {
|
||||
// const { permission } = await authDatasetCollection({
|
||||
// ...props,
|
||||
// collectionId: collection._id,
|
||||
// per,
|
||||
// isRoot
|
||||
// });
|
||||
|
||||
return {
|
||||
teamId,
|
||||
tmbId,
|
||||
file,
|
||||
permission,
|
||||
isRoot
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
}
|
||||
}
|
||||
// return {
|
||||
// teamId,
|
||||
// tmbId,
|
||||
// file,
|
||||
// permission,
|
||||
// isRoot
|
||||
// };
|
||||
// } catch (error) {
|
||||
// return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
// }
|
||||
// }
|
||||
|
||||
/*
|
||||
DatasetData permission is inherited from collection.
|
||||
*/
|
||||
export async function authDatasetData({
|
||||
dataId,
|
||||
...props
|
||||
@@ -268,8 +277,8 @@ export async function authDatasetData({
|
||||
collectionId: String(datasetData.collectionId),
|
||||
sourceName: result.collection.name || '',
|
||||
sourceId: result.collection?.fileId || result.collection?.rawLink,
|
||||
isOwner: String(datasetData.tmbId) === String(result.tmbId),
|
||||
canWrite: result.permission.hasWritePer
|
||||
isOwner: String(datasetData.tmbId) === String(result.tmbId)
|
||||
// permission: result.permission
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import TurndownService from 'turndown';
|
||||
import { ImageType } from '../readFile/type';
|
||||
// @ts-ignore
|
||||
const turndownPluginGfm = require('joplin-turndown-plugin-gfm');
|
||||
|
||||
export const html2md = (html: string): string => {
|
||||
export const html2md = (
|
||||
html: string
|
||||
): {
|
||||
rawText: string;
|
||||
imageList: ImageType[];
|
||||
} => {
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
bulletListMarker: '-',
|
||||
@@ -15,12 +22,32 @@ export const html2md = (html: string): string => {
|
||||
|
||||
try {
|
||||
turndownService.remove(['i', 'script', 'iframe', 'style']);
|
||||
|
||||
turndownService.use(turndownPluginGfm.gfm);
|
||||
|
||||
return turndownService.turndown(html);
|
||||
const base64Regex = /"(data:image\/[^;]+;base64[^"]+)"/g;
|
||||
const imageList: ImageType[] = [];
|
||||
const images = Array.from(html.match(base64Regex) || []);
|
||||
for (const image of images) {
|
||||
const uuid = crypto.randomUUID();
|
||||
const mime = image.split(';')[0].split(':')[1];
|
||||
const base64 = image.split(',')[1];
|
||||
html = html.replace(image, uuid);
|
||||
imageList.push({
|
||||
uuid,
|
||||
base64,
|
||||
mime
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
rawText: turndownService.turndown(html),
|
||||
imageList
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('html 2 markdown error', error);
|
||||
return '';
|
||||
return {
|
||||
rawText: '',
|
||||
imageList: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import mammoth from 'mammoth';
|
||||
import { ReadRawTextByBuffer, ReadFileResponse } from '../type';
|
||||
import mammoth, { images } from 'mammoth';
|
||||
import { ReadRawTextByBuffer, ReadFileResponse, ImageType } from '../type';
|
||||
import { html2md } from '../../htmlStr2Md/utils';
|
||||
|
||||
/**
|
||||
* read docx to markdown
|
||||
*/
|
||||
export const readDocsFile = async ({ buffer }: ReadRawTextByBuffer): Promise<ReadFileResponse> => {
|
||||
const imageList: ImageType[] = [];
|
||||
try {
|
||||
const { value: html } = await mammoth.convertToHtml({
|
||||
buffer
|
||||
});
|
||||
const { value: html } = await mammoth.convertToHtml(
|
||||
{
|
||||
buffer
|
||||
},
|
||||
{
|
||||
convertImage: images.imgElement(async (image) => {
|
||||
const imageBase64 = await image.readAsBase64String();
|
||||
const uuid = crypto.randomUUID();
|
||||
const mime = image.contentType;
|
||||
imageList.push({
|
||||
uuid,
|
||||
base64: imageBase64,
|
||||
mime
|
||||
});
|
||||
return {
|
||||
src: uuid
|
||||
};
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const rawText = html2md(html);
|
||||
const { rawText } = html2md(html);
|
||||
|
||||
return {
|
||||
rawText
|
||||
rawText,
|
||||
imageList
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('error doc read:', error);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { html2md } from '../../htmlStr2Md/utils';
|
||||
export const readHtmlRawText = async (params: ReadRawTextByBuffer): Promise<ReadFileResponse> => {
|
||||
const { rawText: html } = readFileRawText(params);
|
||||
|
||||
const rawText = html2md(html);
|
||||
const { rawText, imageList } = html2md(html);
|
||||
|
||||
return {
|
||||
rawText
|
||||
rawText,
|
||||
imageList
|
||||
};
|
||||
};
|
||||
|
||||
7
packages/service/worker/readFile/type.d.ts
vendored
@@ -8,7 +8,14 @@ export type ReadRawTextProps<T> = {
|
||||
|
||||
export type ReadRawTextByBuffer = ReadRawTextProps<Buffer>;
|
||||
|
||||
export type ImageType = {
|
||||
uuid: string;
|
||||
base64: string;
|
||||
mime: string;
|
||||
};
|
||||
|
||||
export type ReadFileResponse = {
|
||||
rawText: string;
|
||||
formatText?: string;
|
||||
imageList?: ImageType[];
|
||||
};
|
||||
|
||||
@@ -178,11 +178,13 @@ export class WorkerPool<Props = Record<string, any>, Response = any> {
|
||||
|
||||
// Worker error, terminate and delete it.(Un catch error)
|
||||
worker.on('error', (err) => {
|
||||
addLog.warn('Worker error', { err });
|
||||
console.log(err);
|
||||
addLog.error('Worker error', err);
|
||||
this.deleteWorker(workerId);
|
||||
});
|
||||
worker.on('messageerror', (err) => {
|
||||
addLog.warn('Worker error', { err });
|
||||
console.log(err);
|
||||
addLog.error('Worker messageerror', err);
|
||||
this.deleteWorker(workerId);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,3 +9,12 @@ export const getUserFingerprint = async () => {
|
||||
export const hasHttps = () => {
|
||||
return window.location.protocol === 'https:';
|
||||
};
|
||||
|
||||
export const getWebReqUrl = (url: string = '') => {
|
||||
if (!url) return '/';
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
||||
if (!baseUrl) return url;
|
||||
|
||||
if (!url.startsWith('/') || url.startsWith(baseUrl)) return url;
|
||||
return `${baseUrl}${url}`;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import type { ImageProps } from '@chakra-ui/react';
|
||||
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
|
||||
import MyIcon from '../Icon';
|
||||
import { iconPaths } from '../Icon/constants';
|
||||
import MyImage from '../Image/MyImage';
|
||||
|
||||
const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
// @ts-ignore
|
||||
@@ -14,7 +15,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
<MyIcon name={src as any} w={w} borderRadius={props.borderRadius} />
|
||||
</Box>
|
||||
) : (
|
||||
<Image
|
||||
<MyImage
|
||||
fallbackSrc={LOGO_ICON}
|
||||
fallbackStrategy={'onError'}
|
||||
objectFit={'contain'}
|
||||
|
||||
@@ -4,6 +4,7 @@ export const iconPaths = {
|
||||
book: () => import('./icons/book.svg'),
|
||||
change: () => import('./icons/change.svg'),
|
||||
chatSend: () => import('./icons/chatSend.svg'),
|
||||
check: () => import('./icons/check.svg'),
|
||||
closeSolid: () => import('./icons/closeSolid.svg'),
|
||||
collectionLight: () => import('./icons/collectionLight.svg'),
|
||||
collectionSolid: () => import('./icons/collectionSolid.svg'),
|
||||
@@ -59,7 +60,6 @@ export const iconPaths = {
|
||||
'common/playFill': () => import('./icons/common/playFill.svg'),
|
||||
'common/playLight': () => import('./icons/common/playLight.svg'),
|
||||
'common/publishFill': () => import('./icons/common/publishFill.svg'),
|
||||
'common/questionLight': () => import('./icons/common/questionLight.svg'),
|
||||
'common/refreshLight': () => import('./icons/common/refreshLight.svg'),
|
||||
'common/resultLight': () => import('./icons/common/resultLight.svg'),
|
||||
'common/retryLight': () => import('./icons/common/retryLight.svg'),
|
||||
@@ -217,6 +217,7 @@ export const iconPaths = {
|
||||
'core/workflow/template/FileRead': () => import('./icons/core/workflow/template/FileRead.svg'),
|
||||
'core/workflow/template/aiChat': () => import('./icons/core/workflow/template/aiChat.svg'),
|
||||
'core/workflow/template/baseChart': () => import('./icons/core/workflow/template/baseChart.svg'),
|
||||
'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'),
|
||||
'core/workflow/template/codeRun': () => import('./icons/core/workflow/template/codeRun.svg'),
|
||||
'core/workflow/template/customFeedback': () =>
|
||||
import('./icons/core/workflow/template/customFeedback.svg'),
|
||||
@@ -224,18 +225,16 @@ export const iconPaths = {
|
||||
import('./icons/core/workflow/template/datasetConcat.svg'),
|
||||
'core/workflow/template/datasetSearch': () =>
|
||||
import('./icons/core/workflow/template/datasetSearch.svg'),
|
||||
'core/workflow/template/datasource': () =>
|
||||
import('./icons/core/workflow/template/datasource.svg'),
|
||||
'core/workflow/template/duckduckgo': () =>
|
||||
import('./icons/core/workflow/template/duckduckgo.svg'),
|
||||
'core/workflow/template/extractJson': () =>
|
||||
import('./icons/core/workflow/template/extractJson.svg'),
|
||||
'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'),
|
||||
'core/workflow/template/datasource': () =>
|
||||
import('./icons/core/workflow/template/datasource.svg'),
|
||||
'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'),
|
||||
'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'),
|
||||
'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'),
|
||||
'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'),
|
||||
'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'),
|
||||
'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'),
|
||||
'core/workflow/template/httpRequest': () =>
|
||||
import('./icons/core/workflow/template/httpRequest.svg'),
|
||||
'core/workflow/template/ifelse': () => import('./icons/core/workflow/template/ifelse.svg'),
|
||||
@@ -271,6 +270,7 @@ export const iconPaths = {
|
||||
'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'),
|
||||
'core/workflow/template/variableUpdate': () =>
|
||||
import('./icons/core/workflow/template/variableUpdate.svg'),
|
||||
'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'),
|
||||
'core/workflow/template/workflowStart': () =>
|
||||
import('./icons/core/workflow/template/workflowStart.svg'),
|
||||
'core/workflow/touchTable': () => import('./icons/core/workflow/touchTable.svg'),
|
||||
@@ -280,6 +280,7 @@ export const iconPaths = {
|
||||
date: () => import('./icons/date.svg'),
|
||||
delete: () => import('./icons/delete.svg'),
|
||||
drag: () => import('./icons/drag.svg'),
|
||||
edgeAdd: () => import('./icons/edgeAdd.svg'),
|
||||
edit: () => import('./icons/edit.svg'),
|
||||
empty: () => import('./icons/empty.svg'),
|
||||
export: () => import('./icons/export.svg'),
|
||||
@@ -302,6 +303,7 @@ export const iconPaths = {
|
||||
'file/pdf': () => import('./icons/file/pdf.svg'),
|
||||
'file/qaImport': () => import('./icons/file/qaImport.svg'),
|
||||
'file/uploadFile': () => import('./icons/file/uploadFile.svg'),
|
||||
help: () => import('./icons/help.svg'),
|
||||
history: () => import('./icons/history.svg'),
|
||||
infoRounded: () => import('./icons/infoRounded.svg'),
|
||||
kbTest: () => import('./icons/kbTest.svg'),
|
||||
@@ -331,7 +333,6 @@ export const iconPaths = {
|
||||
save: () => import('./icons/save.svg'),
|
||||
stop: () => import('./icons/stop.svg'),
|
||||
'support/account/loginoutLight': () => import('./icons/support/account/loginoutLight.svg'),
|
||||
'support/account/passwordLogin': () => import('./icons/support/account/passwordLogin.svg'),
|
||||
'support/account/plans': () => import('./icons/support/account/plans.svg'),
|
||||
'support/account/promotionLight': () => import('./icons/support/account/promotionLight.svg'),
|
||||
'support/bill/extraDatasetsize': () => import('./icons/support/bill/extraDatasetsize.svg'),
|
||||
|
||||
3
packages/web/components/common/Icon/icons/check.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 17" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5587 3.69438C13.9492 3.30386 14.5824 3.30386 14.9729 3.69438C15.3634 4.08491 15.3634 4.71807 14.9729 5.1086L7.63956 12.4419C7.24904 12.8325 6.61587 12.8325 6.22535 12.4419L2.89201 9.1086C2.50149 8.71807 2.50149 8.08491 2.89201 7.69438C3.28254 7.30386 3.9157 7.30386 4.30623 7.69438L6.93245 10.3206L13.5587 3.69438Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 452 B |
@@ -1 +1,4 @@
|
||||
<?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="1704259732773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4183" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M319.20128 974.56128L348.16 1003.52l655.36-655.36-28.95872-28.95872-655.36 655.36zM675.84 1003.52l327.68-327.68-28.95872-28.95872-327.68 327.68L675.84 1003.52z" fill="#000000" p-id="4184"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9" fill="none">
|
||||
<path d="M0.950928 8.44922L8.32385 1.07629" stroke="#8A95A7" stroke-linecap="round"/>
|
||||
<path d="M4.3418 8.46167L8.32373 4.47974" stroke="#8A95A7" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 272 B |
@@ -1,6 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
|
||||
<path
|
||||
d="M4.25845 0.738983C4.25845 0.606996 4.15146 0.5 4.01947 0.5C2.11725 0.5 0.575195 2.04205 0.575195 3.94428V12.0557C0.575195 13.9579 2.11725 15.5 4.01947 15.5H12.1309C14.0331 15.5 15.5752 13.9579 15.5752 12.0557V3.94428C15.5752 2.04205 14.0331 0.5 12.1309 0.5H10.4017C9.98744 0.5 9.65166 0.835786 9.65166 1.25C9.65166 1.66421 9.98744 2 10.4017 2H12.1309C13.2047 2 14.0752 2.87048 14.0752 3.94428V12.0557C14.0752 13.1295 13.2047 14 12.1309 14H4.01947C2.94568 14 2.0752 13.1295 2.0752 12.0557V3.94428C2.0752 2.87048 2.94568 2 4.01947 2C4.15146 2 4.25845 1.893 4.25845 1.76102V0.738983Z" />
|
||||
<path
|
||||
d="M7.38092 4.3543C7.26179 3.52006 7.01223 2.99621 6.59273 2.70009C5.59427 1.99531 4.85314 2.00002 4.74413 2.00072L4.7369 2.00075H3.9869V0.500749H4.7369C5.00097 0.500749 6.0952 0.512848 7.45775 1.47464C8.37881 2.12479 8.7247 3.15378 8.86586 4.14224C8.98099 4.94841 8.97457 5.85452 8.96865 6.68934C8.96737 6.86971 8.96612 7.04676 8.96612 7.21874C8.96612 7.70483 8.95571 8.16141 8.94035 8.57069L9.57153 7.93951C9.86442 7.64661 10.3393 7.64661 10.6322 7.93951C10.9251 8.2324 10.9251 8.70727 10.6322 9.00017L8.65543 10.9769C8.49629 11.1361 8.28342 11.2087 8.07521 11.1949C7.86701 11.2087 7.65414 11.1361 7.495 10.9769L5.51824 9.00017C5.22535 8.70727 5.22535 8.2324 5.51824 7.93951C5.81113 7.64661 6.28601 7.64661 6.5789 7.93951L7.33793 8.69854L7.42997 8.79058C7.45074 8.33162 7.46612 7.79657 7.46612 7.21874C7.46612 7.02039 7.46735 6.82532 7.46856 6.63372C7.47382 5.80363 7.47866 5.03868 7.38092 4.3543Z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M4.67391 1.54593C4.67391 1.4286 4.5788 1.3335 4.46148 1.3335C2.77062 1.3335 1.3999 2.70421 1.3999 4.39507V11.6053C1.3999 13.2961 2.77062 14.6668 4.46148 14.6668H11.6717C13.3625 14.6668 14.7332 13.2961 14.7332 11.6053V4.39507C14.7332 2.70421 13.3625 1.3335 11.6717 1.3335H10.1345C9.76634 1.3335 9.46786 1.63197 9.46786 2.00016C9.46786 2.36835 9.76634 2.66683 10.1345 2.66683H11.6717C12.6261 2.66683 13.3999 3.44059 13.3999 4.39507V11.6053C13.3999 12.5597 12.6261 13.3335 11.6717 13.3335H4.46148C3.507 13.3335 2.73324 12.5597 2.73324 11.6053V4.39507C2.73324 3.44059 3.507 2.66683 4.46148 2.66683C4.5788 2.66683 4.67391 2.57172 4.67391 2.4544V1.54593Z"/>
|
||||
<path d="M7.44944 4.75954C7.34354 4.01799 7.12171 3.55235 6.74882 3.28913C5.8613 2.66266 5.20252 2.66685 5.10562 2.66747L5.0992 2.6675H4.43253V1.33416H5.0992C5.33393 1.33416 6.30657 1.34492 7.51773 2.19984C8.33645 2.77775 8.6439 3.69241 8.76938 4.57104C8.87172 5.28764 8.86601 6.09307 8.86075 6.83513C8.85961 6.99546 8.8585 7.15284 8.8585 7.30571C8.8585 7.73778 8.84925 8.14363 8.83559 8.50744L9.39664 7.94639C9.65699 7.68604 10.0791 7.68604 10.3395 7.94639C10.5998 8.20674 10.5998 8.62885 10.3395 8.8892L8.58233 10.6463C8.44087 10.7878 8.25166 10.8524 8.06659 10.8401C7.88151 10.8524 7.6923 10.7878 7.55084 10.6463L5.79372 8.8892C5.53337 8.62885 5.53337 8.20674 5.79372 7.94639C6.05407 7.68604 6.47618 7.68604 6.73653 7.94639L7.41122 8.62109L7.49304 8.7029C7.5115 8.29493 7.52517 7.81934 7.52517 7.30571C7.52517 7.1294 7.52626 6.956 7.52734 6.78569C7.53201 6.04784 7.53631 5.36788 7.44944 4.75954Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M8.99997 2.196C5.24222 2.196 2.19596 5.24225 2.19596 9C2.19596 12.7577 5.24222 15.804 8.99997 15.804C12.7577 15.804 15.804 12.7577 15.804 9C15.804 5.24225 12.7577 2.196 8.99997 2.196ZM0.529297 9C0.529297 4.32178 4.32174 0.529331 8.99997 0.529331C13.6782 0.529331 17.4706 4.32178 17.4706 9C17.4706 13.6782 13.6782 17.4707 8.99997 17.4707C4.32174 17.4707 0.529297 13.6782 0.529297 9ZM9.18533 6.03224C8.846 5.97403 8.49702 6.0378 8.20019 6.21224C7.90337 6.38669 7.67786 6.66056 7.56361 6.98534C7.41089 7.41949 6.93512 7.64764 6.50097 7.49491C6.06681 7.34218 5.83866 6.86642 5.99139 6.43226C6.23625 5.73619 6.71956 5.14923 7.35572 4.77536C7.99188 4.40148 8.73983 4.26481 9.4671 4.38956C10.1944 4.5143 10.854 4.89241 11.3292 5.45692C11.8043 6.0213 12.0644 6.73558 12.0634 7.4733C12.063 8.67899 11.1697 9.46897 10.5467 9.88431C10.2098 10.1089 9.8788 10.2738 9.63531 10.382C9.51239 10.4367 9.4088 10.4782 9.33398 10.5067C9.2965 10.5209 9.26605 10.532 9.24377 10.54L9.21658 10.5495L9.20781 10.5525L9.20467 10.5535L9.20342 10.554C9.20317 10.554 9.20239 10.5543 8.93887 9.76373L9.20239 10.5543C8.76577 10.6998 8.29384 10.4639 8.1483 10.0273C8.00281 9.59078 8.23857 9.11902 8.67491 8.97331L8.68542 8.9696C8.69671 8.96559 8.71548 8.95878 8.74065 8.94919C8.79114 8.92996 8.86654 8.89986 8.95842 8.85902C9.14454 8.7763 9.38634 8.65481 9.62222 8.49756C10.1447 8.14925 10.3967 7.79388 10.3967 7.47253L10.3967 7.47129C10.3972 7.127 10.2759 6.79364 10.0542 6.53025C9.83245 6.26686 9.52467 6.09044 9.18533 6.03224ZM8.16663 12.8187C8.16663 12.3584 8.53973 11.9853 8.99997 11.9853H9.0076C9.46784 11.9853 9.84094 12.3584 9.84094 12.8187C9.84094 13.2789 9.46784 13.652 9.0076 13.652H8.99997C8.53973 13.652 8.16663 13.2789 8.16663 12.8187Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,4 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.42469 4.94872C5.16434 5.20906 5.16434 5.63117 5.42469 5.89152C5.68504 6.15187 6.10715 6.15187 6.3675 5.89152L8.00002 4.25901L9.63253 5.89152C9.89288 6.15187 10.315 6.15187 10.5753 5.89152C10.8357 5.63117 10.8357 5.20906 10.5753 4.94872L8.47142 2.8448C8.21107 2.58445 7.78896 2.58445 7.52861 2.8448L5.42469 4.94872ZM5.42469 10.8375C5.16434 11.0979 5.16434 11.52 5.42469 11.7803L7.52861 13.8843C7.56115 13.9168 7.59623 13.9453 7.63319 13.9697C7.89196 14.1405 8.24361 14.1121 8.47142 13.8843L10.5753 11.7803C10.8357 11.52 10.8357 11.0979 10.5753 10.8375C10.315 10.5772 9.89288 10.5772 9.63253 10.8375L8.00002 12.47L6.3675 10.8375C6.10715 10.5772 5.68504 10.5772 5.42469 10.8375Z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.42475 4.58336C5.1644 4.84371 5.1644 5.26582 5.42475 5.52617C5.6851 5.78652 6.10721 5.78652 6.36756 5.52617L8.00008 3.89366L9.63259 5.52617C9.89294 5.78652 10.315 5.78652 10.5754 5.52617C10.8357 5.26582 10.8357 4.84371 10.5754 4.58336L8.47148 2.47944C8.21113 2.21909 7.78902 2.21909 7.52867 2.47944L5.42475 4.58336ZM5.42475 10.4722C5.1644 10.7325 5.1644 11.1546 5.42475 11.415L7.52867 13.5189C7.56122 13.5514 7.59629 13.5799 7.63325 13.6043C7.89202 13.7752 8.24367 13.7467 8.47148 13.5189L10.5754 11.415C10.8357 11.1546 10.8357 10.7325 10.5754 10.4722C10.315 10.2118 9.89294 10.2118 9.63259 10.4722L8.00008 12.1047L6.36756 10.4722C6.10721 10.2118 5.6851 10.2118 5.42475 10.4722Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 823 B After Width: | Height: | Size: 804 B |
@@ -1,11 +1,10 @@
|
||||
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_10832_16007)"/>
|
||||
<path d="M13.9673 10.3122C13.1868 10.8479 13.1935 11.9491 13.863 12.6185C14.3986 13.1541 15.2613 13.1384 15.9008 12.7325C16.5477 12.3219 17.268 12.0318 18.0272 11.8808C19.2374 11.6401 20.4919 11.7636 21.6319 12.2358C22.7719 12.708 23.7463 13.5077 24.4318 14.5337C25.1174 15.5597 25.4833 16.7659 25.4833 17.9999C25.4833 19.2338 25.1174 20.4401 24.4318 21.4661C23.7463 22.4921 22.7719 23.2917 21.6319 23.7639C20.4919 24.2361 19.2374 24.3597 18.0272 24.119C17.268 23.968 16.5477 23.6779 15.9008 23.2673C15.2613 22.8614 14.3986 22.8457 13.863 23.3812C13.1935 24.0507 13.1868 25.1518 13.9673 25.6876C15.0044 26.3995 16.1799 26.8976 17.4252 27.1453C19.234 27.5051 21.1088 27.3204 22.8127 26.6147C24.5165 25.9089 25.9728 24.7138 26.9974 23.1803C28.022 21.6469 28.5689 19.8441 28.5689 17.9999C28.5689 16.1557 28.022 14.3528 26.9974 12.8194C25.9728 11.286 24.5165 10.0908 22.8127 9.38509C21.1088 8.67933 19.234 8.49468 17.4252 8.85447C16.1799 9.10217 15.0044 9.60029 13.9673 10.3122Z" fill="white"/>
|
||||
<path d="M19.2443 22.5497C17.3579 22.5497 15.7397 21.4017 15.0501 19.7663H9.19726C8.22171 19.7663 7.43087 18.9754 7.43087 17.9999C7.43087 17.0243 8.22171 16.2335 9.19726 16.2335H15.0501C15.7397 14.598 17.3579 13.45 19.2443 13.45C21.7571 13.45 23.7942 15.4871 23.7942 17.9999C23.7942 20.5127 21.7571 22.5497 19.2443 22.5497Z" fill="white"/>
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_11_1507)"/>
|
||||
<path d="M9.22505 16.905C8.38096 17.102 7.87904 17.9714 8.13047 18.8009L9.96306 24.8467C10.3439 26.1032 12.0411 26.2986 12.6976 25.1616L13.5842 23.6259C15.0589 24.1044 16.6267 24.2674 18.1843 24.0937C20.4842 23.8372 22.6439 22.8596 24.3544 21.301C26.0649 19.7424 27.2384 17.6826 27.7071 15.4164C28.074 13.6422 27.9941 11.8127 27.4853 10.0898C27.2506 9.29527 26.3514 8.96094 25.5963 9.30173L23.6663 10.1728C22.9113 10.5136 22.597 11.404 22.7351 12.2208C22.8554 12.9325 22.8437 13.6647 22.6957 14.38C22.4458 15.5886 21.8199 16.6872 20.9077 17.5184C19.9954 18.3497 18.8436 18.871 17.617 19.0078C17.1616 19.0586 16.7045 19.0555 16.2549 19.0002L17.0171 17.6799C17.6736 16.5428 16.6558 15.1707 15.3772 15.4692L9.22505 16.905Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_10832_16007" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#68C0FF"/>
|
||||
<stop offset="1" stop-color="#52A2FF"/>
|
||||
<linearGradient id="paint0_linear_11_1507" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF8BFD"/>
|
||||
<stop offset="1" stop-color="#7394FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,11 +1,10 @@
|
||||
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_10829_15860)"/>
|
||||
<path d="M22.0325 10.3122C22.813 10.8479 22.8062 11.9491 22.1368 12.6185C21.6012 13.1541 20.7384 13.1384 20.099 12.7325C19.4521 12.3219 18.7317 12.0318 17.9726 11.8808C16.7623 11.6401 15.5079 11.7636 14.3679 12.2358C13.2279 12.708 12.2535 13.5077 11.5679 14.5337C10.8824 15.5597 10.5165 16.7659 10.5165 17.9999C10.5165 19.2338 10.8824 20.4401 11.5679 21.4661C12.2535 22.4921 13.2279 23.2917 14.3679 23.7639C15.5079 24.2361 16.7624 24.3597 17.9726 24.119C18.7317 23.968 19.4521 23.6779 20.099 23.2673C20.7384 22.8614 21.6012 22.8457 22.1368 23.3812C22.8062 24.0507 22.813 25.1518 22.0325 25.6876C20.9954 26.3995 19.8199 26.8976 18.5746 27.1453C16.7658 27.5051 14.8909 27.3204 13.1871 26.6147C11.4832 25.9089 10.0269 24.7138 9.00232 23.1803C7.97772 21.6469 7.43085 19.8441 7.43085 17.9999C7.43085 16.1557 7.97772 14.3528 9.00232 12.8194C10.0269 11.286 11.4832 10.0908 13.1871 9.38509C14.8909 8.67933 16.7658 8.49468 18.5746 8.85447C19.8199 9.10217 20.9954 9.60029 22.0325 10.3122Z" fill="white"/>
|
||||
<path d="M16.7554 22.5497C18.6418 22.5497 20.2601 21.4017 20.9497 19.7663H26.8025C27.778 19.7663 28.5689 18.9754 28.5689 17.9999C28.5689 17.0243 27.778 16.2335 26.8025 16.2335H20.9497C20.2601 14.598 18.6418 13.45 16.7554 13.45C14.2426 13.45 12.2056 15.4871 12.2056 17.9999C12.2056 20.5127 14.2426 22.5497 16.7554 22.5497Z" fill="white"/>
|
||||
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_11_1494)"/>
|
||||
<path d="M26.7748 18.1758C27.6189 17.9788 28.1208 17.1095 27.8693 16.2799L26.0368 10.2341C25.6559 8.97762 23.9587 8.7822 23.3022 9.91926L22.4156 11.4549C20.9409 10.9764 19.3731 10.8134 17.8155 10.9871C15.5157 11.2437 13.3559 12.2212 11.6454 13.7798C9.93493 15.3384 8.76137 17.3983 8.29272 19.6644C7.92581 21.4386 8.00571 23.2681 8.51454 24.991C8.74918 25.7855 9.64845 26.1199 10.4035 25.7791L12.3335 24.908C13.0886 24.5672 13.4028 23.6768 13.2647 22.86C13.1444 22.1483 13.1562 21.4161 13.3041 20.7008C13.554 19.4922 14.1799 18.3936 15.0922 17.5624C16.0044 16.7311 17.1562 16.2098 18.3828 16.073C18.8382 16.0222 19.2953 16.0254 19.7449 16.0807L18.9827 17.4009C18.3262 18.538 19.344 19.9101 20.6226 19.6117L26.7748 18.1758Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_10829_15860" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#68C0FF"/>
|
||||
<stop offset="1" stop-color="#52A2FF"/>
|
||||
<linearGradient id="paint0_linear_11_1494" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF8BFD"/>
|
||||
<stop offset="1" stop-color="#7394FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
3
packages/web/components/common/Icon/icons/edgeAdd.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09993 1.21182C6.09993 0.604303 5.60744 0.111816 4.99993 0.111816C4.39242 0.111816 3.89993 0.604303 3.89993 1.21182L3.89993 3.11182H1.9999C1.39239 3.11182 0.899902 3.6043 0.899902 4.21182C0.899902 4.81933 1.39239 5.31182 1.9999 5.31182H3.89993L3.89993 7.21182C3.89993 7.81933 4.39242 8.31182 4.99993 8.31182C5.60744 8.31182 6.09993 7.81933 6.09993 7.21182V5.31182H7.9999C8.60742 5.31182 9.0999 4.81933 9.0999 4.21182C9.0999 3.6043 8.60742 3.11182 7.9999 3.11182H6.09993V1.21182Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |
3
packages/web/components/common/Icon/icons/help.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99968 2.55678C4.99348 2.55678 2.55648 4.99379 2.55648 7.99998C2.55648 11.0062 4.99348 13.4432 7.99968 13.4432C11.0059 13.4432 13.4429 11.0062 13.4429 7.99998C13.4429 4.99379 11.0059 2.55678 7.99968 2.55678ZM1.22314 7.99998C1.22314 4.25741 4.2571 1.22345 7.99968 1.22345C11.7423 1.22345 14.7762 4.25741 14.7762 7.99998C14.7762 11.7426 11.7423 14.7765 7.99968 14.7765C4.2571 14.7765 1.22314 11.7426 1.22314 7.99998ZM8.14797 5.62577C7.87651 5.57921 7.59732 5.63022 7.35986 5.76978C7.1224 5.90934 6.942 6.12843 6.8506 6.38825C6.72842 6.73558 6.34781 6.9181 6.00048 6.79591C5.65315 6.67373 5.47064 6.29312 5.59282 5.9458C5.78871 5.38894 6.17536 4.91937 6.68428 4.62027C7.19321 4.32117 7.79157 4.21184 8.37338 4.31163C8.9552 4.41143 9.48292 4.71392 9.86308 5.16552C10.2432 5.61702 10.4512 6.18845 10.4504 6.77862C10.4501 7.74318 9.73548 8.37516 9.23708 8.70743C8.96754 8.88713 8.70274 9.01905 8.50796 9.10562C8.40962 9.14933 8.32674 9.18252 8.26689 9.20532C8.23691 9.21674 8.21254 9.22562 8.19473 9.23195L8.17297 9.23957L8.16595 9.24197L8.16345 9.24282L8.16245 9.24315C8.16225 9.24322 8.16162 9.24343 7.9508 8.61097L8.16162 9.24343C7.81232 9.35986 7.43478 9.17109 7.31835 8.82179C7.20195 8.47261 7.39056 8.0952 7.73963 7.97863L7.74805 7.97567C7.75708 7.97246 7.77209 7.96701 7.79223 7.95934C7.83262 7.94395 7.89294 7.91987 7.96644 7.8872C8.11534 7.82103 8.30878 7.72383 8.49748 7.59803C8.91545 7.31938 9.11709 7.03509 9.11709 6.77801L9.1171 6.77702C9.11751 6.50159 9.02042 6.2349 8.84305 6.02419C8.66567 5.81347 8.41944 5.67234 8.14797 5.62577ZM7.33301 11.0549C7.33301 10.6867 7.63149 10.3883 7.99968 10.3883H8.00579C8.37398 10.3883 8.67246 10.6867 8.67246 11.0549C8.67246 11.4231 8.37398 11.7216 8.00579 11.7216H7.99968C7.63149 11.7216 7.33301 11.4231 7.33301 11.0549Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg t="1709471698048" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4242"
|
||||
width="128" height="128">
|
||||
<path
|
||||
d="M855.158154 945.664H168.999385c-28.081231 0-50.845538-22.843077-50.845539-51.003077V486.833231C118.153846 458.673231 129.457231 433.230769 157.538462 433.230769h708.923076c28.081231 0 39.502769 25.442462 39.50277 53.602462v407.827692c0 28.16-22.764308 51.003077-50.806154 51.003077z m-340.913231-376.595692a99.761231 99.761231 0 0 0-99.603692 99.958154c0 40.251077 23.827692 74.712615 57.974154 90.54523V827.076923a39.384615 39.384615 0 0 0 78.76923 0v-65.417846a99.879385 99.879385 0 0 0 62.424616-92.632615 99.761231 99.761231 0 0 0-99.564308-99.958154z m0.551385-396.524308c-104.841846 0-189.794462 81.329231-197.159385 184.123077H217.718154C229.060923 201.334154 358.321231 78.769231 516.489846 78.769231s287.428923 122.564923 298.732308 277.897846h-103.266462c-7.364923-102.793846-92.317538-184.123077-197.159384-184.123077z"
|
||||
fill="#3B9BF8" p-id="4243"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
7
packages/web/components/common/Image/MyImage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Image, ImageProps } from '@chakra-ui/react';
|
||||
import { getWebReqUrl } from '../../../common/system/utils';
|
||||
const MyImage = (props: ImageProps) => {
|
||||
return <Image {...props} src={getWebReqUrl(props.src)} alt={props.alt || ''} />;
|
||||
};
|
||||
export default React.memo(MyImage);
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { PhotoProvider, PhotoView } from 'react-photo-view';
|
||||
import 'react-photo-view/dist/react-photo-view.css';
|
||||
import { Box, Image, ImageProps } from '@chakra-ui/react';
|
||||
import { ImageProps } from '@chakra-ui/react';
|
||||
import { useSystem } from '../../../hooks/useSystem';
|
||||
import Loading from '../MyLoading';
|
||||
import MyImage from './MyImage';
|
||||
|
||||
const MyPhotoView = ({ ...props }: ImageProps) => {
|
||||
const { isPc } = useSystem();
|
||||
@@ -15,7 +16,7 @@ const MyPhotoView = ({ ...props }: ImageProps) => {
|
||||
loadingElement={<Loading fixed={false} />}
|
||||
>
|
||||
<PhotoView src={props.src}>
|
||||
<Image cursor={'pointer'} {...props} />
|
||||
<MyImage cursor={'pointer'} {...props} />
|
||||
</PhotoView>
|
||||
</PhotoProvider>
|
||||
);
|
||||
|
||||