feat: prompt call tool support reason;perf: ai proxy doc (#3982)

* update schema

* perf: ai proxy doc

* feat: prompt call tool support reason
This commit is contained in:
Archer
2025-03-05 14:11:52 +08:00
committed by archer
parent e1aa068858
commit 3e3f2165db
23 changed files with 508 additions and 298 deletions

View File

@@ -27,7 +27,5 @@
},
"markdown.copyFiles.destination": {
"/docSite/content/**/*": "${documentWorkspaceFolder}/docSite/assets/imgs/"
},
"markdown.copyFiles.overwriteBehavior": "nameIncrementally",
"markdown.copyFiles.transformPath": "const filename = uri.path.split('/').pop(); return `/imgs/${filename}`;"
}
}

View File

@@ -115,16 +115,6 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
<img src="https://img.shields.io/badge/-返回顶部-7d09f1.svg" alt="#" align="right">
</a>
## 🏘️ 社区交流群
扫码加入飞书话题群:
![](https://oss.laf.run/otnvvf-imgs/fastgpt-feishu1.png)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-7d09f1.svg" alt="#" align="right">
</a>
## 🏘️ 加入我们
我们正在寻找志同道合的小伙伴,加速 FastGPT 的发展。你可以通过 [FastGPT 2025 招聘](https://fael3z0zfze.feishu.cn/wiki/P7FOwEmPziVcaYkvVaacnVX1nvg)了解 FastGPT 的招聘信息。
@@ -135,17 +125,25 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
- [Sealos快速部署集群应用](https://github.com/labring/sealos)
- [AI Proxy API调用地址](https://sealos.run/aiproxy/?k=fastgpt-github/)
- [One API多模型管理支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
- [TuShan5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-7d09f1.svg" alt="#" align="right">
</a>
## 🌿 第三方生态
- [COW 个人微信/企微机器人](https://doc.tryfastgpt.ai/docs/use-cases/external-integration/onwechat/)
- [SiliconCloud (硅基流动) —— 开源模型在线体验平台](https://cloud.siliconflow.cn/i/TR9Ym0c4)
- [COW 个人微信/企微机器人](https://doc.tryfastgpt.ai/docs/use-cases/external-integration/onwechat/)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-7d09f1.svg" alt="#" align="right">
</a>
## 🏘️ 社区交流群
扫码加入飞书话题群:
![](https://oss.laf.run/otnvvf-imgs/fastgpt-feishu1.png)
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-7d09f1.svg" alt="#" align="right">

View File

@@ -141,10 +141,9 @@ services:
- AIPROXY_API_ENDPOINT=http://aiproxy:3000
# AI Proxy 的 Admin Token与 AI Proxy 中的环境变量 ADMIN_KEY
- AIPROXY_API_TOKEN=aiproxy
# AI模型的API地址哦。务必加 /v1。这里默认填写了OneApi的访问地址。
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 模型中转地址(如果用了 AI Proxy下面 2 个就不需要了,旧版 OneAPI 用户,使用下面的变量)
# - OPENAI_BASE_URL=http://oneapi:3000/v1
# - CHAT_API_KEY=sk-fastgpt
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥
@@ -180,32 +179,37 @@ services:
container_name: aiproxy
restart: unless-stopped
depends_on:
pgsql:
aiproxy_pg:
condition: service_healthy
ports:
- '3002:3000/tcp'
- '3002:3000'
networks:
- fastgpt
environment:
- ADMIN_KEY=aiproxy # 对应 fastgpt 里的AIPROXY_API_TOKEN
- LOG_DETAIL_STORAGE_HOURS=1 # 日志详情保存时间(小时)
- TZ=Asia/Shanghai
- SQL_DSN=postgres://postgres:aiproxy@pgsql:5432/aiproxy
# 对应 fastgpt 里的AIPROXY_API_TOKEN
- ADMIN_KEY=aiproxy
# 错误日志详情保存时间(小时)
- LOG_DETAIL_STORAGE_HOURS=1
# 数据库连接地址
- SQL_DSN=postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy
# 最大重试次数
- RetryTimes=3
# 不需要计费
- BILLING_ENABLED=false
# 不需要严格检测模型
- DISABLE_MODEL_CONFIG=true
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status']
interval: 5s
timeout: 5s
retries: 10
# AI Proxy
pgsql:
# image: "postgres:latest"
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.7.0 # 阿里云
aiproxy_pg:
# image: pgvector/pgvector:0.8.0-pg15 # docker hub
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.8.0-pg15 # 阿里云
restart: unless-stopped
container_name: pgsql
container_name: aiproxy_pg
volumes:
- ./pgsql:/var/lib/postgresql/data
- ./aiproxy_pg:/var/lib/postgresql/data
networks:
- fastgpt
environment:

View File

@@ -11,8 +11,8 @@ services:
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.8.0-pg15 # 阿里云
container_name: pg
restart: always
ports: # 生产环境建议不要暴露
- 5432:5432
# ports: # 生产环境建议不要暴露
# - 5432:5432
networks:
- fastgpt
environment:
@@ -99,10 +99,9 @@ services:
- AIPROXY_API_ENDPOINT=http://aiproxy:3000
# AI Proxy 的 Admin Token与 AI Proxy 中的环境变量 ADMIN_KEY
- AIPROXY_API_TOKEN=aiproxy
# AI模型的API地址哦。务必加 /v1。这里默认填写了OneApi的访问地址。
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 模型中转地址(如果用了 AI Proxy下面 2 个就不需要了,旧版 OneAPI 用户,使用下面的变量)
# - OPENAI_BASE_URL=http://oneapi:3000/v1
# - CHAT_API_KEY=sk-fastgpt
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥
@@ -137,32 +136,37 @@ services:
container_name: aiproxy
restart: unless-stopped
depends_on:
pgsql:
aiproxy_pg:
condition: service_healthy
ports:
- '3002:3000/tcp'
- '3002:3000'
networks:
- fastgpt
environment:
- ADMIN_KEY=aiproxy # 对应 fastgpt 里的AIPROXY_API_TOKEN
- LOG_DETAIL_STORAGE_HOURS=1 # 日志详情保存时间(小时)
- TZ=Asia/Shanghai
- SQL_DSN=postgres://postgres:aiproxy@pgsql:5432/aiproxy
# 对应 fastgpt 里的AIPROXY_API_TOKEN
- ADMIN_KEY=aiproxy
# 错误日志详情保存时间(小时)
- LOG_DETAIL_STORAGE_HOURS=1
# 数据库连接地址
- SQL_DSN=postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy
# 最大重试次数
- RetryTimes=3
# 不需要计费
- BILLING_ENABLED=false
# 不需要严格检测模型
- DISABLE_MODEL_CONFIG=true
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status']
interval: 5s
timeout: 5s
retries: 10
# AI Proxy
pgsql:
# image: "postgres:latest"
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.7.0 # 阿里云
aiproxy_pg:
# image: pgvector/pgvector:0.8.0-pg15 # docker hub
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.8.0-pg15 # 阿里云
restart: unless-stopped
container_name: pgsql
container_name: aiproxy_pg
volumes:
- ./pgsql:/var/lib/postgresql/data
- ./aiproxy_pg:/var/lib/postgresql/data
networks:
- fastgpt
environment:

View File

@@ -79,10 +79,9 @@ services:
- AIPROXY_API_ENDPOINT=http://aiproxy:3000
# AI Proxy 的 Admin Token与 AI Proxy 中的环境变量 ADMIN_KEY
- AIPROXY_API_TOKEN=aiproxy
# AI模型的API地址哦。务必加 /v1。这里默认填写了OneApi的访问地址。
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 模型中转地址(如果用了 AI Proxy下面 2 个就不需要了,旧版 OneAPI 用户,使用下面的变量)
# - OPENAI_BASE_URL=http://oneapi:3000/v1
# - CHAT_API_KEY=sk-fastgpt
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥
@@ -118,32 +117,37 @@ services:
container_name: aiproxy
restart: unless-stopped
depends_on:
pgsql:
aiproxy_pg:
condition: service_healthy
ports:
- '3002:3000/tcp'
- '3002:3000'
networks:
- fastgpt
environment:
- ADMIN_KEY=aiproxy # 对应 fastgpt 里的AIPROXY_API_TOKEN
- LOG_DETAIL_STORAGE_HOURS=1 # 日志详情保存时间(小时)
- TZ=Asia/Shanghai
- SQL_DSN=postgres://postgres:aiproxy@pgsql:5432/aiproxy
# 对应 fastgpt 里的AIPROXY_API_TOKEN
- ADMIN_KEY=aiproxy
# 错误日志详情保存时间(小时)
- LOG_DETAIL_STORAGE_HOURS=1
# 数据库连接地址
- SQL_DSN=postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy
# 最大重试次数
- RetryTimes=3
# 不需要计费
- BILLING_ENABLED=false
# 不需要严格检测模型
- DISABLE_MODEL_CONFIG=true
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status']
interval: 5s
timeout: 5s
retries: 10
# AI Proxy
pgsql:
# image: "postgres:latest"
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.7.0 # 阿里云
aiproxy_pg:
# image: pgvector/pgvector:0.8.0-pg15 # docker hub
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:v0.8.0-pg15 # 阿里云
restart: unless-stopped
container_name: pgsql
container_name: aiproxy_pg
volumes:
- ./pgsql:/var/lib/postgresql/data
- ./aiproxy_pg:/var/lib/postgresql/data
networks:
- fastgpt
environment:

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -30,7 +30,7 @@ weight: 707
### PgVector版本
非常轻量,适合数据量在 5000 万以下。
非常轻量,适合知识库索引量在 5000 万以下。
{{< table "table-hover table-striped-columns" >}}
| 环境 | 最低配置(单节点) | 推荐配置 |
@@ -149,18 +149,14 @@ curl -o docker-compose.yml https://raw.githubusercontent.com/labring/FastGPT/mai
{{< tab tabName="PgVector版本" >}}
{{< markdownify >}}
```
FE_DOMAIN=你的前端你访问地址,例如 http://192.168.0.1:3000;https://cloud.fastgpt.cn
```
无需操作
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="Milvus版本" >}}
{{< markdownify >}}
```
FE_DOMAIN=你的前端你访问地址,例如 http://192.168.0.1:3000;https://cloud.fastgpt.cn
```
无需操作
{{< /markdownify >}}
{{< /tab >}}
@@ -174,7 +170,6 @@ FE_DOMAIN=你的前端你访问地址,例如 http://192.168.0.1:3000;https://clo
{{% alert icon="🤖" context="success" %}}
1. 修改`MILVUS_ADDRESS``MILVUS_TOKEN`链接参数,分别对应 `zilliz``Public Endpoint``Api key`记得把自己ip加入白名单。
2. 修改FE_DOMAIN=你的前端你访问地址,例如 http://192.168.0.1:3000;https://cloud.fastgpt.cn
{{% /alert %}}
@@ -189,30 +184,28 @@ FE_DOMAIN=你的前端你访问地址,例如 http://192.168.0.1:3000;https://clo
```bash
# 启动容器
docker-compose up -d
# 等待10sOneAPI第一次总是要重启几次才能连上Mysql
sleep 10
# 重启一次oneapi(由于OneAPI的默认Key有点问题不重启的话会提示找不到渠道临时手动重启一次解决等待作者修复)
docker restart oneapi
```
### 4. 访问 FastGPT
目前可以通过 `ip:3000` 直接访问(注意防火墙)。登录用户名为 `root`,密码为`docker-compose.yml`环境变量里设置的 `DEFAULT_ROOT_PSW`
目前可以通过 `ip:3000` 直接访问(注意开放防火墙)。登录用户名为 `root`,密码为`docker-compose.yml`环境变量里设置的 `DEFAULT_ROOT_PSW`
如果需要域名访问,请自行安装并配置 Nginx。
首次运行,会自动初始化 root 用户,密码为 `1234`(与环境变量中的`DEFAULT_ROOT_PSW`一致),日志会提示一次`MongoServerError: Unable to read from a snapshot due to pending collection catalog changes;`可忽略。
首次运行,会自动初始化 root 用户,密码为 `1234`(与环境变量中的`DEFAULT_ROOT_PSW`一致),日志可能会提示一次`MongoServerError: Unable to read from a snapshot due to pending collection catalog changes;`可忽略。
### 5. 配置模型
登录FastGPT后进入“模型提供商”页面,首先配置模型渠道,[点击查看相关教程](/docs/development/modelconfig/ai-proxy)
然后配置具体模型,务必先配置至少一个语言模型和一个向量模型,否则系统无法正常使用
[点击查看模型配置教程](/docs/development/modelConfig/intro/)
- 首次登录FastGPT后系统会提示未配置`语言模型``索引模型`,并自动跳转模型配置页面。系统必须至少有这两类模型才能正常使用。
- 如果系统未正常跳转,可以在`账号-模型提供商`页面,进行模型配置。[点击查看相关教程](/docs/development/modelconfig/ai-proxy)
- 目前已知可能问题:首次进入系统后,整个浏览器 tab 无法响应。此时需要删除该tab重新打开一次即可
## FAQ
### 登录系统后,浏览器无法响应
无法点击任何内容刷新也无效。此时需要删除该tab重新打开一次即可。
### Mongo 副本集自动初始化失败
最新的 docker-compose 示例优化 Mongo 副本集初始化,实现了全自动。目前在 unbuntu20,22 centos7, wsl2, mac, window 均通过测试。仍无法正常启动,大部分是因为 cpu 不支持 AVX 指令集,可以切换 Mongo4.x 版本。

View File

@@ -7,7 +7,7 @@ toc: true
weight: 744
---
从 FastGPT 4.8.23 版本开始,引入 AI Proxy 来进一步方便模型的配置。
`FastGPT 4.8.23` 版本开始,引入 AI Proxy 来进一步方便模型的配置。
AI Proxy 与 One API 类似,也是作为一个 OpenAI 接口管理 & 分发系统,可以通过标准的 OpenAI API 格式访问所有的大模型,开箱即用。
@@ -15,13 +15,29 @@ AI Proxy 与 One API 类似,也是作为一个 OpenAI 接口管理 & 分发系
### Docker 版本
`docker-compose.yml` 文件已加入了 AI Proxy 配置,可直接使用。
`docker-compose.yml` 文件已加入了 AI Proxy 配置,可直接使用。[点击查看最新的 yml 配置](https://raw.githubusercontent.com/labring/FastGPT/main/deploy/docker/docker-compose-pgvector.yml)
## 基础使用
从旧版升级的用户,可以复制 yml 里ai proxy 的配置,加入到旧的 yml 文件中。
## 运行原理
AI proxy 核心模块:
1. 渠道管理:管理各家模型提供商的 API Key 和可用模型列表。
2. 模型调用:根据请求的模型,选中对应的渠道;根据渠道的 API 格式,构造请求体,发送请求;格式化响应体成标准格式返回。
3. 调用日志:详细记录模型调用的日志,并在错误时候可以记录其入参和报错信息,方便排查。
运行流程:
![aiproxy12](/imgs/aiproxy1.png)
## 在 FastGPT 中使用
AI proxy 相关功能,可以在`账号-模型提供商`页面找到。
### 1. 创建渠道
如果 FastGPT 的环境变量中,设置了 AIPROXY_API_ENDPOINT 的值,那么在“模型提供商的配置页面,会多出两个 tab可以直接在 FastGPT 平台上配置模型渠道,以及查看模型实际调用日志。
`模型提供商`的配置页面,点击`模型渠道`,进入渠道配置页面
![aiproxy1](/imgs/aiproxy-1.png)
@@ -36,9 +52,18 @@ AI Proxy 与 One API 类似,也是作为一个 OpenAI 接口管理 & 分发系
1. 渠道名:展示在外部的渠道名称,仅作标识;
2. 厂商:模型对应的厂商,不同厂商对应不同的默认地址和 API 密钥格式;
3. 模型:当前渠道具体可以使用的模型,系统内置了主流的一些模型,如果下拉框中没有想要的选项,可以点击“新增模型”,[增加自定义模型](/docs/development/modelconfig/intro/#新增自定义模型);
4. 模型映射:将 FastGPT 请求的模型,映射到具体提供的模型上
5. 代理地址:具体请求的地址,系统给每个主流渠道配置了默认的地址,如果无需改动则不用填
6. API 密钥:从模型厂商处获取的 API 凭证
4. 模型映射:将 FastGPT 请求的模型,映射到具体提供的模型上。例如:
```json
{
"gpt-4o-test": "gpt-4o",
}
```
FatGPT 中的模型为 `gpt-4o-test`,向 AI Proxy 发起请求时也是 `gpt-4o-test`。AI proxy 在向上游发送请求时,实际的`model``gpt-4o`
5. 代理地址:具体请求的地址,系统给每个主流渠道配置了默认的地址,如果无需改动则不用填。
6. API 密钥:从模型厂商处获取的 API 凭证。注意部分厂商需要提供多个密钥组合,可以根据提示进行输入。
最后点击“新增”,就能在“模型渠道”下看到刚刚配置的渠道
@@ -60,16 +85,15 @@ AI Proxy 与 One API 类似,也是作为一个 OpenAI 接口管理 & 分发系
### 3. 启用模型
最后在模型配置中,可以选择启用对应的模型,这样就能在平台中使用了
最后在`模型配置`中,可以选择启用对应的模型,这样就能在平台中使用了,更多模型配置可以参考[模型配置](/docs/development/modelconfig/intro)
![aiproxy8](/imgs/aiproxy-8.png)
## 渠道设置
## 其他功能介绍
### 优先级
在 FastGPT 中,可以给渠道设置优先级,对于同样的模型,优先级越高的渠道会越优先请求
范围1100。数值越大越容易被优先选中。
![aiproxy9](/imgs/aiproxy-9.png)
@@ -81,13 +105,15 @@ AI Proxy 与 One API 类似,也是作为一个 OpenAI 接口管理 & 分发系
### 调用日志
调用日志 页面,会展示发送到模型处的请求记录,包括具体的输入输出 tokens、请求时间、请求耗时、请求地址等等
`调用日志` 页面,会展示发送到模型处的请求记录,包括具体的输入输出 tokens、请求时间、请求耗时、请求地址等等。错误的请求,则会详细的入参和错误信息,方便排查,但仅会保留 1 小时(环境变量里可配置)。
![aiproxy11](/imgs/aiproxy-11.png)
## 如何从 OneAPI 迁移到 AI Proxy
## 从 OneAPI 迁移到 AI Proxy
可以从任意终端,发起 1 个 HTTP 请求。其中 {{host}} 替换成 AI Proxy 地址,{{admin_key}} 替换成 AI Proxy 中 ADMIN_KEY 的值,参数 dsn 为 OneAPI 的 mysql 连接串
可以从任意终端,发起 1 个 HTTP 请求。其中 `{{host}}` 替换成 AI Proxy 地址,`{{admin_key}}` 替换成 AI Proxy 中 `ADMIN_KEY` 的值
Body 参数 `dsn` 为 OneAPI 的 mysql 连接串。
```bash
curl --location --request POST '{{host}}/api/channels/import/oneapi' \
@@ -100,4 +126,4 @@ curl --location --request POST '{{host}}/api/channels/import/oneapi' \
执行成功的情况下会返回 "success": true
脚本目前不是完全准,可能会有部分渠道遗漏,还需要手动检查
脚本目前不是完全准,仅是简单的做数据映射,主要是迁移`代理地址``模型``API 密钥`,建议迁移后再进行手动检查

View File

@@ -46,6 +46,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv490' \
1. 知识库数据不再限制索引数量,可无限自定义。同时可自动更新输入文本的索引,不影响自定义索引。
2. Markdown 解析,增加链接后中文标点符号检测,增加空格。
3. Prompt 模式工具调用,支持思考模型。同时优化其格式检测,减少空输出的概率。
## 🐛 修复

View File

@@ -1,8 +1,11 @@
import type {
AIChatItemValueItemType,
ChatItemType,
ChatItemValueItemType,
RuntimeUserPromptType,
UserChatItemType
SystemChatItemValueItemType,
UserChatItemType,
UserChatItemValueItemType
} from '../../core/chat/type.d';
import { ChatFileTypeEnum, ChatItemValueTypeEnum, ChatRoleEnum } from '../../core/chat/constants';
import type {
@@ -174,137 +177,24 @@ export const GPTMessages2Chats = (
): ChatItemType[] => {
const chatMessages = messages
.map((item) => {
const value: ChatItemType['value'] = [];
const obj = GPT2Chat[item.role];
if (
obj === ChatRoleEnum.System &&
item.role === ChatCompletionRequestMessageRoleEnum.System
) {
if (Array.isArray(item.content)) {
item.content.forEach((item) => [
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.text
}
})
]);
} else {
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.content
}
});
}
} else if (
obj === ChatRoleEnum.Human &&
item.role === ChatCompletionRequestMessageRoleEnum.User
) {
if (typeof item.content === 'string') {
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.content
}
});
} else if (Array.isArray(item.content)) {
item.content.forEach((item) => {
if (item.type === 'text') {
const value = (() => {
if (
obj === ChatRoleEnum.System &&
item.role === ChatCompletionRequestMessageRoleEnum.System
) {
const value: SystemChatItemValueItemType[] = [];
if (Array.isArray(item.content)) {
item.content.forEach((item) => [
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.text
}
});
} else if (item.type === 'image_url') {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.file,
file: {
type: ChatFileTypeEnum.image,
name: '',
url: item.image_url.url
}
});
} else if (item.type === 'file_url') {
value.push({
// @ts-ignore
type: ChatItemValueTypeEnum.file,
file: {
type: ChatFileTypeEnum.file,
name: item.name,
url: item.url
}
});
}
});
}
} else if (
obj === ChatRoleEnum.AI &&
item.role === ChatCompletionRequestMessageRoleEnum.Assistant
) {
if (item.tool_calls && reserveTool) {
// save tool calls
const toolCalls = item.tool_calls as ChatCompletionMessageToolCall[];
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.tool,
tools: toolCalls.map((tool) => {
let toolResponse =
messages.find(
(msg) =>
msg.role === ChatCompletionRequestMessageRoleEnum.Tool &&
msg.tool_call_id === tool.id
)?.content || '';
toolResponse =
typeof toolResponse === 'string' ? toolResponse : JSON.stringify(toolResponse);
return {
id: tool.id,
toolName: tool.toolName || '',
toolAvatar: tool.toolAvatar || '',
functionName: tool.function.name,
params: tool.function.arguments,
response: toolResponse as string
};
})
});
} else if (item.function_call && reserveTool) {
const functionCall = item.function_call as ChatCompletionMessageFunctionCall;
const functionResponse = messages.find(
(msg) =>
msg.role === ChatCompletionRequestMessageRoleEnum.Function &&
msg.name === item.function_call?.name
) as ChatCompletionFunctionMessageParam;
if (functionResponse) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.tool,
tools: [
{
id: functionCall.id || '',
toolName: functionCall.toolName || '',
toolAvatar: functionCall.toolAvatar || '',
functionName: functionCall.name,
params: functionCall.arguments,
response: functionResponse.content || ''
}
]
});
}
} else if (item.interactive) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.interactive,
interactive: item.interactive
});
} else if (typeof item.content === 'string') {
const lastValue = value[value.length - 1];
if (lastValue && lastValue.type === ChatItemValueTypeEnum.text && lastValue.text) {
lastValue.text.content += item.content;
})
]);
} else {
value.push({
type: ChatItemValueTypeEnum.text,
@@ -313,8 +203,145 @@ export const GPTMessages2Chats = (
}
});
}
return value;
} else if (
obj === ChatRoleEnum.Human &&
item.role === ChatCompletionRequestMessageRoleEnum.User
) {
const value: UserChatItemValueItemType[] = [];
if (typeof item.content === 'string') {
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.content
}
});
} else if (Array.isArray(item.content)) {
item.content.forEach((item) => {
if (item.type === 'text') {
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.text
}
});
} else if (item.type === 'image_url') {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.file,
file: {
type: ChatFileTypeEnum.image,
name: '',
url: item.image_url.url
}
});
} else if (item.type === 'file_url') {
value.push({
// @ts-ignore
type: ChatItemValueTypeEnum.file,
file: {
type: ChatFileTypeEnum.file,
name: item.name,
url: item.url
}
});
}
});
}
return value;
} else if (
obj === ChatRoleEnum.AI &&
item.role === ChatCompletionRequestMessageRoleEnum.Assistant
) {
const value: AIChatItemValueItemType[] = [];
if (typeof item.reasoning_text === 'string') {
value.push({
type: ChatItemValueTypeEnum.reasoning,
reasoning: {
content: item.reasoning_text
}
});
}
if (item.tool_calls && reserveTool) {
// save tool calls
const toolCalls = item.tool_calls as ChatCompletionMessageToolCall[];
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.tool,
tools: toolCalls.map((tool) => {
let toolResponse =
messages.find(
(msg) =>
msg.role === ChatCompletionRequestMessageRoleEnum.Tool &&
msg.tool_call_id === tool.id
)?.content || '';
toolResponse =
typeof toolResponse === 'string' ? toolResponse : JSON.stringify(toolResponse);
return {
id: tool.id,
toolName: tool.toolName || '',
toolAvatar: tool.toolAvatar || '',
functionName: tool.function.name,
params: tool.function.arguments,
response: toolResponse as string
};
})
});
}
if (item.function_call && reserveTool) {
const functionCall = item.function_call as ChatCompletionMessageFunctionCall;
const functionResponse = messages.find(
(msg) =>
msg.role === ChatCompletionRequestMessageRoleEnum.Function &&
msg.name === item.function_call?.name
) as ChatCompletionFunctionMessageParam;
if (functionResponse) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.tool,
tools: [
{
id: functionCall.id || '',
toolName: functionCall.toolName || '',
toolAvatar: functionCall.toolAvatar || '',
functionName: functionCall.name,
params: functionCall.arguments,
response: functionResponse.content || ''
}
]
});
}
}
if (item.interactive) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.interactive,
interactive: item.interactive
});
}
if (typeof item.content === 'string') {
const lastValue = value[value.length - 1];
if (lastValue && lastValue.type === ChatItemValueTypeEnum.text && lastValue.text) {
lastValue.text.content += item.content;
} else {
value.push({
type: ChatItemValueTypeEnum.text,
text: {
content: item.content
}
});
}
}
return value;
}
}
return [];
})();
return {
dataId: item.dataId,

View File

@@ -77,6 +77,7 @@ export type AIChatItemValueItemType = {
| ChatItemValueTypeEnum.reasoning
| ChatItemValueTypeEnum.tool
| ChatItemValueTypeEnum.interactive;
text?: {
content: string;
};

View File

@@ -55,7 +55,7 @@ export const AiChatModule: FlowNodeTemplateType = {
showStatus: true,
isTool: true,
courseUrl: '/docs/guide/workbench/workflow/ai_chat/',
version: '4813',
version: '490',
inputs: [
Input_Template_SettingAiModel,
// --- settings modal

View File

@@ -58,6 +58,13 @@ export const ToolModule: FlowNodeTemplateType = {
valueType: WorkflowIOValueTypeEnum.boolean,
value: true
},
{
key: NodeInputKeyEnum.aiChatReasoning,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.boolean,
value: true
},
{
key: NodeInputKeyEnum.aiChatTopP,
renderTypeList: [FlowNodeInputTypeEnum.hidden],

View File

@@ -245,7 +245,7 @@ export const readRawContentByFileBuffer = async ({
if (result_data.data.status === 'success') {
const result = result_data.data.result.pages
.map((page) => page.md)
.join('\n')
.join('')
// Do some post-processing
.replace(/\\[\(\)]/g, '$')
.replace(/\\[\[\]]/g, '$$')

View File

@@ -75,6 +75,81 @@
"showTopP": true,
"showStopSign": true,
"responseFormatList": ["text", "json_object"]
},
{
"model": "moonshot-v1-8k-vision-preview",
"name": "moonshot-v1-8k-vision-preview",
"maxContext": 8000,
"maxResponse": 4000,
"quoteMaxToken": 6000,
"maxTemperature": 1,
"vision": true,
"toolChoice": true,
"functionCall": false,
"defaultSystemChatPrompt": "",
"datasetProcess": true,
"usedInClassify": true,
"customCQPrompt": "",
"usedInExtractFields": true,
"usedInQueryExtension": true,
"customExtractPrompt": "",
"usedInToolCall": true,
"defaultConfig": {},
"fieldMap": {},
"type": "llm",
"showTopP": true,
"showStopSign": true,
"responseFormatList": ["text", "json_object"]
},
{
"model": "moonshot-v1-32k-vision-preview",
"name": "moonshot-v1-32k-vision-preview",
"maxContext": 32000,
"maxResponse": 4000,
"quoteMaxToken": 32000,
"maxTemperature": 1,
"vision": true,
"toolChoice": true,
"functionCall": false,
"defaultSystemChatPrompt": "",
"datasetProcess": true,
"usedInClassify": true,
"customCQPrompt": "",
"usedInExtractFields": true,
"usedInQueryExtension": true,
"customExtractPrompt": "",
"usedInToolCall": true,
"defaultConfig": {},
"fieldMap": {},
"type": "llm",
"showTopP": true,
"showStopSign": true,
"responseFormatList": ["text", "json_object"]
},
{
"model": "moonshot-v1-128k-vision-preview",
"name": "moonshot-v1-128k-vision-preview",
"maxContext": 128000,
"maxResponse": 4000,
"quoteMaxToken": 60000,
"maxTemperature": 1,
"vision": true,
"toolChoice": true,
"functionCall": false,
"defaultSystemChatPrompt": "",
"datasetProcess": true,
"usedInClassify": true,
"customCQPrompt": "",
"usedInExtractFields": true,
"usedInQueryExtension": true,
"customExtractPrompt": "",
"usedInToolCall": true,
"defaultConfig": {},
"fieldMap": {},
"type": "llm",
"showTopP": true,
"showStopSign": true,
"responseFormatList": ["text", "json_object"]
}
]
}

View File

@@ -9,41 +9,23 @@ const AppTemplateSchema = new Schema({
type: String,
required: true
},
name: {
type: String
},
intro: {
type: String
},
avatar: {
type: String
},
author: {
type: String
},
name: String,
intro: String,
avatar: String,
author: String,
tags: {
type: [String],
default: undefined
},
type: {
type: String
},
isActive: {
type: Boolean
},
userGuide: {
type: Object
},
isQuickTemplate: {
type: Boolean
},
type: String,
isActive: Boolean,
userGuide: Object,
isQuickTemplate: Boolean,
order: {
type: Number,
default: -1
},
workflow: {
type: Object
}
workflow: Object
});
AppTemplateSchema.index({ templateId: 1 });

View File

@@ -55,7 +55,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
userChatInput,
history = 6,
fileUrlList: fileLinks,
aiChatVision
aiChatVision,
aiChatReasoning
}
} = props;
@@ -63,6 +64,9 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
const useVision = aiChatVision && toolModel.vision;
const chatHistories = getHistories(history, histories);
props.params.aiChatVision = aiChatVision && toolModel.vision;
props.params.aiChatReasoning = aiChatReasoning && toolModel.reasoning;
const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges });
// Gets the module to which the tool is connected

View File

@@ -24,7 +24,12 @@ import {
import { AIChatItemType } from '@fastgpt/global/core/chat/type';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils';
import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils';
import {
computedMaxToken,
llmCompletionsBodyFormat,
parseReasoningContent,
parseReasoningStreamContent
} from '../../../../ai/utils';
import { WorkflowResponseType } from '../../type';
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
@@ -58,6 +63,7 @@ export const runToolWithPromptCall = async (
temperature,
maxToken,
aiChatVision,
aiChatReasoning,
aiChatTopP,
aiChatStopSign,
aiChatResponseFormat,
@@ -216,7 +222,7 @@ export const runToolWithPromptCall = async (
const [requestMessages] = await Promise.all([
loadRequestMessages({
messages: filterMessages,
useVision: toolModel.vision && aiChatVision,
useVision: aiChatVision,
origin: requestOrigin
})
]);
@@ -251,22 +257,46 @@ export const runToolWithPromptCall = async (
}
});
const answer = await (async () => {
const { answer, reasoning } = await (async () => {
if (res && isStreamResponse) {
const { answer } = await streamResponse({
const { answer, reasoning } = await streamResponse({
res,
toolNodes,
stream: aiResponse,
workflowStreamResponse
workflowStreamResponse,
aiChatReasoning
});
return answer;
return { answer, reasoning };
} else {
const result = aiResponse as ChatCompletion;
const content = aiResponse.choices?.[0]?.message?.content || '';
const reasoningContent: string = aiResponse.choices?.[0]?.message?.reasoning_content || '';
return result.choices?.[0]?.message?.content || '';
// API already parse reasoning content
if (reasoningContent || !aiChatReasoning) {
return {
answer: content,
reasoning: reasoningContent
};
}
const [think, answer] = parseReasoningContent(content);
return {
answer,
reasoning: think
};
}
})();
if (stream && !isStreamResponse && aiChatReasoning && reasoning) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
reasoning_content: reasoning
})
});
}
const { answer: replaceAnswer, toolJson } = parseAnswer(answer);
if (!answer && !toolJson) {
return Promise.reject(getEmptyResponseTip());
@@ -294,11 +324,16 @@ export const runToolWithPromptCall = async (
}
// No tool is invoked, indicating that the process is over
const gptAssistantResponse: ChatCompletionAssistantMessageParam = {
const gptAssistantResponse: ChatCompletionMessageParam = {
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: replaceAnswer
content: replaceAnswer,
reasoning_text: reasoning
};
const completeMessages = filterMessages.concat(gptAssistantResponse);
const completeMessages = filterMessages.concat({
...gptAssistantResponse,
reasoning_text: undefined
});
const inputTokens = await countGptMessagesTokens(requestMessages);
const outputTokens = await countGptMessagesTokens([gptAssistantResponse]);
@@ -379,9 +414,10 @@ export const runToolWithPromptCall = async (
})();
// 合并工具调用的结果,使用 functionCall 格式存储。
const assistantToolMsgParams: ChatCompletionAssistantMessageParam = {
const assistantToolMsgParams: ChatCompletionMessageParam = {
role: ChatCompletionRequestMessageRoleEnum.Assistant,
function_call: toolJson
function_call: toolJson,
reasoning_text: reasoning
};
// Only toolCall tokens are counted here, Tool response tokens count towards the next reply
@@ -502,12 +538,14 @@ ANSWER: `;
async function streamResponse({
res,
stream,
workflowStreamResponse
workflowStreamResponse,
aiChatReasoning
}: {
res: NextApiResponse;
toolNodes: ToolNodeItemType[];
stream: StreamChatType;
workflowStreamResponse?: WorkflowResponseType;
aiChatReasoning?: boolean;
}) {
const write = responseWriteController({
res,
@@ -515,7 +553,9 @@ async function streamResponse({
});
let startResponseWrite = false;
let textAnswer = '';
let answer = '';
let reasoning = '';
const { parsePart, getStartTagBuffer } = parseReasoningStreamContent();
for await (const part of stream) {
if (res.closed) {
@@ -523,13 +563,21 @@ async function streamResponse({
break;
}
const responseChoice = part.choices?.[0]?.delta;
// console.log(responseChoice, '---===');
const [reasoningContent, content] = parsePart(part, aiChatReasoning);
answer += content;
reasoning += reasoningContent;
if (responseChoice?.content) {
const content = responseChoice?.content || '';
textAnswer += content;
if (aiChatReasoning && reasoningContent) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
reasoning_content: reasoningContent
})
});
}
if (content) {
if (startResponseWrite) {
workflowStreamResponse?.({
write,
@@ -538,18 +586,20 @@ async function streamResponse({
text: content
})
});
} else if (textAnswer.length >= 3) {
textAnswer = textAnswer.trim();
if (textAnswer.startsWith('0')) {
} else if (answer.length >= 3) {
answer = answer.trimStart();
if (/0(:|)/.test(answer)) {
startResponseWrite = true;
// find first : index
const firstIndex = textAnswer.indexOf(':');
textAnswer = textAnswer.substring(firstIndex + 1).trim();
const firstIndex =
answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0');
answer = answer.substring(firstIndex + 2).trim();
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: textAnswer
text: answer
})
});
}
@@ -557,7 +607,23 @@ async function streamResponse({
}
}
return { answer: textAnswer.trim() };
if (answer === '') {
answer = getStartTagBuffer();
if (/0(:|)/.test(answer)) {
// find first : index
const firstIndex = answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0');
answer = answer.substring(firstIndex + 2).trim();
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: answer
})
});
}
}
return { answer, reasoning };
}
const parseAnswer = (
@@ -568,8 +634,7 @@ const parseAnswer = (
} => {
str = str.trim();
// 首先使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS
const prefixReg = /^1(:|)/;
const answerPrefixReg = /^0(:|)/;
const prefixReg = /1(:|)/;
if (prefixReg.test(str)) {
const toolString = sliceJsonStr(str);
@@ -585,13 +650,21 @@ const parseAnswer = (
}
};
} catch (error) {
return {
answer: ERROR_TEXT
};
if (/^1(:|)/.test(str)) {
return {
answer: ERROR_TEXT
};
} else {
return {
answer: str
};
}
}
} else {
const firstIndex = str.indexOf('0:') !== -1 ? str.indexOf('0:') : str.indexOf('0');
const answer = str.substring(firstIndex + 2).trim();
return {
answer: str.replace(answerPrefixReg, '')
answer
};
}
};

View File

@@ -22,6 +22,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.aiChatTemperature]: number;
[NodeInputKeyEnum.aiChatMaxToken]: number;
[NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.aiChatReasoning]?: boolean;
[NodeInputKeyEnum.aiChatTopP]?: number;
[NodeInputKeyEnum.aiChatStopSign]?: string;
[NodeInputKeyEnum.aiChatResponseFormat]?: string;

View File

@@ -563,6 +563,15 @@ async function streamResponse({
// if answer is empty, try to get value from startTagBuffer. (Cause: The response content is too short to exceed the minimum parse length)
if (answer === '') {
answer = getStartTagBuffer();
if (isResponseAnswerText && answer) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: answer
})
});
}
}
return { answer, reasoning };

View File

@@ -27,7 +27,9 @@ parentPort?.on('message', async (props: ReadRawTextProps<Uint8Array>) => {
case 'csv':
return readCsvRawText(params);
default:
return Promise.reject('Only support .txt, .md, .html, .pdf, .docx, pptx, .csv, .xlsx');
return Promise.reject(
`Only support .txt, .md, .html, .pdf, .docx, pptx, .csv, .xlsx. "${params.extension}" is not supported.`
);
}
};

View File

@@ -139,14 +139,14 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
</Td>
<Td>
<MyNumberInput
defaultValue={item.priority || 0}
min={0}
defaultValue={item.priority || 1}
min={1}
max={100}
h={'32px'}
w={'80px'}
onBlur={(e) => {
const val = (() => {
if (!e) return 0;
if (!e) return 1;
return e;
})();
updateChannel({

View File

@@ -130,7 +130,8 @@ export const postCreateChannel = (data: CreateChannelProps) =>
base_url: data.base_url,
models: data.models,
model_mapping: data.model_mapping,
key: data.key
key: data.key,
priority: 1
});
export const putChannelStatus = (id: number, status: ChannelStatusEnum) =>
@@ -146,7 +147,7 @@ export const putChannel = (data: ChannelInfoType) =>
model_mapping: data.model_mapping,
key: data.key,
status: data.status,
priority: data.priority
priority: data.priority ? Math.max(data.priority, 1) : undefined
});
export const deleteChannel = (id: number) => DELETE(`/channel/${id}`);