From 722756914288e00d64b1c26b859225deb5fc2b9f Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Tue, 16 Sep 2025 21:28:52 +0800 Subject: [PATCH] perf: agent plan --- .../core/workflow/dispatch/ai/agent/index.ts | 223 ++++++++++-------- .../dispatch/ai/agent/sub/constants.ts | 21 +- .../ai/agent/sub/plan/ask/constants.ts | 34 ++- .../dispatch/ai/agent/sub/plan/constants.ts | 11 +- .../dispatch/ai/agent/sub/plan/index.ts | 215 ++++++++++------- .../dispatch/ai/agent/sub/plan/prompt.ts | 78 ++++-- .../dispatch/ai/agent/sub/plan/type.ts | 9 + .../core/workflow/dispatch/ai/extract.ts | 1 - .../core/workflow/dispatch/ai/utils.ts | 12 +- 9 files changed, 369 insertions(+), 235 deletions(-) create mode 100644 packages/service/core/workflow/dispatch/ai/agent/sub/plan/type.ts diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index f4e5ad286..f3ba6e519 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -1,4 +1,5 @@ -import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import type { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { ConfirmPlanAgentText, DispatchNodeResponseKeyEnum, @@ -17,6 +18,7 @@ import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { GPTMessages2Chats, + chatValue2RuntimePrompt, chats2GPTMessages, getSystemPrompt_ChatItemType, runtimePrompt2ChatsValue @@ -43,6 +45,7 @@ import { addFilePrompt2Input, getFileInputPrompt } from './sub/file/utils'; import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { dispatchFileRead } from './sub/file'; import { dispatchApp, dispatchPlugin } from './sub/app'; +import { getSubAppsPrompt } from './sub/plan/prompt'; export type DispatchAgentModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.history]?: ChatItemType[]; @@ -84,7 +87,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise params: { model, systemPrompt, - userChatInput, + userChatInput: taskInput, history = 6, fileUrlList: fileLinks, temperature, @@ -119,6 +122,9 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise // Check interactive entry props.node.isEntry = false; + // 交互模式进来的话,这个值才是交互输入的值 + const interactiveInput = lastInteractive ? chatValue2RuntimePrompt(query).text : ''; + try { // Get files const fileUrlInput = inputs.find((item) => item.key === NodeInputKeyEnum.fileUrlList); @@ -165,15 +171,90 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise const toolNode = subAppsMap.get(id) || systemSubInfo[id]; return { name: toolNode?.name || '', - avatar: toolNode?.avatar || '' + avatar: toolNode?.avatar || '', + toolDescription: toolNode?.toolDescription || toolNode?.name || '' }; }; + /* ===== Plan Agent ===== */ + let planMessages: ChatCompletionMessageParam[] = []; + /* + Top agent + 1. 首轮执行:有 + 2. 交互/check:有 + 3. Confirmed plan: 无 + 4. masteragent 的交互时间: 无 + + Sub agent + 只会执行一次,肯定有。 + */ + let masterPlanToolCallMessages: ChatCompletionMessageParam[] = []; + + if ( + // 不为 userSelect 和 userInput,或者如果为 agentPlanCheck 并且 interactiveInput !== ConfirmPlanAgentText 都会执行 + (lastInteractive?.type !== 'userSelect' && + lastInteractive?.type !== 'userInput' && + lastInteractive?.type !== 'agentPlanCheck') || + (lastInteractive?.type === 'agentPlanCheck' && interactiveInput !== ConfirmPlanAgentText) + ) { + // 临时代码 + const tmpText = '正在进行规划生成...\n'; + workflowStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: tmpText + }) + }); + + const { + answerText, + planList, + planToolCallMessages, + completeMessages, + usages, + interactiveResponse + } = await dispatchPlanAgent({ + historyMessages: planHistoryMessages, + userInput: lastInteractive ? interactiveInput : taskInput, + interactive: lastInteractive, + subAppPrompt: getSubAppsPrompt({ subAppList, getSubAppInfo }), + model, + systemPrompt, + temperature, + top_p: aiChatTopP, + stream, + isTopPlanAgent: workflowDispatchDeep === 1 + }); + + const text = `${answerText}${planList ? `\n\`\`\`json\n${JSON.stringify(planList, null, 2)}\n\`\`\`` : ''}`; + workflowStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text + }) + }); + + planMessages = completeMessages; + masterPlanToolCallMessages = planToolCallMessages; + + // TODO: usage 合并 + // Sub agent plan 不会有交互响应。Top agent plan 肯定会有。 + if (interactiveResponse) { + return { + [DispatchNodeResponseKeyEnum.answerText]: `${tmpText}${text}`, + [DispatchNodeResponseKeyEnum.memories]: { + [planMessagesKey]: filterMemoryMessages(planMessages) + }, + [DispatchNodeResponseKeyEnum.interactive]: interactiveResponse + }; + } + } + + /* ===== Master agent ===== */ + // Get master request messages const systemMessages = chats2GPTMessages({ - messages: getSystemPrompt_ChatItemType( - workflowDispatchDeep === 1 ? getMasterAgentDefaultPrompt() : systemPrompt - ), + messages: getSystemPrompt_ChatItemType(getMasterAgentDefaultPrompt()), reserveId: false }); const historyMessages: ChatCompletionMessageParam[] = (() => { @@ -184,87 +265,24 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise return chats2GPTMessages({ messages: chatHistories, reserveId: false }); })(); - if (lastInteractive?.type !== 'userSelect' && lastInteractive?.type !== 'userInput') { - userChatInput = query[0].text?.content ?? userChatInput; - } const userMessages = chats2GPTMessages({ messages: [ { obj: ChatRoleEnum.Human, value: runtimePrompt2ChatsValue({ - text: addFilePrompt2Input({ query: userChatInput, filePrompt: fileInputPrompt }), + text: addFilePrompt2Input({ query: taskInput, filePrompt: fileInputPrompt }), files: [] }) } ], reserveId: false }); - - const requestMessages = [...systemMessages, ...historyMessages, ...userMessages]; - - // TODO: 执行 plan function(只有lastInteractive userselect/userInput 时候,才不需要进入 plan) - if ( - lastInteractive?.type !== 'userSelect' && - lastInteractive?.type !== 'userInput' && - userChatInput !== ConfirmPlanAgentText && - workflowDispatchDeep === 1 - ) { - const planRequestMessages = [...planHistoryMessages, ...userMessages]; - const { completeMessages, toolMessages, usages, interactiveResponse } = - await dispatchPlanAgent({ - messages: planRequestMessages, - subApps: subAppList, - model, - temperature, - top_p: aiChatTopP, - systemPrompt, - stream, - onReasoning: ({ text }: { text: string }) => { - workflowStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - reasoning_content: text - }) - }); - }, - onStreaming: ({ text }: { text: string }) => { - workflowStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text - }) - }); - } - }); - - if (toolMessages) requestMessages.push(...toolMessages); - - return { - [DispatchNodeResponseKeyEnum.memories]: { - [masterMessagesKey]: filterMemoryMessages(requestMessages), - [planMessagesKey]: filterMemoryMessages(completeMessages) - }, - [DispatchNodeResponseKeyEnum.interactive]: interactiveResponse - - // Mock: 返回 plan user input - // [DispatchNodeResponseKeyEnum.interactive]: { - // type: 'agentPlanAskUserForm', - // params: { - // description: '测试', - // inputForm: [ - // { - // type: FlowNodeInputTypeEnum.input, - // key: 'test1', - // label: '测试1', - // value: '', - // valueType: WorkflowIOValueTypeEnum.string, - // required: true - // } - // ] - // } - // } - }; - } + const requestMessages = [ + ...systemMessages, + ...historyMessages, + ...masterPlanToolCallMessages, + ...userMessages + ]; const dispatchFlowResponse: ChatHistoryItemResType[] = []; const { @@ -371,7 +389,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise isEnd: true }; } - // TODO: 现在是程序中强制执行的 Plan + // TODO: 可能会再次触发 plan // else if (toolId === SubAppIds.plan) { // const { completeMessages, response, usages, interactiveResponse } = // await dispatchPlanAgent({ @@ -393,18 +411,26 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise // }; // } else if (toolId === SubAppIds.model) { - const { systemPrompt, task } = parseToolArgs<{ + const params = parseToolArgs<{ systemPrompt: string; task: string; }>(call.function.arguments); + if (!params) { + return { + response: 'params is not object', + usages: [], + isEnd: false + }; + } + const { response, usages } = await dispatchModelAgent({ model, temperature, top_p: aiChatTopP, stream, - systemPrompt, - task, + systemPrompt: params.systemPrompt, + task: params.task, onReasoning, onStreaming }); @@ -414,10 +440,17 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise isEnd: false }; } else if (toolId === SubAppIds.fileRead) { - const { file_indexes } = parseToolArgs<{ + const params = parseToolArgs<{ file_indexes: string[]; }>(call.function.arguments); - if (!Array.isArray(file_indexes)) { + if (!params) { + return { + response: 'params is not object', + usages: [], + isEnd: false + }; + } + if (!Array.isArray(params.file_indexes)) { return { response: 'file_indexes is not array', usages: [], @@ -425,7 +458,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }; } - const files = file_indexes.map((index) => ({ + const files = params.file_indexes.map((index) => ({ index, url: filesMap[index] })); @@ -453,6 +486,15 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise } const toolCallParams = parseToolArgs(call.function.arguments); + + if (!toolCallParams) { + return { + response: 'params is not object', + usages: [], + isEnd: false + }; + } + // Get params const requestParams = (() => { const params: Record = toolCallParams; @@ -582,16 +624,12 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise const previewAssistantResponses = filterToolResponseToPreview(assistantResponses); return { - data: { - [NodeOutputKeyEnum.answerText]: previewAssistantResponses - .filter((item) => item.text?.content) - .map((item) => item.text?.content || '') - .join('') - }, + // 目前 Master 不会触发交互 + // [DispatchNodeResponseKeyEnum.interactive]: interactiveResponse, // TODO: 需要对 memoryMessages 单独建表存储 [DispatchNodeResponseKeyEnum.memories]: { [masterMessagesKey]: filterMemoryMessages(completeMessages), - [planMessagesKey]: [filterMemoryMessages(planHistoryMessages)] + [planMessagesKey]: [filterMemoryMessages(planMessages)] }, [DispatchNodeResponseKeyEnum.assistantResponses]: previewAssistantResponses, [DispatchNodeResponseKeyEnum.nodeResponse]: { @@ -601,7 +639,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise toolCallOutputTokens: outputTokens, childTotalPoints: toolTotalPoints, model: modelName, - query: userChatInput, + query: taskInput, historyPreview: getHistoryPreview( GPTMessages2Chats({ messages: completeMessages, reserveTool: false }), 10000, @@ -621,8 +659,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }, // Tool usage ...subAppUsages - ], - [DispatchNodeResponseKeyEnum.interactive]: interactiveResponse + ] }; } catch (error) { return getNodeErrResponse({ error }); diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/constants.ts index d674a9e38..7ba75ddba 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/constants.ts @@ -1,4 +1,3 @@ -import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import { i18nT } from '../../../../../../../web/i18n/utils'; export enum SubAppIds { @@ -9,25 +8,33 @@ export enum SubAppIds { fileRead = 'file_read' } -export const systemSubInfo: Record = { +export const systemSubInfo: Record< + string, + { name: string; avatar: string; toolDescription: string } +> = { [SubAppIds.plan]: { name: i18nT('chat:plan_agent'), - avatar: 'common/detail' + avatar: 'common/detail', + toolDescription: '分析和拆解用户问题,制定分步计划。' }, [SubAppIds.fileRead]: { name: i18nT('chat:file_parse'), - avatar: 'core/workflow/template/readFiles' + avatar: 'core/workflow/template/readFiles', + toolDescription: '读取文件内容,并返回文件内容。' }, [SubAppIds.ask]: { name: 'Ask Agent', - avatar: 'core/workflow/template/agent' + avatar: 'core/workflow/template/agent', + toolDescription: '询问用户问题,并返回用户回答。' }, [SubAppIds.stop]: { name: 'Stop Agent', - avatar: 'core/workflow/template/agent' + avatar: 'core/workflow/template/agent', + toolDescription: '停止当前任务。' }, [SubAppIds.model]: { name: 'Model Agent', - avatar: 'core/workflow/template/agent' + avatar: 'core/workflow/template/agent', + toolDescription: '调用 LLM 模型完成一些通用任务。' } }; 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 03c892133..13e1d3441 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 @@ -1,19 +1,28 @@ import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import { SubAppIds } from '../../constants'; -export type AskAgentToolParamsType = Partial<{ - mode: 'select' | 'formInput' | 'input'; - prompt: string; - options: string[]; - form: { - field: string; - type: 'textInput' | 'numberInput' | 'singleSelect' | 'multiSelect'; - required: boolean; - options: string[]; - }[]; -}>; +export type AskAgentToolParamsType = + | { + mode: 'select'; + prompt?: string; + options: string[]; + } + | { + mode: 'formInput'; + prompt?: string; + form: { + field: string; + type: 'textInput' | 'numberInput' | 'singleSelect' | 'multiSelect'; + required: boolean; + options: string[]; + }[]; + } + | { + mode: 'input'; + prompt: string; + }; -export const AskAgentTool: ChatCompletionTool = { +export const PlanAgentAskTool: ChatCompletionTool = { type: 'function', function: { name: SubAppIds.ask, @@ -30,7 +39,6 @@ export const AskAgentTool: ChatCompletionTool = { 2. mode = "input" - 用于自由文本输入,适合用户提供个性化或开放式回答。 - prompt: 展示的问题提示,引导用户填写。 - - options: 此模式下通常留空或忽略。 - 场景示例: * 需要用户补充说明原因、填写备注、输入 URL/编号等。 * 当 "select" 的选项无法覆盖用户真实答案时,可以再调用一次 "input" 追问。 diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/constants.ts index 1ba0af251..1086c595a 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/constants.ts @@ -1,11 +1,18 @@ import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; -import { SubAppIds } from '../constants'; +import { SubAppIds, systemSubInfo } from '../constants'; +import type { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +export const PlanCheckInteractive: InteractiveNodeResponseType = { + type: 'agentPlanCheck', + params: { + confirmed: false + } +}; export const PlanAgentTool: ChatCompletionTool = { type: 'function', function: { name: SubAppIds.plan, - description: '分析和拆解用户问题,制定分步计划。', + description: systemSubInfo[SubAppIds.plan].toolDescription, parameters: {} } }; 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 56d41f049..13e1f980a 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 @@ -2,17 +2,21 @@ import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/global/core/ai/type.d'; -import { createLLMResponse, type ResponseEvents } from '../../../../../../ai/llm/request'; +import { createLLMResponse } from '../../../../../../ai/llm/request'; import { getPlanAgentPrompt } from './prompt'; import { getLLMModel } from '../../../../../../ai/model'; import { formatModelChars2Points } from '../../../../../../../support/wallet/usage/utils'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { SubAppIds } from '../constants'; -import type { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import type { + InteractiveNodeResponseType, + WorkflowInteractiveResponseType +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { parseToolArgs } from '../../../utils'; -import { AskAgentTool, type AskAgentToolParamsType } from './ask/constants'; +import { PlanAgentAskTool, type AskAgentToolParamsType } from './ask/constants'; +import { PlanCheckInteractive } from './constants'; +import type { AgentPlanType } from './type'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; type PlanAgentConfig = { model: string; @@ -23,49 +27,67 @@ type PlanAgentConfig = { }; type DispatchPlanAgentProps = PlanAgentConfig & { - messages: ChatCompletionMessageParam[]; - subApps: ChatCompletionTool[]; - onReasoning: ResponseEvents['onReasoning']; - onStreaming: ResponseEvents['onStreaming']; + historyMessages: ChatCompletionMessageParam[]; + userInput: string; + interactive?: WorkflowInteractiveResponseType; + + subAppPrompt: string; + isTopPlanAgent: boolean; }; type DispatchPlanAgentResponse = { - response: string; - usages: ChatNodeUsageType[]; + answerText: string; + planList?: AgentPlanType; + planToolCallMessages: ChatCompletionMessageParam[]; completeMessages: ChatCompletionMessageParam[]; - toolMessages?: ChatCompletionMessageParam[]; + usages: ChatNodeUsageType[]; interactiveResponse?: InteractiveNodeResponseType; }; export const dispatchPlanAgent = async ({ - messages, - - subApps, + historyMessages, + userInput, + interactive, + subAppPrompt, model, systemPrompt, temperature, top_p, stream, - onReasoning, - onStreaming + isTopPlanAgent }: DispatchPlanAgentProps): Promise => { const modelData = getLLMModel(model); const requestMessages: ChatCompletionMessageParam[] = [ { role: 'system', - content: getPlanAgentPrompt(systemPrompt) + content: getPlanAgentPrompt(subAppPrompt, systemPrompt) }, - ...messages.filter((item) => item.role !== 'system') + ...historyMessages.filter((item) => item.role !== 'system') ]; - // TODO: 考虑一下 plan 要不要挂上 master 的工具组 - // const filterPlanTools = subApps.filter((item) => item.function.name !== SubAppIds.plan); - // filterPlanTools.push(AskAgentTool); - const tools = [AskAgentTool]; + // 分类:query/user select/user form + const lastMessages = requestMessages[requestMessages.length - 1]; + if ( + (interactive?.type === 'agentPlanAskUserSelect' || + interactive?.type === 'agentPlanAskUserForm') && + lastMessages.role === 'assistant' && + lastMessages.tool_calls + ) { + requestMessages.push({ + role: 'tool', + tool_call_id: lastMessages.tool_calls[0].id, + content: userInput + }); + } else { + requestMessages.push({ + role: 'user', + content: userInput + }); + } + console.log(JSON.stringify({ requestMessages }, null, 2)); const { - reasoningText, answerText, toolCalls = [], usage, @@ -79,73 +101,50 @@ export const dispatchPlanAgent = async ({ top_p, stream, - tools, + tools: isTopPlanAgent ? [PlanAgentAskTool] : [], tool_choice: 'auto', toolCallMode: modelData.toolChoice ? 'toolChoice' : 'prompt', - parallel_tool_calls: true - }, - onReasoning, - onStreaming + parallel_tool_calls: false + } }); - if (!answerText && !reasoningText && !toolCalls.length) { + if (!answerText && !toolCalls.length) { return Promise.reject(getEmptyResponseTip()); } - // TODO: 需要考虑多个 Interactive 并发的情况 - let interactiveResponse: InteractiveNodeResponseType = { - type: 'agentPlanCheck', - params: {} - }; + /* + 正常输出情况: + 1. text: 正常生成plan + 2. toolCall: 调用ask工具 + 3. text + toolCall: 可能生成 plan + 调用ask工具 + */ - for await (const call of toolCalls) { - const toolId = call.function.name; - - if (toolId === SubAppIds.ask) { - const params = parseToolArgs(call.function.arguments); - - if (params.mode === 'select') { - interactiveResponse = { - type: 'agentPlanAskUserSelect', - params: { - description: params?.prompt ?? '选择选项', - userSelectOptions: params?.options?.map((v, i) => { - return { key: `option${i}`, value: v }; - }) - } - } as InteractiveNodeResponseType; - } - if (params.mode === 'input') { - interactiveResponse = { - type: 'agentPlanAskQuery', - params: { - content: params?.prompt ?? '输入详细信息' - } - }; - } - - completeMessages.push({ - tool_call_id: call.id, - role: ChatCompletionRequestMessageRoleEnum.Tool, - content: '等待用户输入内容' - }); + // Text: 回答的文本;planList: 结构化的plan,只能有其中一个有值 + const { text, planList } = (() => { + if (!answerText) + return { + text: '', + planList: undefined + }; + const params = parseToolArgs(answerText); + if (!params || !params.task || !params.steps) { + return { + text: answerText, + planList: undefined + }; } - } - - const { totalPoints, modelName } = formatModelChars2Points({ - model: modelData.model, - inputTokens: usage.inputTokens, - outputTokens: usage.outputTokens - }); - - const toolMessages: ChatCompletionMessageParam[] = []; - if (answerText) { - const toolId = getNanoid(6); - const toolCall: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, + return { + text: '', + planList: params + }; + })(); + const callPlanId = getNanoid(6); + const planToolCallMessages: ChatCompletionMessageParam[] = [ + { + role: 'assistant', tool_calls: [ { - id: toolId, + id: callPlanId, type: 'function', function: { name: SubAppIds.plan, @@ -153,17 +152,56 @@ export const dispatchPlanAgent = async ({ } } ] - }; - const toolCallResponse: ChatCompletionMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Tool, - tool_call_id: toolId, - content: answerText - }; - toolMessages.push(toolCall, toolCallResponse); - } + }, + { + role: 'tool', + tool_call_id: callPlanId, + content: planList ? JSON.stringify(planList) : text || 'Create plan error' + } + ]; + + // 只有顶层有交互模式 + const interactiveResponse: InteractiveNodeResponseType | undefined = (() => { + if (!isTopPlanAgent) return; + + const tooCall = toolCalls[0]; + if (tooCall) { + const params = parseToolArgs(tooCall.function.arguments); + if (params?.mode === 'select') { + return { + type: 'agentPlanAskUserSelect', + params: { + description: params.prompt ?? '', + userSelectOptions: params.options.filter(Boolean).map((v, i) => { + return { key: `option${i}`, value: v }; + }) + } + }; + } + if (params?.mode === 'input' && params.prompt) { + return { + type: 'agentPlanAskQuery', + params: { + content: params.prompt ?? '' + } + }; + } + } + + // Plan 没有主动交互,则强制触发 check + return PlanCheckInteractive; + })(); + + const { totalPoints, modelName } = formatModelChars2Points({ + model: modelData.model, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + }); return { - response: answerText, + answerText: text, + planList, + planToolCallMessages, usages: [ { moduleName: modelName, @@ -174,7 +212,6 @@ export const dispatchPlanAgent = async ({ } ], completeMessages, - toolMessages, interactiveResponse }; }; 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 6a38c318a..7c1f8ef18 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 @@ -1,24 +1,52 @@ -export const getPlanAgentPrompt = (background?: string) => { +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import { SubAppIds } from '../constants'; + +export const getSubAppsPrompt = ({ + subAppList, + getSubAppInfo +}: { + subAppList: ChatCompletionTool[]; + getSubAppInfo: (id: string) => { + name: string; + avatar: string; + toolDescription: string; + }; +}) => { + return subAppList + .map((item) => { + const info = getSubAppInfo(item.function.name); + if (!info) return ''; + return `@${info.name}(${info.toolDescription})`; + }) + .filter(Boolean) + .join('; '); +}; + +/* + subAppsPrompt: + @名字(功能); @名字(功能) +*/ +export const getPlanAgentPrompt = (subAppsPrompt: string, systemPrompt?: string) => { return ` 你是一个专业的项目规划助手,擅长将复杂任务分解为结构化的执行计划。 ${ - background - ? ` -${background} -` + systemPrompt + ? ` +${systemPrompt} +` : '' } + -1. 解析用户输入,提取核心目标、关键要素、约束与本地化偏好。 -2. 评估任务复杂度, 据此确定阶段数量。 -3. 禁止调用除"ask_agent"以外的任何工具. -4. 语言风格本地化(根据用户输入语言进行术语与语序调整)。 -5. 严格按照 JSON Schema 生成完整计划,不得输出多余内容。 -6. 仅在缺少关键信息时使用"ask_agent"工具询问用户(如:未指定目的地、预算、时间等必要细节) -7. 如果信息充足或用户已回答询问,必须直接输出JSON格式的完整计划,不再调用工具 +- 解析用户输入,提取核心目标、关键要素、约束与本地化偏好。 +- 在缺少完成任务的关键信息时,使用 [${SubAppIds.ask}] 工具来询问用户(如:未指定目的地、预算、时间等必要细节) +- 你还可以使用这些工具来设计本轮执行计划: """${subAppsPrompt}"""。注意,你只有这些工具可以进行调用。 +${systemPrompt ? '- 制定本轮计划时,严格参考 中的内容进行设计,设计的计划不偏离。' : ''} +- 输出语言风格本地化(根据用户输入语言进行术语与语序调整)。 +- 严格按照 JSON Schema 生成完整计划,不得输出多余内容。 @@ -34,21 +62,21 @@ ${background} }, "steps": { "type": "array", - "description": "阶段步骤列表", + "description": "完成任务的步骤列表", "items": { "type": "object", "properties": { "id": { "type": "string", - "description": "唯一标识" + "description": "步骤的唯一标识" }, "title": { "type": "string", - "description": "阶段标题" + "description": "步骤标题" }, "description": { "type": "string", - "description": "阶段描述, 并在末尾@对应任务将要移交使用的工具/子智能体" + "description": "步骤的具体描述, 可以使用@符号声明需要用到的工具。" }, }, "required": ["id", "title", "description"] @@ -66,23 +94,21 @@ ${background} - 保持中立、客观;必要时指出风险与依赖。 - - + { "task": "[主题] 深度调研计划", "steps": [ { - "id": "[id]", - "title": "[阶段名称]", - "description": "[阶段描述] @sub_agent" + "id": "step1", + "title": "[步骤名称]", + "description": "[步骤描述] @网络搜索" }, { - "id": "[id]", - "title": "[阶段名称]", - "description": "[阶段描述] @sub_agent" + "id": "step2", + "title": "[步骤名称]", + "description": "[步骤描述] @webhook机器人" } ] } - -`; +`; }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/type.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/type.ts new file mode 100644 index 000000000..1d3ffda33 --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/type.ts @@ -0,0 +1,9 @@ +export type AgentPlanStepType = { + id: string; + title: string; + description: string; +}; +export type AgentPlanType = { + task: string; + steps: AgentPlanStepType[]; +}; diff --git a/packages/service/core/workflow/dispatch/ai/extract.ts b/packages/service/core/workflow/dispatch/ai/extract.ts index 4a092f47e..f25073ae2 100644 --- a/packages/service/core/workflow/dispatch/ai/extract.ts +++ b/packages/service/core/workflow/dispatch/ai/extract.ts @@ -23,7 +23,6 @@ import { } from '@fastgpt/global/core/ai/type'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { ModelTypeEnum } from '../../../../../global/core/ai/model'; import { getExtractJsonPrompt, getExtractJsonToolPrompt diff --git a/packages/service/core/workflow/dispatch/ai/utils.ts b/packages/service/core/workflow/dispatch/ai/utils.ts index 04e4ab3a0..56cbdf212 100644 --- a/packages/service/core/workflow/dispatch/ai/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/utils.ts @@ -1,4 +1,8 @@ -import { replaceVariable, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; +import { + replaceVariable, + sliceJsonStr, + sliceStrStartEnd +} from '@fastgpt/global/common/string/tools'; import type { AIChatItemValueItemType, UserChatItemValueItemType @@ -174,10 +178,10 @@ export const getToolNodesByIds = ({ }); }; -export const parseToolArgs = >(toolArgs: string): T => { +export const parseToolArgs = >(toolArgs: string) => { try { - return json5.parse(toolArgs) as T; + return json5.parse(sliceJsonStr(toolArgs)) as T; } catch { - return {} as T; + return; } };