diff --git a/docSite/content/zh-cn/docs/development/upgrading/498.md b/docSite/content/zh-cn/docs/development/upgrading/498.md index fc758bdc5..4fc0b0d2c 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/498.md +++ b/docSite/content/zh-cn/docs/development/upgrading/498.md @@ -10,7 +10,9 @@ weight: 792 ## 🚀 新增内容 -1. qwen3 模型预设 +1. 支持 Toolcalls 并行执行。 +2. 将所有内置任务,从非 stream 模式调整成 stream 模式,避免部分模型不支持非 stream 模式。如需覆盖,则可以在模型`额外 Body`参数中,强制指定`stream=false`。 +3. qwen3 模型预设 ## ⚙️ 优化 diff --git a/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md b/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md index e2f974e56..4f7c6087d 100644 --- a/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md @@ -563,7 +563,7 @@ HTTP模块中,需要设置 3 个工具参数: "hidden" ], "label": "", - "value": 1500, + "value": 5000, "valueType": "number" }, { diff --git a/packages/global/common/string/password.ts b/packages/global/common/string/password.ts new file mode 100644 index 000000000..4d4d85bb6 --- /dev/null +++ b/packages/global/common/string/password.ts @@ -0,0 +1,18 @@ +export const checkPasswordRule = (password: string) => { + const patterns = [ + /\d/, // Contains digits + /[a-z]/, // Contains lowercase letters + /[A-Z]/, // Contains uppercase letters + /[!@#$%^&*()_+=-]/ // Contains special characters + ]; + const validChars = /^[\dA-Za-z!@#$%^&*()_+=-]{6,100}$/; + + // Check length and valid characters + if (!validChars.test(password)) return false; + + // Count how many patterns are satisfied + const matchCount = patterns.filter((pattern) => pattern.test(password)).length; + + // Must satisfy at least 2 patterns + return matchCount >= 2; +}; diff --git a/packages/global/core/ai/prompt/AIChat.ts b/packages/global/core/ai/prompt/AIChat.ts index f28ec4fb0..e5d82075f 100644 --- a/packages/global/core/ai/prompt/AIChat.ts +++ b/packages/global/core/ai/prompt/AIChat.ts @@ -88,8 +88,8 @@ export const Prompt_userQuotePromptList: PromptTemplateItem[] = [ - 保持答案与 中描述的一致。 - 使用 Markdown 语法优化回答格式。 - 使用与问题相同的语言回答。 -- 使用 [id](QUOTE) 格式来引用中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" +- 使用 [id](CITE) 格式来引用中的知识,其中 CITE 是固定常量, id 为引文中的 id。 +- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。" - 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`, ['4.9.2']: `使用 标记中的内容作为本次对话的参考: @@ -146,8 +146,8 @@ export const Prompt_userQuotePromptList: PromptTemplateItem[] = [ - 保持答案与 中描述的一致。 - 使用 Markdown 语法优化回答格式。 - 使用与问题相同的语言回答。 -- 使用 [id](QUOTE) 格式来引用中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" +- 使用 [id](CITE) 格式来引用中的知识,其中 CITE 是固定常量, id 为引文中的 id。 +- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。" - 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。 问题:"""{{question}}"""`, @@ -217,8 +217,8 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [ - 保持答案与 中描述的一致。 - 使用 Markdown 语法优化回答格式。 - 使用与问题相同的语言回答。 -- 使用 [id](QUOTE) 格式来引用中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" +- 使用 [id](CITE) 格式来引用中的知识,其中 CITE 是固定常量, id 为引文中的 id。 +- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。" - 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`, ['4.9.2']: `使用 标记中的内容作为本次对话的参考: @@ -271,8 +271,8 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [ - 保持答案与 中描述的一致。 - 使用 Markdown 语法优化回答格式。 - 使用与问题相同的语言回答。 -- 使用 [id](QUOTE) 格式来引用中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" +- 使用 [id](CITE) 格式来引用中的知识,其中 CITE 是固定常量, id 为引文中的 id。 +- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。" - 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。 问题:"""{{question}}"""`, @@ -321,24 +321,13 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [ } ]; -export const getQuotePrompt = ( - version?: string, - role: 'user' | 'system' = 'user', - parseQuote = true -) => { +export const getQuotePrompt = (version?: string, role: 'user' | 'system' = 'user') => { const quotePromptTemplates = role === 'user' ? Prompt_userQuotePromptList : Prompt_systemQuotePromptList; const defaultTemplate = quotePromptTemplates[0].value; - return parseQuote - ? getPromptByVersion(version, defaultTemplate) - : getPromptByVersion(version, defaultTemplate).replace( - `- 使用 [id](QUOTE) 格式来引用中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" -- 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`, - '' - ); + return getPromptByVersion(version, defaultTemplate); }; // Document quote prompt diff --git a/packages/global/core/ai/prompt/agent.ts b/packages/global/core/ai/prompt/agent.ts index c9a7c9c9f..08293bc3f 100644 --- a/packages/global/core/ai/prompt/agent.ts +++ b/packages/global/core/ai/prompt/agent.ts @@ -60,7 +60,7 @@ export const getExtractJsonToolPrompt = (version?: string) => { """ - {{description}} - 不是每个参数都是必须生成的,如果没有合适的参数值,不要生成该参数,或返回空字符串。 -- 需要结合前面的对话内容,一起生成合适的参数。 +- 需要结合历史记录,一起生成合适的参数。 """ 本次输入内容: """{{content}}""" diff --git a/packages/global/core/ai/prompt/dataset.ts b/packages/global/core/ai/prompt/dataset.ts index 346cace1c..aec3cfa5c 100644 --- a/packages/global/core/ai/prompt/dataset.ts +++ b/packages/global/core/ai/prompt/dataset.ts @@ -1,6 +1,5 @@ -export const getDatasetSearchToolResponsePrompt = (parseQuote: boolean) => { - return parseQuote - ? `## Role +export const getDatasetSearchToolResponsePrompt = () => { + return `## Role 你是一个知识库回答助手,可以 "quotes" 中的内容作为本次对话的参考。为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记。 ## Rules @@ -9,16 +8,7 @@ export const getDatasetSearchToolResponsePrompt = (parseQuote: boolean) => { - 保持答案与 "quotes" 中描述的一致。 - 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。 - 使用与问题相同的语言回答。 -- 使用 [id](QUOTE) 格式来引用 "quotes" 中的知识,其中 QUOTE 是固定常量, id 为引文中的 id。 -- 在每段话结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](QUOTE)。" -- 每段话至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。` - : `## Role -你是一个知识库回答助手,可以 "quotes" 中的内容作为本次对话的参考。 - -## Rules -- 如果你不清楚答案,你需要澄清。 -- 避免提及你是从 "quotes" 获取的知识。 -- 保持答案与 "quotes" 中描述的一致。 -- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。 -- 使用与问题相同的语言回答。`; +- 使用 [id](CITE) 格式来引用 "quotes" 中的知识,其中 CITE 是固定常量, id 为引文中的 id。 +- 在每段话结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。" +- 每段话至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`; }; diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index ce77cc207..4379738db 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -60,6 +60,7 @@ export type ChatCompletionAssistantToolParam = { tool_calls: ChatCompletionMessageToolCall[]; }; export type ChatCompletionMessageToolCall = ChatCompletionMessageToolCall & { + index?: number; toolName?: string; toolAvatar?: string; }; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index 9333ec278..1aefdbb17 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -1,9 +1,15 @@ import { DispatchNodeResponseType } from '../workflow/runtime/type'; import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { ChatItemValueTypeEnum, ChatRoleEnum, ChatSourceEnum } from './constants'; -import { ChatHistoryItemResType, ChatItemType, UserChatItemValueItemType } from './type.d'; +import { + AIChatItemValueItemType, + ChatHistoryItemResType, + ChatItemType, + UserChatItemValueItemType +} from './type.d'; import { sliceStrStartEnd } from '../../common/string/tools'; import { PublishChannelEnum } from '../../support/outLink/constant'; +import { removeDatasetCiteText } from '../../../service/core/ai/utils'; // Concat 2 -> 1, and sort by role export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => { @@ -77,6 +83,7 @@ export const getHistoryPreview = ( }); }; +// Filter workflow public response export const filterPublicNodeResponseData = ({ flowResponses = [], responseDetail = false @@ -112,6 +119,40 @@ export const filterPublicNodeResponseData = ({ }); }; +// Remove dataset cite in ai response +export const removeAIResponseCite = ( + value: T, + retainCite: boolean +): T => { + if (retainCite) return value; + + if (typeof value === 'string') { + return removeDatasetCiteText(value, false) as T; + } + + return value.map((item) => { + if (item.text?.content) { + return { + ...item, + text: { + ...item.text, + content: removeDatasetCiteText(item.text.content, false) + } + }; + } + if (item.reasoning?.content) { + return { + ...item, + reasoning: { + ...item.reasoning, + content: removeDatasetCiteText(item.reasoning.content, false) + } + }; + } + return item; + }) as T; +}; + export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => { return ( input?.filter((item) => { diff --git a/packages/global/core/dataset/training/utils.ts b/packages/global/core/dataset/training/utils.ts index 895837abb..72bbbc2d7 100644 --- a/packages/global/core/dataset/training/utils.ts +++ b/packages/global/core/dataset/training/utils.ts @@ -8,7 +8,7 @@ import { export const minChunkSize = 64; // min index and chunk size // Chunk size -export const chunkAutoChunkSize = 1500; +export const chunkAutoChunkSize = 1000; export const getMaxChunkSize = (model: LLMModelItemType) => { return Math.max(model.maxContext - model.maxResponse, 2000); }; diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 9828826ee..c35e6b950 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -58,7 +58,7 @@ export type ChatDispatchProps = { chatConfig: AppSchema['chatConfig']; lastInteractive?: WorkflowInteractiveResponseType; // last interactive response stream: boolean; - parseQuote?: boolean; + retainDatasetCite?: boolean; maxRunTimes: number; isToolCall?: boolean; workflowStreamResponse?: WorkflowResponseType; diff --git a/packages/global/core/workflow/template/system/datasetSearch.ts b/packages/global/core/workflow/template/system/datasetSearch.ts index 8313c5127..0bcce572d 100644 --- a/packages/global/core/workflow/template/system/datasetSearch.ts +++ b/packages/global/core/workflow/template/system/datasetSearch.ts @@ -54,7 +54,7 @@ export const DatasetSearchModule: FlowNodeTemplateType = { key: NodeInputKeyEnum.datasetMaxTokens, renderTypeList: [FlowNodeInputTypeEnum.hidden], label: '', - value: 1500, + value: 5000, valueType: WorkflowIOValueTypeEnum.number }, { diff --git a/packages/service/core/ai/config/provider/Qwen.json b/packages/service/core/ai/config/provider/Qwen.json index f6dbb8e39..84aea9ae3 100644 --- a/packages/service/core/ai/config/provider/Qwen.json +++ b/packages/service/core/ai/config/provider/Qwen.json @@ -1,79 +1,6 @@ { "provider": "Qwen", "list": [ - { - "model": "qwen-vl-plus", - "name": "qwen-vl-plus", - "maxContext": 32000, - "maxResponse": 2000, - "quoteMaxToken": 20000, - "maxTemperature": 1.2, - "vision": true, - "toolChoice": false, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "type": "llm", - "showTopP": true, - "showStopSign": true - }, - { - "model": "qwen-plus", - "name": "Qwen-plus", - "maxContext": 64000, - "maxResponse": 8000, - "quoteMaxToken": 60000, - "maxTemperature": 1, - "vision": false, - "toolChoice": true, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true, - "responseFormatList": ["text", "json_object"] - }, - { - "model": "qwen-turbo", - "name": "Qwen-turbo", - "maxContext": 128000, - "maxResponse": 8000, - "quoteMaxToken": 100000, - "maxTemperature": 1, - "vision": false, - "toolChoice": true, - "functionCall": false, - "defaultSystemChatPrompt": "", - "datasetProcess": true, - "usedInClassify": true, - "customCQPrompt": "", - "usedInExtractFields": true, - "usedInQueryExtension": true, - "customExtractPrompt": "", - "usedInToolCall": true, - "defaultConfig": {}, - "fieldMap": {}, - "type": "llm", - "showTopP": true, - "showStopSign": true, - "responseFormatList": ["text", "json_object"] - }, - { "model": "qwen-max", "name": "Qwen-max", @@ -123,6 +50,78 @@ "showTopP": true, "showStopSign": true }, + { + "model": "qwen-plus", + "name": "Qwen-plus", + "maxContext": 64000, + "maxResponse": 8000, + "quoteMaxToken": 60000, + "maxTemperature": 1, + "vision": false, + "toolChoice": true, + "functionCall": false, + "defaultSystemChatPrompt": "", + "datasetProcess": true, + "usedInClassify": true, + "customCQPrompt": "", + "usedInExtractFields": true, + "usedInQueryExtension": true, + "customExtractPrompt": "", + "usedInToolCall": true, + "defaultConfig": {}, + "fieldMap": {}, + "type": "llm", + "showTopP": true, + "showStopSign": true, + "responseFormatList": ["text", "json_object"] + }, + { + "model": "qwen-vl-plus", + "name": "qwen-vl-plus", + "maxContext": 32000, + "maxResponse": 2000, + "quoteMaxToken": 20000, + "maxTemperature": 1.2, + "vision": true, + "toolChoice": false, + "functionCall": false, + "defaultSystemChatPrompt": "", + "datasetProcess": true, + "usedInClassify": true, + "customCQPrompt": "", + "usedInExtractFields": true, + "usedInQueryExtension": true, + "customExtractPrompt": "", + "usedInToolCall": true, + "type": "llm", + "showTopP": true, + "showStopSign": true + }, + { + "model": "qwen-turbo", + "name": "Qwen-turbo", + "maxContext": 128000, + "maxResponse": 8000, + "quoteMaxToken": 100000, + "maxTemperature": 1, + "vision": false, + "toolChoice": true, + "functionCall": false, + "defaultSystemChatPrompt": "", + "datasetProcess": true, + "usedInClassify": true, + "customCQPrompt": "", + "usedInExtractFields": true, + "usedInQueryExtension": true, + "customExtractPrompt": "", + "usedInToolCall": true, + "defaultConfig": {}, + "fieldMap": {}, + "type": "llm", + "showTopP": true, + "showStopSign": true, + "responseFormatList": ["text", "json_object"] + }, { "model": "qwen3-235b-a22b", "name": "qwen3-235b-a22b", @@ -142,7 +141,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -168,7 +169,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -194,7 +197,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -220,7 +225,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -246,7 +253,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -272,7 +281,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -298,7 +309,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -324,7 +337,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": true, @@ -350,7 +365,9 @@ "usedInQueryExtension": false, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": false, @@ -375,7 +392,9 @@ "usedInQueryExtension": false, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": {}, + "defaultConfig": { + "stream": true + }, "fieldMap": {}, "type": "llm", "showTopP": false, diff --git a/packages/service/core/ai/functions/createQuestionGuide.ts b/packages/service/core/ai/functions/createQuestionGuide.ts index d8eca0def..f0ed6bf0d 100644 --- a/packages/service/core/ai/functions/createQuestionGuide.ts +++ b/packages/service/core/ai/functions/createQuestionGuide.ts @@ -2,7 +2,7 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d' import { createChatCompletion } from '../config'; import { countGptMessagesTokens, countPromptTokens } from '../../../common/string/tiktoken/index'; import { loadRequestMessages } from '../../chat/utils'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '../utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '../utils'; import { QuestionGuidePrompt, QuestionGuideFooterPrompt @@ -42,12 +42,12 @@ export async function createQuestionGuide({ temperature: 0.1, max_tokens: 200, messages: requestMessages, - stream: false + stream: true }, model ) }); - const { text: answer, usage } = await llmResponseToAnswerText(response); + const { text: answer, usage } = await formatLLMResponse(response); const start = answer.indexOf('['); const end = answer.lastIndexOf(']'); diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index c94a8acb4..7e35be6ae 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -4,7 +4,7 @@ import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { countGptMessagesTokens, countPromptTokens } from '../../../common/string/tiktoken/index'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getLLMModel } from '../model'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '../utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '../utils'; import { addLog } from '../../../common/system/log'; import { filterGPTMessageByMaxContext } from '../../chat/utils'; import json5 from 'json5'; @@ -170,7 +170,7 @@ assistant: ${chatBg} const { response } = await createChatCompletion({ body: llmCompletionsBodyFormat( { - stream: false, + stream: true, model: modelData.model, temperature: 0.1, messages @@ -178,7 +178,7 @@ assistant: ${chatBg} modelData ) }); - const { text: answer, usage } = await llmResponseToAnswerText(response); + const { text: answer, usage } = await formatLLMResponse(response); const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(messages)); const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); diff --git a/packages/service/core/ai/utils.test.ts b/packages/service/core/ai/utils.test.ts deleted file mode 100644 index 77e2f9166..000000000 --- a/packages/service/core/ai/utils.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { parseReasoningStreamContent } from './utils'; -import { expect, test } from 'vitest'; - -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/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts index 161b8e21c..8ac187309 100644 --- a/packages/service/core/ai/utils.ts +++ b/packages/service/core/ai/utils.ts @@ -5,10 +5,12 @@ import { CompletionFinishReason, StreamChatType, UnStreamChatType, - CompletionUsage + CompletionUsage, + ChatCompletionMessageToolCall } from '@fastgpt/global/core/ai/type'; import { getLLMModel } from './model'; import { getLLMDefaultUsage } from '@fastgpt/global/core/ai/constants'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; /* Count response max token @@ -105,33 +107,84 @@ export const llmStreamResponseToAnswerText = async ( ): Promise<{ text: string; usage?: CompletionUsage; + toolCalls?: ChatCompletionMessageToolCall[]; }> => { let answer = ''; let usage = getLLMDefaultUsage(); + let toolCalls: ChatCompletionMessageToolCall[] = []; + let callingTool: { name: string; arguments: string } | null = null; + for await (const part of response) { usage = part.usage || usage; + const responseChoice = part.choices?.[0]?.delta; - const content = part.choices?.[0]?.delta?.content || ''; + const content = responseChoice?.content || ''; answer += content; + + // Tool calls + if (responseChoice?.tool_calls?.length) { + responseChoice.tool_calls.forEach((toolCall) => { + const index = toolCall.index; + + if (toolCall.id || callingTool) { + // 有 id,代表新 call 工具 + if (toolCall.id) { + callingTool = { + name: toolCall.function?.name || '', + arguments: toolCall.function?.arguments || '' + }; + } else if (callingTool) { + // Continue call(Perhaps the name of the previous function was incomplete) + callingTool.name += toolCall.function?.name || ''; + callingTool.arguments += toolCall.function?.arguments || ''; + } + + if (!callingTool) { + return; + } + + // New tool, add to list. + const toolId = getNanoid(); + toolCalls[index] = { + ...toolCall, + id: toolId, + type: 'function', + function: callingTool + }; + callingTool = null; + } else { + /* arg 追加到当前工具的参数里 */ + const arg: string = toolCall?.function?.arguments ?? ''; + const currentTool = toolCalls[index]; + if (currentTool && arg) { + currentTool.function.arguments += arg; + } + } + }); + } } return { text: parseReasoningContent(answer)[1], - usage + usage, + toolCalls }; }; export const llmUnStreamResponseToAnswerText = async ( response: UnStreamChatType ): Promise<{ text: string; + toolCalls?: ChatCompletionMessageToolCall[]; usage?: CompletionUsage; }> => { const answer = response.choices?.[0]?.message?.content || ''; + const toolCalls = response.choices?.[0]?.message?.tool_calls; return { text: answer, - usage: response.usage + usage: response.usage, + toolCalls }; }; -export const llmResponseToAnswerText = async (response: StreamChatType | UnStreamChatType) => { +export const formatLLMResponse = async (response: StreamChatType | UnStreamChatType) => { if ('iterator' in response) { return llmStreamResponseToAnswerText(response); } @@ -155,20 +208,31 @@ export const parseReasoningContent = (text: string): [string, string] => { return [thinkContent, answerContent]; }; -// Parse tags to think and answer - stream response -export const parseReasoningStreamContent = () => { - let isInThinkTag: boolean | undefined; +export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean) => { + return retainDatasetCite ? text : text.replace(/\[([a-f0-9]{24})\]\(CITE\)/g, ''); +}; - const startTag = ''; +// Parse llm stream part +export const parseLLMStreamResponse = () => { + let isInThinkTag: boolean | undefined = undefined; let startTagBuffer = ''; - - const endTag = ''; let endTagBuffer = ''; + const thinkStartChars = ''; + const thinkEndChars = ''; + + let citeBuffer = ''; + const maxCiteBufferLength = 32; // [Object](CITE)总长度为32 + /* parseThinkTag - 只控制是否主动解析 ,如果接口已经解析了,则不再解析。 + retainDatasetCite - */ - const parsePart = ( + const parsePart = ({ + part, + parseThinkTag = true, + retainDatasetCite = true + }: { part: { choices: { delta: { @@ -177,147 +241,209 @@ export const parseReasoningStreamContent = () => { }; finish_reason?: CompletionFinishReason; }[]; - }, - parseThinkTag = false - ): { + }; + parseThinkTag?: boolean; + retainDatasetCite?: boolean; + }): { reasoningContent: string; content: string; + responseContent: string; finishReason: CompletionFinishReason; } => { - const content = part.choices?.[0]?.delta?.content || ''; const finishReason = part.choices?.[0]?.finish_reason || null; - + const content = part.choices?.[0]?.delta?.content || ''; // @ts-ignore const reasoningContent = part.choices?.[0]?.delta?.reasoning_content || ''; - if (reasoningContent || !parseThinkTag) { - isInThinkTag = false; - return { reasoningContent, content, finishReason }; - } + const isStreamEnd = !!finishReason; - if (!content) { - return { - reasoningContent: '', - content: '', - finishReason - }; - } - - // 如果不在 think 标签中,或者有 reasoningContent(接口已解析),则返回 reasoningContent 和 content - if (isInThinkTag === false) { - return { - reasoningContent: '', - content, - finishReason - }; - } - - // 检测是否为 think 标签开头的数据 - if (isInThinkTag === undefined) { - // Parse content think and answer - startTagBuffer += content; - // 太少内容时候,暂时不解析 - if (startTagBuffer.length < startTag.length) { - return { - reasoningContent: '', - content: '', - finishReason - }; - } - - if (startTagBuffer.startsWith(startTag)) { - isInThinkTag = true; - return { - reasoningContent: startTagBuffer.slice(startTag.length), - content: '', - finishReason - }; - } - - // 如果未命中 think 标签,则认为不在 think 标签中,返回 buffer 内容作为 content - isInThinkTag = false; - return { - reasoningContent: '', - content: startTagBuffer, - finishReason - }; - } - - // 确认是 think 标签内容,开始返回 think 内容,并实时检测 - /* - 检测 方案。 - 存储所有疑似 的内容,直到检测到完整的 标签或超出 长度。 - content 返回值包含以下几种情况: - abc - 完全未命中尾标签 - abc - 完全命中尾标签 - abcabc - 完全命中尾标签 - abc - 完全命中尾标签 - k>abc - 命中一部分尾标签 - */ - // endTagBuffer 专门用来记录疑似尾标签的内容 - if (endTagBuffer) { - endTagBuffer += content; - if (endTagBuffer.includes(endTag)) { + // Parse think + const { reasoningContent: parsedThinkReasoningContent, content: parsedThinkContent } = (() => { + if (reasoningContent || !parseThinkTag) { isInThinkTag = false; - const answer = endTagBuffer.slice(endTag.length); + return { reasoningContent, content }; + } + + if (!content) { return { reasoningContent: '', - content: answer, - finishReason - }; - } else if (endTagBuffer.length >= endTag.length) { - // 缓存内容超出尾标签长度,且仍未命中 ,则认为本次猜测 失败,仍处于 think 阶段。 - const tmp = endTagBuffer; - endTagBuffer = ''; - return { - reasoningContent: tmp, - content: '', - finishReason + content: '' }; } - return { - reasoningContent: '', - content: '', - finishReason - }; - } else if (content.includes(endTag)) { - // 返回内容,完整命中,直接结束 - isInThinkTag = false; - const [think, answer] = content.split(endTag); - return { - reasoningContent: think, - content: answer, - finishReason - }; - } 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; + + // 如果不在 think 标签中,或者有 reasoningContent(接口已解析),则返回 reasoningContent 和 content + if (isInThinkTag === false) { + return { + reasoningContent: '', + content + }; + } + + // 检测是否为 think 标签开头的数据 + if (isInThinkTag === undefined) { + // Parse content think and answer + startTagBuffer += content; + // 太少内容时候,暂时不解析 + if (startTagBuffer.length < thinkStartChars.length) { + if (isStreamEnd) { + const tmpContent = startTagBuffer; + startTagBuffer = ''; + return { + reasoningContent: '', + content: tmpContent + }; + } return { - reasoningContent: think, - content: '', - finishReason + reasoningContent: '', + content: '' }; } + + if (startTagBuffer.startsWith(thinkStartChars)) { + isInThinkTag = true; + return { + reasoningContent: startTagBuffer.slice(thinkStartChars.length), + content: '' + }; + } + + // 如果未命中 think 标签,则认为不在 think 标签中,返回 buffer 内容作为 content + isInThinkTag = false; + return { + reasoningContent: '', + content: startTagBuffer + }; } + + // 确认是 think 标签内容,开始返回 think 内容,并实时检测 + /* + 检测 方案。 + 存储所有疑似 的内容,直到检测到完整的 标签或超出 长度。 + content 返回值包含以下几种情况: + abc - 完全未命中尾标签 + abc - 完全命中尾标签 + abcabc - 完全命中尾标签 + abc - 完全命中尾标签 + k>abc - 命中一部分尾标签 + */ + // endTagBuffer 专门用来记录疑似尾标签的内容 + if (endTagBuffer) { + endTagBuffer += content; + if (endTagBuffer.includes(thinkEndChars)) { + isInThinkTag = false; + const answer = endTagBuffer.slice(thinkEndChars.length); + return { + reasoningContent: '', + content: answer + }; + } else if (endTagBuffer.length >= thinkEndChars.length) { + // 缓存内容超出尾标签长度,且仍未命中 ,则认为本次猜测 失败,仍处于 think 阶段。 + const tmp = endTagBuffer; + endTagBuffer = ''; + return { + reasoningContent: tmp, + content: '' + }; + } + return { + reasoningContent: '', + content: '' + }; + } else if (content.includes(thinkEndChars)) { + // 返回内容,完整命中,直接结束 + isInThinkTag = false; + const [think, answer] = content.split(thinkEndChars); + return { + reasoningContent: think, + content: answer + }; + } else { + // 无 buffer,且未命中 ,开始疑似 检测。 + for (let i = 1; i < thinkEndChars.length; i++) { + const partialEndTag = thinkEndChars.slice(0, i); + // 命中一部分尾标签 + if (content.endsWith(partialEndTag)) { + const think = content.slice(0, -partialEndTag.length); + endTagBuffer += partialEndTag; + return { + reasoningContent: think, + content: '' + }; + } + } + } + + // 完全未命中尾标签,还是 think 阶段。 + return { + reasoningContent: content, + content: '' + }; + })(); + + // Parse datset cite + if (retainDatasetCite) { + return { + reasoningContent: parsedThinkReasoningContent, + content: parsedThinkContent, + responseContent: parsedThinkContent, + finishReason + }; } - // 完全未命中尾标签,还是 think 阶段。 + // 缓存包含 [ 的字符串,直到超出 maxCiteBufferLength 再一次性返回 + const parseCite = (text: string) => { + // 结束时,返回所有剩余内容 + if (isStreamEnd) { + const content = citeBuffer + text; + return { + content: removeDatasetCiteText(content, false) + }; + } + + // 新内容包含 [,初始化缓冲数据 + if (text.includes('[')) { + const index = text.indexOf('['); + const beforeContent = citeBuffer + text.slice(0, index); + citeBuffer = text.slice(index); + + // beforeContent 可能是:普通字符串,带 [ 的字符串 + return { + content: removeDatasetCiteText(beforeContent, false) + }; + } + // 处于 Cite 缓冲区,判断是否满足条件 + else if (citeBuffer) { + citeBuffer += text; + + // 检查缓冲区长度是否达到完整Quote长度或已经流结束 + if (citeBuffer.length >= maxCiteBufferLength) { + const content = removeDatasetCiteText(citeBuffer, false); + citeBuffer = ''; + + return { + content + }; + } else { + // 暂时不返回内容 + return { content: '' }; + } + } + + return { + content: text + }; + }; + const { content: pasedCiteContent } = parseCite(parsedThinkContent); + return { - reasoningContent: content, - content: '', + reasoningContent: parsedThinkReasoningContent, + content: parsedThinkContent, + responseContent: pasedCiteContent, finishReason }; }; - const getStartTagBuffer = () => startTagBuffer; - return { - parsePart, - getStartTagBuffer + parsePart }; }; diff --git a/packages/service/core/app/plugin/utils.ts b/packages/service/core/app/plugin/utils.ts index 43e0db8aa..800b1b4d8 100644 --- a/packages/service/core/app/plugin/utils.ts +++ b/packages/service/core/app/plugin/utils.ts @@ -31,5 +31,6 @@ export const computedPluginUsage = async ({ return plugin.hasTokenFee ? pluginCurrentCost + childrenUsages : pluginCurrentCost; } + // Personal plugins are charged regardless of whether they are successful or not return childrenUsages; }; diff --git a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts index ec54bf5e9..d41a2be73 100644 --- a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts @@ -19,7 +19,7 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getHandleId } from '@fastgpt/global/core/workflow/utils'; import { loadRequestMessages } from '../../../chat/utils'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '../../../ai/utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '../../../ai/utils'; import { addLog } from '../../../../common/system/log'; import { ModelTypeEnum } from '../../../../../global/core/ai/model'; import { replaceVariable } from '@fastgpt/global/common/string/tools'; @@ -135,13 +135,13 @@ const completions = async ({ model: cqModel.model, temperature: 0.01, messages: requestMessages, - stream: false + stream: true }, cqModel ), userKey: externalProvider.openaiAccount }); - const { text: answer, usage } = await llmResponseToAnswerText(response); + const { text: answer, usage } = await formatLLMResponse(response); // console.log(JSON.stringify(chats2GPTMessages({ messages, reserveId: false }), null, 2)); // console.log(answer, '----'); diff --git a/packages/service/core/workflow/dispatch/agent/extract.ts b/packages/service/core/workflow/dispatch/agent/extract.ts index 2d4b682a4..34d98fff6 100644 --- a/packages/service/core/workflow/dispatch/agent/extract.ts +++ b/packages/service/core/workflow/dispatch/agent/extract.ts @@ -30,7 +30,7 @@ import { import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '../../../ai/utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '../../../ai/utils'; import { ModelTypeEnum } from '../../../../../global/core/ai/model'; import { getExtractJsonPrompt, @@ -226,10 +226,10 @@ const toolChoice = async (props: ActionProps) => { } ]; - const { response } = (await createChatCompletion({ + const { response } = await createChatCompletion({ body: llmCompletionsBodyFormat( { - stream: false, + stream: true, model: extractModel.model, temperature: 0.01, messages: filterMessages, @@ -239,16 +239,15 @@ const toolChoice = async (props: ActionProps) => { extractModel ), userKey: externalProvider.openaiAccount - })) as { response: UnStreamChatType }; + }); + const { toolCalls, usage } = await formatLLMResponse(response); const arg: Record = (() => { try { - return json5.parse( - response?.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments || '' - ); + return json5.parse(toolCalls?.[0]?.function?.arguments || ''); } catch (error) { console.log(agentFunction.parameters); - console.log(response.choices?.[0]?.message?.tool_calls?.[0]?.function); + console.log(toolCalls?.[0]?.function); console.log('Your model may not support tool_call', error); return {}; } @@ -257,11 +256,10 @@ const toolChoice = async (props: ActionProps) => { const AIMessages: ChatCompletionMessageParam[] = [ { role: ChatCompletionRequestMessageRoleEnum.Assistant, - tool_calls: response.choices?.[0]?.message?.tool_calls + tool_calls: toolCalls } ]; - const usage = response.usage; const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(filterMessages, tools)); const outputTokens = usage?.completion_tokens || (await countGptMessagesTokens(AIMessages)); return { @@ -321,13 +319,13 @@ Human: ${content}` model: extractModel.model, temperature: 0.01, messages: requestMessages, - stream: false + stream: true }, extractModel ), userKey: externalProvider.openaiAccount }); - const { text: answer, usage } = await llmResponseToAnswerText(response); + const { text: answer, usage } = await formatLLMResponse(response); const inputTokens = usage?.prompt_tokens || (await countMessagesTokens(messages)); const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); diff --git a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts index daf074efb..011223ca2 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts @@ -26,7 +26,12 @@ import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools import { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; -import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils'; +import { + computedMaxToken, + llmCompletionsBodyFormat, + removeDatasetCiteText, + parseLLMStreamResponse +} from '../../../../ai/utils'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; @@ -48,6 +53,7 @@ export const runToolWithFunctionCall = async ( runtimeEdges, externalProvider, stream, + retainDatasetCite = true, workflowStreamResponse, params: { temperature, @@ -261,7 +267,8 @@ export const runToolWithFunctionCall = async ( res, toolNodes, stream: aiResponse, - workflowStreamResponse + workflowStreamResponse, + retainDatasetCite }); return { @@ -288,8 +295,18 @@ export const runToolWithFunctionCall = async ( ] : []; + const answer = result.choices?.[0]?.message?.content || ''; + if (answer) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + text: removeDatasetCiteText(answer, retainDatasetCite) + }) + }); + } + return { - answer: result.choices?.[0]?.message?.content || '', + answer, functionCalls: toolCalls, inputTokens: usage?.prompt_tokens, outputTokens: usage?.completion_tokens @@ -509,12 +526,14 @@ async function streamResponse({ res, toolNodes, stream, - workflowStreamResponse + workflowStreamResponse, + retainDatasetCite }: { res: NextApiResponse; toolNodes: ToolNodeItemType[]; stream: StreamChatType; workflowStreamResponse?: WorkflowResponseType; + retainDatasetCite?: boolean; }) { const write = responseWriteController({ res, @@ -526,6 +545,8 @@ async function streamResponse({ let functionId = getNanoid(); let usage = getLLMDefaultUsage(); + const { parsePart } = parseLLMStreamResponse(); + for await (const part of stream) { usage = part.usage || usage; if (res.closed) { @@ -533,17 +554,21 @@ async function streamResponse({ break; } + const { content: toolChoiceContent, responseContent } = parsePart({ + part, + parseThinkTag: false, + retainDatasetCite + }); + const responseChoice = part.choices?.[0]?.delta; + textAnswer += toolChoiceContent; - if (responseChoice.content) { - const content = responseChoice?.content || ''; - textAnswer += content; - + if (responseContent) { workflowStreamResponse?.({ write, event: SseResponseEventEnum.answer, data: textAdaptGptResponse({ - text: content + text: responseContent }) }); } else if (responseChoice.function_call) { diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index 215f79ed2..e7a4960f1 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -29,8 +29,9 @@ import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; import { computedMaxToken, llmCompletionsBodyFormat, + removeDatasetCiteText, parseReasoningContent, - parseReasoningStreamContent + parseLLMStreamResponse } from '../../../../ai/utils'; import { WorkflowResponseType } from '../../type'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; @@ -60,6 +61,7 @@ export const runToolWithPromptCall = async ( runtimeEdges, externalProvider, stream, + retainDatasetCite = true, workflowStreamResponse, params: { temperature, @@ -275,7 +277,8 @@ export const runToolWithPromptCall = async ( toolNodes, stream: aiResponse, workflowStreamResponse, - aiChatReasoning + aiChatReasoning, + retainDatasetCite }); return { @@ -318,7 +321,7 @@ export const runToolWithPromptCall = async ( workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, data: textAdaptGptResponse({ - reasoning_content: reasoning + reasoning_content: removeDatasetCiteText(reasoning, retainDatasetCite) }) }); } @@ -344,7 +347,7 @@ export const runToolWithPromptCall = async ( workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, data: textAdaptGptResponse({ - text: replaceAnswer + text: removeDatasetCiteText(replaceAnswer, retainDatasetCite) }) }); } @@ -566,13 +569,15 @@ async function streamResponse({ res, stream, workflowStreamResponse, - aiChatReasoning + aiChatReasoning, + retainDatasetCite }: { res: NextApiResponse; toolNodes: ToolNodeItemType[]; stream: StreamChatType; workflowStreamResponse?: WorkflowResponseType; aiChatReasoning?: boolean; + retainDatasetCite?: boolean; }) { const write = responseWriteController({ res, @@ -585,7 +590,7 @@ async function streamResponse({ let finish_reason: CompletionFinishReason = null; let usage = getLLMDefaultUsage(); - const { parsePart, getStartTagBuffer } = parseReasoningStreamContent(); + const { parsePart } = parseLLMStreamResponse(); for await (const part of stream) { usage = part.usage || usage; @@ -595,11 +600,16 @@ async function streamResponse({ break; } - const { reasoningContent, content, finishReason } = parsePart(part, aiChatReasoning); + const { reasoningContent, content, responseContent, finishReason } = parsePart({ + part, + parseThinkTag: aiChatReasoning, + retainDatasetCite + }); finish_reason = finish_reason || finishReason; answer += content; reasoning += reasoningContent; + // Reasoning response if (aiChatReasoning && reasoningContent) { workflowStreamResponse?.({ write, @@ -612,13 +622,15 @@ async function streamResponse({ if (content) { if (startResponseWrite) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: content - }) - }); + if (responseContent) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: responseContent + }) + }); + } } else if (answer.length >= 3) { answer = answer.trimStart(); if (/0(:|:)/.test(answer)) { @@ -640,22 +652,6 @@ async function streamResponse({ } } - if (answer === '') { - answer = getStartTagBuffer(); - if (/0(:|:)/.test(answer)) { - // find first : index - const firstIndex = answer.indexOf('0:') !== -1 ? answer.indexOf('0:') : answer.indexOf('0:'); - answer = answer.substring(firstIndex + 2).trim(); - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: answer - }) - }); - } - } - return { answer, reasoning, finish_reason, usage }; } diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index 2b61bd371..6a1e183b4 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -26,7 +26,12 @@ import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/in import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; -import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils'; +import { + computedMaxToken, + llmCompletionsBodyFormat, + removeDatasetCiteText, + parseLLMStreamResponse +} from '../../../../ai/utils'; import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; @@ -89,12 +94,13 @@ export const runToolWithToolChoice = async ( interactiveEntryToolParams, ...workflowProps } = props; - const { + let { res, requestOrigin, runtimeNodes, runtimeEdges, stream, + retainDatasetCite = true, externalProvider, workflowStreamResponse, params: { @@ -104,9 +110,11 @@ export const runToolWithToolChoice = async ( aiChatTopP, aiChatStopSign, aiChatResponseFormat, - aiChatJsonSchema + aiChatJsonSchema, + aiChatReasoning } } = workflowProps; + aiChatReasoning = !!aiChatReasoning && !!toolModel.reasoning; if (maxRunToolTimes <= 0 && response) { return response; @@ -279,6 +287,7 @@ export const runToolWithToolChoice = async ( messages: requestMessages, tools, tool_choice: 'auto', + parallel_tool_calls: true, temperature, max_tokens, top_p: aiChatTopP, @@ -288,7 +297,7 @@ export const runToolWithToolChoice = async ( }, toolModel ); - // console.log(JSON.stringify(filterMessages, null, 2), '==requestMessages'); + // console.log(JSON.stringify(requestBody, null, 2), '==requestMessages'); /* Run llm */ const { response: aiResponse, @@ -320,7 +329,9 @@ export const runToolWithToolChoice = async ( res, workflowStreamResponse, toolNodes, - stream: aiResponse + stream: aiResponse, + aiChatReasoning, + retainDatasetCite }); return { @@ -335,11 +346,38 @@ export const runToolWithToolChoice = async ( const finish_reason = result.choices?.[0]?.finish_reason as CompletionFinishReason; const calls = result.choices?.[0]?.message?.tool_calls || []; const answer = result.choices?.[0]?.message?.content || ''; + // @ts-ignore + const reasoningContent = result.choices?.[0]?.message?.reasoning_content || ''; const usage = result.usage; - // 加上name和avatar + if (aiChatReasoning && reasoningContent) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + reasoning_content: removeDatasetCiteText(reasoningContent, retainDatasetCite) + }) + }); + } + + // 格式化 toolCalls const toolCalls = calls.map((tool) => { const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); + + // 不支持 stream 模式的模型的这里需要补一个响应给客户端 + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: tool.id, + toolName: toolNode?.name || '', + toolAvatar: toolNode?.avatar || '', + functionName: tool.function.name, + params: tool.function?.arguments ?? '', + response: '' + } + } + }); + return { ...tool, toolName: toolNode?.name || '', @@ -347,27 +385,11 @@ export const runToolWithToolChoice = async ( }; }); - // 不支持 stream 模式的模型的流失响应 - toolCalls.forEach((tool) => { - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: tool.id, - toolName: tool.toolName, - toolAvatar: tool.toolAvatar, - functionName: tool.function.name, - params: tool.function?.arguments ?? '', - response: '' - } - } - }); - }); if (answer) { workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, data: textAdaptGptResponse({ - text: answer + text: removeDatasetCiteText(answer, retainDatasetCite) }) }); } @@ -627,12 +649,16 @@ async function streamResponse({ res, toolNodes, stream, - workflowStreamResponse + workflowStreamResponse, + aiChatReasoning, + retainDatasetCite }: { res: NextApiResponse; toolNodes: ToolNodeItemType[]; stream: StreamChatType; workflowStreamResponse?: WorkflowResponseType; + aiChatReasoning: boolean; + retainDatasetCite?: boolean; }) { const write = responseWriteController({ res, @@ -642,105 +668,130 @@ async function streamResponse({ let textAnswer = ''; let callingTool: { name: string; arguments: string } | null = null; let toolCalls: ChatCompletionMessageToolCall[] = []; - let finishReason: CompletionFinishReason = null; + let finish_reason: CompletionFinishReason = null; let usage = getLLMDefaultUsage(); + const { parsePart } = parseLLMStreamResponse(); + for await (const part of stream) { usage = part.usage || usage; if (res.closed) { stream.controller?.abort(); - finishReason = 'close'; + finish_reason = 'close'; break; } + const { + reasoningContent, + content: toolChoiceContent, + responseContent, + finishReason + } = parsePart({ + part, + parseThinkTag: true, + retainDatasetCite + }); + textAnswer += toolChoiceContent; + finish_reason = finishReason || finish_reason; + const responseChoice = part.choices?.[0]?.delta; - const finish_reason = part.choices?.[0]?.finish_reason as CompletionFinishReason; - finishReason = finishReason || finish_reason; - - if (responseChoice?.content) { - const content = responseChoice.content || ''; - textAnswer += content; + // Reasoning response + if (aiChatReasoning && reasoningContent) { workflowStreamResponse?.({ write, event: SseResponseEventEnum.answer, data: textAdaptGptResponse({ - text: content + reasoning_content: reasoningContent }) }); } - if (responseChoice?.tool_calls?.[0]) { - // @ts-ignore - const toolCall: ChatCompletionMessageToolCall = responseChoice.tool_calls[0]; - // In a stream response, only one tool is returned at a time. If have id, description is executing a tool - if (toolCall.id || callingTool) { - // Start call tool - if (toolCall.id) { - callingTool = { - name: toolCall.function?.name || '', - arguments: toolCall.function?.arguments || '' - }; - } else if (callingTool) { - // Continue call - callingTool.name += toolCall.function.name || ''; - callingTool.arguments += toolCall.function.arguments || ''; - } + if (responseContent) { + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: responseContent + }) + }); + } + // Parse tool calls + if (responseChoice?.tool_calls?.length) { + responseChoice.tool_calls.forEach((toolCall) => { + const index = toolCall.index; - const toolFunction = callingTool!; + // Call new tool + if (toolCall.id || callingTool) { + // 有 id,代表新 call 工具 + if (toolCall.id) { + callingTool = { + name: toolCall.function?.name || '', + arguments: toolCall.function?.arguments || '' + }; + } else if (callingTool) { + // Continue call(Perhaps the name of the previous function was incomplete) + callingTool.name += toolCall.function?.name || ''; + callingTool.arguments += toolCall.function?.arguments || ''; + } - const toolNode = toolNodes.find((item) => item.nodeId === toolFunction.name); + if (!callingTool) { + return; + } - if (toolNode) { - // New tool, add to list. - const toolId = getNanoid(); - toolCalls.push({ - ...toolCall, - id: toolId, - type: 'function', - function: toolFunction, - toolName: toolNode.name, - toolAvatar: toolNode.avatar - }); + const toolNode = toolNodes.find((item) => item.nodeId === callingTool!.name); - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: toolId, - toolName: toolNode.name, - toolAvatar: toolNode.avatar, - functionName: toolFunction.name, - params: toolFunction?.arguments ?? '', - response: '' + if (toolNode) { + // New tool, add to list. + const toolId = getNanoid(); + toolCalls[index] = { + ...toolCall, + id: toolId, + type: 'function', + function: callingTool, + toolName: toolNode.name, + toolAvatar: toolNode.avatar + }; + + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: toolId, + toolName: toolNode.name, + toolAvatar: toolNode.avatar, + functionName: callingTool.name, + params: callingTool?.arguments ?? '', + response: '' + } } - } - }); - callingTool = null; - } - } else { - /* arg 插入最后一个工具的参数里 */ - const arg: string = toolCall?.function?.arguments ?? ''; - const currentTool = toolCalls[toolCalls.length - 1]; - if (currentTool && arg) { - currentTool.function.arguments += arg; + }); + callingTool = null; + } + } else { + /* arg 追加到当前工具的参数里 */ + const arg: string = toolCall?.function?.arguments ?? ''; + const currentTool = toolCalls[index]; + if (currentTool && arg) { + currentTool.function.arguments += arg; - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.toolParams, - data: { - tool: { - id: currentTool.id, - toolName: '', - toolAvatar: '', - params: arg, - response: '' + workflowStreamResponse?.({ + write, + event: SseResponseEventEnum.toolParams, + data: { + tool: { + id: currentTool.id, + toolName: '', + toolAvatar: '', + params: arg, + response: '' + } } - } - }); + }); + } } - } + }); } } - return { answer: textAnswer, toolCalls, finish_reason: finishReason, usage }; + return { answer: textAnswer, toolCalls: toolCalls.filter(Boolean), finish_reason, usage }; } diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index a7514cf67..50a2b030d 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -4,7 +4,11 @@ import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/co 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 } from '../../../ai/utils'; +import { + removeDatasetCiteText, + parseReasoningContent, + parseLLMStreamResponse +} from '../../../ai/utils'; import { createChatCompletion } from '../../../ai/config'; import type { ChatCompletionMessageParam, @@ -75,7 +79,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { @@ -223,7 +226,8 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise // User role or prompt include question const quoteRole = aiChatQuoteRole === 'user' || datasetQuotePrompt.includes('{{question}}') ? 'user' : 'system'; - const defaultQuotePrompt = getQuotePrompt(version, quoteRole, parseQuote); + const defaultQuotePrompt = getQuotePrompt(version, quoteRole); const datasetQuotePromptTemplate = datasetQuotePrompt || defaultQuotePrompt; @@ -539,7 +539,8 @@ async function streamResponse({ workflowStreamResponse, aiChatReasoning, parseThinkTag, - isResponseAnswerText + isResponseAnswerText, + retainDatasetCite = true }: { res: NextApiResponse; stream: StreamChatType; @@ -547,6 +548,7 @@ async function streamResponse({ aiChatReasoning?: boolean; parseThinkTag?: boolean; isResponseAnswerText?: boolean; + retainDatasetCite: boolean; }) { const write = responseWriteController({ res, @@ -557,7 +559,7 @@ async function streamResponse({ let finish_reason: CompletionFinishReason = null; let usage: CompletionUsage = getLLMDefaultUsage(); - const { parsePart, getStartTagBuffer } = parseReasoningStreamContent(); + const { parsePart } = parseLLMStreamResponse(); for await (const part of stream) { usage = part.usage || usage; @@ -568,7 +570,11 @@ async function streamResponse({ break; } - const { reasoningContent, content, finishReason } = parsePart(part, parseThinkTag); + const { reasoningContent, content, responseContent, finishReason } = parsePart({ + part, + parseThinkTag, + retainDatasetCite + }); finish_reason = finish_reason || finishReason; answer += content; reasoning += reasoningContent; @@ -583,26 +589,12 @@ async function streamResponse({ }); } - if (isResponseAnswerText && content) { + if (isResponseAnswerText && responseContent) { workflowStreamResponse?.({ write, event: SseResponseEventEnum.answer, data: textAdaptGptResponse({ - text: content - }) - }); - } - } - - // if answer is empty, try to get value from startTagBuffer. (Cause: The response content is too short to exceed the minimum parse length) - if (answer === '') { - answer = getStartTagBuffer(); - if (isResponseAnswerText && answer) { - workflowStreamResponse?.({ - write, - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: answer + text: responseContent }) }); } diff --git a/packages/service/core/workflow/dispatch/dataset/concat.ts b/packages/service/core/workflow/dispatch/dataset/concat.ts index f11a63b42..0177ca8c8 100644 --- a/packages/service/core/workflow/dispatch/dataset/concat.ts +++ b/packages/service/core/workflow/dispatch/dataset/concat.ts @@ -21,7 +21,7 @@ export async function dispatchDatasetConcat( props: DatasetConcatProps ): Promise { const { - params: { limit = 1500, ...quoteMap } + params: { limit = 6000, ...quoteMap } } = props as DatasetConcatProps; const quoteList = Object.values(quoteMap).filter((list) => Array.isArray(list)); diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index 84f658520..a4c7331a0 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -55,11 +55,10 @@ export async function dispatchDatasetSearch( runningUserInfo: { tmbId }, histories, node, - parseQuote = true, params: { datasets = [], similarity, - limit = 1500, + limit = 5000, userChatInput = '', authTmbId = false, collectionFilterMatch, @@ -114,7 +113,6 @@ export async function dispatchDatasetSearch( if (datasetIds.length === 0) { return emptyResult; } - // console.log(concatQueries, rewriteQuery, aiExtensionResult); // get vector const vectorModel = getEmbeddingModel( @@ -267,7 +265,7 @@ export async function dispatchDatasetSearch( [DispatchNodeResponseKeyEnum.nodeResponse]: responseData, nodeDispatchUsages, [DispatchNodeResponseKeyEnum.toolResponses]: { - prompt: getDatasetSearchToolResponsePrompt(parseQuote), + prompt: getDatasetSearchToolResponsePrompt(), quotes: searchRes.map((item) => ({ id: item.id, sourceName: item.sourceName, diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index c21bb96eb..20bdaa312 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -135,7 +135,7 @@ export async function dispatchWorkFlow(data: Props): Promise void; event: SseResponseEventEnum; data: Record; - stream?: boolean; // Focus set stream response }) => { - const useStreamResponse = stream ?? streamResponse; + const useStreamResponse = streamResponse; if (!res || res.closed || !useStreamResponse) return; // Forbid show detail - const detailEvent: Record = { - [SseResponseEventEnum.error]: 1, - [SseResponseEventEnum.flowNodeStatus]: 1, - [SseResponseEventEnum.flowResponses]: 1, - [SseResponseEventEnum.interactive]: 1, - [SseResponseEventEnum.toolCall]: 1, - [SseResponseEventEnum.toolParams]: 1, - [SseResponseEventEnum.toolResponse]: 1, - [SseResponseEventEnum.updateVariables]: 1, - [SseResponseEventEnum.flowNodeResponse]: 1 + const notDetailEvent: Record = { + [SseResponseEventEnum.answer]: 1, + [SseResponseEventEnum.fastAnswer]: 1 }; - if (!detail && detailEvent[event]) return; + if (!detail && !notDetailEvent[event]) return; // Forbid show running status const statusEvent: Record = { diff --git a/packages/templates/src/CQ/template.json b/packages/templates/src/CQ/template.json index 6c04a80d5..7909383eb 100644 --- a/packages/templates/src/CQ/template.json +++ b/packages/templates/src/CQ/template.json @@ -308,7 +308,7 @@ "key": "limit", "renderTypeList": ["hidden"], "label": "", - "value": 1500, + "value": 5000, "valueType": "number" }, { diff --git a/packages/templates/src/simpleDatasetChat/template.json b/packages/templates/src/simpleDatasetChat/template.json index 4c88b3086..93f5aefa1 100644 --- a/packages/templates/src/simpleDatasetChat/template.json +++ b/packages/templates/src/simpleDatasetChat/template.json @@ -211,7 +211,7 @@ "key": "limit", "renderTypeList": ["hidden"], "label": "", - "value": 1500, + "value": 5000, "valueType": "number" }, { diff --git a/projects/app/src/components/Markdown/A.tsx b/projects/app/src/components/Markdown/A.tsx index 9336467a3..750112cdb 100644 --- a/projects/app/src/components/Markdown/A.tsx +++ b/projects/app/src/components/Markdown/A.tsx @@ -21,16 +21,16 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import { getCollectionSourceData } from '@fastgpt/global/core/dataset/collection/utils'; import Markdown from '.'; import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils'; +import { Types } from 'mongoose'; -const A = ({ children, chatAuthData, ...props }: any) => { +const A = ({ children, chatAuthData, showAnimation, ...props }: any) => { const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); + const content = useMemo(() => String(children), [children]); // empty href link if (!props.href && typeof children?.[0] === 'string') { - const text = useMemo(() => String(children), [children]); - return ( ); } - // Quote - if (props.href?.startsWith('QUOTE') && typeof children?.[0] === 'string') { + // Cite + if ( + (props.href?.startsWith('CITE') || props.href?.startsWith('QUOTE')) && + typeof children?.[0] === 'string' + ) { + if (!Types.ObjectId.isValid(content)) { + return <>; + } + const { data: quoteData, loading, @@ -74,6 +81,7 @@ const A = ({ children, chatAuthData, ...props }: any) => { onClose={onClose} onOpen={() => { onOpen(); + if (showAnimation) return; getQuoteDataById(String(children)); }} trigger={'hover'} @@ -90,7 +98,7 @@ const A = ({ children, chatAuthData, ...props }: any) => { - + diff --git a/projects/app/src/components/Markdown/index.tsx b/projects/app/src/components/Markdown/index.tsx index a7d71d5ae..051c4e8e6 100644 --- a/projects/app/src/components/Markdown/index.tsx +++ b/projects/app/src/components/Markdown/index.tsx @@ -60,9 +60,9 @@ const MarkdownRender = ({ img: Image, pre: RewritePre, code: Code, - a: (props: any) => + a: (props: any) => }; - }, [chatAuthData]); + }, [chatAuthData, showAnimation]); const formatSource = useMemo(() => { if (showAnimation || forbidZhFormat) return source; diff --git a/projects/app/src/components/Markdown/utils.ts b/projects/app/src/components/Markdown/utils.ts index bc0122303..d274be4ac 100644 --- a/projects/app/src/components/Markdown/utils.ts +++ b/projects/app/src/components/Markdown/utils.ts @@ -27,14 +27,14 @@ export const mdTextFormat = (text: string) => { return match; }); - // 处理 [quote:id] 格式引用,将 [quote:675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](QUOTE) + // 处理 [quote:id] 格式引用,将 [quote:675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](CITE) text = text // .replace( // /([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g, // '$1$3 $2$4' // ) - // 处理 格式引用,将 [675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](QUOTE) - .replace(/\[([a-f0-9]{24})\](?!\()/g, '[$1](QUOTE)'); + // 处理 格式引用,将 [675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](CITE) + .replace(/\[([a-f0-9]{24})\](?!\()/g, '[$1](CITE)'); // 处理链接后的中文标点符号,增加空格 text = text.replace(/(https?:\/\/[^\s,。!?;:、]+)([,。!?;:、])/g, '$1 $2'); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index d9d9a790b..43ca56112 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -240,11 +240,6 @@ const ChatItem = (props: Props) => { quoteId?: string; }) => { if (!setQuoteData) return; - if (isChatting) - return toast({ - title: t('chat:chat.waiting_for_response'), - status: 'info' - }); const collectionIdList = collectionId ? [collectionId] @@ -277,18 +272,7 @@ const ChatItem = (props: Props) => { } }); }, - [ - setQuoteData, - isChatting, - toast, - t, - quoteList, - isShowReadRawSource, - appId, - chatId, - chat.dataId, - outLinkAuthData - ] + [setQuoteData, quoteList, isShowReadRawSource, appId, chatId, chat.dataId, outLinkAuthData] ); useEffect(() => { diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 1c1e0a08c..d5ca6aa43 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -96,8 +96,6 @@ const RenderText = React.memo(function RenderText({ text: string; chatItemDataId: string; }) { - const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail); - const appId = useContextSelector(ChatBoxContext, (v) => v.appId); const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId); const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData); @@ -106,10 +104,8 @@ const RenderText = React.memo(function RenderText({ if (!text) return ''; // Remove quote references if not showing response detail - return isResponseDetail - ? text - : text.replace(/\[([a-f0-9]{24})\]\(QUOTE\)/g, '').replace(/\[([a-f0-9]{24})\](?!\()/g, ''); - }, [text, isResponseDetail]); + return text; + }, [text]); const chatAuthData = useCreation(() => { return { appId, chatId, chatItemDataId, ...outLinkAuthData }; diff --git a/projects/app/src/components/core/dataset/SearchParamsTip.tsx b/projects/app/src/components/core/dataset/SearchParamsTip.tsx index 526f10b0c..e231ca963 100644 --- a/projects/app/src/components/core/dataset/SearchParamsTip.tsx +++ b/projects/app/src/components/core/dataset/SearchParamsTip.tsx @@ -12,7 +12,7 @@ import { getWebLLMModel } from '@/web/common/system/utils'; const SearchParamsTip = ({ searchMode, similarity = 0, - limit = 1500, + limit = 5000, responseEmptyText, usingReRank = false, datasetSearchUsingExtensionQuery, diff --git a/projects/app/src/pageComponents/account/info/UpdatePswModal.tsx b/projects/app/src/pageComponents/account/info/UpdatePswModal.tsx index e517e0a38..c49bdf60a 100644 --- a/projects/app/src/pageComponents/account/info/UpdatePswModal.tsx +++ b/projects/app/src/pageComponents/account/info/UpdatePswModal.tsx @@ -5,8 +5,8 @@ import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { updatePasswordByOld } from '@/web/support/user/api'; -import { checkPasswordRule } from '@/web/support/user/login/constants'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { checkPasswordRule } from '@fastgpt/global/common/string/password'; type FormType = { oldPsw: string; diff --git a/projects/app/src/pageComponents/login/ForgetPasswordForm.tsx b/projects/app/src/pageComponents/login/ForgetPasswordForm.tsx index abf37eb1f..82fb9aa60 100644 --- a/projects/app/src/pageComponents/login/ForgetPasswordForm.tsx +++ b/projects/app/src/pageComponents/login/ForgetPasswordForm.tsx @@ -1,7 +1,7 @@ import React, { Dispatch } from 'react'; import { FormControl, Box, Input, Button } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; -import { LoginPageTypeEnum, checkPasswordRule } from '@/web/support/user/login/constants'; +import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { postFindPassword } from '@/web/support/user/api'; import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import type { ResLogin } from '@/global/support/api/userRes.d'; @@ -9,6 +9,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useTranslation } from 'next-i18next'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { checkPasswordRule } from '@fastgpt/global/common/string/password'; interface Props { setPageType: Dispatch<`${LoginPageTypeEnum}`>; diff --git a/projects/app/src/pageComponents/login/RegisterForm.tsx b/projects/app/src/pageComponents/login/RegisterForm.tsx index 647241f14..2cd5f75b5 100644 --- a/projects/app/src/pageComponents/login/RegisterForm.tsx +++ b/projects/app/src/pageComponents/login/RegisterForm.tsx @@ -1,7 +1,7 @@ import React, { Dispatch } from 'react'; import { FormControl, Box, Input, Button } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; -import { LoginPageTypeEnum, checkPasswordRule } from '@/web/support/user/login/constants'; +import { LoginPageTypeEnum } from '@/web/support/user/login/constants'; import { postRegister } from '@/web/support/user/api'; import { useSendCode } from '@/web/support/user/hooks/useSendCode'; import type { ResLogin } from '@/global/support/api/userRes'; @@ -19,6 +19,7 @@ import { getSourceDomain, removeFastGPTSem } from '@/web/support/marketing/utils'; +import { checkPasswordRule } from '@fastgpt/global/common/string/password'; interface Props { loginSuccess: (e: ResLogin) => void; diff --git a/projects/app/src/pages/api/core/ai/model/test.ts b/projects/app/src/pages/api/core/ai/model/test.ts index 7b96281a1..2756afd97 100644 --- a/projects/app/src/pages/api/core/ai/model/test.ts +++ b/projects/app/src/pages/api/core/ai/model/test.ts @@ -16,7 +16,7 @@ import { reRankRecall } from '@fastgpt/service/core/ai/rerank'; import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions'; import { isProduction } from '@fastgpt/global/common/system/constants'; import * as fs from 'fs'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '@fastgpt/service/core/ai/utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils'; export type testQuery = { model: string; channelId?: number }; @@ -78,7 +78,7 @@ const testLLMModel = async (model: LLMModelItemType, headers: Record { + if (item.obj === ChatRoleEnum.AI) { + item.value = removeAIResponseCite(item.value, false); + } + }); + } return { list: isPlugin ? histories : transformPreviewHistories(histories, responseDetail), diff --git a/projects/app/src/pages/api/core/dataset/searchTest.ts b/projects/app/src/pages/api/core/dataset/searchTest.ts index ae82144c8..316a67b49 100644 --- a/projects/app/src/pages/api/core/dataset/searchTest.ts +++ b/projects/app/src/pages/api/core/dataset/searchTest.ts @@ -19,7 +19,7 @@ async function handler(req: ApiRequestProps): Promise; // Global variables or plugin inputs }; @@ -107,7 +108,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { stream = false, detail = false, - parseQuote = false, + retainDatasetCite = false, messages = [], variables = {}, responseChatItemId = getNanoid(), @@ -187,6 +188,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatId }); })(); + retainDatasetCite = retainDatasetCite && !!responseDetail; const isPlugin = app.type === AppTypeEnum.plugin; // Check message type @@ -291,7 +293,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatConfig, histories: newHistories, stream, - parseQuote, + retainDatasetCite, maxRunTimes: WORKFLOW_MAX_RUN_TIMES, workflowStreamResponse: workflowResponseWrite }); @@ -406,17 +408,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return assistantResponses; })(); + const formatResponseContent = removeAIResponseCite(responseContent, retainDatasetCite); const error = flowResponses[flowResponses.length - 1]?.error; res.json({ ...(detail ? { responseData: feResponseData, newVariables } : {}), error, - id: chatId || '', + id: saveChatId, model: '', usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 }, choices: [ { - message: { role: 'assistant', content: responseContent }, + message: { role: 'assistant', content: formatResponseContent }, finish_reason: 'stop', index: 0 } diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index f463ef465..1925b9fb5 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -30,6 +30,7 @@ import { concatHistories, filterPublicNodeResponseData, getChatTitleFromChatMessage, + removeAIResponseCite, removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools'; @@ -74,7 +75,7 @@ export type Props = ChatCompletionCreateParams & responseChatItemId?: string; stream?: boolean; detail?: boolean; - parseQuote?: boolean; + retainDatasetCite?: boolean; variables: Record; // Global variables or plugin inputs }; @@ -107,7 +108,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { stream = false, detail = false, - parseQuote = false, + retainDatasetCite = false, messages = [], variables = {}, responseChatItemId = getNanoid(), @@ -187,6 +188,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatId }); })(); + retainDatasetCite = retainDatasetCite && !!responseDetail; const isPlugin = app.type === AppTypeEnum.plugin; // Check message type @@ -290,7 +292,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatConfig, histories: newHistories, stream, - parseQuote, + retainDatasetCite, maxRunTimes: WORKFLOW_MAX_RUN_TIMES, workflowStreamResponse: workflowResponseWrite, version: 'v2', @@ -401,6 +403,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return assistantResponses; })(); + const formatResponseContent = removeAIResponseCite(responseContent, retainDatasetCite); const error = flowResponses[flowResponses.length - 1]?.error; res.json({ @@ -411,7 +414,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 }, choices: [ { - message: { role: 'assistant', content: responseContent }, + message: { role: 'assistant', content: formatResponseContent }, finish_reason: 'stop', index: 0 } diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index 183377d30..640244f1e 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -87,6 +87,7 @@ const OutLink = (props: Props) => { const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData); const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData); + const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); @@ -162,7 +163,8 @@ const OutLink = (props: Props) => { }, responseChatItemId, chatId: completionChatId, - ...outLinkAuthData + ...outLinkAuthData, + retainDatasetCite: isResponseDetail }, onMessage: generatingMessage, abortCtrl: controller @@ -200,6 +202,7 @@ const OutLink = (props: Props) => { chatId, customVariables, outLinkAuthData, + isResponseDetail, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat, diff --git a/projects/app/src/service/events/generateQA.ts b/projects/app/src/service/events/generateQA.ts index 8356cc8d6..18f738328 100644 --- a/projects/app/src/service/events/generateQA.ts +++ b/projects/app/src/service/events/generateQA.ts @@ -17,7 +17,7 @@ import { } from '@fastgpt/service/common/string/tiktoken/index'; import { pushDataListToTrainingQueueByCollectionId } from '@fastgpt/service/core/dataset/training/controller'; import { loadRequestMessages } from '@fastgpt/service/core/chat/utils'; -import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '@fastgpt/service/core/ai/utils'; +import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils'; import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { chunkAutoChunkSize, @@ -140,7 +140,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`; modelData ) }); - const { text: answer, usage } = await llmResponseToAnswerText(chatResponse); + const { text: answer, usage } = await formatLLMResponse(chatResponse); const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(messages)); const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer)); diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts index f535e1c44..529f71b7f 100644 --- a/projects/app/src/service/support/mcp/utils.ts +++ b/projects/app/src/service/support/mcp/utils.ts @@ -37,6 +37,7 @@ import { saveChat } from '@fastgpt/service/core/chat/saveChat'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { removeDatasetCiteText } from '@fastgpt/service/core/ai/utils'; export const pluginNodes2InputSchema = ( nodes: { flowNodeType: FlowNodeTypeEnum; inputs: FlowNodeInputItemType[] }[] @@ -288,7 +289,7 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps })(); // Format response content - responseContent = responseContent.trim().replace(/\[\w+\]\(QUOTE\)/g, ''); + responseContent = removeDatasetCiteText(responseContent.trim(), false); return responseContent; }; diff --git a/projects/app/src/web/common/api/fetch.ts b/projects/app/src/web/common/api/fetch.ts index 0758091ec..31d4485da 100644 --- a/projects/app/src/web/common/api/fetch.ts +++ b/projects/app/src/web/common/api/fetch.ts @@ -132,7 +132,7 @@ export const streamFetch = ({ variables, detail: true, stream: true, - parseQuote: true + retainDatasetCite: data.retainDatasetCite ?? true }) }; diff --git a/projects/app/src/web/support/user/login/constants.ts b/projects/app/src/web/support/user/login/constants.ts index 0ec533b1c..ca24fc4df 100644 --- a/projects/app/src/web/support/user/login/constants.ts +++ b/projects/app/src/web/support/user/login/constants.ts @@ -4,22 +4,3 @@ export enum LoginPageTypeEnum { forgetPassword = 'forgetPassword', wechat = 'wechat' } - -export const checkPasswordRule = (password: string) => { - const patterns = [ - /\d/, // Contains digits - /[a-z]/, // Contains lowercase letters - /[A-Z]/, // Contains uppercase letters - /[!@#$%^&*()_+=-]/ // Contains special characters - ]; - const validChars = /^[\dA-Za-z!@#$%^&*()_+=-]{6,100}$/; - - // Check length and valid characters - if (!validChars.test(password)) return false; - - // Count how many patterns are satisfied - const matchCount = patterns.filter((pattern) => pattern.test(password)).length; - - // Must satisfy at least 2 patterns - return matchCount >= 2; -}; diff --git a/test/cases/components/Markdown/utils.test.ts b/test/cases/components/Markdown/utils.test.ts index 3d9737924..53b73fc3e 100644 --- a/test/cases/components/Markdown/utils.test.ts +++ b/test/cases/components/Markdown/utils.test.ts @@ -16,7 +16,7 @@ describe('Markdown utils', () => { it('should convert quote references to proper markdown links', () => { const input = '[123456789012345678901234]'; - const expected = '[123456789012345678901234](QUOTE)'; + const expected = '[123456789012345678901234](CITE)'; expect(mdTextFormat(input)).toBe(expected); }); @@ -35,7 +35,7 @@ describe('Markdown utils', () => { const input = 'Math \\[x^2\\] with link https://test.com,and quote [123456789012345678901234]'; const expected = - 'Math $$x^2$$ with link https://test.com ,and quote [123456789012345678901234](QUOTE)'; + 'Math $$x^2$$ with link https://test.com ,and quote [123456789012345678901234](CITE)'; expect(mdTextFormat(input)).toBe(expected); }); }); diff --git a/test/cases/function/packages/global/common/string/chunks.json b/test/cases/function/packages/global/common/string/chunks.json deleted file mode 100644 index 1b9a525b4..000000000 --- a/test/cases/function/packages/global/common/string/chunks.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "测试的呀,第一个表格\n\n| 序号 | 姓名 | 年龄 | 职业 | 城市 |\n| --- | --- | --- | --- | --- |\n| 1 | 张三 | 25 | 工程师 | 北京 |\n| 2 | 李四 | 30 | 教师 | 上海 |\n| 3 | 王五 | 28 | 医生 | 广州 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 4 | 赵六 | 35 | 律师 | 深圳 |\n| 5 | 孙七 | 27 | 设计师 | 杭州 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 7 | 吴九 | 29 | 销售 | 武汉 |\n| 8 | 郑十 | 31 | 记者 | 南京 |\n| 9 | 刘一 | 33 | 建筑师 | 天津 |\n| 10 | 陈二 | 26 | 程序员 | 重庆 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1001 | 杨一 | 34 | 程序员 | 厦门 |\n| 1002 | 杨二 | 34 | 程序员 | 厦门 |\n| 1003 | 杨三 | 34 | 程序员 | 厦门 |", - "| 序号 | 姓名 | 年龄 | 职业 | 城市 |\n| --- | --- | --- | --- | --- |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 1004 | 杨四 | 34 | 程序员 | 厦门 |\n| 1005 | 杨五 | 34 | 程序员 | 厦门 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 9 | 刘一 | 33 | 建筑师 | 天津 |\n| 10 | 陈二 | 26 | 程序员 | 重庆 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1001 | 杨一 | 34 | 程序员 | 厦门 |\n| 1002 | 杨二 | 34 | 程序员 | 厦门 |\n| 1003 | 杨三 | 34 | 程序员 | 厦门 |\n| 1004 | 杨四 | 34 | 程序员 | 厦门 |\n| 1005 | 杨五 | 34 | 程序员 | 厦门 |\n\n| 序号 | 姓名 | 年龄 | 职业 | 城市 |\n| --- | --- | --- | --- | --- |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |\n| 1000 | 黄末 | 28 | 作家 | 厦门 |", - "这是第二段了,第二表格\n\n| 序号 | 姓名 | 年龄 | 职业 | 城市 |\n| --- | --- | --- | --- | --- |\n| 1 | 张三 | 25 | 工程师 | 北京 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 2 | 李四 | 30 | 教师 | 上海 |\n| 3 | 王五 | 28 | 医生 | 广州 |\n| 4 | 赵六 | 35 | 律师 | 深圳 |\n| 5 | 孙七 | 27 | 设计师 | 杭州 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 7 | 吴九 | 29 | 销售 | 武汉 |\n| 8 | 郑十 | 31 | 记者 | 南京 |\n| 9 | 刘一 | 33 | 建筑师 | 天津 |\n| 10 | 陈二 | 26 | 程序员 | 重庆 |\n| 10004 | 黄末 | 28 | 作家 | 厦门 |\n| 10013 | 杨一 | 34 | 程序员 | 厦门 |\n\n\n结束了\n\n| 序号22 | 姓名 | 年龄 | 职业 | 城市 |\n| --- | --- | --- | --- | --- |\n| 1 | 张三 | 25 | 工程师 | 北京 |\n| 2 | 李四 | 30 | 教师 | 上海 |\n| 3 | 王五 | 28 | 医生 | 广州 |\n| 4 | 赵六 | 35 | 律师 | 深圳 |\n| 5 | 孙七 | 27 | 设计师 | 杭州 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 6 | 周八 | 32 | 会计 | 成都 |\n| 7 | 吴九 | 29 | 销售 | 武汉 |\n| 8 | 郑十 | 31 | 记者 | 南京 |\n| 9 | 刘一 | 33 | 建筑师 | 天津 |\n| 10 | 陈二 | 26 | 程序员 | 重庆 |\n| 10002 | 黄末 | 28 | 作家 | 厦门 |\n| 10012 | 杨一 | 34 | 程序员 | 厦门 |" -] \ No newline at end of file diff --git a/test/cases/global/common/string/utils.test.ts b/test/cases/function/packages/global/common/string/password.test.ts similarity index 95% rename from test/cases/global/common/string/utils.test.ts rename to test/cases/function/packages/global/common/string/password.test.ts index 1a0322fda..3c84913ec 100644 --- a/test/cases/global/common/string/utils.test.ts +++ b/test/cases/function/packages/global/common/string/password.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { checkPasswordRule } from '@/web/support/user/login/constants'; +import { checkPasswordRule } from '@fastgpt/global/common/string/password'; describe('PasswordRule', () => { it('should be a valid password', () => { diff --git a/test/cases/function/packages/global/common/string/textSplitter.test.ts b/test/cases/function/packages/global/common/string/textSplitter.test.ts index 2437bddf3..6b889b473 100644 --- a/test/cases/function/packages/global/common/string/textSplitter.test.ts +++ b/test/cases/function/packages/global/common/string/textSplitter.test.ts @@ -1,6 +1,5 @@ import { it, expect } from 'vitest'; // 必须显式导入 import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter'; -import * as fs from 'fs'; const simpleChunks = (chunks: string[]) => { return chunks.map((chunk) => chunk.replace(/\s+/g, '')); diff --git a/test/cases/function/packages/service/core/ai/parseStreamResponse.test.ts b/test/cases/function/packages/service/core/ai/parseStreamResponse.test.ts new file mode 100644 index 000000000..38e625ff9 --- /dev/null +++ b/test/cases/function/packages/service/core/ai/parseStreamResponse.test.ts @@ -0,0 +1,340 @@ +import { CompletionFinishReason } from '@fastgpt/global/core/ai/type'; +import { parseLLMStreamResponse } from '@fastgpt/service/core/ai/utils'; +import { describe, expect, it } from 'vitest'; + +describe('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: '过程 { + it(`Reasoning test:${index}`, () => { + const { parsePart } = parseLLMStreamResponse(); + + 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({ + part: formatPart, + parseThinkTag: true, + retainDatasetCite: false + }); + answer += content; + reasoning += reasoningContent; + }); + expect(answer).toBe(part.correct.answer); + expect(reasoning).toBe(part.correct.reasoning); + }); + }); +}); + +describe('Parse dataset cite content test', async () => { + const partList = [ + { + // 完整的 + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE)' } + ], + correct: { + content: '知识库问答系统[67e517e74767063e882d6861](CITE)', + responseContent: '知识库问答系统' + } + }, + { + // 缺失结尾 + data: [ + { content: '知识库问答系统' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE' } + ], + correct: { + content: '知识库问答系统[67e517e74767063e882d6861](CITE', + responseContent: '知识库问答系统[67e517e74767063e882d6861](CITE' + } + }, + { + // ObjectId 不正确 + data: [ + { content: '知识库问答系统' }, + { content: '[67e517e747' }, + { content: '67882d' }, + { content: '6861](CITE)' } + ], + correct: { + content: '知识库问答系统[67e517e74767882d6861](CITE)', + responseContent: '知识库问答系统[67e517e74767882d6861](CITE)' + } + }, + { + // 其他链接 + data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[](https://fastgpt.cn)' }], + correct: { + content: '知识库问答系统[](https://fastgpt.cn)', + responseContent: '知识库问答系统[](https://fastgpt.cn)' + } + }, + { + // 不完整的其他链接 + data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }], + correct: { + content: '知识库问答系统[](https://fastgp', + responseContent: '知识库问答系统[](https://fastgp' + } + }, + { + // 开头 + data: [{ content: '[知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }], + correct: { + content: '[知识库问答系统[](https://fastgp', + responseContent: '[知识库问答系统[](https://fastgp' + } + }, + { + // 结尾 + data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[' }], + correct: { + content: '知识库问答系统[', + responseContent: '知识库问答系统[' + } + }, + { + // 中间 + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[' }, + { content: '问答系统]' } + ], + correct: { + content: '知识库问答系统[问答系统]', + responseContent: '知识库问答系统[问答系统]' + } + }, + { + // 双链接 + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[](https://fastgpt.cn)' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE)' } + ], + correct: { + content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CITE)', + responseContent: '知识库问答系统[](https://fastgpt.cn)' + } + }, + { + // 双链接缺失部分 + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[](https://fastgpt.cn)' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CIT' } + ], + correct: { + content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CIT', + responseContent: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CIT' + } + }, + { + // 双Cite + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE)' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE)' } + ], + correct: { + content: '知识库问答系统[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6861](CITE)', + responseContent: '知识库问答系统' + } + }, + { + // 双Cite-第一个假Cite + data: [ + { content: '知识库' }, + { content: '问答系统' }, + { content: '[67e517e747' }, + { content: '6861](CITE)' }, + { content: '[67e517e747' }, + { content: '67063e882d' }, + { content: '6861](CITE)' } + ], + correct: { + content: '知识库问答系统[67e517e7476861](CITE)[67e517e74767063e882d6861](CITE)', + responseContent: '知识库问答系统[67e517e7476861](CITE)' + } + } + ]; + + partList.forEach((part, index) => { + it(`Dataset cite test: ${index}`, () => { + const { parsePart } = parseLLMStreamResponse(); + + let answer = ''; + let responseContent = ''; + part.data.forEach((item, index) => { + const formatPart = { + choices: [ + { + delta: { + role: 'assistant', + content: item.content, + reasoning_content: '' + }, + finish_reason: (index === part.data.length - 1 + ? 'stop' + : null) as CompletionFinishReason + } + ] + }; + const { content, responseContent: newResponseContent } = parsePart({ + part: formatPart, + parseThinkTag: false, + retainDatasetCite: false + }); + answer += content; + responseContent += newResponseContent; + }); + + expect(answer).toEqual(part.correct.content); + expect(responseContent).toEqual(part.correct.responseContent); + }); + }); +});