mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 07:31:19 +00:00
Test completion v2 (#4438)
* add v2 completions (#4364) * add v2 completions * completion config * config version * fix * frontend * doc * fix * fix: completions v2 api --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -18,12 +18,14 @@ weight: 852
|
||||
{{% alert icon="🤖 " context="success" %}}
|
||||
* 该接口的 API Key 需使用`应用特定的 key`,否则会报错。
|
||||
|
||||
* 对话现在有`v1`和`v2`两个接口,可以按需使用,v2 自 4.9.4 版本新增,v1 接口同时不再维护
|
||||
|
||||
* 有些包调用时,`BaseUrl`需要添加`v1`路径,有些不需要,如果出现404情况,可补充`v1`重试。
|
||||
{{% /alert %}}
|
||||
|
||||
## 请求简易应用和工作流
|
||||
|
||||
对话接口兼容`GPT`的接口!如果你的项目使用的是标准的`GPT`官方接口,可以直接通过修改`BaseUrl`和 `Authorization`来访问 FastGpt 应用,不过需要注意下面几个规则:
|
||||
`v1`对话接口兼容`GPT`的接口!如果你的项目使用的是标准的`GPT`官方接口,可以直接通过修改`BaseUrl`和 `Authorization`来访问 FastGpt 应用,不过需要注意下面几个规则:
|
||||
|
||||
{{% alert icon="🤖 " context="success" %}}
|
||||
* 传入的`model`,`temperature`等参数字段均无效,这些字段由编排决定,不会根据 API 参数改变。
|
||||
@@ -32,6 +34,100 @@ weight: 852
|
||||
|
||||
### 请求
|
||||
|
||||
#### v2
|
||||
|
||||
v1,v2 接口请求参数一致,仅请求地址不一样。
|
||||
|
||||
{{< tabs tabTotal="3" >}}
|
||||
{{< tab tabName="基础请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/v2/chat/completions' \
|
||||
--header 'Authorization: fastgpt-xxxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"chatId": "my_chatId",
|
||||
"stream": false,
|
||||
"detail": false,
|
||||
"responseChatItemId": "my_responseChatItemId",
|
||||
"variables": {
|
||||
"uid": "asdfadsfasfd2323",
|
||||
"name": "张三"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你是谁"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="图片/文件请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
* 仅`messages`有部分区别,其他参数一致。
|
||||
* 目前不支持上传文件,需上传到自己的对象存储中,获取对应的文件链接。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/v2/chat/completions' \
|
||||
--header 'Authorization: Bearer fastgpt-xxxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"chatId": "abcd",
|
||||
"stream": false,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "导演是谁"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "图片链接"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file_url",
|
||||
"name": "文件名",
|
||||
"url": "文档链接,支持 txt md html word pdf ppt csv excel"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="参数说明" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
{{% alert context="info" %}}
|
||||
- headers.Authorization: Bearer {{apikey}}
|
||||
- chatId: string | undefined 。
|
||||
- 为 `undefined` 时(不传入),不使用 FastGpt 提供的上下文功能,完全通过传入的 messages 构建上下文。
|
||||
- 为`非空字符串`时,意味着使用 chatId 进行对话,自动从 FastGpt 数据库取历史记录,并使用 messages 数组最后一个内容作为用户问题,其余 message 会被忽略。请自行确保 chatId 唯一,长度小于250,通常可以是自己系统的对话框ID。
|
||||
- messages: 结构与 [GPT接口](https://platform.openai.com/docs/api-reference/chat/object) chat模式一致。
|
||||
- responseChatItemId: string | undefined 。如果传入,则会将该值作为本次对话的响应消息的 ID,FastGPT 会自动将该 ID 存入数据库。请确保,在当前`chatId`下,`responseChatItemId`是唯一的。
|
||||
- detail: 是否返回中间值(模块状态,响应的完整结果等),`stream模式`下会通过`event`进行区分,`非stream模式`结果保存在`responseData`中。
|
||||
- variables: 模块变量,一个对象,会替换模块中,输入框内容里的`{{key}}`
|
||||
{{% /alert %}}
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
#### v1
|
||||
|
||||
{{< tabs tabTotal="3" >}}
|
||||
{{< tab tabName="基础请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
@@ -65,7 +161,7 @@ curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
{{< markdownify >}}
|
||||
|
||||
* 仅`messages`有部分区别,其他参数一致。
|
||||
* 目前不支持上次文件,需上传到自己的对象存储中,获取对应的文件链接。
|
||||
* 目前不支持上传文件,需上传到自己的对象存储中,获取对应的文件链接。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
@@ -116,14 +212,188 @@ curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
- variables: 模块变量,一个对象,会替换模块中,输入框内容里的`{{key}}`
|
||||
{{% /alert %}}
|
||||
|
||||
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### 响应
|
||||
|
||||
#### v2
|
||||
|
||||
v2 接口比起 v1,主要变变化在于:会在每个节点运行结束后及时返回 response,而不是等工作流结束后再统一返回。
|
||||
|
||||
{{< tabs tabTotal="5" >}}
|
||||
{{< tab tabName="detail=false,stream=false 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "",
|
||||
"model": "",
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 1
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "我是一个人工智能助手,旨在回答问题和提供信息。如果你有任何问题或者需要帮助,随时问我!"
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="detail=false,stream=true 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
|
||||
```bash
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"你好"},"index":0,"finish_reason":null}]}
|
||||
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"!"},"index":0,"finish_reason":null}]}
|
||||
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"今天"},"index":0,"finish_reason":null}]}
|
||||
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"过得怎么样?"},"index":0,"finish_reason":null}]}
|
||||
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":null},"index":0,"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="detail=true,stream=false 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```json
|
||||
{
|
||||
"responseData": [
|
||||
{
|
||||
"id": "iSol79OFrBH1I9kC",
|
||||
"nodeId": "448745",
|
||||
"moduleName": "common:core.module.template.work_start",
|
||||
"moduleType": "workflowStart",
|
||||
"runningTime": 0
|
||||
},
|
||||
{
|
||||
"id": "t1T94WCy6Su3BK4V",
|
||||
"nodeId": "fjLpE3XPegmoGtbU",
|
||||
"moduleName": "AI 对话",
|
||||
"moduleType": "chatNode",
|
||||
"runningTime": 1.46,
|
||||
"totalPoints": 0,
|
||||
"model": "GPT-4o-mini",
|
||||
"tokens": 64,
|
||||
"inputTokens": 10,
|
||||
"outputTokens": 54,
|
||||
"query": "你是谁",
|
||||
"reasoningText": "",
|
||||
"historyPreview": [
|
||||
{
|
||||
"obj": "Human",
|
||||
"value": "你是谁"
|
||||
},
|
||||
{
|
||||
"obj": "AI",
|
||||
"value": "我是一个人工智能助手,旨在帮助回答问题和提供信息。如果你有任何问题或需要帮助,请告诉我!"
|
||||
}
|
||||
],
|
||||
"contextTotalLen": 2
|
||||
}
|
||||
],
|
||||
"newVariables": {
|
||||
|
||||
},
|
||||
"id": "",
|
||||
"model": "",
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 1
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "我是一个人工智能助手,旨在帮助回答问题和提供信息。如果你有任何问题或需要帮助,请告诉我!"
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
|
||||
{{< tab tabName="detail=true,stream=true 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
event: flowNodeResponse
|
||||
data: {"id":"iYv2uA9rCWAtulWo","nodeId":"workflowStartNodeId","moduleName":"流程开始","moduleType":"workflowStart","runningTime":0}
|
||||
|
||||
event: flowNodeStatus
|
||||
data: {"status":"running","name":"AI 对话"}
|
||||
|
||||
event: answer
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"你好"},"index":0,"finish_reason":null}]}
|
||||
|
||||
event: answer
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"!"},"index":0,"finish_reason":null}]}
|
||||
|
||||
event: answer
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"今天"},"index":0,"finish_reason":null}]}
|
||||
|
||||
event: answer
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":"过得怎么样?"},"index":0,"finish_reason":null}]}
|
||||
|
||||
event: flowNodeResponse
|
||||
data: {"id":"pVzLBF7M3Ol4n7s6","nodeId":"ixe20AHN3jy74pKf","moduleName":"AI 对话","moduleType":"chatNode","runningTime":1.48,"totalPoints":0.0042,"model":"Qwen-plus","tokens":28,"inputTokens":8,"outputTokens":20,"query":"你好","reasoningText":"","historyPreview":[{"obj":"Human","value":"你好"},{"obj":"AI","value":"你好!今天过得怎么样?"}],"contextTotalLen":2}
|
||||
|
||||
event: answer
|
||||
data: {"id":"","object":"","created":0,"model":"","choices":[{"delta":{"role":"assistant","content":null},"index":0,"finish_reason":"stop"}]}
|
||||
|
||||
event: answer
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="event值" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
event取值:
|
||||
|
||||
- answer: 返回给客户端的文本(最终会算作回答)
|
||||
- fastAnswer: 指定回复返回给客户端的文本(最终会算作回答)
|
||||
- toolCall: 执行工具
|
||||
- toolParams: 工具参数
|
||||
- toolResponse: 工具返回
|
||||
- flowNodeStatus: 运行到的节点状态
|
||||
- flowNodeResponse: 单个节点详细响应
|
||||
- updateVariables: 更新变量
|
||||
- error: 报错
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
#### v1
|
||||
|
||||
{{< tabs tabTotal="5" >}}
|
||||
{{< tab tabName="detail=false,stream=false 响应" >}}
|
||||
{{< markdownify >}}
|
||||
@@ -475,6 +745,8 @@ curl --location --request POST 'https://api.fastgpt.in/api/v1/chat/completions'
|
||||
|
||||
### 请求示例
|
||||
|
||||
#### v1
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
--header 'Authorization: Bearer test-xxxxx' \
|
||||
@@ -488,8 +760,25 @@ curl --location --request POST 'http://localhost:3000/api/v1/chat/completions' \
|
||||
}'
|
||||
```
|
||||
|
||||
#### v2
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/v2/chat/completions' \
|
||||
--header 'Authorization: Bearer test-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"stream": false,
|
||||
"chatId": "test",
|
||||
"variables": {
|
||||
"query":"你好"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
#### v1
|
||||
|
||||
{{< tabs tabTotal="3" >}}
|
||||
|
||||
{{< tab tabName="detail=true,stream=false 响应" >}}
|
||||
@@ -649,6 +938,149 @@ event取值:
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
#### v2
|
||||
|
||||
{{< tabs tabTotal="3" >}}
|
||||
|
||||
{{< tab tabName="detail=true,stream=false 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
* 插件的输出可以通过查找`responseData`中, `moduleType=pluginOutput`的元素,其`pluginOutput`是插件的输出。
|
||||
* 流输出,仍可以通过`choices`进行获取。
|
||||
|
||||
```json
|
||||
{
|
||||
"responseData": [
|
||||
{
|
||||
"id": "bsH1ZdbYkz9iJwYa",
|
||||
"nodeId": "pluginInput",
|
||||
"moduleName": "workflow:template.plugin_start",
|
||||
"moduleType": "pluginInput",
|
||||
"runningTime": 0
|
||||
},
|
||||
{
|
||||
"id": "zDgfqSPhbYZFHVIn",
|
||||
"nodeId": "h4Gr4lJtFVQ6qI4c",
|
||||
"moduleName": "AI 对话",
|
||||
"moduleType": "chatNode",
|
||||
"runningTime": 1.44,
|
||||
"totalPoints": 0,
|
||||
"model": "GPT-4o-mini",
|
||||
"tokens": 34,
|
||||
"inputTokens": 8,
|
||||
"outputTokens": 26,
|
||||
"query": "你好",
|
||||
"reasoningText": "",
|
||||
"historyPreview": [
|
||||
{
|
||||
"obj": "Human",
|
||||
"value": "你好"
|
||||
},
|
||||
{
|
||||
"obj": "AI",
|
||||
"value": "你好!有什么我可以帮助你的吗?"
|
||||
}
|
||||
],
|
||||
"contextTotalLen": 2
|
||||
},
|
||||
{
|
||||
"id": "uLLwKKRZvufXzgF4",
|
||||
"nodeId": "pluginOutput",
|
||||
"moduleName": "common:core.module.template.self_output",
|
||||
"moduleType": "pluginOutput",
|
||||
"runningTime": 0,
|
||||
"totalPoints": 0,
|
||||
"pluginOutput": {
|
||||
"result": "你好!有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"newVariables": {
|
||||
|
||||
},
|
||||
"id": "test",
|
||||
"model": "",
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 1
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!有什么我可以帮助你的吗?"
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
|
||||
{{< tab tabName="detail=true,stream=true 响应" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
* 插件的输出可以通过获取`event=flowResponses`中的字符串,并将其反序列化后得到一个数组。同样的,查找 `moduleType=pluginOutput`的元素,其`pluginOutput`是插件的输出。
|
||||
* 流输出,仍和对话接口一样获取。
|
||||
|
||||
```bash
|
||||
data: {"event":"flowNodeResponse","data":"{\"id\":\"q8ablUOqHGgqLIRM\",\"nodeId\":\"pluginInput\",\"moduleName\":\"workflow:template.plugin_start\",\"moduleType\":\"pluginInput\",\"runningTime\":0}"}
|
||||
|
||||
data: {"event":"flowNodeStatus","data":"{\"status\":\"running\",\"name\":\"AI 对话\"}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"你好\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"!\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"有什么\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"我\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"可以\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"帮助\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"你\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"的吗\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"?\"},\"index\":0,\"finish_reason\":null}]}"}
|
||||
|
||||
data: {"event":"flowNodeResponse","data":"{\"id\":\"rqlXLUap8QeiN7Kf\",\"nodeId\":\"h4Gr4lJtFVQ6qI4c\",\"moduleName\":\"AI 对话\",\"moduleType\":\"chatNode\",\"runningTime\":1.79,\"totalPoints\":0,\"model\":\"GPT-4o-mini\",\"tokens\":137,\"inputTokens\":111,\"outputTokens\":26,\"query\":\"你好\",\"reasoningText\":\"\",\"historyPreview\":[{\"obj\":\"Human\",\"value\":\"[{\\\"renderTypeList\\\":[\\\"reference\\\"],\\\"selectedTypeInde\\n\\n...[hide 174 chars]...\\n\\ncanSelectImg\\\":true,\\\"required\\\":false,\\\"value\\\":\\\"你好\\\"}]\"},{\"obj\":\"AI\",\"value\":\"你好!有什么我可以帮助你的吗?\"},{\"obj\":\"Human\",\"value\":\"你好\"},{\"obj\":\"AI\",\"value\":\"你好!有什么我可以帮助你的吗?\"}],\"contextTotalLen\":4}"}
|
||||
|
||||
data: {"event":"flowNodeResponse","data":"{\"id\":\"lHCpHI0MrM00HQlX\",\"nodeId\":\"pluginOutput\",\"moduleName\":\"common:core.module.template.self_output\",\"moduleType\":\"pluginOutput\",\"runningTime\":0,\"totalPoints\":0,\"pluginOutput\":{\"result\":\"你好!有什么我可以帮助你的吗?\"}}"}
|
||||
|
||||
data: {"event":"answer","data":"{\"id\":\"\",\"object\":\"\",\"created\":0,\"model\":\"\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":null},\"index\":0,\"finish_reason\":\"stop\"}]}"}
|
||||
|
||||
data: {"event":"answer","data":"[DONE]"}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="输出获取" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
event取值:
|
||||
|
||||
- answer: 返回给客户端的文本(最终会算作回答)
|
||||
- fastAnswer: 指定回复返回给客户端的文本(最终会算作回答)
|
||||
- toolCall: 执行工具
|
||||
- toolParams: 工具参数
|
||||
- toolResponse: 工具返回
|
||||
- flowNodeStatus: 运行到的节点状态
|
||||
- flowNodeResponse: 单个节点详细响应
|
||||
- updateVariables: 更新变量
|
||||
- error: 报错
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
# 对话 CRUD
|
||||
|
||||
|
@@ -5,6 +5,7 @@ export enum SseResponseEventEnum {
|
||||
answer = 'answer', // animation stream
|
||||
fastAnswer = 'fastAnswer', // direct answer text, not animation
|
||||
flowNodeStatus = 'flowNodeStatus', // update node status
|
||||
flowNodeResponse = 'flowNodeResponse', // node response
|
||||
|
||||
toolCall = 'toolCall', // tool start
|
||||
toolParams = 'toolParams', // tool params return
|
||||
|
@@ -59,6 +59,7 @@ export type ChatDispatchProps = {
|
||||
isToolCall?: boolean;
|
||||
workflowStreamResponse?: WorkflowResponseType;
|
||||
workflowDispatchDeep?: number;
|
||||
version?: 'v1' | 'v2';
|
||||
};
|
||||
|
||||
export type ModuleDispatchProps<T> = ChatDispatchProps & {
|
||||
|
@@ -130,6 +130,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
timezone,
|
||||
externalProvider,
|
||||
stream = false,
|
||||
version = 'v1',
|
||||
...props
|
||||
} = data;
|
||||
|
||||
@@ -626,6 +627,21 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
};
|
||||
})();
|
||||
|
||||
// Response node response
|
||||
if (
|
||||
version === 'v2' &&
|
||||
!props.isToolCall &&
|
||||
!props.runningAppInfo.isChildApp &&
|
||||
formatResponseData
|
||||
) {
|
||||
props.workflowStreamResponse?.({
|
||||
event: SseResponseEventEnum.flowNodeResponse,
|
||||
data: {
|
||||
...formatResponseData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add output default value
|
||||
node.outputs.forEach((item) => {
|
||||
if (!item.required) return;
|
||||
|
@@ -53,7 +53,8 @@ export const getWorkflowResponseWrite = ({
|
||||
[SseResponseEventEnum.toolCall]: 1,
|
||||
[SseResponseEventEnum.toolParams]: 1,
|
||||
[SseResponseEventEnum.toolResponse]: 1,
|
||||
[SseResponseEventEnum.updateVariables]: 1
|
||||
[SseResponseEventEnum.updateVariables]: 1,
|
||||
[SseResponseEventEnum.flowNodeResponse]: 1
|
||||
};
|
||||
if (!detail && detailEvent[event]) return;
|
||||
|
||||
|
@@ -219,7 +219,8 @@ const ChatBox = ({
|
||||
tool,
|
||||
interactive,
|
||||
autoTTSResponse,
|
||||
variables
|
||||
variables,
|
||||
nodeResponse
|
||||
}: generatingMessageProps & { autoTTSResponse?: boolean }) => {
|
||||
setChatRecords((state) =>
|
||||
state.map((item, index) => {
|
||||
@@ -232,7 +233,14 @@ const ChatBox = ({
|
||||
JSON.stringify(item.value[item.value.length - 1])
|
||||
);
|
||||
|
||||
if (event === SseResponseEventEnum.flowNodeStatus && status) {
|
||||
if (event === SseResponseEventEnum.flowNodeResponse && nodeResponse) {
|
||||
return {
|
||||
...item,
|
||||
responseData: item.responseData
|
||||
? [...item.responseData, nodeResponse]
|
||||
: [nodeResponse]
|
||||
};
|
||||
} else if (event === SseResponseEventEnum.flowNodeStatus && status) {
|
||||
return {
|
||||
...item,
|
||||
status,
|
||||
@@ -518,36 +526,34 @@ const ChatBox = ({
|
||||
reserveTool: true
|
||||
});
|
||||
|
||||
const {
|
||||
responseData,
|
||||
responseText,
|
||||
isNewChat = false
|
||||
} = await onStartChat({
|
||||
const { responseText } = await onStartChat({
|
||||
messages, // 保证最后一条是 Human 的消息
|
||||
responseChatItemId: responseChatId,
|
||||
controller: abortSignal,
|
||||
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
|
||||
variables: requestVariables
|
||||
});
|
||||
if (responseData?.[responseData.length - 1]?.error) {
|
||||
toast({
|
||||
title: t(responseData[responseData.length - 1].error?.message),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Set last chat finish status
|
||||
let newChatHistories: ChatSiteItemType[] = [];
|
||||
setChatRecords((state) => {
|
||||
newChatHistories = state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
|
||||
// Check node response error
|
||||
const responseData = mergeChatResponseData(item.responseData || []);
|
||||
if (responseData[responseData.length - 1]?.error) {
|
||||
toast({
|
||||
title: t(responseData[responseData.length - 1].error?.message),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: ChatStatusEnum.finish,
|
||||
time: new Date(),
|
||||
responseData: item.responseData
|
||||
? mergeChatResponseData([...item.responseData, ...responseData])
|
||||
: responseData
|
||||
responseData
|
||||
};
|
||||
});
|
||||
return newChatHistories;
|
||||
@@ -567,7 +573,7 @@ const ChatBox = ({
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
toast({
|
||||
title: t(getErrText(err, 'core.chat.error.Chat error') as any),
|
||||
title: t(getErrText(err, t('common:core.chat.error.Chat error') as any)),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { StreamResponseType } from '@/web/common/api/fetch';
|
||||
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
|
||||
import {
|
||||
ChatHistoryItemResType,
|
||||
ChatSiteItemType,
|
||||
ToolModuleResponseItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
|
||||
export type generatingMessageProps = {
|
||||
@@ -12,6 +16,7 @@ export type generatingMessageProps = {
|
||||
tool?: ToolModuleResponseItemType;
|
||||
interactive?: WorkflowInteractiveResponseType;
|
||||
variables?: Record<string, any>;
|
||||
nodeResponse?: ChatHistoryItemResType;
|
||||
};
|
||||
|
||||
export type StartChatFnProps = {
|
||||
|
@@ -182,7 +182,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
histories: newHistories,
|
||||
stream: true,
|
||||
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
|
||||
workflowStreamResponse: workflowResponseWrite
|
||||
workflowStreamResponse: workflowResponseWrite,
|
||||
version: 'v2'
|
||||
});
|
||||
|
||||
workflowResponseWrite({
|
||||
@@ -197,11 +198,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
responseWrite({
|
||||
res,
|
||||
event: SseResponseEventEnum.flowResponses,
|
||||
data: JSON.stringify(flowResponses)
|
||||
});
|
||||
|
||||
// save chat
|
||||
const isInteractiveRequest = !!getLastInteractiveValue(histories);
|
||||
|
641
projects/app/src/pages/api/v2/chat/completions.ts
Normal file
641
projects/app/src/pages/api/v2/chat/completions.ts
Normal file
@@ -0,0 +1,641 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
|
||||
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';
|
||||
import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse,
|
||||
getLastInteractiveValue
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
|
||||
import { getChatItems } from '@fastgpt/service/core/chat/controller';
|
||||
import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat';
|
||||
import { responseWrite } from '@fastgpt/service/common/response';
|
||||
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
|
||||
import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
|
||||
import requestIp from 'request-ip';
|
||||
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
|
||||
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
|
||||
import {
|
||||
concatHistories,
|
||||
filterPublicNodeResponseData,
|
||||
getChatTitleFromChatMessage,
|
||||
removeEmptyUserInput
|
||||
} from '@fastgpt/global/core/chat/utils';
|
||||
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
|
||||
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
|
||||
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { AuthOutLinkChatProps } from '@fastgpt/global/support/outLink/api';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import {
|
||||
getPluginRunUserQuery,
|
||||
updatePluginInputByVariables
|
||||
} from '@fastgpt/global/core/workflow/utils';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
|
||||
import { rewriteNodeOutputByHistories } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
|
||||
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
|
||||
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
|
||||
import { ExternalProviderType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
|
||||
appId?: string;
|
||||
customUid?: string; // non-undefined: will be the priority provider for the logger.
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type Props = ChatCompletionCreateParams &
|
||||
FastGptWebChatProps &
|
||||
OutLinkChatAuthProps & {
|
||||
messages: ChatCompletionMessageParam[];
|
||||
responseChatItemId?: string;
|
||||
stream?: boolean;
|
||||
detail?: boolean;
|
||||
variables: Record<string, any>; // Global variables or plugin inputs
|
||||
};
|
||||
|
||||
type AuthResponseType = {
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
timezone: string;
|
||||
externalProvider: ExternalProviderType;
|
||||
app: AppSchema;
|
||||
responseDetail?: boolean;
|
||||
showNodeStatus?: boolean;
|
||||
authType: `${AuthUserTypeEnum}`;
|
||||
apikey?: string;
|
||||
responseAllData: boolean;
|
||||
outLinkUserId?: string;
|
||||
sourceName?: string;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.on('close', () => {
|
||||
res.end();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
res.end();
|
||||
});
|
||||
|
||||
let {
|
||||
chatId,
|
||||
appId,
|
||||
customUid,
|
||||
// share chat
|
||||
shareId,
|
||||
outLinkUid,
|
||||
// team chat
|
||||
teamId: spaceTeamId,
|
||||
teamToken,
|
||||
|
||||
stream = false,
|
||||
detail = false,
|
||||
messages = [],
|
||||
variables = {},
|
||||
responseChatItemId = getNanoid(),
|
||||
metadata
|
||||
} = req.body as Props;
|
||||
|
||||
const originIp = requestIp.getClientIp(req);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
if (!Array.isArray(messages)) {
|
||||
throw new Error('messages is not array');
|
||||
}
|
||||
|
||||
/*
|
||||
Web params: chatId + [Human]
|
||||
API params: chatId + [Human]
|
||||
API params: [histories, Human]
|
||||
*/
|
||||
const chatMessages = GPTMessages2Chats(messages);
|
||||
|
||||
// Computed start hook params
|
||||
const startHookText = (() => {
|
||||
// Chat
|
||||
const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType | undefined;
|
||||
if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text;
|
||||
|
||||
// plugin
|
||||
return JSON.stringify(variables);
|
||||
})();
|
||||
|
||||
/*
|
||||
1. auth app permission
|
||||
2. auth balance
|
||||
3. get app
|
||||
4. parse outLink token
|
||||
*/
|
||||
const {
|
||||
teamId,
|
||||
tmbId,
|
||||
timezone,
|
||||
externalProvider,
|
||||
app,
|
||||
responseDetail,
|
||||
authType,
|
||||
sourceName,
|
||||
apikey,
|
||||
responseAllData,
|
||||
outLinkUserId = customUid,
|
||||
showNodeStatus
|
||||
} = await (async () => {
|
||||
// share chat
|
||||
if (shareId && outLinkUid) {
|
||||
return authShareChat({
|
||||
shareId,
|
||||
outLinkUid,
|
||||
chatId,
|
||||
ip: originIp,
|
||||
question: startHookText
|
||||
});
|
||||
}
|
||||
// team space chat
|
||||
if (spaceTeamId && appId && teamToken) {
|
||||
return authTeamSpaceChat({
|
||||
teamId: spaceTeamId,
|
||||
teamToken,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
}
|
||||
|
||||
/* parse req: api or token */
|
||||
return authHeaderRequest({
|
||||
req,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
})();
|
||||
const isPlugin = app.type === AppTypeEnum.plugin;
|
||||
|
||||
// Check message type
|
||||
if (isPlugin) {
|
||||
detail = true;
|
||||
} else {
|
||||
if (messages.length === 0) {
|
||||
throw new Error('messages is empty');
|
||||
}
|
||||
}
|
||||
|
||||
// Get obj=Human history
|
||||
const userQuestion: UserChatItemType = (() => {
|
||||
if (isPlugin) {
|
||||
return getPluginRunUserQuery({
|
||||
pluginInputs: getPluginInputsFromStoreNodes(app.modules),
|
||||
variables,
|
||||
files: variables.files
|
||||
});
|
||||
}
|
||||
|
||||
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;
|
||||
if (!latestHumanChat) {
|
||||
throw new Error('User question is empty');
|
||||
}
|
||||
return latestHumanChat;
|
||||
})();
|
||||
|
||||
// Get and concat history;
|
||||
const limit = getMaxHistoryLimitFromNodes(app.modules);
|
||||
const [{ histories }, { nodes, edges, chatConfig }, chatDetail] = await Promise.all([
|
||||
getChatItems({
|
||||
appId: app._id,
|
||||
chatId,
|
||||
offset: 0,
|
||||
limit,
|
||||
field: `dataId obj value nodeOutputs`
|
||||
}),
|
||||
getAppLatestVersion(app._id, app),
|
||||
MongoChat.findOne({ appId: app._id, chatId }, 'source variableList variables')
|
||||
]);
|
||||
|
||||
// Get store variables(Api variable precedence)
|
||||
if (chatDetail?.variables) {
|
||||
variables = {
|
||||
...chatDetail.variables,
|
||||
...variables
|
||||
};
|
||||
}
|
||||
|
||||
// Get chat histories
|
||||
const newHistories = concatHistories(histories, chatMessages);
|
||||
|
||||
// Get runtimeNodes
|
||||
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories));
|
||||
if (isPlugin) {
|
||||
// Assign values to runtimeNodes using variables
|
||||
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
|
||||
// Plugin runtime does not need global variables(It has been injected into the pluginInputNode)
|
||||
variables = {};
|
||||
}
|
||||
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes);
|
||||
|
||||
const workflowResponseWrite = getWorkflowResponseWrite({
|
||||
res,
|
||||
detail,
|
||||
streamResponse: stream,
|
||||
id: chatId,
|
||||
showNodeStatus
|
||||
});
|
||||
|
||||
/* start flow controller */
|
||||
const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => {
|
||||
if (app.version === 'v2') {
|
||||
return dispatchWorkFlow({
|
||||
res,
|
||||
requestOrigin: req.headers.origin,
|
||||
mode: 'chat',
|
||||
timezone,
|
||||
externalProvider,
|
||||
|
||||
runningAppInfo: {
|
||||
id: String(app._id),
|
||||
teamId: String(app.teamId),
|
||||
tmbId: String(app.tmbId)
|
||||
},
|
||||
runningUserInfo: {
|
||||
teamId,
|
||||
tmbId
|
||||
},
|
||||
uid: String(outLinkUserId || tmbId),
|
||||
|
||||
chatId,
|
||||
responseChatItemId,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
chatConfig,
|
||||
histories: newHistories,
|
||||
stream,
|
||||
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
|
||||
workflowStreamResponse: workflowResponseWrite,
|
||||
version: 'v2'
|
||||
});
|
||||
}
|
||||
return Promise.reject('您的工作流版本过低,请重新发布一次');
|
||||
})();
|
||||
|
||||
// save chat
|
||||
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
|
||||
const source = (() => {
|
||||
if (shareId) {
|
||||
return ChatSourceEnum.share;
|
||||
}
|
||||
if (authType === 'apikey') {
|
||||
return ChatSourceEnum.api;
|
||||
}
|
||||
if (spaceTeamId) {
|
||||
return ChatSourceEnum.team;
|
||||
}
|
||||
return ChatSourceEnum.online;
|
||||
})();
|
||||
|
||||
const isInteractiveRequest = !!getLastInteractiveValue(histories);
|
||||
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userQuestion.value);
|
||||
|
||||
const newTitle = isPlugin
|
||||
? variables.cTime ?? getSystemTime(timezone)
|
||||
: getChatTitleFromChatMessage(userQuestion);
|
||||
|
||||
const aiResponse: AIChatItemType & { dataId?: string } = {
|
||||
dataId: responseChatItemId,
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: assistantResponses,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses
|
||||
};
|
||||
|
||||
const saveChatId = chatId || getNanoid(24);
|
||||
if (isInteractiveRequest) {
|
||||
await updateInteractiveChat({
|
||||
chatId: saveChatId,
|
||||
appId: app._id,
|
||||
userInteractiveVal,
|
||||
aiResponse,
|
||||
newVariables
|
||||
});
|
||||
} else {
|
||||
await saveChat({
|
||||
chatId: saveChatId,
|
||||
appId: app._id,
|
||||
teamId,
|
||||
tmbId: tmbId,
|
||||
nodes,
|
||||
appChatConfig: chatConfig,
|
||||
variables: newVariables,
|
||||
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
|
||||
newTitle,
|
||||
shareId,
|
||||
outLinkUid: outLinkUserId,
|
||||
source,
|
||||
sourceName: sourceName || '',
|
||||
content: [userQuestion, aiResponse],
|
||||
metadata: {
|
||||
originIp,
|
||||
...metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
/* select fe response field */
|
||||
const feResponseData = responseAllData
|
||||
? flowResponses
|
||||
: filterPublicNodeResponseData({ flowResponses, responseDetail });
|
||||
|
||||
if (stream) {
|
||||
workflowResponseWrite({
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text: null,
|
||||
finish_reason: 'stop'
|
||||
})
|
||||
});
|
||||
responseWrite({
|
||||
res,
|
||||
event: detail ? SseResponseEventEnum.answer : undefined,
|
||||
data: '[DONE]'
|
||||
});
|
||||
|
||||
res.end();
|
||||
} else {
|
||||
const responseContent = (() => {
|
||||
if (assistantResponses.length === 0) return '';
|
||||
if (assistantResponses.length === 1 && assistantResponses[0].text?.content)
|
||||
return assistantResponses[0].text?.content;
|
||||
|
||||
if (!detail) {
|
||||
return assistantResponses
|
||||
.map((item) => item?.text?.content)
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return assistantResponses;
|
||||
})();
|
||||
const error = flowResponses[flowResponses.length - 1]?.error;
|
||||
|
||||
res.json({
|
||||
...(detail ? { responseData: feResponseData, newVariables } : {}),
|
||||
error,
|
||||
id: chatId || '',
|
||||
model: '',
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 },
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: responseContent },
|
||||
finish_reason: 'stop',
|
||||
index: 0
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// add record
|
||||
const { totalPoints } = createChatUsage({
|
||||
appName: app.name,
|
||||
appId: app._id,
|
||||
teamId,
|
||||
tmbId: tmbId,
|
||||
source: getUsageSourceByAuthType({ shareId, authType }),
|
||||
flowUsages
|
||||
});
|
||||
|
||||
if (shareId) {
|
||||
pushResult2Remote({ outLinkUid, shareId, appName: app.name, flowResponses });
|
||||
addOutLinkUsage({
|
||||
shareId,
|
||||
totalPoints
|
||||
});
|
||||
}
|
||||
if (apikey) {
|
||||
updateApiKeyUsage({
|
||||
apikey,
|
||||
totalPoints
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (stream) {
|
||||
sseErrRes(res, err);
|
||||
res.end();
|
||||
} else {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default NextAPI(handler);
|
||||
|
||||
const authShareChat = async ({
|
||||
chatId,
|
||||
...data
|
||||
}: AuthOutLinkChatProps & {
|
||||
shareId: string;
|
||||
chatId?: string;
|
||||
}): Promise<AuthResponseType> => {
|
||||
const {
|
||||
teamId,
|
||||
tmbId,
|
||||
timezone,
|
||||
externalProvider,
|
||||
appId,
|
||||
authType,
|
||||
responseDetail,
|
||||
showNodeStatus,
|
||||
uid,
|
||||
sourceName
|
||||
} = await authOutLinkChatStart(data);
|
||||
const app = await MongoApp.findById(appId).lean();
|
||||
|
||||
if (!app) {
|
||||
return Promise.reject('app is empty');
|
||||
}
|
||||
|
||||
// get chat
|
||||
const chat = await MongoChat.findOne({ appId, chatId }).lean();
|
||||
if (chat && (chat.shareId !== data.shareId || chat.outLinkUid !== uid)) {
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
return {
|
||||
sourceName,
|
||||
teamId,
|
||||
tmbId,
|
||||
app,
|
||||
timezone,
|
||||
externalProvider,
|
||||
apikey: '',
|
||||
authType,
|
||||
responseAllData: false,
|
||||
responseDetail,
|
||||
outLinkUserId: uid,
|
||||
showNodeStatus
|
||||
};
|
||||
};
|
||||
const authTeamSpaceChat = async ({
|
||||
appId,
|
||||
teamId,
|
||||
teamToken,
|
||||
chatId
|
||||
}: {
|
||||
appId: string;
|
||||
teamId: string;
|
||||
teamToken: string;
|
||||
chatId?: string;
|
||||
}): Promise<AuthResponseType> => {
|
||||
const { uid } = await authTeamSpaceToken({
|
||||
teamId,
|
||||
teamToken
|
||||
});
|
||||
|
||||
const app = await MongoApp.findById(appId).lean();
|
||||
if (!app) {
|
||||
return Promise.reject('app is empty');
|
||||
}
|
||||
|
||||
const [chat, { timezone, externalProvider }] = await Promise.all([
|
||||
MongoChat.findOne({ appId, chatId }).lean(),
|
||||
getUserChatInfoAndAuthTeamPoints(app.tmbId)
|
||||
]);
|
||||
|
||||
if (chat && (String(chat.teamId) !== teamId || chat.outLinkUid !== uid)) {
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId,
|
||||
tmbId: app.tmbId,
|
||||
app,
|
||||
timezone,
|
||||
externalProvider,
|
||||
authType: AuthUserTypeEnum.outLink,
|
||||
apikey: '',
|
||||
responseAllData: false,
|
||||
responseDetail: true,
|
||||
outLinkUserId: uid
|
||||
};
|
||||
};
|
||||
const authHeaderRequest = async ({
|
||||
req,
|
||||
appId,
|
||||
chatId
|
||||
}: {
|
||||
req: NextApiRequest;
|
||||
appId?: string;
|
||||
chatId?: string;
|
||||
}): Promise<AuthResponseType> => {
|
||||
const {
|
||||
appId: apiKeyAppId,
|
||||
teamId,
|
||||
tmbId,
|
||||
authType,
|
||||
sourceName,
|
||||
apikey
|
||||
} = await authCert({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
const { app } = await (async () => {
|
||||
if (authType === AuthUserTypeEnum.apikey) {
|
||||
const currentAppId = apiKeyAppId || appId;
|
||||
if (!currentAppId) {
|
||||
return Promise.reject(
|
||||
'Key is error. You need to use the app key rather than the account key.'
|
||||
);
|
||||
}
|
||||
const app = await MongoApp.findById(currentAppId);
|
||||
|
||||
if (!app) {
|
||||
return Promise.reject('app is empty');
|
||||
}
|
||||
|
||||
appId = String(app._id);
|
||||
|
||||
return {
|
||||
app
|
||||
};
|
||||
} else {
|
||||
// token_auth
|
||||
if (!appId) {
|
||||
return Promise.reject('appId is empty');
|
||||
}
|
||||
const { app } = await authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
return {
|
||||
app
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
const [{ timezone, externalProvider }, chat] = await Promise.all([
|
||||
getUserChatInfoAndAuthTeamPoints(tmbId),
|
||||
MongoChat.findOne({ appId, chatId }).lean()
|
||||
]);
|
||||
|
||||
if (
|
||||
chat &&
|
||||
(String(chat.teamId) !== teamId ||
|
||||
// There's no need to distinguish who created it if it's apiKey auth
|
||||
(authType === AuthUserTypeEnum.token && String(chat.tmbId) !== tmbId))
|
||||
) {
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId,
|
||||
tmbId,
|
||||
timezone,
|
||||
externalProvider,
|
||||
app,
|
||||
apikey,
|
||||
authType,
|
||||
sourceName,
|
||||
responseAllData: true,
|
||||
responseDetail: true
|
||||
};
|
||||
};
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '20mb'
|
||||
},
|
||||
responseLimit: '20mb'
|
||||
}
|
||||
};
|
@@ -1,8 +1,6 @@
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import {
|
||||
// refer to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
|
||||
EventStreamContentType,
|
||||
@@ -21,7 +19,6 @@ type StreamFetchProps = {
|
||||
};
|
||||
export type StreamResponseType = {
|
||||
responseText: string;
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[];
|
||||
};
|
||||
type ResponseQueueItemType =
|
||||
| {
|
||||
@@ -40,7 +37,7 @@ type ResponseQueueItemType =
|
||||
class FatalError extends Error {}
|
||||
|
||||
export const streamFetch = ({
|
||||
url = '/api/v1/chat/completions',
|
||||
url = '/api/v2/chat/completions',
|
||||
data,
|
||||
onMessage,
|
||||
abortCtrl
|
||||
@@ -55,7 +52,6 @@ export const streamFetch = ({
|
||||
let responseText = '';
|
||||
let responseQueue: ResponseQueueItemType[] = [];
|
||||
let errMsg: string | undefined;
|
||||
let responseData: ChatHistoryItemResType[] = [];
|
||||
let finished = false;
|
||||
|
||||
const finish = () => {
|
||||
@@ -63,8 +59,7 @@ export const streamFetch = ({
|
||||
return failedFinish();
|
||||
}
|
||||
return resolve({
|
||||
responseText,
|
||||
responseData
|
||||
responseText
|
||||
});
|
||||
};
|
||||
const failedFinish = (err?: any) => {
|
||||
@@ -168,7 +163,7 @@ export const streamFetch = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
onmessage({ event, data }) {
|
||||
onmessage: ({ event, data }) => {
|
||||
if (data === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
@@ -178,9 +173,12 @@ export const streamFetch = ({
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
return {};
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
if (typeof parseJson !== 'object') return;
|
||||
|
||||
// console.log(parseJson, event);
|
||||
if (event === SseResponseEventEnum.answer) {
|
||||
const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || '';
|
||||
@@ -222,8 +220,11 @@ export const streamFetch = ({
|
||||
event,
|
||||
...parseJson
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.flowResponses && Array.isArray(parseJson)) {
|
||||
responseData = parseJson;
|
||||
} else if (event === SseResponseEventEnum.flowNodeResponse) {
|
||||
onMessage({
|
||||
event,
|
||||
nodeResponse: parseJson
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.updateVariables) {
|
||||
onMessage({
|
||||
event,
|
||||
|
Reference in New Issue
Block a user