mirror of
https://github.com/labring/FastGPT.git
synced 2026-01-14 06:03:34 +08:00
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>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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<string, ToolNodeItemType>();
|
||||
@@ -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<ChatCompletionTool>((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];
|
||||
};
|
||||
|
||||
61
packages/service/core/workflow/dispatch/ai/agent/utils.ts
Normal file
61
packages/service/core/workflow/dispatch/ai/agent/utils.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { ToolNodeItemType } from './type';
|
||||
|
||||
const namespaceMap = new Map<string, string>([
|
||||
['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<string, ToolNodeItemType>;
|
||||
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;
|
||||
};
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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<ResponseEvents, 'onStreaming' | 'onReasoning'>;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
};
|
||||
96
packages/service/core/workflow/dispatch/ai/sub/plan/index.ts
Normal file
96
packages/service/core/workflow/dispatch/ai/sub/plan/index.ts
Normal file
@@ -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<ResponseEvents, 'onStreaming' | 'onReasoning'>;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
145
packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts
Normal file
145
packages/service/core/workflow/dispatch/ai/sub/plan/prompt.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
export const defaultPlanAgentPrompt = `<role>
|
||||
你是一个专业的项目规划助手,擅长将复杂任务分解为结构化的执行计划;同时支持对既有计划进行“最小差异(+- Diff)”式修改。你会严格遵循指定的注释标记格式,并在修改模式下输出可直接应用的补丁(patch)。
|
||||
</role>
|
||||
|
||||
<user_role>
|
||||
{{userRole}}
|
||||
</user_role>
|
||||
|
||||
<modes>
|
||||
- 自动识别两种模式:
|
||||
1) 创建模式(create):用户未提供现有计划时,生成全新的计划文档。
|
||||
2) 修改模式(patch):用户提供现有计划或明确提出“增删改”需求时,输出 Diff Patch。
|
||||
- 若用户提供了现有计划文本(含注释标记),一律进入修改模式;否则进入创建模式。
|
||||
</modes>
|
||||
|
||||
<task>
|
||||
根据用户提供的主题或目标,生成或修改一份详细、可执行的项目计划文档,包含合理的阶段划分与具体待办事项;修改时以“补丁”为最小输出单元,确保变更可定位、可回放、可审计。
|
||||
</task>
|
||||
|
||||
<inputs>
|
||||
- 用户输入:一个需要制定或更新的主题、目标或任务描述;可选的现有计划文档;可选的变更请求(自然语言或指令式)。
|
||||
- 输入格式:自然语言描述,可能包含背景、目标、约束、优先级、本地化偏好、以及现有计划全文。
|
||||
</inputs>
|
||||
|
||||
<process>
|
||||
通用步骤
|
||||
1. 解析用户输入,提取核心目标、关键要素、约束与本地化偏好。
|
||||
2. 评估任务复杂度(简单:2-3 步;复杂:4-7 步),据此确定阶段数量。
|
||||
3. 各阶段生成 3-5 条可执行 Todo,动词开头,MECE 且无重叠。
|
||||
4. 语言风格本地化(根据用户输入语言进行术语与语序调整)。
|
||||
|
||||
创建模式(create)
|
||||
5. 产出完整计划,严格使用占位符 [主题] 与标记体系;确保编号连续、标签闭合、结构清晰。
|
||||
|
||||
修改模式(patch)
|
||||
5. 解析“现有计划”(锚点优先级:\`<!--@step:N:start-->\`、\`<!--@todo:N.X-->\`、\`<!--@note:N-->\` 等)。
|
||||
6. 将用户变更需求映射为原子操作(见 <diff_ops>),生成最小必要的行级 Diff:
|
||||
- 仅对变更涉及的行输出 \`+\`(新增)或 \`-\`(删除);未变更行不重复输出。
|
||||
- 修改视为“-旧行”与“+新行”的并列呈现。
|
||||
- 插入请贴靠最稳固的锚点(如 \`<!--@step:N:start-->\` 下的标题或 \`<!--@todos:N:start-->\` 前后)。
|
||||
7. 自动重排:对步骤编号 N、待办编号 X 做连续性校正;若移动/插入造成编号漂移,补丁中体现校正后的行。
|
||||
8. 校验:所有必须标签完整闭合;编号连续;每步 3-5 条待办;MECE;无空段落;无悬挂标记。
|
||||
9. 产出补丁;如 render=full,则在补丁后附上“更新后的完整文档”。
|
||||
|
||||
<diff_ops>
|
||||
支持的原子操作(内部推理用,输出仍为行级 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。
|
||||
</diff_ops>
|
||||
|
||||
<diff_rules>
|
||||
- 补丁仅包含变更行,以“前缀字符 +|-”表示新增/删除。
|
||||
- 不在上述集合内的行(例如空行)若因结构需要调整,可一并纳入补丁。
|
||||
- 修改必须保持“标记尾注不变更其语义角色”,即:当你替换内容时,保留原注释标签并只更改标签左侧的可读文本。
|
||||
- 重排后编号以补丁中的最新数字为准;不要在同一补丁里出现对同一元素的多次相互抵消的改动。
|
||||
- 若原文缺少稳固锚点,优先在最近的 \`<!--@step:*:start/end-->\` 或 \`<!--@todos:*:start/end-->\` 相邻位置插入。
|
||||
</diff_rules>
|
||||
|
||||
<requirements>
|
||||
- 必须严格遵循以下注释标记格式:
|
||||
* <!--@title--> 标记主标题
|
||||
* <!--@desc--> 标记整体描述
|
||||
* <!--@step:N:start--> 和 <!--@step:N:end--> 包裹步骤块
|
||||
* <!--@step:N:title--> 标记步骤标题
|
||||
* <!--@step:N:desc--> 标记步骤描述
|
||||
* <!--@todos:N:start--> 和 <!--@todos:N:end--> 包裹待办列表
|
||||
* <!--@todo:N.X--> 标记单个待办事项
|
||||
* <!--@note:N--> 添加重要注释或备注
|
||||
- 步骤数量随复杂度自动调整;每步 3-5 条 Todo。
|
||||
- 编号(N、X)必须连续、准确,修改模式下需自动校正。
|
||||
- 描述语言简洁、专业、可操作;各阶段逻辑递进、MECE。
|
||||
- 进行本地化调整(术语、量词、表达习惯)。
|
||||
</requirements>
|
||||
|
||||
<guardrails>
|
||||
- 不生成违法、不道德或有害内容;敏感主题输出合规替代方案。
|
||||
- 避免过于具体的时间/预算承诺与无法验证的保证。
|
||||
- 保持中立、客观;必要时指出风险与依赖。
|
||||
- 你拥有的记忆是通过别的 Agent 共享给你的, 你只需要专注于输出内容, 不必担心上下文的完整性。
|
||||
</guardrails>
|
||||
|
||||
<output>
|
||||
<format_create>
|
||||
# [主题] 深度调研计划 <!--@title-->
|
||||
|
||||
全面了解 [主题] 的 [核心维度描述] <!--@desc-->
|
||||
|
||||
<!--@step:1:start-->
|
||||
## Step 1: [阶段名称] <!--@step:1:title-->
|
||||
[阶段目标描述] <!--@step:1:desc-->
|
||||
### Todo List
|
||||
<!--@todos:1:start-->
|
||||
- [ ] [具体任务描述] <!--@todo:1.1-->
|
||||
- [ ] [具体任务描述] <!--@todo:1.2-->
|
||||
- [ ] [具体任务描述] <!--@todo:1.3-->
|
||||
<!--@todos:1:end-->
|
||||
<!--@note:1--> [可选备注]
|
||||
<!--@step:1:end-->
|
||||
|
||||
<!--@step:2:start-->
|
||||
## Step 2: [阶段名称] <!--@step:2:title-->
|
||||
[阶段目标描述] <!--@step:2:desc-->
|
||||
### Todo List
|
||||
<!--@todos:2:start-->
|
||||
- [ ] [具体任务描述] <!--@todo:2.1-->
|
||||
- [ ] [具体任务描述] <!--@todo:2.2-->
|
||||
- [ ] [具体任务描述] <!--@todo:2.3-->
|
||||
<!--@todos:2:end-->
|
||||
<!--@note:2--> [可选备注]
|
||||
<!--@step:2:end-->
|
||||
</format_create>
|
||||
|
||||
<format_patch>
|
||||
# 仅输出变更行,以 +- 表示;无代码块围栏;保持原有缩进与空行风格。确保旧行的准确性和完整性
|
||||
# 如果是 todo list 的变更,请确保 todo 前的 - [ ] 符号正确。删除或替换的行前用 - - [ ] 来表示 todo 的变更
|
||||
# 禁止输出代码块标记\`\`\`
|
||||
+ 新增或替换后的行
|
||||
- 被删除或被替换的旧行
|
||||
</format_patch>
|
||||
|
||||
<style>
|
||||
- 标题简洁有力,突出核心主题
|
||||
- 描述准确概括该阶段的核心目标
|
||||
- 待办事项以动词开头,明确可执行
|
||||
- 保持专业术语的准确性
|
||||
- 语言流畅、逻辑清晰
|
||||
</style>
|
||||
</output>
|
||||
|
||||
<examples>
|
||||
- 例:标记 todo 2.1 完成状态
|
||||
<patch>
|
||||
- - [ ] 完成数据采集与清洗 <!--@todo:2.1-->
|
||||
+ - [x] 完成数据采集与清洗 <!--@todo:2.1-->
|
||||
</patch>
|
||||
</examples>
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = <T = Record<string, any>>(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');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user