From 4d20274a972f0e928833f130ae4ddb0b3c52f7d7 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Mon, 17 Feb 2025 20:57:36 +0800 Subject: [PATCH] feat: think tag parse (#3805) (#3808) * feat: think tag parse * remove some model config * feat: parse think tag test --- .../zh-cn/docs/development/upgrading/490.md | 14 + packages/global/core/ai/type.d.ts | 7 +- .../global/core/workflow/runtime/utils.ts | 135 +++++ packages/service/core/ai/config.ts | 47 +- .../service/core/ai/config/provider/PPIO.json | 461 +----------------- packages/service/core/ai/utils.ts | 25 +- .../core/workflow/dispatch/chat/oneapi.ts | 88 ++-- .../service/core/workflow/dispatch/index.ts | 17 +- .../app/src/pages/api/v1/chat/utils.test.ts | 145 ++++++ projects/app/src/web/common/api/fetch.ts | 14 +- 10 files changed, 418 insertions(+), 535 deletions(-) create mode 100644 docSite/content/zh-cn/docs/development/upgrading/490.md create mode 100644 projects/app/src/pages/api/v1/chat/utils.test.ts diff --git a/docSite/content/zh-cn/docs/development/upgrading/490.md b/docSite/content/zh-cn/docs/development/upgrading/490.md new file mode 100644 index 000000000..45821f6b6 --- /dev/null +++ b/docSite/content/zh-cn/docs/development/upgrading/490.md @@ -0,0 +1,14 @@ +--- +title: 'V4.9.0(进行中)' +description: 'FastGPT V4.9.0 更新说明' +icon: 'upgrade' +draft: false +toc: true +weight: 803 +--- + + +## 完整更新内容 + +1. 新增 - AI 对话节点解析 标签内容,便于各类模型进行思考链输出。 +2. 修复 - 思考链流输出时,有时与正文顺序偏差。 \ No newline at end of file diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index a245af7ca..1d288ec98 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -1,14 +1,12 @@ import openai from 'openai'; import type { ChatCompletionMessageToolCall, - ChatCompletionChunk, ChatCompletionMessageParam as SdkChatCompletionMessageParam, ChatCompletionToolMessageParam, ChatCompletionContentPart as SdkChatCompletionContentPart, ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam, ChatCompletionToolMessageParam as SdkChatCompletionToolMessageParam, - ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam, - ChatCompletionContentPartText + ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam } from 'openai/resources'; import { ChatMessageTypeEnum } from './constants'; import { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; @@ -71,7 +69,8 @@ export type ChatCompletionMessageFunctionCall = }; // Stream response -export type StreamChatType = Stream; +export type StreamChatType = Stream; +export type UnStreamChatType = openai.Chat.Completions.ChatCompletion; export default openai; export * from 'openai'; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 643733a7f..10e01ceae 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -10,6 +10,7 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; import { replaceVariable, valToStr } from '../../../common/string/tools'; +import { ChatCompletionChunk } from 'openai/resources'; export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => { let limit = 10; @@ -419,3 +420,137 @@ export function rewriteNodeOutputByHistories( }; }); } + +// Parse tags to think and answer - unstream response +export const parseReasoningContent = (text: string): [string, string] => { + const regex = /([\s\S]*?)<\/think>/; + const match = text.match(regex); + + if (!match) { + return ['', text]; + } + + const thinkContent = match[1].trim(); + + // Add answer (remaining text after think tag) + const answerContent = text.slice(match.index! + match[0].length); + + return [thinkContent, answerContent]; +}; + +// Parse tags to think and answer - stream response +export const parseReasoningStreamContent = () => { + let isInThinkTag: boolean | undefined; + + const startTag = ''; + let startTagBuffer = ''; + + const endTag = ''; + let endTagBuffer = ''; + + /* + parseReasoning - 只控制是否主动解析 ,如果接口已经解析了,仍然会返回 think 内容。 + */ + const parsePart = ( + part: { + choices: { + delta: { + content?: string; + reasoning_content?: string; + }; + }[]; + }, + parseReasoning = false + ): [string, string] => { + const content = part.choices?.[0]?.delta?.content || ''; + + // @ts-ignore + const reasoningContent = part.choices?.[0]?.delta?.reasoning_content || ''; + if (reasoningContent || !parseReasoning) { + isInThinkTag = false; + return [reasoningContent, content]; + } + + if (!content) { + return ['', '']; + } + + // 如果不在 think 标签中,或者有 reasoningContent(接口已解析),则返回 reasoningContent 和 content + if (isInThinkTag === false) { + return ['', content]; + } + + // 检测是否为 think 标签开头的数据 + if (isInThinkTag === undefined) { + // Parse content think and answer + startTagBuffer += content; + // 太少内容时候,暂时不解析 + if (startTagBuffer.length < startTag.length) { + return ['', '']; + } + + if (startTagBuffer.startsWith(startTag)) { + isInThinkTag = true; + return [startTagBuffer.slice(startTag.length), '']; + } + + // 如果未命中 think 标签,则认为不在 think 标签中,返回 buffer 内容作为 content + isInThinkTag = false; + return ['', startTagBuffer]; + } + + // 确认是 think 标签内容,开始返回 think 内容,并实时检测 + /* + 检测 方案。 + 存储所有疑似 的内容,直到检测到完整的 标签或超出 长度。 + content 返回值包含以下几种情况: + abc - 完全未命中尾标签 + abc - 完全命中尾标签 + abcabc - 完全命中尾标签 + abc - 完全命中尾标签 + k>abc - 命中一部分尾标签 + */ + // endTagBuffer 专门用来记录疑似尾标签的内容 + if (endTagBuffer) { + endTagBuffer += content; + if (endTagBuffer.includes(endTag)) { + isInThinkTag = false; + const answer = endTagBuffer.slice(endTag.length); + return ['', answer]; + } else if (endTagBuffer.length >= endTag.length) { + // 缓存内容超出尾标签长度,且仍未命中 ,则认为本次猜测 失败,仍处于 think 阶段。 + const tmp = endTagBuffer; + endTagBuffer = ''; + return [tmp, '']; + } + return ['', '']; + } else if (content.includes(endTag)) { + // 返回内容,完整命中,直接结束 + isInThinkTag = false; + const [think, answer] = content.split(endTag); + return [think, answer]; + } else { + // 无 buffer,且未命中 ,开始疑似 检测。 + for (let i = 1; i < endTag.length; i++) { + const partialEndTag = endTag.slice(0, i); + // 命中一部分尾标签 + if (content.endsWith(partialEndTag)) { + const think = content.slice(0, -partialEndTag.length); + endTagBuffer += partialEndTag; + return [think, '']; + } + } + } + + // 完全未命中尾标签,还是 think 阶段。 + return [content, '']; + }; + + const getStartTagBuffer = () => startTagBuffer; + + return { + parsePart, + getStartTagBuffer + }; +}; diff --git a/packages/service/core/ai/config.ts b/packages/service/core/ai/config.ts index 859bbb80d..3fa62eec9 100644 --- a/packages/service/core/ai/config.ts +++ b/packages/service/core/ai/config.ts @@ -1,7 +1,9 @@ import OpenAI from '@fastgpt/global/core/ai'; import { ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming + ChatCompletionCreateParamsStreaming, + StreamChatType, + UnStreamChatType } from '@fastgpt/global/core/ai/type'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { addLog } from '../../common/system/log'; @@ -38,29 +40,30 @@ export const getAxiosConfig = (props?: { userKey?: OpenaiAccountType }) => { }; }; -type CompletionsBodyType = - | ChatCompletionCreateParamsNonStreaming - | ChatCompletionCreateParamsStreaming; -type InferResponseType = - T extends ChatCompletionCreateParamsStreaming - ? OpenAI.Chat.Completions.ChatCompletionChunk - : OpenAI.Chat.Completions.ChatCompletion; - -export const createChatCompletion = async ({ +export const createChatCompletion = async ({ body, userKey, timeout, options }: { - body: T; + body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; userKey?: OpenaiAccountType; timeout?: number; options?: OpenAI.RequestOptions; -}): Promise<{ - response: InferResponseType; - isStreamResponse: boolean; - getEmptyResponseTip: () => string; -}> => { +}): Promise< + { + getEmptyResponseTip: () => string; + } & ( + | { + response: StreamChatType; + isStreamResponse: true; + } + | { + response: UnStreamChatType; + isStreamResponse: false; + } + ) +> => { try { const modelConstantsData = getLLMModel(body.model); @@ -96,9 +99,17 @@ export const createChatCompletion = async ({ return i18nT('chat:LLM_model_response_empty'); }; + if (isStreamResponse) { + return { + response, + isStreamResponse: true, + getEmptyResponseTip + }; + } + return { - response: response as InferResponseType, - isStreamResponse, + response, + isStreamResponse: false, getEmptyResponseTip }; } catch (error) { diff --git a/packages/service/core/ai/config/provider/PPIO.json b/packages/service/core/ai/config/provider/PPIO.json index 9adef6d72..a59ca6f06 100644 --- a/packages/service/core/ai/config/provider/PPIO.json +++ b/packages/service/core/ai/config/provider/PPIO.json @@ -1,461 +1,4 @@ { "provider": "PPIO", - "list": [ - { - "model": "deepseek/deepseek-r1/community", - "name": "deepseek/deepseek-r1/community", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-v3/community", - "name": "deepseek/deepseek-v3/community", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-r1", - "name": "deepseek/deepseek-r1", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-v3", - "name": "deepseek/deepseek-v3", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-r1-distill-llama-70b", - "name": "deepseek/deepseek-r1-distill-llama-70b", - "maxContext": 32000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-r1-distill-qwen-32b", - "name": "deepseek/deepseek-r1-distill-qwen-32b", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-r1-distill-qwen-14b", - "name": "deepseek/deepseek-r1-distill-qwen-14b", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "deepseek/deepseek-r1-distill-llama-8b", - "name": "deepseek/deepseek-r1-distill-llama-8b", - "maxContext": 32000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "qwen/qwen-2.5-72b-instruct", - "name": "qwen/qwen-2.5-72b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "qwen/qwen-2-vl-72b-instruct", - "name": "qwen/qwen-2-vl-72b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "meta-llama/llama-3.2-3b-instruct", - "name": "meta-llama/llama-3.2-3b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "qwen/qwen2.5-32b-instruct", - "name": "qwen/qwen2.5-32b-instruct", - "maxContext": 32000, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "baichuan/baichuan2-13b-chat", - "name": "baichuan/baichuan2-13b-chat", - "maxContext": 14336, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "meta-llama/llama-3.1-70b-instruct", - "name": "meta-llama/llama-3.1-70b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "meta-llama/llama-3.1-8b-instruct", - "name": "meta-llama/llama-3.1-8b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "01-ai/yi-1.5-34b-chat", - "name": "01-ai/yi-1.5-34b-chat", - "maxContext": 16384, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "01-ai/yi-1.5-9b-chat", - "name": "01-ai/yi-1.5-9b-chat", - "maxContext": 16384, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "thudm/glm-4-9b-chat", - "name": "thudm/glm-4-9b-chat", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "qwen/qwen-2-7b-instruct", - "name": "qwen/qwen-2-7b-instruct", - "maxContext": 32768, - "maxResponse": 8000, - "quoteMaxToken": 50000, - "maxTemperature": 2, - "vision": false, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true - } - ] -} \ No newline at end of file + "list": [] +} diff --git a/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts index 6ff350b8b..7627b2c87 100644 --- a/packages/service/core/ai/utils.ts +++ b/packages/service/core/ai/utils.ts @@ -37,25 +37,26 @@ export const computedTemperature = ({ return temperature; }; -type CompletionsBodyType = ( +type CompletionsBodyType = | ChatCompletionCreateParamsNonStreaming - | ChatCompletionCreateParamsStreaming -) & { - response_format?: any; - json_schema?: string; - stop?: string; -}; + | ChatCompletionCreateParamsStreaming; type InferCompletionsBody = T extends { stream: true } ? ChatCompletionCreateParamsStreaming - : ChatCompletionCreateParamsNonStreaming; + : T extends { stream: false } + ? ChatCompletionCreateParamsNonStreaming + : ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; export const llmCompletionsBodyFormat = ( - body: T, + body: T & { + response_format?: any; + json_schema?: string; + stop?: string; + }, model: string | LLMModelItemType ): InferCompletionsBody => { const modelData = typeof model === 'string' ? getLLMModel(model) : model; if (!modelData) { - return body as InferCompletionsBody; + return body as unknown as InferCompletionsBody; } const response_format = body.response_format; @@ -91,9 +92,7 @@ export const llmCompletionsBodyFormat = ( }); } - // console.log(requestBody); - - return requestBody as InferCompletionsBody; + return requestBody as unknown as InferCompletionsBody; }; export const llmStreamResponseToText = async (response: StreamChatType) => { diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index 347d164ab..6006ba5a4 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -3,13 +3,13 @@ import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../chat import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; +import { + parseReasoningContent, + parseReasoningStreamContent, + textAdaptGptResponse +} from '@fastgpt/global/core/workflow/runtime/utils'; import { createChatCompletion } from '../../../ai/config'; -import type { - ChatCompletion, - ChatCompletionMessageParam, - StreamChatType -} from '@fastgpt/global/core/ai/type.d'; +import type { ChatCompletionMessageParam, StreamChatType } from '@fastgpt/global/core/ai/type.d'; import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { postTextCensor } from '../../../../common/api/requestPlusApi'; @@ -195,7 +195,13 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { - if (res && isStreamResponse) { + if (isStreamResponse) { + if (!res) { + return { + answerText: '', + reasoningText: '' + }; + } // sse response const { answer, reasoning } = await streamResponse({ res, @@ -210,34 +216,49 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { + const content = response.choices?.[0]?.message?.content || ''; + // @ts-ignore + const reasoningContent: string = response.choices?.[0]?.message?.reasoning_content || ''; + + // API already parse reasoning content + if (reasoningContent || !aiChatReasoning) { + return { + content, + reasoningContent + }; + } + + const [think, answer] = parseReasoningContent(content); + return { + content: answer, + reasoningContent: think + }; + })(); // Some models do not support streaming if (stream) { - if (isResponseAnswerText && answer) { + if (aiChatReasoning && reasoningContent) { workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, data: textAdaptGptResponse({ - text: answer + reasoning_content: reasoningContent }) }); } - if (aiChatReasoning && reasoning) { + if (isResponseAnswerText && content) { workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, data: textAdaptGptResponse({ - reasoning_content: reasoning + text: content }) }); } } return { - answerText: answer, - reasoningText: reasoning + answerText: content, + reasoningText: reasoningContent }; } })(); @@ -267,7 +288,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise item.key === NodeInputKeyEnum.aiChatReasoning + )?.value; + if (isResponseReasoningText) { + chatAssistantResponse.push({ + type: ChatItemValueTypeEnum.reasoning, + reasoning: { + content: reasoningText + } + }); + } } if (answerText) { // save assistant text response diff --git a/projects/app/src/pages/api/v1/chat/utils.test.ts b/projects/app/src/pages/api/v1/chat/utils.test.ts new file mode 100644 index 000000000..2f766abad --- /dev/null +++ b/projects/app/src/pages/api/v1/chat/utils.test.ts @@ -0,0 +1,145 @@ +import '@/pages/api/__mocks__/base'; +import { parseReasoningStreamContent } from '@fastgpt/global/core/workflow/runtime/utils'; + +test('Parse reasoning stream content test', async () => { + const partList = [ + { + data: [{ content: '你好1' }, { content: '你好2' }, { content: '你好3' }], + correct: { answer: '你好1你好2你好3', reasoning: '' } + }, + { + data: [ + { reasoning_content: '这是' }, + { reasoning_content: '思考' }, + { reasoning_content: '过程' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '' }, + { content: '这是' }, + { content: '思考' }, + { content: '过程' }, + { content: '' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '' }, + { content: '这是' }, + { content: '思考' }, + { content: '过程' }, + { content: '' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '这是' }, + { content: '思考' }, + { content: '过程' }, + { content: '' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '这是' }, + { content: '思考' }, + { content: '过程' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '这是' }, + { content: '思考' }, + { content: '过程' }, + { content: '你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '这是' }, + { content: '思考' }, + { content: '过程你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } + }, + { + data: [ + { content: '这是' }, + { content: '思考' }, + { content: '过程你好1' }, + { content: '你好2' }, + { content: '你好3' } + ], + correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程这是' }, + { content: '思考' }, + { content: '过程 { + const { parsePart } = parseReasoningStreamContent(); + + let answer = ''; + let reasoning = ''; + part.data.forEach((item) => { + const formatPart = { + choices: [ + { + delta: { + role: 'assistant', + content: item.content, + reasoning_content: item.reasoning_content + } + } + ] + }; + const [reasoningContent, content] = parsePart(formatPart, true); + answer += content; + reasoning += reasoningContent; + }); + + expect(answer).toBe(part.correct.answer); + expect(reasoning).toBe(part.correct.reasoning); + }); +}); diff --git a/projects/app/src/web/common/api/fetch.ts b/projects/app/src/web/common/api/fetch.ts index d02656be7..424117b70 100644 --- a/projects/app/src/web/common/api/fetch.ts +++ b/projects/app/src/web/common/api/fetch.ts @@ -24,7 +24,11 @@ export type StreamResponseType = { [DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[]; }; type ResponseQueueItemType = - | { event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer; text: string } + | { + event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer; + text?: string; + reasoningText?: string; + } | { event: SseResponseEventEnum.interactive; [key: string]: any } | { event: @@ -79,7 +83,7 @@ export const streamFetch = ({ if (abortCtrl.signal.aborted) { responseQueue.forEach((item) => { onMessage(item); - if (isAnswerEvent(item.event)) { + if (isAnswerEvent(item.event) && item.text) { responseText += item.text; } }); @@ -91,7 +95,7 @@ export const streamFetch = ({ for (let i = 0; i < fetchCount; i++) { const item = responseQueue[i]; onMessage(item); - if (isAnswerEvent(item.event)) { + if (isAnswerEvent(item.event) && item.text) { responseText += item.text; } } @@ -180,7 +184,7 @@ export const streamFetch = ({ // console.log(parseJson, event); if (event === SseResponseEventEnum.answer) { const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || ''; - onMessage({ + pushDataToQueue({ event, reasoningText }); @@ -194,7 +198,7 @@ export const streamFetch = ({ } } else if (event === SseResponseEventEnum.fastAnswer) { const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || ''; - onMessage({ + pushDataToQueue({ event, reasoningText });