From 32ea07cb9c8798048cd553573ae41b88711402c0 Mon Sep 17 00:00:00 2001 From: francis <1485059943@qq.com> Date: Mon, 8 Sep 2025 10:55:18 +0800 Subject: [PATCH] feat: add plan agent & model agent (#5577) * refactor: agent call (#5572) * feat: Add relevant functions related to agent invocation, including plan parsing, state management and tool invocation * Refactor agent call logic and utilities - Simplified the `runAgentCall` function by removing unnecessary complexity and restructuring the flow for better readability and maintainability. - Introduced helper functions to create tools from tool nodes and to prepare agent messages, enhancing modularity. - Removed the `utils.ts` file as its functions were integrated into the main logic, streamlining the codebase. - Updated the dispatch logic in `index.ts` to utilize the new helper functions and improve clarity. - Adjusted the handling of interactive modes and tool calls to ensure proper response formatting and error handling. * refactor: clean up the processing logic of the interactive mode and remove the unused tool creation functions * feat: add relevant constants for proxy configuration and update the proxy call logic * refactor: remove unused configuration variables from workflow properties * refactor: remove unused configuration variables from dispatchRunAgents props * fix: build error * refactor: update FlowNodeTypeEnum values and consolidate utility functions * refactor: simplify conditional checks in tool call and reasoning handlers * feat: add default agent prompt for improved response handling * refactor: rename directory with agent->tool, agentCall->agnet * refactor: rename dispatchRunAgents to dispatchRunAgent for consistency * refactor: rename toolCall to tools for consistency in FlowNodeTypeEnum * refactor: rename agents to toolCall for consistency in nodeTypes mapping * refactor: remove unused runtimeEdges parameter from dispatchRunAgent * refactor: update runAgentCall and dispatchRunAgent to use structured requestProps and workflowProps * refactor: streamline requestProps and handleToolResponse in runAgentCall and dispatchRunAgent * refactor: restructure RunAgentCallProps and update requestProps to requestParams for clarity * refactor: enhance interactiveEntryToolParams handling in runAgentCall for improved response management * refactor: flatten RunAgentCallProps structure and update dispatchRunAgent to use direct properties * fix: correct initialization of interactiveResponse in runAgentCall * agent call code * fix: agent call stop sign * feat: add plan agent tools and default generated prompts * feat: add model agent tools and related functions * chore: rename enum value * fix: optimize isEndSign assignment and update default plan prompt format * fix: update transferPlanAgent to use histories instead of sharedContext and rename default prompt variable * fix: update transferPlanAgent to use ChatItemType and adapt message structure * feat: add ModelAgentTool and PlanAgentTool with detailed descriptions and parameters fix: update error handling in transferModelAgent and transferPlanAgent to return error messages refactor: simplify isEndSign assignment in runAgentCall * feat: enhance agent call handling and response processing with context support * feat: refactor agent prompts and add utility functions for system prompt parsing * feat: add plan agent tools and default generated prompts * feat: add model agent tools and related functions * chore: rename enum value * fix: optimize isEndSign assignment and update default plan prompt format * fix: update transferPlanAgent to use histories instead of sharedContext and rename default prompt variable * fix: update transferPlanAgent to use ChatItemType and adapt message structure * feat: add ModelAgentTool and PlanAgentTool with detailed descriptions and parameters fix: update error handling in transferModelAgent and transferPlanAgent to return error messages refactor: simplify isEndSign assignment in runAgentCall * feat: enhance agent call handling and response processing with context support * feat: refactor agent prompts and add utility functions for system prompt parsing * feat: add AskAgentTool to support the interactive questioning function * Update request.ts --------- Co-authored-by: archer <545436317@qq.com> --- packages/service/core/ai/llm/agentCall.ts | 10 +- .../workflow/dispatch/ai/agent/constants.ts | 50 ++---- .../core/workflow/dispatch/ai/agent/index.ts | 150 +++++++++++++++--- .../core/workflow/dispatch/ai/agent/utils.ts | 61 +++++++ .../workflow/dispatch/ai/sub/ask/constants.ts | 61 +++++++ .../dispatch/ai/sub/model/constants.ts | 24 +++ .../workflow/dispatch/ai/sub/model/index.ts | 87 ++++++++++ .../dispatch/ai/sub/plan/constants.ts | 22 +++ .../workflow/dispatch/ai/sub/plan/index.ts | 96 +++++++++++ .../workflow/dispatch/ai/sub/plan/prompt.ts | 145 +++++++++++++++++ .../workflow/dispatch/ai/tool/toolCall.ts | 11 +- .../core/workflow/dispatch/ai/utils.ts | 50 ++++++ 12 files changed, 700 insertions(+), 67 deletions(-) create mode 100644 packages/service/core/workflow/dispatch/ai/agent/utils.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/ask/constants.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/model/constants.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/model/index.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/plan/constants.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/plan/index.ts create mode 100644 packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts diff --git a/packages/service/core/ai/llm/agentCall.ts b/packages/service/core/ai/llm/agentCall.ts index a4182fba8..0ae95161d 100644 --- a/packages/service/core/ai/llm/agentCall.ts +++ b/packages/service/core/ai/llm/agentCall.ts @@ -32,7 +32,10 @@ type RunAgentCallProps = { name: string; avatar: string; }; - handleToolResponse: (e: ChatCompletionMessageToolCall) => Promise<{ + handleToolResponse: (e: { + call: ChatCompletionMessageToolCall; + context: ChatCompletionMessageParam[]; + }) => Promise<{ response: string; usages: ChatNodeUsageType[]; isEnd: boolean; @@ -124,7 +127,10 @@ export const runAgentCall = async ({ let isEndSign = false; for await (const tool of toolCalls) { // TODO: 加入交互节点处理 - const { response, usages, isEnd } = await handleToolResponse(tool); + const { response, usages, isEnd } = await handleToolResponse({ + call: tool, + context: requestMessages + }); if (isEnd) { isEndSign = true; diff --git a/packages/service/core/workflow/dispatch/ai/agent/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/constants.ts index 811e53d4b..972c1519e 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/constants.ts @@ -1,50 +1,30 @@ import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; -export const getTopAgentDefaultPrompt = () => { - return `你是一位Supervisor Agent,具备以下核心能力: +export enum SubAppIds { + plan = 'plan_agent', + stop = 'stop_agent', + model = 'model_agent', + fileRead = 'file_read' +} -## 核心能力 -1. **计划制定与管理**:根据用户需求制定详细的执行计划,并实时跟踪和调整计划进度 -2. **工具调用编排**:可以调用各种工具来完成特定任务,支持并行和串行工具调用 -3. **上下文理解**:能够理解对话历史、文档内容和当前状态 -4. **自主决策**:根据当前情况和计划进度做出最优决策 - -## 工作流程 -1. **需求分析**:深入理解用户需求,识别关键目标和约束条件 -2. **计划制定**:使用 plan_agent 工具制定详细的执行计划 -3. **工具编排**:根据计划选择和调用合适的工具 -4. **结果处理**:分析工具返回结果,判断是否满足预期 -5. **计划调整**:根据执行结果动态调整计划 -6. **最终输出**:给出完整、准确的回答 - -## 特殊指令 +export const getTopAgentConstantPrompt = () => { + return `## 特殊指令 - 对于复杂任务,必须先使用 plan_agent 制定计划 - 在执行过程中如需调整计划,再次调用 plan_agent - 始终保持计划的可见性和可追踪性 -- 遇到错误时要有容错和重试机制 - -请始终保持专业、准确、有条理的回答风格,确保用户能够清楚了解执行进度和结果。`; +- 每次有新的进度完成时,都要调用 plan_agent 更新计划 +- 遇到错误时要有容错和重试机制`; }; -export const PlanAgentTool: ChatCompletionTool = { - type: 'function', - function: { - name: 'plan_agent', - description: - '如果用户的任务非常复杂,可以先使用 plan_agent 制定计划,然后根据计划使用其他工具来完成任务。' - } -}; - -export const StopAgentId = 'stop_agent'; export const StopAgentTool: ChatCompletionTool = { type: 'function', function: { - name: StopAgentId, - description: '如果完成了所有的任务,可调用次工具。' + name: SubAppIds.stop, + description: '如果完成了所有的任务,可调用此工具。' } }; -/* +/* 结构: [url1,url2,url2] [ @@ -52,7 +32,7 @@ export const StopAgentTool: ChatCompletionTool = { {id:2,url: url2} ] */ -export const getFileReadTool = (urls: string[]): ChatCompletionTool => { +export const getFileReadTool = (urls?: string[]): ChatCompletionTool => { return { type: 'function', function: { @@ -64,7 +44,7 @@ export const getFileReadTool = (urls: string[]): ChatCompletionTool => { file_path: { type: 'string', description: '文件ID', - enum: urls.map((url, index) => `${index + 1}`) + enum: urls?.map((_, index) => `${index + 1}`) } }, required: ['file_path'] diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 83e59caea..38c4dab9a 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -27,18 +27,25 @@ import { import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils'; import { getHistoryPreview } from '@fastgpt/global/core/chat/utils'; import { + applyDiff, filterToolResponseToPreview, formatToolResponse, getToolNodesByIds, - initToolNodes + initToolNodes, + parseToolArgs } from '../utils'; -import { getTopAgentDefaultPrompt, StopAgentId, StopAgentTool } from './constants'; +import { getFileReadTool, getTopAgentConstantPrompt, StopAgentTool, SubAppIds } from './constants'; import { runWorkflow } from '../..'; -import json5 from 'json5'; import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import type { ToolNodeItemType } from './type'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; +import { transferPlanAgent } from '../sub/plan'; +import { transferModelAgent } from '../sub/model'; +import { PlanAgentTool } from '../sub/plan/constants'; +import { ModelAgentTool } from '../sub/model/constants'; +import { getSubIdsByAgentSystem, parseAgentSystem } from './utils'; +import { getChildAppPreviewNode } from '../../../../../core/app/plugin/controller'; export type DispatchAgentModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.history]?: ChatItemType[]; @@ -82,7 +89,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise history = 6, fileUrlList: fileLinks, temperature, - aiChatTopP + aiChatTopP, + planConfig } } = props; @@ -96,7 +104,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise } // Init tool params - const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges }); + // const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges }); + const toolNodeIds = getSubIdsByAgentSystem(systemPrompt); const toolNodes = getToolNodesByIds({ toolNodeIds, runtimeNodes }); // TODO: 补充系统 agent const toolNodesMap = new Map(); @@ -111,12 +120,14 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }; }; - const subApps = getSubApps({ toolNodes }); + const subApps = getSubApps({ toolNodes, urls: fileLinks }); + + const combinedSystemPrompt = `${parseAgentSystem({ systemPrompt, toolNodesMap })}\n\n${getTopAgentConstantPrompt()}`; // TODO: 把 files 加入 query 中。 const messages: ChatItemType[] = (() => { const value: ChatItemType[] = [ - ...getSystemPrompt_ChatItemType(systemPrompt || getTopAgentDefaultPrompt()), + ...getSystemPrompt_ChatItemType(combinedSystemPrompt), // Add file input prompt to histories ...chatHistories, { @@ -160,15 +171,107 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise isAborted: res ? () => res.closed : undefined, getToolInfo, - handleToolResponse: async (call) => { + handleToolResponse: async ({ call, context }) => { const toolId = call.function.name; - if (toolId === StopAgentId) { + if (toolId === SubAppIds.stop) { return { response: '', usages: [], isEnd: true }; + } else if (toolId === SubAppIds.plan) { + const planModel = planConfig?.model ?? model; + const { instruction } = parseToolArgs<{ instruction: string }>(call.function.arguments); + + const { content, inputTokens, outputTokens } = await transferPlanAgent({ + model: planModel, + instruction, + histories: GPTMessages2Chats({ + messages: context.slice(1, -1), + getToolInfo + }), + onStreaming({ text, fullText }) { + //TODO: 需要一个新的 plan sse event + if (!fullText) return; + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolResponse, + data: { + tool: { + id: call.id, + toolName: '', + toolAvatar: '', + params: '', + response: sliceStrStartEnd(fullText, 5000, 5000) + } + } + }); + } + }); + + const lastPlanCallIndex = context + .slice(0, -1) + .findLastIndex( + (c) => + c.role === 'assistant' && + c.tool_calls?.some((tc) => tc.function?.name === SubAppIds.plan) + ); + const originalContent = + lastPlanCallIndex !== -1 ? (context[lastPlanCallIndex + 1].content as string) : ''; + + const applyedContent = applyDiff({ + original: originalContent, + patch: content + }); + + // workflowStreamResponse?.({ + // event: SseResponseEventEnum.toolResponse, + // data: { + // tool: { + // id: call.id, + // toolName: '', + // toolAvatar: '', + // params: '', + // response: sliceStrStartEnd(applyedContent, 5000, 5000) + // } + // } + // }); + + return { + response: content, + usages: [], + isEnd: false + }; + } else if (toolId === SubAppIds.model) { + const { systemPrompt, task } = parseToolArgs<{ systemPrompt: string; task: string }>( + call.function.arguments + ); + + const { content, inputTokens, outputTokens } = await transferModelAgent({ + model, + systemPrompt, + task, + onStreaming({ text, fullText }) { + if (!fullText) return; + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolResponse, + data: { + tool: { + id: call.id, + toolName: '', + toolAvatar: '', + params: '', + response: sliceStrStartEnd(fullText, 5000, 5000) + } + } + }); + } + }); + return { + response: content, + usages: [], + isEnd: false + }; } const node = toolNodesMap.get(toolId); @@ -180,14 +283,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }; } - const startParams = (() => { - try { - return json5.parse(call.function.arguments); - } catch { - return {}; - } - })(); - + const startParams = parseToolArgs(call.function.arguments); initToolNodes(runtimeNodes, [node.nodeId], startParams); const { toolResponses, flowUsages, flowResponses } = await runWorkflow({ ...props, @@ -324,9 +420,20 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise } }; -const getSubApps = ({ toolNodes }: { toolNodes: ToolNodeItemType[] }): ChatCompletionTool[] => { +const getSubApps = ({ + toolNodes, + urls +}: { + toolNodes: ToolNodeItemType[]; + urls?: string[]; +}): ChatCompletionTool[] => { // System Tools: Plan Agent, stop sign, model agent. - const systemTools: ChatCompletionTool[] = []; + const systemTools: ChatCompletionTool[] = [ + PlanAgentTool, + StopAgentTool, + ModelAgentTool, + getFileReadTool(urls) + ]; // Node Tools const nodeTools = toolNodes.map((item: ToolNodeItemType) => { @@ -335,7 +442,7 @@ const getSubApps = ({ toolNodes }: { toolNodes: ToolNodeItemType[] }): ChatCompl type: 'function', function: { name: item.nodeId, - description: item.intro || item.name, + description: `调用${item.flowNodeType}:${item.name || item.intro}完成任务`, parameters: item.jsonSchema } }; @@ -358,7 +465,7 @@ const getSubApps = ({ toolNodes }: { toolNodes: ToolNodeItemType[] }): ChatCompl type: 'function', function: { name: item.nodeId, - description: item.toolDescription || item.intro || item.name, + description: `调用${item.flowNodeType}:${item.name || item.toolDescription || item.intro}完成任务`, parameters: { type: 'object', properties, @@ -367,6 +474,7 @@ const getSubApps = ({ toolNodes }: { toolNodes: ToolNodeItemType[] }): ChatCompl } }; }); + console.dir(nodeTools, { depth: null }); return [...systemTools, ...nodeTools]; }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/utils.ts new file mode 100644 index 000000000..8178de20d --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/agent/utils.ts @@ -0,0 +1,61 @@ +import type { ToolNodeItemType } from './type'; + +const namespaceMap = new Map([ + ['a', '子应用'], + ['t', '工具'], + ['d', '知识库'], + ['m', '模型'] +]); + +// e.g: {{@a.appId@}} -> a.appId +const buildPattern = (options?: { prefix?: string }): RegExp => { + const config = { + prefix: '@', + ...options + }; + + const escapedPrefix = config.prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`\\{\\{${escapedPrefix}([^${escapedPrefix}]+)${escapedPrefix}\\}\\}`, 'g'); +}; + +export const getSubIdsByAgentSystem = ( + systemPrompt: string, + options?: { prefix?: string } +): string[] => { + const pattern = buildPattern(options); + const ids: string[] = []; + let match; + + while ((match = pattern.exec(systemPrompt)) !== null) { + const fullName = match[1]; + const [, id] = fullName.split('.'); + if (id) { + ids.push(id); + } + } + + return ids; +}; + +export const parseAgentSystem = ({ + systemPrompt, + toolNodesMap, + options +}: { + systemPrompt: string; + toolNodesMap: Map; + options?: { prefix?: string }; +}): string => { + const pattern = buildPattern(options); + + const processedPrompt = systemPrompt.replace(pattern, (_, toolName) => { + const [namespace, id] = toolName.split('.'); + const toolNode = toolNodesMap.get(id); + const name = toolNode?.name || toolNode?.toolDescription || toolNode?.intro || 'unknown'; + + const prefix = namespaceMap.get(namespace) ?? 'unknown'; + return `${prefix}:${name}`; + }); + + return processedPrompt; +}; diff --git a/packages/service/core/workflow/dispatch/ai/sub/ask/constants.ts b/packages/service/core/workflow/dispatch/ai/sub/ask/constants.ts new file mode 100644 index 000000000..4503cf664 --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/ask/constants.ts @@ -0,0 +1,61 @@ +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import { SubAppIds } from '../../agent/constants'; + +export const AskAgentTool: ChatCompletionTool = { + type: 'function', + function: { + name: SubAppIds.ask, + description: '调用此工具,向用户发起交互式提问', + parameters: { + type: 'object', + properties: { + mode: { + type: 'string', + enum: ['userSelect', 'formInput', 'userInput'], + description: '交互模式' + }, + prompt: { + type: 'string', + description: '向用户展示的提示信息' + }, + options: { + type: 'array', + description: '当 mode=userSelect 时可供选择的选项', + items: { + type: 'string' + } + }, + form: { + type: 'array', + description: '当 mode=formInput 时需要填写的表单字段列表', + items: { + type: 'object', + properties: { + field: { + type: 'string', + description: '字段名,如 name, age, 同时会展示给用户一样的label' + }, + type: { + type: 'string', + enum: ['textInput', 'numberInput', 'singleSelect', 'multiSelect'], + description: '字段输入类型' + }, + required: { type: 'boolean', description: '该字段是否必填', default: false }, + options: { + type: 'array', + description: '当 type 为 singleSelect 或 multiSelect 时的可选项', + items: { type: 'string' } + } + }, + required: ['field', 'type'] + } + }, + userInput: { + type: 'string', + description: '当 mode=userInput 时用户自由输入的内容' + } + }, + required: ['mode', 'prompt'] + } + } +}; diff --git a/packages/service/core/workflow/dispatch/ai/sub/model/constants.ts b/packages/service/core/workflow/dispatch/ai/sub/model/constants.ts new file mode 100644 index 000000000..85013fd1f --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/model/constants.ts @@ -0,0 +1,24 @@ +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import { SubAppIds } from '../../agent/constants'; + +export const ModelAgentTool: ChatCompletionTool = { + type: 'function', + function: { + name: SubAppIds.model, + description: '完成一些简单通用型任务, 可以调用此工具。', + parameters: { + type: 'object', + properties: { + systemPrompt: { + type: 'string', + description: '注入给此 agent 的系统提示词' + }, + task: { + type: 'string', + description: '此 agent 本轮需要完成的任务' + } + }, + required: ['systemPrompt', 'task'] + } + } +}; diff --git a/packages/service/core/workflow/dispatch/ai/sub/model/index.ts b/packages/service/core/workflow/dispatch/ai/sub/model/index.ts new file mode 100644 index 000000000..e4a27badb --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/model/index.ts @@ -0,0 +1,87 @@ +import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; +import { addLog } from '../../../../../../common/system/log'; +import { createLLMResponse, type ResponseEvents } from '../../../../../ai/llm/request'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import { chats2GPTMessages, getSystemPrompt_ChatItemType } from '@fastgpt/global/core/chat/adapt'; +import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { getErrText } from '@fastgpt/global/common/error/utils'; + +type ModelAgentConfig = { + model: string; + temperature?: number; + top_p?: number; + stream?: boolean; +}; + +type transferModelAgentProps = { + systemPrompt?: string; + task?: string; +} & ModelAgentConfig & + Pick; + +export async function transferModelAgent({ + systemPrompt = '', + task = '', + + onStreaming, + onReasoning, + + model, + temperature = 0.7, + top_p, + stream = true +}: transferModelAgentProps): Promise<{ + content: string; + inputTokens: number; + outputTokens: number; +}> { + try { + const messages: ChatItemType[] = [ + ...getSystemPrompt_ChatItemType(systemPrompt), + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: task + } + } + ] + } + ]; + const adaptedMessages: ChatCompletionMessageParam[] = chats2GPTMessages({ + messages, + reserveId: false + }); + + const { + answerText, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model, + temperature, + messages: adaptedMessages, + top_p, + stream + }, + onStreaming, + onReasoning + }); + + return { + content: answerText, + inputTokens, + outputTokens + }; + } catch (error) { + const err = getErrText(error); + addLog.warn('call model_agent failed'); + return { + content: err, + inputTokens: 0, + outputTokens: 0 + }; + } +} diff --git a/packages/service/core/workflow/dispatch/ai/sub/plan/constants.ts b/packages/service/core/workflow/dispatch/ai/sub/plan/constants.ts new file mode 100644 index 000000000..e9a83e3ee --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/plan/constants.ts @@ -0,0 +1,22 @@ +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import { SubAppIds } from '../../agent/constants'; + +export const PlanAgentTool: ChatCompletionTool = { + type: 'function', + function: { + name: SubAppIds.plan, + description: + '如果用户的任务非常复杂,可以先使用 plan_agent 制定计划,然后根据计划使用其他工具来完成任务。同时,plan_agent 负责维护整个任务的上下文和状态。可以更新或修改计划中的内容. 但是 plan_agent 不能直接执行任务。', + parameters: { + type: 'object', + properties: { + instruction: { + type: 'string', + description: + '给 plan_agent 的指令, 例如: "制定一个包含以下步骤的计划:xxx", "将 xxx 待办事项标记为已完成"' + } + }, + required: ['instruction'] + } + } +}; diff --git a/packages/service/core/workflow/dispatch/ai/sub/plan/index.ts b/packages/service/core/workflow/dispatch/ai/sub/plan/index.ts new file mode 100644 index 000000000..d22237dd6 --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/plan/index.ts @@ -0,0 +1,96 @@ +import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; +import { addLog } from '../../../../../../common/system/log'; +import { createLLMResponse, type ResponseEvents } from '../../../../../ai/llm/request'; +import { defaultPlanAgentPrompt } from './prompt'; +import { replaceVariable } from '@fastgpt/global/common/string/tools'; +import { chats2GPTMessages, getSystemPrompt_ChatItemType } from '@fastgpt/global/core/chat/adapt'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { getErrText } from '@fastgpt/global/common/error/utils'; + +type PlanAgentConfig = { + model: string; + customSystemPrompt?: string; + temperature?: number; + top_p?: number; + stream?: boolean; +}; + +type transferPlanAgentProps = { + histories: ChatItemType[]; + instruction?: string; +} & PlanAgentConfig & + Pick; + +export async function transferPlanAgent({ + instruction = '', + histories, + + onStreaming, + onReasoning, + + model, + customSystemPrompt, + temperature = 0, + top_p, + stream = true +}: transferPlanAgentProps): Promise<{ + content: string; + inputTokens: number; + outputTokens: number; +}> { + try { + const messages: ChatItemType[] = [ + ...getSystemPrompt_ChatItemType( + replaceVariable(defaultPlanAgentPrompt, { + userRole: customSystemPrompt + }) + ), + ...histories, + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: instruction + } + } + ] + } + ]; + const adaptedMessages: ChatCompletionMessageParam[] = chats2GPTMessages({ + messages, + reserveId: false + }); + + const { + answerText, + usage: { inputTokens, outputTokens } + } = await createLLMResponse({ + body: { + model, + temperature, + messages: adaptedMessages, + top_p, + stream + }, + onStreaming, + onReasoning + }); + + return { + content: answerText, + inputTokens, + outputTokens + }; + } catch (error) { + const err = getErrText(error); + addLog.warn('call plan_agent failed'); + return { + content: err, + inputTokens: 0, + outputTokens: 0 + }; + } +} diff --git a/packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts b/packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts new file mode 100644 index 000000000..2110f2ddb --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts @@ -0,0 +1,145 @@ +export const defaultPlanAgentPrompt = ` +你是一个专业的项目规划助手,擅长将复杂任务分解为结构化的执行计划;同时支持对既有计划进行“最小差异(+- Diff)”式修改。你会严格遵循指定的注释标记格式,并在修改模式下输出可直接应用的补丁(patch)。 + + + +{{userRole}} + + + +- 自动识别两种模式: + 1) 创建模式(create):用户未提供现有计划时,生成全新的计划文档。 + 2) 修改模式(patch):用户提供现有计划或明确提出“增删改”需求时,输出 Diff Patch。 +- 若用户提供了现有计划文本(含注释标记),一律进入修改模式;否则进入创建模式。 + + + +根据用户提供的主题或目标,生成或修改一份详细、可执行的项目计划文档,包含合理的阶段划分与具体待办事项;修改时以“补丁”为最小输出单元,确保变更可定位、可回放、可审计。 + + + +- 用户输入:一个需要制定或更新的主题、目标或任务描述;可选的现有计划文档;可选的变更请求(自然语言或指令式)。 +- 输入格式:自然语言描述,可能包含背景、目标、约束、优先级、本地化偏好、以及现有计划全文。 + + + +通用步骤 +1. 解析用户输入,提取核心目标、关键要素、约束与本地化偏好。 +2. 评估任务复杂度(简单:2-3 步;复杂:4-7 步),据此确定阶段数量。 +3. 各阶段生成 3-5 条可执行 Todo,动词开头,MECE 且无重叠。 +4. 语言风格本地化(根据用户输入语言进行术语与语序调整)。 + +创建模式(create) +5. 产出完整计划,严格使用占位符 [主题] 与标记体系;确保编号连续、标签闭合、结构清晰。 + +修改模式(patch) +5. 解析“现有计划”(锚点优先级:\`\`、\`\`、\`\` 等)。 +6. 将用户变更需求映射为原子操作(见 ),生成最小必要的行级 Diff: + - 仅对变更涉及的行输出 \`+\`(新增)或 \`-\`(删除);未变更行不重复输出。 + - 修改视为“-旧行”与“+新行”的并列呈现。 + - 插入请贴靠最稳固的锚点(如 \`\` 下的标题或 \`\` 前后)。 +7. 自动重排:对步骤编号 N、待办编号 X 做连续性校正;若移动/插入造成编号漂移,补丁中体现校正后的行。 +8. 校验:所有必须标签完整闭合;编号连续;每步 3-5 条待办;MECE;无空段落;无悬挂标记。 +9. 产出补丁;如 render=full,则在补丁后附上“更新后的完整文档”。 + + +支持的原子操作(内部推理用,输出仍为行级 Diff): +- ADD_STEP(after N | before N | at end) 新增步骤(含标题、描述、Todos、可选备注) +- REMOVE_STEP(N) 删除步骤 +- UPDATE_STEP(N, title/desc/note=…) 更新步骤标题/描述/备注 +- MOVE_STEP(N -> M) 移动步骤至序号 M(重排后编号连续) +- ADD_TODO(N, at k) 在步骤 N 的第 k 个位置插入 Todo +- REMOVE_TODO(N.k) 删除 Todo +- UPDATE_TODO(N.k, text=…) 更新 Todo 文本 +- MOVE_TODO(N.k -> M.t) 移动 Todo 到其他步骤/位置 +- RENAME_THEME(text=…) 更新主标题或整体描述中的 [主题] 文本描述(占位仍用 [主题]) +说明:若用户未显式给出操作,你需从自然语言中归纳为上述操作的序列并生成对应行级 Diff。 + + + +- 补丁仅包含变更行,以“前缀字符 +|-”表示新增/删除。 +- 不在上述集合内的行(例如空行)若因结构需要调整,可一并纳入补丁。 +- 修改必须保持“标记尾注不变更其语义角色”,即:当你替换内容时,保留原注释标签并只更改标签左侧的可读文本。 +- 重排后编号以补丁中的最新数字为准;不要在同一补丁里出现对同一元素的多次相互抵消的改动。 +- 若原文缺少稳固锚点,优先在最近的 \`\` 或 \`\` 相邻位置插入。 + + + +- 必须严格遵循以下注释标记格式: + * 标记主标题 + * 标记整体描述 + * 包裹步骤块 + * 标记步骤标题 + * 标记步骤描述 + * 包裹待办列表 + * 标记单个待办事项 + * 添加重要注释或备注 +- 步骤数量随复杂度自动调整;每步 3-5 条 Todo。 +- 编号(N、X)必须连续、准确,修改模式下需自动校正。 +- 描述语言简洁、专业、可操作;各阶段逻辑递进、MECE。 +- 进行本地化调整(术语、量词、表达习惯)。 + + + +- 不生成违法、不道德或有害内容;敏感主题输出合规替代方案。 +- 避免过于具体的时间/预算承诺与无法验证的保证。 +- 保持中立、客观;必要时指出风险与依赖。 +- 你拥有的记忆是通过别的 Agent 共享给你的, 你只需要专注于输出内容, 不必担心上下文的完整性。 + + + + + # [主题] 深度调研计划 + + 全面了解 [主题] 的 [核心维度描述] + + + ## Step 1: [阶段名称] + [阶段目标描述] + ### Todo List + + - [ ] [具体任务描述] + - [ ] [具体任务描述] + - [ ] [具体任务描述] + + [可选备注] + + + + ## Step 2: [阶段名称] + [阶段目标描述] + ### Todo List + + - [ ] [具体任务描述] + - [ ] [具体任务描述] + - [ ] [具体任务描述] + + [可选备注] + + + + + # 仅输出变更行,以 +- 表示;无代码块围栏;保持原有缩进与空行风格。确保旧行的准确性和完整性 + # 如果是 todo list 的变更,请确保 todo 前的 - [ ] 符号正确。删除或替换的行前用 - - [ ] 来表示 todo 的变更 + # 禁止输出代码块标记\`\`\` + + 新增或替换后的行 + - 被删除或被替换的旧行 + + + + + + +- 例:标记 todo 2.1 完成状态 + +- - [ ] 完成数据采集与清洗 ++ - [x] 完成数据采集与清洗 + + +`; diff --git a/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts b/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts index b3ad4b684..d2c746725 100644 --- a/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts +++ b/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts @@ -13,7 +13,7 @@ import json5 from 'json5'; import type { DispatchFlowResponse } from '../../type'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import type { AIChatItemType } from '@fastgpt/global/core/chat/type'; -import { formatToolResponse, initToolCallEdges, initToolNodes } from '../utils'; +import { formatToolResponse, initToolCallEdges, initToolNodes, parseToolArgs } from '../utils'; import { computedMaxToken } from '../../../../ai/utils'; import { sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; @@ -368,13 +368,7 @@ export const runToolCall = async ( if (!toolNode) continue; - const startParams = (() => { - try { - return json5.parse(tool.function.arguments); - } catch (error) { - return {}; - } - })(); + const startParams = parseToolArgs(tool.function.arguments); initToolNodes(runtimeNodes, [toolNode.nodeId], startParams); const toolRunResponse = await runWorkflow({ @@ -384,7 +378,6 @@ export const runToolCall = async ( }); const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); - const toolMsgParams: ChatCompletionToolMessageParam = { tool_call_id: tool.id, role: ChatCompletionRequestMessageRoleEnum.Tool, diff --git a/packages/service/core/workflow/dispatch/ai/utils.ts b/packages/service/core/workflow/dispatch/ai/utils.ts index f33b76dd7..d52e22528 100644 --- a/packages/service/core/workflow/dispatch/ai/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/utils.ts @@ -11,6 +11,7 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; import type { ToolNodeItemType } from './tool/type'; +import json5 from 'json5'; export const filterToolResponseToPreview = (response: AIChatItemValueItemType[]) => { return response.map((item) => { @@ -167,3 +168,52 @@ export const getToolNodesByIds = ({ }; }); }; + +export const parseToolArgs = >(toolArgs: string): T => { + try { + return json5.parse(toolArgs) as T; + } catch { + return {} as T; + } +}; + +/** + * 简单版 diff apply + * @param original 原始文本 + * @param patch diff patch 文本(带 + 和 -) + */ +export const applyDiff = ({ original, patch }: { original: string; patch: string }): string => { + if (!original) return patch; + + let result = original.split('\n'); + + const patchLines = patch.split('\n'); + for (let i = 0; i < patchLines.length; i++) { + const line = patchLines[i]; + + if (line.startsWith('-')) { + const oldContent = line.slice(1).trim(); + const next = patchLines[i + 1]; + + // 下一个是对应的 + 行 → 替换 + if (next && next.startsWith('+')) { + const newContent = next.slice(1).trim(); // 也要 trim + const idx = result.findIndex((l) => l.trim() === oldContent); + if (idx !== -1) { + // 保留原有的缩进 + const indent = result[idx].match(/^(\s*)/)?.[1] || ''; + result[idx] = indent + newContent; // 保留缩进 + } + i++; // 跳过下一个 + 行 + } else { + // 单独的删除行 + const idx = result.findIndex((l) => l.trim() === oldContent); + if (idx !== -1) { + result.splice(idx, 1); + } + } + } + } + + return result.join('\n'); +};