From fde46941af1cf2068dd8554c5af892c59d605b89 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 24 Oct 2025 14:09:00 +0800 Subject: [PATCH] fix: update interactive --- packages/service/common/mongo/sessionRun.ts | 37 ++--- packages/service/core/chat/saveChat.ts | 127 ++++++++++++------ .../core/workflow/dispatch/ai/agent/index.ts | 2 +- .../ai/agent/sub/plan/ask/constants.ts | 23 +++- .../dispatch/ai/agent/sub/plan/index.ts | 5 +- .../dispatch/ai/agent/sub/plan/prompt.ts | 90 ++++++++++--- .../app/src/pages/api/core/chat/chatTest.ts | 4 +- .../app/src/pages/api/v1/chat/completions.ts | 4 +- .../app/src/pages/api/v2/chat/completions.ts | 4 +- projects/app/src/service/core/app/utils.ts | 6 +- projects/app/src/service/support/mcp/utils.ts | 4 +- test/cases/service/support/mcp/utils.test.ts | 2 +- 12 files changed, 217 insertions(+), 91 deletions(-) diff --git a/packages/service/common/mongo/sessionRun.ts b/packages/service/common/mongo/sessionRun.ts index cce0d6228..668eba1d3 100644 --- a/packages/service/common/mongo/sessionRun.ts +++ b/packages/service/common/mongo/sessionRun.ts @@ -1,28 +1,31 @@ +import { retryFn } from '@fastgpt/global/common/system/utils'; import { addLog } from '../system/log'; import { connectionMongo, type ClientSession } from './index'; const timeout = 60000; export const mongoSessionRun = async (fn: (session: ClientSession) => Promise) => { - const session = await connectionMongo.startSession(); + return retryFn(async () => { + const session = await connectionMongo.startSession(); - try { - session.startTransaction({ - maxCommitTimeMS: timeout - }); - const result = await fn(session); + try { + session.startTransaction({ + maxCommitTimeMS: timeout + }); + const result = await fn(session); - await session.commitTransaction(); + await session.commitTransaction(); - return result as T; - } catch (error) { - if (!session.transaction.isCommitted) { - await session.abortTransaction(); - } else { - addLog.warn('Un catch mongo session error', { error }); + return result as T; + } catch (error) { + if (!session.transaction.isCommitted) { + await session.abortTransaction(); + } else { + addLog.warn('Un catch mongo session error', { error }); + } + return Promise.reject(error); + } finally { + await session.endSession(); } - return Promise.reject(error); - } finally { - await session.endSession(); - } + }); }; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 6eda7297b..0a19bfb3f 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -1,4 +1,8 @@ -import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { + AIChatItemType, + ChatHistoryItemResType, + UserChatItemType +} from '@fastgpt/global/core/chat/type.d'; import { MongoApp } from '../app/schema'; import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -108,7 +112,7 @@ const formatAiContent = ({ }; } return responseItem; - }); + }) as ChatHistoryItemResType[] | undefined; return { aiResponse: { @@ -142,7 +146,7 @@ const getChatDataLog = async ({ }; }; -export async function saveChat(props: Props) { +export const pushChatRecords = async (props: Props) => { beforProcess(props); const { @@ -332,7 +336,7 @@ export async function saveChat(props: Props) { } catch (error) { addLog.error(`Save chat history error`, error); } -} +}; /* 更新交互节点,包含两种情况: @@ -352,18 +356,32 @@ export const updateInteractiveChat = async (props: Props) => { if (!chatItem || chatItem.obj !== ChatRoleEnum.AI) return; - // Update interactive value + // Get interactive value const interactiveValue = chatItem.value[chatItem.value.length - 1]; - if (!interactiveValue || !interactiveValue.interactive) { return; } interactiveValue.interactive.params = interactiveValue.interactive.params || {}; - // Get interactive value + // Get interactive response const { text: userInteractiveVal } = chatValue2RuntimePrompt(userContent.value); + + // 拿到的是实参 + const finalInteractive = extractDeepestInteractive(interactiveValue.interactive); + /* + 需要追加一条 chat_items 记录,而不是修改原来的。 + 1. Ask query: 用户肯定会输入一条新消息 + 2. Plan check 非确认模式,用户也是输入一条消息。 + */ + const pushNewItems = + finalInteractive.type === 'agentPlanAskQuery' || + (finalInteractive.type === 'agentPlanCheck' && userInteractiveVal !== ConfirmPlanAgentText); + + if (pushNewItems) { + return await pushChatRecords(props); + } + const parsedUserInteractiveVal = (() => { - const { text: userInteractiveVal } = chatValue2RuntimePrompt(userContent.value); try { return JSON.parse(userInteractiveVal); } catch (err) { @@ -376,40 +394,42 @@ export const updateInteractiveChat = async (props: Props) => { errorMsg }); - // 拿到的是实参 - const finalInteractive = extractDeepestInteractive(interactiveValue.interactive); - const pushNewItems = - finalInteractive.type === 'agentPlanAskQuery' || - (finalInteractive.type === 'agentPlanCheck' && userInteractiveVal !== ConfirmPlanAgentText); - + /* + 在原来 chat_items 上更新。 + 1. 更新交互响应结果 + 2. 合并 chat_item 数据 + 3. 合并 chat_item_response 数据 + */ // Update interactive value - if ( - finalInteractive.type === 'userSelect' || - finalInteractive.type === 'agentPlanAskUserSelect' - ) { - finalInteractive.params.userSelectedVal = userInteractiveVal; - } else if ( - (finalInteractive.type === 'userInput' || finalInteractive.type === 'agentPlanAskUserForm') && - typeof parsedUserInteractiveVal === 'object' - ) { - finalInteractive.params.inputForm = finalInteractive.params.inputForm.map((item) => { - const itemValue = parsedUserInteractiveVal[item.label]; - return itemValue !== undefined - ? { - ...item, - value: itemValue - } - : item; - }); - finalInteractive.params.submitted = true; - } else if (finalInteractive.type === 'paymentPause') { - chatItem.value.pop(); - } else if (finalInteractive.type === 'agentPlanCheck') { - finalInteractive.params.confirmed = true; + { + if ( + finalInteractive.type === 'userSelect' || + finalInteractive.type === 'agentPlanAskUserSelect' + ) { + finalInteractive.params.userSelectedVal = userInteractiveVal; + } else if ( + (finalInteractive.type === 'userInput' || finalInteractive.type === 'agentPlanAskUserForm') && + typeof parsedUserInteractiveVal === 'object' + ) { + finalInteractive.params.inputForm = finalInteractive.params.inputForm.map((item) => { + const itemValue = parsedUserInteractiveVal[item.label]; + return itemValue !== undefined + ? { + ...item, + value: itemValue + } + : item; + }); + finalInteractive.params.submitted = true; + } else if (finalInteractive.type === 'paymentPause') { + chatItem.value.pop(); + } else if (finalInteractive.type === 'agentPlanCheck') { + finalInteractive.params.confirmed = true; + } } // Update current items - if (!pushNewItems) { + { if (aiContent.customFeedbacks) { chatItem.customFeedbacks = chatItem.customFeedbacks ? [...chatItem.customFeedbacks, ...aiContent.customFeedbacks] @@ -435,9 +455,38 @@ export const updateInteractiveChat = async (props: Props) => { ? +(chatItem.durationSeconds + durationSeconds).toFixed(2) : durationSeconds; } - chatItem.markModified('value'); + chatItem.markModified('value'); await mongoSessionRun(async (session) => { + // Merge chat item respones + if (nodeResponses) { + const lastResponse = await MongoChatItemResponse.findOne({ + appId, + chatId, + chatItemDataId: chatItem.dataId + }) + .sort({ + _id: -1 + }) + .lean() + .session(session); + + const newResponses = lastResponse?.data + ? mergeChatResponseData([lastResponse?.data, ...nodeResponses]) + : nodeResponses; + + await MongoChatItemResponse.create( + newResponses.map((item) => ({ + teamId, + appId, + chatId, + chatItemDataId: chatItem.dataId, + data: item + })), + { session, ordered: true, ...writePrimary } + ); + } + await chatItem.save({ session }); await MongoChat.updateOne( { diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 05a9329fe..1bc805089 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -260,7 +260,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise if (result) return result; } - addLog.debug(`Start agentPlan`, { + addLog.debug(`Start master agent`, { agentPlan: JSON.stringify(agentPlan, null, 2) }); diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/ask/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/ask/constants.ts index d490b61e9..d1502be2c 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/ask/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/ask/constants.ts @@ -9,14 +9,29 @@ export const PlanAgentAskTool: ChatCompletionTool = { type: 'function', function: { name: SubAppIds.ask, - description: `在涉及用户个人偏好、具体场景细节或需要用户确认的重要决策点、流程实在进行不下去的情况下时,使用该工具来向用户搜集信息`, + description: `当缺少制定计划所必需的前置信息时调用此工具。使用场景: +1. 用户问题不明确,需要澄清具体目标或需求 +2. 缺少关键的用户偏好信息(如技术水平、预算、时间等) +3. 需要了解用户的具体场景或约束条件 +4. 任何影响计划制定的关键信息缺失 + +调用此工具会暂停计划生成,先向用户收集信息后再继续。`, parameters: { type: 'object', properties: { questions: { - description: '要向用户确认的问题列表', - items: { type: 'string' }, - type: 'array' + description: `要向用户提出的问题列表。每个问题应该: +- 具体明确,针对缺失的关键信息 +- 有助于制定更准确的计划 +- 避免过于宽泛或模糊的问题 +示例:["您想学习什么主题?", "您的当前水平如何(初级/中级/高级)?", "您每天可以投入多长时间学习?"]`, + items: { + type: 'string', + description: '一个具体的、有针对性的问题' + }, + type: 'array', + minItems: 1, + maxItems: 10 } }, required: ['questions'] diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts index 6227168e5..ce5943408 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts @@ -94,7 +94,10 @@ export const dispatchPlanAgent = async ({ } console.log('Plan request messages'); - console.dir(requestMessages, { depth: null }); + console.dir( + { requestMessages, tools: isTopPlanAgent ? [PlanAgentAskTool] : [] }, + { depth: null } + ); const { answerText, toolCalls = [], diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts index 8fcff5a85..6fbeb0e3d 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts @@ -42,28 +42,47 @@ export const getPlanAgentSystemPrompt = ({ 1. **渐进式规划**:只规划到下一个关键信息点或决策点 2. **适应性标记**:通过 'replan' 标识需要基于执行结果调整的任务节点 3. **最小化假设**:不对未知信息做过多预设,而是通过执行步骤获取 + 4. **前置信息优先**:制定计划前,优先收集必要的前置信息,而不是将信息收集作为计划的一部分 - - 以下是在生成计划时可以参考的工具描述(**该阶段不应该直接通过 tool call 的方式来调用工具,只作为一个描述**) + 以下是在计划执行过程中可以使用的工具: ${subAppPrompt} - - [@${SubAppIds.ask}]: 在缺少前置的关键信息或用户的问题不明确时,向用户提问; + - [@${SubAppIds.ask}]:在缺少制定计划所需的关键前置信息时,向用户提问收集信息; - - - subAppsPrompt 提供的是工具描述和背景信息 - - @符号仅用于在输出json 中的 description 中标记可能使用的工具类型 - - 不要尝试直接生成实际的工具调用 - - 工具引用应该是描述性的,而非指令性的 - + + **重要**:区分两种不同的工具使用场景: + + 1. **前置信息收集(通过 tool call 调用 ask 工具)**: + - 当你发现**缺少制定计划所必需的前置信息**时(如用户偏好、具体场景、关键参数、约束条件等) + - 这类信息如果缺失会导致**无法生成有效的计划**或计划过于笼统 + - 此时应该**立即调用 ${SubAppIds.ask} 工具**,暂停计划生成,先收集必要信息 + - **不要**将"询问用户XXX"、"收集用户信息"等作为计划的步骤 + - 例如:不知道用户的技术水平、预算范围、时间限制、具体目标等关键信息 + + 2. **执行过程中的工具使用(在 plan 的 description 中用 @ 标记)**: + - 当计划步骤**执行时**需要使用某个工具来完成任务 + - 此时在步骤的 description 中使用 @符号标记工具 + - 例如:"使用 @search_tool 搜索最新的技术文档" + - 这仅作为描述性标记,实际调用由执行引擎完成 + - - 提取核心目标、关键要素、约束与本地化偏好 - - 如果用户提供了前置规划信息,优先基于用户的步骤安排和偏好来生成计划 - - 仅在涉及用户个人偏好、具体场景细节时,才使用 [${SubAppIds.ask}] 询问 - - 输出语言风格本地化(根据用户输入语言进行术语与语序调整) - - 如果用户有自己输入的plan,应该按照他的plan流程来规划,但是在需要决策的地方进行中断,并把 replan 字段设置为需要决策的步骤id - - subAppsPrompt 仅作为参考和背景信息,不直接生成 tool call function 的调用 - - 在 description 中可以通过 @符号引用工具,但这仅作为描述性标记,不代表实际调用 - - 严格按照 JSON Schema 生成完整计划,不得输出多余内容。 + 1. **前置信息检查**: + - 首先判断是否具备制定计划所需的所有关键信息 + - 如果缺少用户偏好、具体场景细节、关键约束、目标参数等前置信息 + - **立即调用 ${SubAppIds.ask} 工具**,提出清晰的问题列表收集信息 + - **切记**:不要将"询问用户"、"收集信息"作为计划的步骤 + + 2. **计划生成**: + - 在获得必要的前置信息后,再开始制定具体计划 + - 提取核心目标、关键要素、约束与本地化偏好 + - 如果用户提供了前置规划信息,优先基于用户的步骤安排和偏好来生成计划 + - 输出语言风格本地化(根据用户输入语言进行术语与语序调整) + - 在步骤的 description 中可以使用 @符号标记执行时需要的工具 + - 严格按照 JSON Schema 生成完整计划,不得输出多余内容 + + 3. **决策点处理**: + - 如果计划中存在需要基于执行结果做决策的节点,使用 replan 字段标记 + - 如果用户有自己输入的plan,按照其流程规划,在需要决策的地方设置 replan - 必须严格输出 JSON @@ -139,7 +158,44 @@ export const getPlanAgentSystemPrompt = ({ - **说明探索重点**:"搜索相关案例,关注:1)实施成本 2)成功率 3)常见问题" + + **场景**:用户说"帮我规划一个学习计划",但没有说明学习什么、学习目标、时间限制等关键信息。 + + **正确做法**:直接调用 ask_agent 工具收集前置信息 + \`\`\` + // 通过 tool call 调用 ask_agent + { + "name": "ask_agent", + "arguments": { + "questions": [ + "您想学习什么主题或技能?", + "您的当前水平如何(零基础/初级/中级/高级)?", + "您希望达到什么样的学习目标?", + "您计划投入多长时间学习(每天/每周)?", + "您是否有特定的学习偏好(视频/书籍/实践项目)?" + ] + } + } + \`\`\` + + **错误做法**:将信息收集作为计划步骤(不要这样做) + \`\`\`json + { + "task": "制定学习计划", + "steps": [ + { + "id": "step1", + "title": "收集用户信息", + "description": "询问用户学习主题、目标、时间等信息" // ❌ 错误 + } + ] + } + \`\`\` + + + **场景**:用户已经提供了明确的学习主题和目标,可以直接制定计划。 + \`\`\`json { "task": "[主题] 的完整了解和学习", diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index 31f688c0b..606a295d2 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -43,7 +43,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; -import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; +import { pushChatRecords, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; import { getLocale } from '@fastgpt/service/common/middle/i18n'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; @@ -237,7 +237,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (isInteractiveRequest) { await updateInteractiveChat(params); } else { - await saveChat(params); + await pushChatRecords(params); } } catch (err: any) { res.status(500); diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 11b6cbecf..509ab23a7 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -18,7 +18,7 @@ import { } from '@fastgpt/global/core/workflow/runtime/utils'; import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getChatItems } from '@fastgpt/service/core/chat/controller'; -import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; +import { pushChatRecords, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; @@ -357,7 +357,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (isInteractiveRequest) { await updateInteractiveChat(params); } else { - await saveChat(params); + await pushChatRecords(params); } addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index 8c95f127e..a6f4d5fb7 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -18,7 +18,7 @@ import { } from '@fastgpt/global/core/workflow/runtime/utils'; import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getChatItems } from '@fastgpt/service/core/chat/controller'; -import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; +import { pushChatRecords, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; @@ -358,7 +358,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (isInteractiveRequest) { await updateInteractiveChat(params); } else { - await saveChat(params); + await pushChatRecords(params); } addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 80e02a08d..850f2ade3 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -14,7 +14,7 @@ import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants' import { addLog } from '@fastgpt/service/common/system/log'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; -import { saveChat } from '@fastgpt/service/core/chat/saveChat'; +import { pushChatRecords } from '@fastgpt/service/core/chat/saveChat'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { getUserChatInfo } from '@fastgpt/service/support/user/team/utils'; @@ -89,7 +89,7 @@ export const getScheduleTriggerApp = async () => { const error = flowResponses[flowResponses.length - 1]?.error; // Save chat - await saveChat({ + await pushChatRecords({ chatId, appId: app._id, teamId: String(app.teamId), @@ -116,7 +116,7 @@ export const getScheduleTriggerApp = async () => { } catch (error) { addLog.error('Schedule trigger error', error); - await saveChat({ + await pushChatRecords({ chatId, appId: app._id, teamId: String(app.teamId), diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts index 1ab4f100d..175f58bfc 100644 --- a/projects/app/src/service/support/mcp/utils.ts +++ b/projects/app/src/service/support/mcp/utils.ts @@ -29,7 +29,7 @@ import { import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { getChatTitleFromChatMessage, removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; -import { saveChat } from '@fastgpt/service/core/chat/saveChat'; +import { pushChatRecords } from '@fastgpt/service/core/chat/saveChat'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils'; @@ -243,7 +243,7 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps memories: system_memories }; const newTitle = isPlugin ? 'Mcp call' : getChatTitleFromChatMessage(userQuestion); - await saveChat({ + await pushChatRecords({ chatId, appId: app._id, teamId: app.teamId, diff --git a/test/cases/service/support/mcp/utils.test.ts b/test/cases/service/support/mcp/utils.test.ts index 920f765cd..683e5404e 100644 --- a/test/cases/service/support/mcp/utils.test.ts +++ b/test/cases/service/support/mcp/utils.test.ts @@ -46,7 +46,7 @@ vi.mock('@fastgpt/service/core/workflow/dispatch', () => ({ })); vi.mock('@fastgpt/service/core/chat/saveChat', () => ({ - saveChat: vi.fn() + pushChatRecords: vi.fn() })); describe('pluginNodes2InputSchema', () => {