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: 'think>' },
- { 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: 'think>' },
+ { 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);
+ });
+ });
+});