From 54defd8a3c1199cff6a13798a37556380dfb3bf5 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Sat, 1 Feb 2025 18:04:44 +0800 Subject: [PATCH] perf: max_token count;feat: support resoner output;fix: member scroll (#3681) * perf: supplement assistant empty response * check array * perf: max_token count * feat: support resoner output * member scroll * update provider order * i18n --- .../zh-cn/docs/development/upgrading/4820.md | 17 ++- packages/global/core/ai/model.d.ts | 3 +- packages/global/core/ai/provider.ts | 2 +- packages/global/core/app/type.d.ts | 1 + packages/global/core/chat/constants.ts | 3 +- packages/global/core/chat/type.d.ts | 11 +- packages/global/core/workflow/constants.ts | 4 +- .../global/core/workflow/runtime/type.d.ts | 1 + .../global/core/workflow/runtime/utils.ts | 13 +- .../workflow/template/system/aiChat/index.ts | 11 +- .../core/workflow/template/system/tools.ts | 4 +- .../service/common/file/image/controller.ts | 2 +- .../core/ai/config/provider/DeepSeek.json | 9 +- .../core/ai/config/provider/OpenAI.json | 28 ++-- .../core/ai/functions/queryExtension.ts | 127 +++++++++++------- packages/service/core/ai/utils.ts | 21 +-- packages/service/core/chat/utils.ts | 31 +---- .../core/workflow/dispatch/agent/extract.ts | 6 +- .../dispatch/agent/runTool/functionCall.ts | 17 ++- .../dispatch/agent/runTool/promptCall.ts | 17 ++- .../dispatch/agent/runTool/toolChoice.ts | 18 +-- .../core/workflow/dispatch/chat/oneapi.ts | 93 ++++++++----- .../service/core/workflow/dispatch/index.ts | 9 ++ .../web/components/common/Icon/constants.ts | 1 + .../common/Icon/icons/core/chat/think.svg | 1 + packages/web/hooks/useScrollPagination.tsx | 2 +- packages/web/i18n/en/account.json | 4 +- packages/web/i18n/en/app.json | 1 + packages/web/i18n/en/chat.json | 1 + packages/web/i18n/zh-CN/account.json | 4 +- packages/web/i18n/zh-CN/app.json | 1 + packages/web/i18n/zh-CN/chat.json | 1 + packages/web/i18n/zh-Hant/account.json | 4 +- packages/web/i18n/zh-Hant/app.json | 1 + packages/web/i18n/zh-Hant/chat.json | 1 + .../core/ai/AISettingModal/index.tsx | 78 ++++++----- .../core/chat/ChatContainer/ChatBox/index.tsx | 20 +++ .../core/chat/ChatContainer/type.d.ts | 1 + .../core/chat/components/AIResponseBox.tsx | 52 +++++++ .../account/model/ModelConfigTable.tsx | 8 ++ .../team/GroupManage/GroupManageMember.tsx | 8 +- .../account/team/MemberTable.tsx | 10 +- .../team/OrgManage/OrgMemberManageModal.tsx | 58 ++++---- .../RenderInput/templates/SettingLLMModel.tsx | 4 +- projects/app/src/web/common/api/fetch.ts | 6 + projects/app/src/web/core/app/templates.ts | 13 +- 46 files changed, 462 insertions(+), 266 deletions(-) create mode 100644 packages/web/components/common/Icon/icons/core/chat/think.svg diff --git a/docSite/content/zh-cn/docs/development/upgrading/4820.md b/docSite/content/zh-cn/docs/development/upgrading/4820.md index ecf160a9b..07b7ac301 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4820.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4820.md @@ -31,9 +31,14 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4820' \ ## 完整更新内容 1. 新增 - 可视化模型参数配置。预设超过 100 个模型配置。同时支持所有类型模型的一键测试。(预计下个版本会完全支持在页面上配置渠道)。 -2. 新增 - 使用记录导出和仪表盘。 -3. 新增 - markdown 语法扩展,支持音视频(代码块 audio 和 video)。 -4. 优化 - 页面组件抽离,减少页面组件路由。 -5. 优化 - 全文检索,忽略大小写。 -6. 优化 - 问答生成和增强索引改成流输出,避免部分模型超时。 -7. 优化 - 自动给 assistant 空 content,补充 null,同时合并连续的 text assistant,避免部分模型抛错。 \ No newline at end of file +2. 新增 - DeepSeek resoner 模型支持输出思考过程。 +3. 新增 - 使用记录导出和仪表盘。 +4. 新增 - markdown 语法扩展,支持音视频(代码块 audio 和 video)。 +5. 新增 - 调整 max_tokens 计算逻辑。优先保证 max_tokens 为配置值,如超出最大上下文,则减少历史记录。例如:如果申请 8000 的 max_tokens,则上下文长度会减少 8000。 +6. 优化 - 问题优化增加上下文过滤,避免超出上下文。 +7. 优化 - 页面组件抽离,减少页面组件路由。 +8. 优化 - 全文检索,忽略大小写。 +9. 优化 - 问答生成和增强索引改成流输出,避免部分模型超时。 +10. 优化 - 自动给 assistant 空 content,补充 null,同时合并连续的 text assistant,避免部分模型抛错。 +11. 优化 - 调整图片 Host, 取消上传时补充 FE_DOMAIN,改成发送对话前 +12. 修复 - 部分场景成员列表无法触底加载。 \ No newline at end of file diff --git a/packages/global/core/ai/model.d.ts b/packages/global/core/ai/model.d.ts index 9fa75b277..235dd0e6f 100644 --- a/packages/global/core/ai/model.d.ts +++ b/packages/global/core/ai/model.d.ts @@ -29,10 +29,11 @@ export type LLMModelItemType = PriceType & maxContext: number; maxResponse: number; quoteMaxToken: number; - maxTemperature: number; + maxTemperature?: number; censor?: boolean; vision?: boolean; + reasoning?: boolean; // diff function model datasetProcess?: boolean; // dataset diff --git a/packages/global/core/ai/provider.ts b/packages/global/core/ai/provider.ts index 1f10b9ebc..f1bfc9370 100644 --- a/packages/global/core/ai/provider.ts +++ b/packages/global/core/ai/provider.ts @@ -11,8 +11,8 @@ export type ModelProviderIdType = | 'AliCloud' | 'Qwen' | 'Doubao' - | 'ChatGLM' | 'DeepSeek' + | 'ChatGLM' | 'Ernie' | 'Moonshot' | 'MiniMax' diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 899628936..ec30e4b51 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -117,6 +117,7 @@ export type SettingAIDataType = { isResponseAnswerText?: boolean; maxHistories?: number; [NodeInputKeyEnum.aiChatVision]?: boolean; // Is open vision mode + [NodeInputKeyEnum.aiChatReasoning]?: boolean; // Is open reasoning mode }; // variable diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index 13aa64934..34c6ff1e0 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -25,7 +25,8 @@ export enum ChatItemValueTypeEnum { text = 'text', file = 'file', tool = 'tool', - interactive = 'interactive' + interactive = 'interactive', + reasoning = 'reasoning' } export enum ChatSourceEnum { diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index a9171b156..4e010c68f 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -70,14 +70,23 @@ export type SystemChatItemType = { obj: ChatRoleEnum.System; value: SystemChatItemValueItemType[]; }; + export type AIChatItemValueItemType = { - type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.tool | ChatItemValueTypeEnum.interactive; + type: + | ChatItemValueTypeEnum.text + | ChatItemValueTypeEnum.reasoning + | ChatItemValueTypeEnum.tool + | ChatItemValueTypeEnum.interactive; text?: { content: string; }; + reasoning?: { + content: string; + }; tools?: ToolModuleResponseItemType[]; interactive?: WorkflowInteractiveResponseType; }; + export type AIChatItemType = { obj: ChatRoleEnum.AI; value: AIChatItemValueItemType[]; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 175f0c19d..9aa4620bd 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -141,6 +141,7 @@ export enum NodeInputKeyEnum { aiChatDatasetQuote = 'quoteQA', aiChatVision = 'aiChatVision', stringQuoteText = 'stringQuoteText', + aiChatReasoning = 'aiChatReasoning', // dataset datasetSelectList = 'datasets', @@ -220,7 +221,8 @@ export enum NodeOutputKeyEnum { // common userChatInput = 'userChatInput', history = 'history', - answerText = 'answerText', // module answer. the value will be show and save to history + answerText = 'answerText', // node answer. the value will be show and save to history + reasoningText = 'reasoningText', // node reasoning. the value will be show but not save to history success = 'success', failed = 'failed', error = 'error', diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 995302b65..fdf1bfeba 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -220,6 +220,7 @@ export type AIChatNodeProps = { [NodeInputKeyEnum.aiChatMaxToken]?: number; [NodeInputKeyEnum.aiChatIsResponseText]: boolean; [NodeInputKeyEnum.aiChatVision]?: boolean; + [NodeInputKeyEnum.aiChatReasoning]?: boolean; [NodeInputKeyEnum.aiChatQuoteRole]?: AiChatQuoteRoleType; [NodeInputKeyEnum.aiChatQuoteTemplate]?: string; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 01aeaa6e0..27c819ba6 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -364,12 +364,14 @@ export function replaceEditorVariable({ export const textAdaptGptResponse = ({ text, + reasoning_content, model = '', finish_reason = null, extraData = {} }: { model?: string; - text: string | null; + text?: string | null; + reasoning_content?: string | null; finish_reason?: null | 'stop'; extraData?: Object; }) => { @@ -381,10 +383,11 @@ export const textAdaptGptResponse = ({ model, choices: [ { - delta: - text === null - ? {} - : { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: text }, + delta: { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: text, + ...(reasoning_content && { reasoning_content }) + }, index: 0, finish_reason } diff --git a/packages/global/core/workflow/template/system/aiChat/index.ts b/packages/global/core/workflow/template/system/aiChat/index.ts index a346371b2..103d5f311 100644 --- a/packages/global/core/workflow/template/system/aiChat/index.ts +++ b/packages/global/core/workflow/template/system/aiChat/index.ts @@ -63,14 +63,14 @@ export const AiChatModule: FlowNodeTemplateType = { key: NodeInputKeyEnum.aiChatTemperature, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', - value: 0, + value: undefined, valueType: WorkflowIOValueTypeEnum.number }, { key: NodeInputKeyEnum.aiChatMaxToken, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', - value: 2000, + value: undefined, valueType: WorkflowIOValueTypeEnum.number }, @@ -91,6 +91,13 @@ export const AiChatModule: FlowNodeTemplateType = { valueType: WorkflowIOValueTypeEnum.boolean, value: true }, + { + key: NodeInputKeyEnum.aiChatReasoning, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true + }, // settings modal --- { ...Input_Template_System_Prompt, diff --git a/packages/global/core/workflow/template/system/tools.ts b/packages/global/core/workflow/template/system/tools.ts index 29406a193..f1472e495 100644 --- a/packages/global/core/workflow/template/system/tools.ts +++ b/packages/global/core/workflow/template/system/tools.ts @@ -43,14 +43,14 @@ export const ToolModule: FlowNodeTemplateType = { key: NodeInputKeyEnum.aiChatTemperature, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', - value: 0, + value: undefined, valueType: WorkflowIOValueTypeEnum.number }, { key: NodeInputKeyEnum.aiChatMaxToken, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', - value: 2000, + value: undefined, valueType: WorkflowIOValueTypeEnum.number }, { diff --git a/packages/service/common/file/image/controller.ts b/packages/service/common/file/image/controller.ts index 27bc7ade5..5734f7ab9 100644 --- a/packages/service/common/file/image/controller.ts +++ b/packages/service/common/file/image/controller.ts @@ -40,7 +40,7 @@ export async function uploadMongoImg({ expiredTime: forever ? undefined : addHours(new Date(), 1) }); - return `${process.env.FE_DOMAIN || ''}${process.env.NEXT_PUBLIC_BASE_URL || ''}${imageBaseUrl}${String(_id)}.${extension}`; + return `${process.env.NEXT_PUBLIC_BASE_URL || ''}${imageBaseUrl}${String(_id)}.${extension}`; } const getIdFromPath = (path?: string) => { diff --git a/packages/service/core/ai/config/provider/DeepSeek.json b/packages/service/core/ai/config/provider/DeepSeek.json index 1fb0b525f..1aee2c1ab 100644 --- a/packages/service/core/ai/config/provider/DeepSeek.json +++ b/packages/service/core/ai/config/provider/DeepSeek.json @@ -27,8 +27,9 @@ "maxContext": 64000, "maxResponse": 4096, "quoteMaxToken": 60000, - "maxTemperature": 1.5, + "maxTemperature": null, "vision": false, + "reasoning": true, "toolChoice": false, "functionCall": false, "defaultSystemChatPrompt": "", @@ -39,11 +40,9 @@ "usedInQueryExtension": true, "customExtractPrompt": "", "usedInToolCall": true, - "defaultConfig": { - "temperature": null - }, + "defaultConfig": {}, "fieldMap": {}, "type": "llm" } ] -} \ No newline at end of file +} diff --git a/packages/service/core/ai/config/provider/OpenAI.json b/packages/service/core/ai/config/provider/OpenAI.json index 4bfcc461e..b45942518 100644 --- a/packages/service/core/ai/config/provider/OpenAI.json +++ b/packages/service/core/ai/config/provider/OpenAI.json @@ -50,10 +50,10 @@ "maxContext": 128000, "maxResponse": 4000, "quoteMaxToken": 120000, - "maxTemperature": 1.2, + "maxTemperature": null, "vision": false, "toolChoice": false, - "functionCall": true, + "functionCall": false, "defaultSystemChatPrompt": "", "datasetProcess": true, "usedInClassify": true, @@ -63,8 +63,10 @@ "customExtractPrompt": "", "usedInToolCall": true, "defaultConfig": { - "temperature": 1, - "max_tokens": null + "stream": false + }, + "fieldMap": { + "max_tokens": "max_completion_tokens" }, "type": "llm" }, @@ -74,10 +76,10 @@ "maxContext": 128000, "maxResponse": 4000, "quoteMaxToken": 120000, - "maxTemperature": 1.2, + "maxTemperature": null, "vision": false, "toolChoice": false, - "functionCall": true, + "functionCall": false, "defaultSystemChatPrompt": "", "datasetProcess": true, "usedInClassify": true, @@ -87,10 +89,11 @@ "customExtractPrompt": "", "usedInToolCall": true, "defaultConfig": { - "temperature": 1, - "max_tokens": null, "stream": false }, + "fieldMap": { + "max_tokens": "max_completion_tokens" + }, "type": "llm" }, { @@ -99,10 +102,10 @@ "maxContext": 195000, "maxResponse": 8000, "quoteMaxToken": 120000, - "maxTemperature": 1.2, + "maxTemperature": null, "vision": false, "toolChoice": false, - "functionCall": true, + "functionCall": false, "defaultSystemChatPrompt": "", "datasetProcess": true, "usedInClassify": true, @@ -112,10 +115,11 @@ "customExtractPrompt": "", "usedInToolCall": true, "defaultConfig": { - "temperature": 1, - "max_tokens": null, "stream": false }, + "fieldMap": { + "max_tokens": "max_completion_tokens" + }, "type": "llm" }, { diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index 31bb9ee8d..c4b85ffcd 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -2,10 +2,12 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; import { createChatCompletion } from '../config'; import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { countGptMessagesTokens, countPromptTokens } from '../../../common/string/tiktoken/index'; -import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; +import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getLLMModel } from '../model'; import { llmCompletionsBodyFormat } from '../utils'; import { addLog } from '../../../common/system/log'; +import { filterGPTMessageByMaxContext } from '../../chat/utils'; +import json5 from 'json5'; /* query extension - 问题扩展 @@ -13,72 +15,73 @@ import { addLog } from '../../../common/system/log'; */ const title = global.feConfigs?.systemTitle || 'FastAI'; -const defaultPrompt = `作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。 +const defaultPrompt = `## 你的任务 +你作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。 生成的问题要求指向对象清晰明确,并与“原问题语言相同”。 -参考 标中的示例来完成任务。 +## 参考示例 - 历史记录: """ +null """ 原问题: 介绍下剧情。 检索词: ["介绍下故事的背景。","故事的主题是什么?","介绍下故事的主要人物。"] ---------------- 历史记录: """ -Q: 对话背景。 -A: 当前对话是关于 Nginx 的介绍和使用等。 +user: 对话背景。 +assistant: 当前对话是关于 Nginx 的介绍和使用等。 """ 原问题: 怎么下载 检索词: ["Nginx 如何下载?","下载 Nginx 需要什么条件?","有哪些渠道可以下载 Nginx?"] ---------------- 历史记录: """ -Q: 对话背景。 -A: 当前对话是关于 Nginx 的介绍和使用等。 -Q: 报错 "no connection" -A: 报错"no connection"可能是因为…… +user: 对话背景。 +assistant: 当前对话是关于 Nginx 的介绍和使用等。 +user: 报错 "no connection" +assistant: 报错"no connection"可能是因为…… """ 原问题: 怎么解决 检索词: ["Nginx报错"no connection"如何解决?","造成'no connection'报错的原因。","Nginx提示'no connection',要怎么办?"] ---------------- 历史记录: """ -Q: 护产假多少天? -A: 护产假的天数根据员工所在的城市而定。请提供您所在的城市,以便我回答您的问题。 +user: How long is the maternity leave? +assistant: The number of days of maternity leave depends on the city in which the employee is located. Please provide your city so that I can answer your questions. """ -原问题: 沈阳 -检索词: ["沈阳的护产假多少天?","沈阳的护产假政策。","沈阳的护产假标准。"] +原问题: ShenYang +检索词: ["How many days is maternity leave in Shenyang?","Shenyang's maternity leave policy.","The standard of maternity leave in Shenyang."] ---------------- 历史记录: """ -Q: 作者是谁? -A: ${title} 的作者是 labring。 +user: 作者是谁? +assistant: ${title} 的作者是 labring。 """ 原问题: Tell me about him 检索词: ["Introduce labring, the author of ${title}." ," Background information on author labring." "," Why does labring do ${title}?"] ---------------- 历史记录: """ -Q: 对话背景。 -A: 关于 ${title} 的介绍和使用等问题。 +user: 对话背景。 +assistant: 关于 ${title} 的介绍和使用等问题。 """ 原问题: 你好。 检索词: ["你好"] ---------------- 历史记录: """ -Q: ${title} 如何收费? -A: ${title} 收费可以参考…… +user: ${title} 如何收费? +assistant: ${title} 收费可以参考…… """ 原问题: 你知道 laf 么? 检索词: ["laf 的官网地址是多少?","laf 的使用教程。","laf 有什么特点和优势。"] ---------------- 历史记录: """ -Q: ${title} 的优势 -A: 1. 开源 +user: ${title} 的优势 +assistant: 1. 开源 2. 简便 3. 扩展性强 """ @@ -87,18 +90,20 @@ A: 1. 开源 ---------------- 历史记录: """ -Q: 什么是 ${title}? -A: ${title} 是一个 RAG 平台。 -Q: 什么是 Laf? -A: Laf 是一个云函数开发平台。 +user: 什么是 ${title}? +assistant: ${title} 是一个 RAG 平台。 +user: 什么是 Laf? +assistant: Laf 是一个云函数开发平台。 """ 原问题: 它们有什么关系? 检索词: ["${title}和Laf有什么关系?","介绍下${title}","介绍下Laf"] - ------ +## 输出要求 -下面是正式的任务: +1. 输出格式为 JSON 数组,数组中每个元素为字符串。无需对输出进行任何解释。 +2. 输出语言与原问题相同。原问题为中文则输出中文;原问题为英文则输出英文。 + +## 开始任务 历史记录: """ @@ -125,26 +130,39 @@ export const queryExtension = async ({ outputTokens: number; }> => { const systemFewShot = chatBg - ? `Q: 对话背景。 -A: ${chatBg} + ? `user: 对话背景。 +assistant: ${chatBg} ` : ''; - const historyFewShot = histories - .map((item) => { - const role = item.obj === 'Human' ? 'Q' : 'A'; - return `${role}: ${chatValue2RuntimePrompt(item.value).text}`; - }) - .join('\n'); - const concatFewShot = `${systemFewShot}${historyFewShot}`.trim(); const modelData = getLLMModel(model); + const filterHistories = await filterGPTMessageByMaxContext({ + messages: chats2GPTMessages({ messages: histories, reserveId: false }), + maxContext: modelData.maxContext - 1000 + }); + + const historyFewShot = filterHistories + .map((item) => { + const role = item.role; + const content = item.content; + if ((role === 'user' || role === 'assistant') && content) { + if (typeof content === 'string') { + return `${role}: ${content}`; + } else { + return `${role}: ${content.map((item) => (item.type === 'text' ? item.text : '')).join('\n')}`; + } + } + }) + .filter(Boolean) + .join('\n'); + const concatFewShot = `${systemFewShot}${historyFewShot}`.trim(); const messages = [ { role: 'user', content: replaceVariable(defaultPrompt, { query: `${query}`, - histories: concatFewShot + histories: concatFewShot || 'null' }) } ] as any; @@ -154,7 +172,7 @@ A: ${chatBg} { stream: false, model: modelData.model, - temperature: 0.01, + temperature: 0.1, messages }, modelData @@ -172,22 +190,41 @@ A: ${chatBg} }; } + const start = answer.indexOf('['); + const end = answer.lastIndexOf(']'); + if (start === -1 || end === -1) { + addLog.warn('Query extension failed, not a valid JSON', { + answer + }); + return { + rawQuery: query, + extensionQueries: [], + model, + inputTokens: 0, + outputTokens: 0 + }; + } + // Intercept the content of [] and retain [] - answer = answer.match(/\[.*?\]/)?.[0] || ''; - answer = answer.replace(/\\"/g, '"'); + const jsonStr = answer + .substring(start, end + 1) + .replace(/(\\n|\\)/g, '') + .replace(/ /g, ''); try { - const queries = JSON.parse(answer) as string[]; + const queries = json5.parse(jsonStr) as string[]; return { rawQuery: query, - extensionQueries: Array.isArray(queries) ? queries : [], + extensionQueries: (Array.isArray(queries) ? queries : []).slice(0, 5), model, inputTokens: await countGptMessagesTokens(messages), outputTokens: await countPromptTokens(answer) }; } catch (error) { - addLog.error(`Query extension error`, error); + addLog.warn('Query extension failed, not a valid JSON', { + answer + }); return { rawQuery: query, extensionQueries: [], diff --git a/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts index 74b4e718f..40951a372 100644 --- a/packages/service/core/ai/utils.ts +++ b/packages/service/core/ai/utils.ts @@ -2,33 +2,23 @@ import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, - ChatCompletionMessageParam, StreamChatType } from '@fastgpt/global/core/ai/type'; -import { countGptMessagesTokens } from '../../common/string/tiktoken'; import { getLLMModel } from './model'; -export const computedMaxToken = async ({ +/* + Count response max token +*/ +export const computedMaxToken = ({ maxToken, - model, - filterMessages = [] + model }: { maxToken?: number; model: LLMModelItemType; - filterMessages: ChatCompletionMessageParam[]; }) => { if (maxToken === undefined) return; maxToken = Math.min(maxToken, model.maxResponse); - const tokensLimit = model.maxContext; - - /* count response max token */ - const promptsToken = await countGptMessagesTokens(filterMessages); - maxToken = promptsToken + maxToken > tokensLimit ? tokensLimit - promptsToken : maxToken; - - if (maxToken <= 0) { - maxToken = 200; - } return maxToken; }; @@ -40,6 +30,7 @@ export const computedTemperature = ({ model: LLMModelItemType; temperature: number; }) => { + if (typeof model.maxTemperature !== 'number') return undefined; temperature = +(model.maxTemperature * (temperature / 10)).toFixed(2); temperature = Math.max(temperature, 0.01); diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 130b894e8..b0b62170a 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -14,36 +14,19 @@ import { serverRequestBaseUrl } from '../../common/api/serverRequest'; import { i18nT } from '../../../web/i18n/utils'; import { addLog } from '../../common/system/log'; -export const filterGPTMessageByMaxTokens = async ({ +export const filterGPTMessageByMaxContext = async ({ messages = [], - maxTokens + maxContext }: { messages: ChatCompletionMessageParam[]; - maxTokens: number; + maxContext: number; }) => { if (!Array.isArray(messages)) { return []; } - const rawTextLen = messages.reduce((sum, item) => { - if (typeof item.content === 'string') { - return sum + item.content.length; - } - if (Array.isArray(item.content)) { - return ( - sum + - item.content.reduce((sum, item) => { - if (item.type === 'text') { - return sum + item.text.length; - } - return sum; - }, 0) - ); - } - return sum; - }, 0); // If the text length is less than half of the maximum token, no calculation is required - if (rawTextLen < maxTokens * 0.5) { + if (messages.length < 4) { return messages; } @@ -55,7 +38,7 @@ export const filterGPTMessageByMaxTokens = async ({ const chatPrompts: ChatCompletionMessageParam[] = messages.slice(chatStartIndex); // reduce token of systemPrompt - maxTokens -= await countGptMessagesTokens(systemPrompts); + maxContext -= await countGptMessagesTokens(systemPrompts); // Save the last chat prompt(question) const question = chatPrompts.pop(); @@ -73,9 +56,9 @@ export const filterGPTMessageByMaxTokens = async ({ } const tokens = await countGptMessagesTokens([assistant, user]); - maxTokens -= tokens; + maxContext -= tokens; /* 整体 tokens 超出范围,截断 */ - if (maxTokens < 0) { + if (maxContext < 0) { break; } diff --git a/packages/service/core/workflow/dispatch/agent/extract.ts b/packages/service/core/workflow/dispatch/agent/extract.ts index 61a0737bf..431bdb4f6 100644 --- a/packages/service/core/workflow/dispatch/agent/extract.ts +++ b/packages/service/core/workflow/dispatch/agent/extract.ts @@ -1,5 +1,5 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../chat/utils'; +import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../chat/utils'; import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; import { countMessagesTokens, @@ -175,9 +175,9 @@ ${description ? `- ${description}` : ''} } ]; const adaptMessages = chats2GPTMessages({ messages, reserveId: false }); - const filterMessages = await filterGPTMessageByMaxTokens({ + const filterMessages = await filterGPTMessageByMaxContext({ messages: adaptMessages, - maxTokens: extractModel.maxContext + maxContext: extractModel.maxContext }); const requestMessages = await loadRequestMessages({ messages: filterMessages, diff --git a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts index 92cdfa9cd..fa7e27187 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts @@ -1,5 +1,5 @@ import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; +import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, StreamChatType, @@ -172,10 +172,14 @@ export const runToolWithFunctionCall = async ( }; }); + const max_tokens = computedMaxToken({ + model: toolModel, + maxToken + }); const filterMessages = ( - await filterGPTMessageByMaxTokens({ + await filterGPTMessageByMaxContext({ messages, - maxTokens: toolModel.maxContext - 300 // filter token. not response maxToken + maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken }) ).map((item) => { if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant && item.function_call) { @@ -190,16 +194,11 @@ export const runToolWithFunctionCall = async ( } return item; }); - const [requestMessages, max_tokens] = await Promise.all([ + const [requestMessages] = await Promise.all([ loadRequestMessages({ messages: filterMessages, useVision: toolModel.vision && aiChatVision, origin: requestOrigin - }), - computedMaxToken({ - model: toolModel, - maxToken, - filterMessages }) ]); const requestBody = llmCompletionsBodyFormat( diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index 7f380b55d..331496852 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -1,5 +1,5 @@ import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; +import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, StreamChatType, @@ -196,21 +196,20 @@ export const runToolWithPromptCall = async ( return Promise.reject('Prompt call invalid input'); } - const filterMessages = await filterGPTMessageByMaxTokens({ + const max_tokens = computedMaxToken({ + model: toolModel, + maxToken + }); + const filterMessages = await filterGPTMessageByMaxContext({ messages, - maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken + maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken }); - const [requestMessages, max_tokens] = await Promise.all([ + const [requestMessages] = await Promise.all([ loadRequestMessages({ messages: filterMessages, useVision: toolModel.vision && aiChatVision, origin: requestOrigin - }), - computedMaxToken({ - model: toolModel, - maxToken, - filterMessages }) ]); const requestBody = llmCompletionsBodyFormat( diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index 2b0f7d457..b2b845f75 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -1,5 +1,5 @@ import { createChatCompletion } from '../../../../ai/config'; -import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; +import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, ChatCompletionMessageToolCall, @@ -228,11 +228,16 @@ export const runToolWithToolChoice = async ( }; }); + const max_tokens = computedMaxToken({ + model: toolModel, + maxToken + }); + // Filter histories by maxToken const filterMessages = ( - await filterGPTMessageByMaxTokens({ + await filterGPTMessageByMaxContext({ messages, - maxTokens: toolModel.maxContext - 300 // filter token. not response maxToken + maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken }) ).map((item) => { if (item.role === 'assistant' && item.tool_calls) { @@ -248,16 +253,11 @@ export const runToolWithToolChoice = async ( return item; }); - const [requestMessages, max_tokens] = await Promise.all([ + const [requestMessages] = await Promise.all([ loadRequestMessages({ messages: filterMessages, useVision: toolModel.vision && aiChatVision, origin: requestOrigin - }), - computedMaxToken({ - model: toolModel, - maxToken, - filterMessages }) ]); const requestBody = llmCompletionsBodyFormat( diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index 23dccfe93..93f23651f 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -1,5 +1,5 @@ import type { NextApiResponse } from 'next'; -import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../chat/utils'; +import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../chat/utils'; import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -58,6 +58,7 @@ export type ChatProps = ModuleDispatchProps< >; export type ChatResponse = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; + [NodeOutputKeyEnum.reasoningText]?: string; [NodeOutputKeyEnum.history]: ChatItemType[]; }>; @@ -87,22 +88,24 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { - // censor model and system key if (modelConstantsData.censor && !externalProvider.openaiAccount?.key) { return postTextCensor({ text: `${systemPrompt} @@ -149,18 +158,11 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { + const { answerText, reasoningText } = await (async () => { if (res && isStreamResponse) { // sse response - const { answer } = await streamResponse({ + const { answer, reasoning } = await streamResponse({ res, stream: response, + aiChatReasoning, workflowStreamResponse }); return { - answerText: answer + answerText: answer, + reasoningText: reasoning }; } else { const unStreamResponse = response as ChatCompletion; const answer = unStreamResponse.choices?.[0]?.message?.content || ''; - + const reasoning = aiChatReasoning + ? // @ts-ignore + unStreamResponse.choices?.[0]?.message?.reasoning_content || '' + : ''; if (stream) { // Some models do not support streaming - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: answer - }) - }); + reasoning && + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + text: answer, + reasoning_content: reasoning + }) + }); } return { - answerText: answer + answerText: answer, + reasoningText: reasoning }; } })(); @@ -241,6 +251,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise, 'nodeResponse' @@ -251,6 +253,13 @@ export async function dispatchWorkFlow(data: Props): Promise import('./icons/core/chat/sideLine.svg'), 'core/chat/speaking': () => import('./icons/core/chat/speaking.svg'), 'core/chat/stopSpeech': () => import('./icons/core/chat/stopSpeech.svg'), + 'core/chat/think': () => import('./icons/core/chat/think.svg'), 'core/dataset/commonDataset': () => import('./icons/core/dataset/commonDataset.svg'), 'core/dataset/commonDatasetColor': () => import('./icons/core/dataset/commonDatasetColor.svg'), 'core/dataset/commonDatasetOutline': () => diff --git a/packages/web/components/common/Icon/icons/core/chat/think.svg b/packages/web/components/common/Icon/icons/core/chat/think.svg new file mode 100644 index 000000000..260a944a9 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/think.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 4ecc77c44..a326a20b7 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -304,7 +304,7 @@ export function useScrollPagination< ); return ( - + {scrollLoadType === 'top' && total > 0 && isLoading && ( {t('common:common.is_requesting')} diff --git a/packages/web/i18n/en/account.json b/packages/web/i18n/en/account.json index 6d5827bae..f4251adfb 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -49,10 +49,10 @@ "model.output_price": "Output price", "model.output_price_tip": "The language model output price. If this item is configured, the model comprehensive price will be invalid.", "model.param_name": "Parameter name", - "model.request_auth": "Custom token", + "model.request_auth": "Custom key", "model.request_auth_tip": "When making a request to a custom request address, carry the request header: Authorization: Bearer xxx to make the request.", "model.request_url": "Custom url", - "model.request_url_tip": "If this value is filled in, a request will be made directly to this address without going through OneAPI", + "model.request_url_tip": "If you fill in this value, you will initiate a request directly without passing. \nYou need to follow the API format of Openai and fill in the full request address, such as\n\nLLM: {Host}}/v1/Chat/Completions\n\nEmbedding: {host}}/v1/embeddings\n\nSTT: {Host}/v1/Audio/Transcriptions\n\nTTS: {Host}}/v1/Audio/Speech\n\nRERARARARARARARANK: {Host}}/v1/RERARARARARARARARARARANK", "model.test_model": "Model testing", "model.tool_choice": "Tool choice", "model.tool_choice_tag": "ToolCall", diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index c280bcd09..c3ca4a379 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -109,6 +109,7 @@ "publish_channel": "Publish", "publish_success": "Publish Successful", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", + "reasoning_response": "Output thinking", "saved_success": "Saved successfully! \nTo use this version externally, click Save and Publish", "search_app": "Search apps", "setting_app": "Workflow", diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index b930d44af..45604e158 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -2,6 +2,7 @@ "AI_input_is_empty": "The content passed to the AI ​​node is empty", "Delete_all": "Clear All Lexicon", "LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.", + "ai_reasoning": "Thinking process", "chat_history": "Conversation History", "chat_input_guide_lexicon_is_empty": "Lexicon not configured yet", "chat_test_app": "Debug-{{name}}", diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index f4359db83..02901e774 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -49,10 +49,10 @@ "model.output_price": "模型输出价格", "model.output_price_tip": "语言模型输出价格,如果配置了该项,则模型综合价格会失效", "model.param_name": "参数名", - "model.request_auth": "自定义请求 Tokens", + "model.request_auth": "自定义请求 Key", "model.request_auth_tip": "向自定义请求地址发起请求时候,携带请求头:Authorization: Bearer xxx 进行请求", "model.request_url": "自定义请求地址", - "model.request_url_tip": "如果填写该值,则会直接向该地址发起请求,不经过 OneAPI", + "model.request_url_tip": "如果填写该值,则会直接向该地址发起请求,不经过 OneAPI。需要遵循 OpenAI 的 API格式,并填写完整请求地址,例如:\nLLM: {{host}}/v1/chat/completions\nEmbedding: {{host}}/v1/embeddings\nSTT: {{host}}/v1/audio/transcriptions\nTTS: {{host}}/v1/audio/speech\nRerank: {{host}}/v1/rerank", "model.test_model": "模型测试", "model.tool_choice": "支持工具调用", "model.tool_choice_tag": "工具调用", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 59c7f1339..d75cd9ea5 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -109,6 +109,7 @@ "publish_channel": "发布渠道", "publish_success": "发布成功", "question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。", + "reasoning_response": "输出思考", "saved_success": "保存成功!如需在外部使用该版本,请点击“保存并发布”", "search_app": "搜索应用", "setting_app": "应用配置", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index 856401274..e75ae0a21 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -2,6 +2,7 @@ "AI_input_is_empty": "传入AI 节点的内容为空", "Delete_all": "清空词库", "LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常", + "ai_reasoning": "思考过程", "chat_history": "聊天记录", "chat_input_guide_lexicon_is_empty": "还没有配置词库", "chat_test_app": "调试-{{name}}", diff --git a/packages/web/i18n/zh-Hant/account.json b/packages/web/i18n/zh-Hant/account.json index 41cee42f9..5edb5b08e 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -48,10 +48,10 @@ "model.output_price": "模型輸出價格", "model.output_price_tip": "語言模型輸出價格,如果配置了該項,則模型綜合價格會失效", "model.param_name": "參數名", - "model.request_auth": "自訂請求 Tokens", + "model.request_auth": "自訂請求 Key", "model.request_auth_tip": "向自訂請求地址發起請求時候,攜帶請求頭:Authorization: Bearer xxx 進行請求", "model.request_url": "自訂請求地址", - "model.request_url_tip": "如果填入該值,則會直接向該位址發起請求,不經過 OneAPI", + "model.request_url_tip": "如果填寫該值,則會直接向該地址發起請求,不經過 OneAPI。\n需要遵循 OpenAI 的 API格式,並填寫完整請求地址,例如:\n\nLLM: {{host}}/v1/chat/completions\n\nEmbedding: {{host}}/v1/embeddings\n\nSTT: {{host}}/v1/audio/transcriptions\n\nTTS: {{host}}/v1/audio/speech\n\nRerank: {{host}}/v1/rerank", "model.test_model": "模型測試", "model.tool_choice": "支援工具調用", "model.tool_choice_tag": "工具調用", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 7d13b5465..c2b7651f3 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -109,6 +109,7 @@ "publish_channel": "發布通道", "publish_success": "發布成功", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", + "reasoning_response": "輸出思考", "saved_success": "保存成功!\n如需在外部使用該版本,請點擊“儲存並發布”", "search_app": "搜尋應用程式", "setting_app": "應用程式設定", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index cb44628d1..2980ba08b 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -2,6 +2,7 @@ "AI_input_is_empty": "傳送至 AI 節點的內容為空", "Delete_all": "清除所有詞彙", "LLM_model_response_empty": "模型流程回應為空,請檢查模型流程輸出是否正常", + "ai_reasoning": "思考過程", "chat_history": "對話紀錄", "chat_input_guide_lexicon_is_empty": "尚未設定詞彙庫", "chat_test_app": "調試-{{name}}", diff --git a/projects/app/src/components/core/ai/AISettingModal/index.tsx b/projects/app/src/components/core/ai/AISettingModal/index.tsx index 65e385419..5059119c7 100644 --- a/projects/app/src/components/core/ai/AISettingModal/index.tsx +++ b/projects/app/src/components/core/ai/AISettingModal/index.tsx @@ -72,6 +72,7 @@ const AIChatSettingsModal = ({ defaultValues: defaultData }); const model = watch('model'); + const reasoning = watch(NodeInputKeyEnum.aiChatReasoning); const showResponseAnswerText = watch(NodeInputKeyEnum.aiChatIsResponseText) !== undefined; const showVisionSwitch = watch(NodeInputKeyEnum.aiChatVision) !== undefined; const showMaxHistoriesSlider = watch('maxHistories') !== undefined; @@ -84,6 +85,8 @@ const AIChatSettingsModal = ({ return getWebLLMModel(model); }, [model]); const llmSupportVision = !!selectedModel?.vision; + const llmSupportTemperature = typeof selectedModel?.maxTemperature === 'number'; + const llmSupportReasoning = !!selectedModel?.reasoning; const tokenLimit = useMemo(() => { return selectedModel?.maxResponse || 4096; @@ -258,36 +261,51 @@ const AIChatSettingsModal = ({ /> - - - - {t('app:temperature')} - - - { - setValue('temperature', e.target.checked ? 0 : undefined); - }} - /> - - - - { - setValue(NodeInputKeyEnum.aiChatTemperature, e); - setRefresh(!refresh); - }} - /> - - - + {llmSupportTemperature && ( + + + + {t('app:temperature')} + + + { + setValue('temperature', e.target.checked ? 0 : undefined); + }} + /> + + + { + setValue(NodeInputKeyEnum.aiChatTemperature, e); + setRefresh(!refresh); + }} + /> + + + )} + {llmSupportReasoning && ( + + + {t('app:reasoning_response')} + { + const value = e.target.checked; + setValue(NodeInputKeyEnum.aiChatReasoning, value); + }} + /> + + + )} {showResponseAnswerText && ( diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index 54534b622..56cb41e7f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -201,6 +201,7 @@ const ChatBox = ({ ({ event, text = '', + reasoningText, status, name, tool, @@ -247,6 +248,25 @@ const ChatBox = ({ value: item.value.slice(0, -1).concat(lastValue) }; } + } else if (event === SseResponseEventEnum.answer && reasoningText) { + if (lastValue.type === ChatItemValueTypeEnum.reasoning && lastValue.reasoning) { + lastValue.reasoning.content += reasoningText; + return { + ...item, + value: item.value.slice(0, -1).concat(lastValue) + }; + } else { + const val: AIChatItemValueItemType = { + type: ChatItemValueTypeEnum.reasoning, + reasoning: { + content: reasoningText + } + }; + return { + ...item, + value: item.value.concat(val) + }; + } } else if (event === SseResponseEventEnum.toolCall && tool) { const val: AIChatItemValueItemType = { type: ChatItemValueTypeEnum.tool, diff --git a/projects/app/src/components/core/chat/ChatContainer/type.d.ts b/projects/app/src/components/core/chat/ChatContainer/type.d.ts index 74774e227..a8b8494a7 100644 --- a/projects/app/src/components/core/chat/ChatContainer/type.d.ts +++ b/projects/app/src/components/core/chat/ChatContainer/type.d.ts @@ -6,6 +6,7 @@ import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/t export type generatingMessageProps = { event: SseResponseEventEnum; text?: string; + reasoningText?: string; name?: string; status?: 'running' | 'finish'; tool?: ToolModuleResponseItemType; diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 2f14b6f8c..552171a71 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -8,6 +8,7 @@ import { Box, Button, Flex, + HStack, Textarea } from '@chakra-ui/react'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; @@ -139,6 +140,55 @@ ${toolResponse}`} }, (prevProps, nextProps) => isEqual(prevProps, nextProps) ); +const RenderResoningContent = React.memo(function RenderResoningContent({ + content, + showAnimation +}: { + content: string; + showAnimation: boolean; +}) { + const { t } = useTranslation(); + + return ( + + + + + + {t('chat:ai_reasoning')} + + + {showAnimation && } + + + + + + + + ); +}); const RenderUserSelectInteractive = React.memo(function RenderInteractive({ interactive }: { @@ -290,6 +340,8 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => { return ( ); + if (value.type === ChatItemValueTypeEnum.reasoning && value.reasoning) + return ; if (value.type === ChatItemValueTypeEnum.tool && value.tools) return ; if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) { diff --git a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx index 980b2146d..bce13c11c 100644 --- a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx +++ b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx @@ -803,6 +803,10 @@ const ModelEditModal = ({ { + if (!e) { + setValue('defaultConfig', undefined); + return; + } try { setValue('defaultConfig', JSON.parse(e)); } catch (error) { @@ -1009,6 +1013,10 @@ const ModelEditModal = ({ value={JSON.stringify(getValues('defaultConfig'), null, 2)} resize onChange={(e) => { + if (!e) { + setValue('defaultConfig', undefined); + return; + } try { setValue('defaultConfig', JSON.parse(e)); } catch (error) { diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx index 95528e7ec..847e28d5e 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx @@ -14,7 +14,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import Tag from '@fastgpt/web/components/common/Tag'; import { useTranslation } from 'next-i18next'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from '../context'; @@ -50,6 +50,8 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers); const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData); const [hoveredMemberId, setHoveredMemberId] = useState(); + + const selectedMembersRef = useRef(null); const [members, setMembers] = useState(group?.members || []); const [searchKey, setSearchKey] = useState(''); @@ -155,7 +157,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro setSearchKey(e.target.value); }} /> - + {filtered.map((member) => { return ( void; editGro {t('common:chosen') + ': ' + members.length} - + {members.map((member) => { return ( - - + + @@ -246,9 +246,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { ))}
-
- -
+ + +
diff --git a/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx b/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx index fd5a990ae..8ecd21739 100644 --- a/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx +++ b/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx @@ -121,36 +121,34 @@ function OrgMemberManageModal({ setSearchKey(e.target.value); }} /> - - - {filterMembers.map((member) => { - return ( - handleToggleSelect(member.tmbId)} - > - } - pointerEvents="none" - /> - - {member.memberName} - - ); - })} - - + + {filterMembers.map((member) => { + return ( + handleToggleSelect(member.tmbId)} + > + } + pointerEvents="none" + /> + + {member.memberName} + + ); + })} +
{`${t('common:chosen')}:${selectedMembers.length}`} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx index 5fbb8601e..5e4e2c557 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx @@ -38,7 +38,9 @@ const SelectAiModelRender = ({ item, inputs = [], nodeId }: RenderInputProps) => (input) => input.key === NodeInputKeyEnum.aiChatIsResponseText )?.value, aiChatVision: - inputs.find((input) => input.key === NodeInputKeyEnum.aiChatVision)?.value ?? true + inputs.find((input) => input.key === NodeInputKeyEnum.aiChatVision)?.value ?? true, + aiChatReasoning: + inputs.find((input) => input.key === NodeInputKeyEnum.aiChatReasoning)?.value ?? true }), [inputs] ); diff --git a/projects/app/src/web/common/api/fetch.ts b/projects/app/src/web/common/api/fetch.ts index ba82ed797..960a049e9 100644 --- a/projects/app/src/web/common/api/fetch.ts +++ b/projects/app/src/web/common/api/fetch.ts @@ -186,6 +186,12 @@ export const streamFetch = ({ text: item }); } + + const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || ''; + onMessage({ + event, + reasoningText + }); } else if (event === SseResponseEventEnum.fastAnswer) { const text = parseJson.choices?.[0]?.delta?.content || ''; pushDataToQueue({ diff --git a/projects/app/src/web/core/app/templates.ts b/projects/app/src/web/core/app/templates.ts index 1b128e11f..59a05ba8d 100644 --- a/projects/app/src/web/core/app/templates.ts +++ b/projects/app/src/web/core/app/templates.ts @@ -1,7 +1,7 @@ import { parseCurl } from '@fastgpt/global/common/string/http'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppSchema } from '@fastgpt/global/core/app/type'; -import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum, @@ -150,7 +150,7 @@ export const emptyTemplates: Record< key: 'temperature', renderTypeList: [FlowNodeInputTypeEnum.hidden], label: '', - value: 0, + value: undefined, valueType: WorkflowIOValueTypeEnum.number, min: 0, max: 10, @@ -160,7 +160,7 @@ export const emptyTemplates: Record< key: 'maxToken', renderTypeList: [FlowNodeInputTypeEnum.hidden], label: '', - value: 2000, + value: undefined, valueType: WorkflowIOValueTypeEnum.number, min: 100, max: 4000, @@ -221,6 +221,13 @@ export const emptyTemplates: Record< debugLabel: i18nT('common:core.module.Dataset quote.label'), description: '', valueType: WorkflowIOValueTypeEnum.datasetQuote + }, + { + key: NodeInputKeyEnum.aiChatReasoning, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true } ], outputs: [