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:
Archer
2025-04-03 00:21:34 +08:00
committed by archer
parent e4c4941a50
commit 85ce23869d
10 changed files with 1140 additions and 40 deletions

View File

@@ -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 。如果传入,则会将该值作为本次对话的响应消息的 IDFastGPT 会自动将该 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

View File

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

View File

@@ -59,6 +59,7 @@ export type ChatDispatchProps = {
isToolCall?: boolean;
workflowStreamResponse?: WorkflowResponseType;
workflowDispatchDeep?: number;
version?: 'v1' | 'v2';
};
export type ModuleDispatchProps<T> = ChatDispatchProps & {

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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